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 [![Build Static](https://github.com/dotkernel/api/actions/workflows/continuous-integration.yml/badge.svg?branch=5.0)](https://github.com/dotkernel/api/actions/workflows/continuous-integration.yml) [![codecov](https://codecov.io/gh/dotkernel/api/graph/badge.svg?token=53FN78G5CK)](https://codecov.io/gh/dotkernel/api) [![Qodana](https://github.com/dotkernel/api/actions/workflows/qodana_code_quality.yml/badge.svg?branch=5.0)](https://github.com/dotkernel/api/actions/workflows/qodana_code_quality.yml) - -[![SymfonyInsight](https://insight.symfony.com/projects/7f9143cc-5e3c-4cfc-992c-377a001fde3e/big.svg)](https://insight.symfony.com/projects/7f9143cc-5e3c-4cfc-992c-377a001fde3e) +[![PHPStan](https://github.com/dotkernel/api/actions/workflows/static-analysis.yml/badge.svg?branch=5.0)](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());