diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml
new file mode 100644
index 00000000..6a700f5d
--- /dev/null
+++ b/.github/workflows/static-analysis.yml
@@ -0,0 +1,54 @@
+on:
+ - push
+
+name: Run PHPStan checks
+
+jobs:
+ mutation:
+ name: PHPStan ${{ matrix.php }}-${{ matrix.os }}
+
+ runs-on: ${{ matrix.os }}
+
+ strategy:
+ matrix:
+ os:
+ - ubuntu-latest
+
+ php:
+ - "8.2"
+ - "8.3"
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Install PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: "${{ matrix.php }}"
+ coverage: pcov
+ ini-values: assert.exception=1, zend.assertions=1, error_reporting=-1, log_errors_max_len=0, display_errors=On
+ tools: composer:v2, cs2pr
+
+ - name: Determine composer cache directory
+ run: echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_ENV
+
+ - name: Cache dependencies installed with composer
+ uses: actions/cache@v4
+ with:
+ path: ${{ env.COMPOSER_CACHE_DIR }}
+ key: php${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }}
+ restore-keys: |
+ php${{ matrix.php }}-composer-
+
+ - name: Install dependencies with composer
+ run: composer install --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi
+
+ - name: Setup project
+ run: |
+ mv config/autoload/local.php.dist config/autoload/local.php
+ mv config/autoload/mail.local.php.dist config/autoload/mail.local.php
+ mv config/autoload/local.test.php.dist config/autoload/local.test.php
+
+ - name: Run static analysis with PHPStan
+ run: vendor/bin/phpstan analyse
diff --git a/README.md b/README.md
index f83d7680..8f44298a 100644
--- a/README.md
+++ b/README.md
@@ -13,8 +13,7 @@ Based on Enrico Zimuel's [Zend Expressive API - Skeleton example](https://github
[](https://github.com/dotkernel/api/actions/workflows/continuous-integration.yml)
[](https://codecov.io/gh/dotkernel/api)
[](https://github.com/dotkernel/api/actions/workflows/qodana_code_quality.yml)
-
-[](https://insight.symfony.com/projects/7f9143cc-5e3c-4cfc-992c-377a001fde3e)
+[](https://github.com/dotkernel/api/actions/workflows/static-analysis.yml)
## Getting Started
diff --git a/composer.json b/composer.json
index a7b2360c..26bd952c 100644
--- a/composer.json
+++ b/composer.json
@@ -82,10 +82,12 @@
"laminas/laminas-development-mode": "^3.12.0",
"laminas/laminas-http": "^2.19.0",
"mezzio/mezzio-tooling": "^2.9.0",
+ "phpstan/phpstan": "^2.0",
+ "phpstan/phpstan-doctrine": "^2.0",
+ "phpstan/phpstan-phpunit": "^2.0",
"phpunit/phpunit": "^10.5.10",
"roave/security-advisories": "dev-latest",
- "symfony/var-dumper": "^7.1",
- "vimeo/psalm": "^5.22.0"
+ "symfony/var-dumper": "^7.1"
},
"autoload": {
"psr-4": {
diff --git a/phpstan.neon b/phpstan.neon
new file mode 100644
index 00000000..3503bcd5
--- /dev/null
+++ b/phpstan.neon
@@ -0,0 +1,16 @@
+includes:
+ - vendor/phpstan/phpstan-doctrine/extension.neon
+ - vendor/phpstan/phpstan-phpunit/extension.neon
+parameters:
+ level: 5
+ paths:
+ - src
+ - test
+ treatPhpDocTypesAsCertain: false
+ ignoreErrors:
+ -
+ message: '#Call to an undefined method.*setAllowOverride#'
+ path: test/Functional/AbstractFunctionalTest.php
+ -
+ message: '#Call to an undefined method.*setService#'
+ path: test/Functional/AbstractFunctionalTest.php
diff --git a/psalm-baseline.xml b/psalm-baseline.xml
deleted file mode 100644
index 7ae36187..00000000
--- a/psalm-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
- getSecret
- getSecret
-
-
-
-
- matching
- matching
-
-
-
-
- matching
-
-
-
-
- matching
-
-
-
diff --git a/psalm.xml b/psalm.xml
deleted file mode 100644
index ed4c186a..00000000
--- a/psalm.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/src/Admin/src/Service/AdminService.php b/src/Admin/src/Service/AdminService.php
index 52c1ba0f..c9c5e045 100644
--- a/src/Admin/src/Service/AdminService.php
+++ b/src/Admin/src/Service/AdminService.php
@@ -64,11 +64,7 @@ public function deleteAdmin(Admin $admin): void
public function exists(string $identity = ''): bool
{
- try {
- return $this->findOneBy(['identity' => $identity]) instanceof Admin;
- } catch (NotFoundException) {
- return false;
- }
+ return $this->adminRepository->findOneBy(['identity' => $identity]) instanceof Admin;
}
public function existsOther(string $identity = '', string $uuid = ''): bool
diff --git a/src/App/src/Entity/OAuthAuthCode.php b/src/App/src/Entity/OAuthAuthCode.php
index 44564d6b..c01109b2 100644
--- a/src/App/src/Entity/OAuthAuthCode.php
+++ b/src/App/src/Entity/OAuthAuthCode.php
@@ -57,7 +57,7 @@ public function setId(int $id): self
return $this;
}
- public function getId(): ?int
+ public function getId(): int
{
return $this->id;
}
@@ -74,9 +74,9 @@ public function getClient(): ClientEntityInterface
return $this->client;
}
- public function getIdentifier(): ?int
+ public function getIdentifier(): string
{
- return $this->getId();
+ return (string) $this->getId();
}
/**
diff --git a/src/App/src/Repository/OAuthClientRepository.php b/src/App/src/Repository/OAuthClientRepository.php
index f5db5e78..8b667625 100644
--- a/src/App/src/Repository/OAuthClientRepository.php
+++ b/src/App/src/Repository/OAuthClientRepository.php
@@ -52,7 +52,7 @@ public function getClientEntity($clientIdentifier): ?ClientEntityInterface
public function validateClient($clientIdentifier, $clientSecret, $grantType): bool
{
$client = $this->getClientEntity($clientIdentifier);
- if (! $client instanceof ClientEntityInterface) {
+ if (! $client instanceof OAuthClient) {
return false;
}
diff --git a/src/App/src/UserIdentity.php b/src/App/src/UserIdentity.php
index 1760e1cf..66fa4e27 100644
--- a/src/App/src/UserIdentity.php
+++ b/src/App/src/UserIdentity.php
@@ -9,7 +9,7 @@
class UserIdentity implements UserInterface
{
protected string $identity;
- /** @var iterable $roles */
+ /** @var array $roles */
protected array $roles;
protected array $details;
diff --git a/test/Functional/AbstractFunctionalTest.php b/test/Functional/AbstractFunctionalTest.php
index 21399833..3149b291 100644
--- a/test/Functional/AbstractFunctionalTest.php
+++ b/test/Functional/AbstractFunctionalTest.php
@@ -18,7 +18,6 @@
use Fig\Http\Message\RequestMethodInterface;
use Fig\Http\Message\StatusCodeInterface;
use Laminas\Diactoros\ServerRequest;
-use Laminas\ServiceManager\ServiceManager;
use Mezzio\Application;
use Mezzio\MiddlewareFactory;
use PHPUnit\Framework\TestCase;
@@ -31,7 +30,6 @@
use function array_merge;
use function getenv;
-use function method_exists;
use function putenv;
use function realpath;
@@ -41,7 +39,7 @@ class AbstractFunctionalTest extends TestCase
use DatabaseTrait;
protected Application $app;
- protected ContainerInterface|ServiceManager $container;
+ protected ContainerInterface $container;
protected const DEFAULT_PASSWORD = 'dotkernel';
/**
@@ -59,12 +57,8 @@ public function setUp(): void
$this->ensureTestMode();
- if (method_exists($this, 'runMigrations')) {
- $this->runMigrations();
- }
- if (method_exists($this, 'runSeeders')) {
- $this->runSeeders();
- }
+ $this->runMigrations();
+ $this->runSeeders();
}
public function tearDown(): void
@@ -132,7 +126,7 @@ protected function getEntityManager(): EntityManagerInterface
return $this->container->get(EntityManagerInterface::class);
}
- protected function getContainer(): ContainerInterface|ServiceManager
+ protected function getContainer(): ContainerInterface
{
return $this->container;
}
@@ -150,7 +144,7 @@ private function ensureTestMode(): void
);
}
- if (! $this->getEntityManager()->getConnection()->getParams()['memory'] ?? false) {
+ if (! ($this->getEntityManager()->getConnection()->getParams()['memory'] ?? false)) {
throw new RuntimeException(
'You are running tests in a non in-memory database. Did you forget to create local.test.php?'
);
@@ -271,7 +265,7 @@ private function createRequest(
string $body = 'php://input',
string $protocol = '1.1'
): ServerRequestInterface {
- if (method_exists($this, 'isAuthenticated') && $this->isAuthenticated()) {
+ if ($this->isAuthenticated()) {
$headers = array_merge($headers, $this->getAuthorizationHeader());
}
diff --git a/test/Unit/Admin/Service/AdminServiceTest.php b/test/Unit/Admin/Service/AdminServiceTest.php
index 97756175..52561e5f 100644
--- a/test/Unit/Admin/Service/AdminServiceTest.php
+++ b/test/Unit/Admin/Service/AdminServiceTest.php
@@ -21,8 +21,8 @@
class AdminServiceTest extends TestCase
{
private Subject|MockObject $subject;
- private AdminRoleService $adminRoleService;
- private AdminRepository $adminRepository;
+ private AdminRoleService|MockObject $adminRoleService;
+ private AdminRepository|MockObject $adminRepository;
/**
* @throws \PHPUnit\Framework\MockObject\Exception
diff --git a/test/Unit/App/Middleware/AuthenticationMiddlewareTest.php b/test/Unit/App/Middleware/AuthenticationMiddlewareTest.php
index efa8aa21..fc95d3b8 100644
--- a/test/Unit/App/Middleware/AuthenticationMiddlewareTest.php
+++ b/test/Unit/App/Middleware/AuthenticationMiddlewareTest.php
@@ -10,6 +10,7 @@
use Mezzio\Authentication\AuthenticationInterface;
use Mezzio\Authentication\UserInterface;
use PHPUnit\Framework\MockObject\Exception;
+use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
@@ -18,10 +19,10 @@
class AuthenticationMiddlewareTest extends TestCase
{
private Subject $subject;
- private AuthenticationInterface $auth;
+ private AuthenticationInterface|MockObject $auth;
private ServerRequestInterface $request;
- private RequestHandlerInterface $handler;
- private ResponseInterface $response;
+ private RequestHandlerInterface|MockObject $handler;
+ private ResponseInterface|MockObject $response;
/**
* @throws Exception
diff --git a/test/Unit/App/Middleware/AuthorizationMiddlewareTest.php b/test/Unit/App/Middleware/AuthorizationMiddlewareTest.php
index a0dda116..14b1ce96 100644
--- a/test/Unit/App/Middleware/AuthorizationMiddlewareTest.php
+++ b/test/Unit/App/Middleware/AuthorizationMiddlewareTest.php
@@ -19,6 +19,7 @@
use Mezzio\Authentication\UserInterface;
use Mezzio\Authorization\AuthorizationInterface;
use PHPUnit\Framework\MockObject\Exception;
+use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
@@ -29,12 +30,12 @@
class AuthorizationMiddlewareTest extends TestCase
{
- private Subject $subject;
- private UserRepository $userRepository;
- private AdminRepository $adminRepository;
- private AuthorizationInterface $authorization;
+ private Subject|MockObject $subject;
+ private UserRepository|MockObject $userRepository;
+ private AdminRepository|MockObject $adminRepository;
+ private AuthorizationInterface|MockObject $authorization;
private ServerRequestInterface $request;
- private RequestHandlerInterface $handler;
+ private RequestHandlerInterface|MockObject $handler;
private ResponseInterface $response;
/**
diff --git a/test/Unit/App/Middleware/ContentNegotiationMiddlewareTest.php b/test/Unit/App/Middleware/ContentNegotiationMiddlewareTest.php
index b972f1e8..07c743c6 100644
--- a/test/Unit/App/Middleware/ContentNegotiationMiddlewareTest.php
+++ b/test/Unit/App/Middleware/ContentNegotiationMiddlewareTest.php
@@ -101,9 +101,10 @@ public function testCannotResolveRepresentation(): void
public function testFormatAcceptRequest(): void
{
- $this->assertIsArray(
- $this->subject->formatAcceptRequest('application/json')
- );
+ $accept = $this->subject->formatAcceptRequest('application/json');
+
+ $this->assertNotEmpty($accept);
+ $this->assertSame(['application/json'], $accept);
}
public function testCheckAccept(): void
diff --git a/test/Unit/App/Middleware/DeprecationMiddlewareTest.php b/test/Unit/App/Middleware/DeprecationMiddlewareTest.php
index 77a8bf4e..c561891d 100644
--- a/test/Unit/App/Middleware/DeprecationMiddlewareTest.php
+++ b/test/Unit/App/Middleware/DeprecationMiddlewareTest.php
@@ -18,6 +18,7 @@
use Mezzio\Router\Route;
use Mezzio\Router\RouteResult;
use PHPUnit\Framework\MockObject\Exception;
+use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
@@ -31,9 +32,9 @@
class DeprecationMiddlewareTest extends TestCase
{
- private Subject $subject;
- private ServerRequestInterface $request;
- private RequestHandlerInterface $handler;
+ private Subject|MockObject $subject;
+ private ServerRequestInterface|MockObject $request;
+ private RequestHandlerInterface|MockObject $handler;
private ResponseInterface $response;
private const VERSIONING_CONFIG = [
@@ -131,8 +132,6 @@ public function handle(ServerRequestInterface $request): ResponseInterface
$response = $this->subject->process($this->request, $this->handler);
- $this->assertInstanceOf(ResponseInterface::class, $response);
- $this->assertIsArray($response->getHeaders());
$this->assertTrue($response->hasHeader('sunset'));
$this->assertTrue($response->hasHeader('link'));
$this->assertSame('2038-01-01', $response->getHeader('sunset')[0]);
@@ -196,8 +195,6 @@ public function process(
$response = $this->subject->process($this->request, $this->handler);
- $this->assertInstanceOf(ResponseInterface::class, $response);
- $this->assertIsArray($response->getHeaders());
$this->assertTrue($response->hasHeader('sunset'));
$this->assertTrue($response->hasHeader('link'));
$this->assertSame('2038-01-01', $response->getHeader('sunset')[0]);
@@ -255,8 +252,6 @@ public function post(): ResponseInterface
$response = $this->subject->process($this->request, $this->handler);
- $this->assertInstanceOf(ResponseInterface::class, $response);
- $this->assertIsArray($response->getHeaders());
$this->assertTrue($response->hasHeader('sunset'));
$this->assertTrue($response->hasHeader('link'));
$this->assertSame('2038-01-01', $response->getHeader('sunset')[0]);
diff --git a/test/Unit/User/Service/UserAvatarServiceTest.php b/test/Unit/User/Service/UserAvatarServiceTest.php
index 1566f104..43d9d07b 100644
--- a/test/Unit/User/Service/UserAvatarServiceTest.php
+++ b/test/Unit/User/Service/UserAvatarServiceTest.php
@@ -10,11 +10,12 @@
use Api\User\Service\UserAvatarService;
use Laminas\Diactoros\UploadedFile;
use PHPUnit\Framework\MockObject\Exception;
+use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
class UserAvatarServiceTest extends TestCase
{
- private UserAvatarService $subject;
+ private UserAvatarService|MockObject $subject;
private UploadedFile $uploadedFile;
/**
@@ -50,7 +51,6 @@ public function testCreateAvatarOverwrite(): void
$user = $this->getUser();
$avatar = $this->subject->createAvatar($user, $this->uploadedFile);
- $this->assertInstanceOf(UserAvatar::class, $avatar);
$this->assertSame($fileName, $avatar->getName());
}
@@ -64,7 +64,6 @@ public function testCreateAvatarDefault(): void
$user = new User();
$avatar = $this->subject->createAvatar($user, $this->uploadedFile);
- $this->assertInstanceOf(UserAvatar::class, $avatar);
$this->assertSame($fileName, $avatar->getName());
}
diff --git a/test/Unit/User/Service/UserServiceTest.php b/test/Unit/User/Service/UserServiceTest.php
index 720643f1..e07b8c57 100644
--- a/test/Unit/User/Service/UserServiceTest.php
+++ b/test/Unit/User/Service/UserServiceTest.php
@@ -21,6 +21,7 @@
use Dot\Mail\Service\MailService;
use Exception;
use Mezzio\Template\TemplateRendererInterface;
+use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use function array_merge;
@@ -28,11 +29,11 @@
class UserServiceTest extends TestCase
{
- private Subject $subject;
- private UserRoleService $userRoleService;
- private UserRepository $userRepository;
- private UserDetailRepository $userDetailRepository;
- private UserResetPasswordRepository $userResetPasswordRepository;
+ private Subject|MockObject $subject;
+ private UserRoleService|MockObject $userRoleService;
+ private UserRepository|MockObject $userRepository;
+ private UserDetailRepository|MockObject $userDetailRepository;
+ private UserResetPasswordRepository|MockObject $userResetPasswordRepository;
/**
* @throws \PHPUnit\Framework\MockObject\Exception
@@ -142,7 +143,6 @@ public function testCreateUser(): void
$this->assertSame($data['identity'], $user->getIdentity());
$this->assertTrue(User::verifyPassword($data['password'], $user->getPassword()));
- $this->assertInstanceOf(UserDetail::class, $user->getDetail());
$this->assertSame($data['detail']['firstName'], $user->getDetail()->getFirstName());
$this->assertSame($data['detail']['lastName'], $user->getDetail()->getLastName());
$this->assertSame($data['detail']['email'], $user->getDetail()->getEmail());