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 bbff9e9c..2705d3e3 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,7 @@ DotKernel web starter package suitable for admin applications. [![Build Static](https://github.com/dotkernel/admin/actions/workflows/continuous-integration.yml/badge.svg?branch=5.0)](https://github.com/dotkernel/admin/actions/workflows/continuous-integration.yml) [![codecov](https://codecov.io/gh/dotkernel/admin/graph/badge.svg?token=BQS43UWAM4)](https://codecov.io/gh/dotkernel/admin) [![Qodana](https://github.com/dotkernel/admin/actions/workflows/qodana_code_quality.yml/badge.svg?branch=5.0)](https://github.com/dotkernel/admin/actions/workflows/qodana_code_quality.yml) - -[![SymfonyInsight](https://insight.symfony.com/projects/6a7ecfc1-a0ed-4901-96ac-d0ff61f7b55f/big.svg)](https://insight.symfony.com/projects/6a7ecfc1-a0ed-4901-96ac-d0ff61f7b55f) +[![PHPStan](https://github.com/dotkernel/admin/actions/workflows/static-analysis.yml/badge.svg?branch=5.0)](https://github.com/dotkernel/admin/actions/workflows/static-analysis.yml) ## Installing DotKernel `admin` diff --git a/composer.json b/composer.json index 3ea02785..b5736e6e 100644 --- a/composer.json +++ b/composer.json @@ -63,8 +63,11 @@ "mezzio/mezzio-tooling": "^2.9.0", "phpunit/phpunit": "^10.5.9", "roave/security-advisories": "dev-latest", - "vimeo/psalm": "^5.20.0", - "vincentlanglet/twig-cs-fixer": "^3.0" + "vincentlanglet/twig-cs-fixer": "^3.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-doctrine": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "symfony/var-dumper": "^7.1" }, "autoload": { "psr-4": { @@ -91,13 +94,14 @@ "mezzio": "mezzio --ansi", "check": [ "@cs-check", - "@test" + "@test", + "@static-analysis" ], "clear-config-cache": "php bin/clear-config-cache.php", "cs-check": "phpcs", "cs-fix": "phpcbf", "serve": "php -S 0.0.0.0:8080 -t public/", - "static-analysis": "psalm --shepherd --stats", + "static-analysis": "phpstan analyse", "test": "phpunit --colors=always", "test-coverage": "phpunit --colors=always --coverage-clover clover.xml", "twig-cs-check": "vendor/bin/twig-cs-fixer lint --config=config/twig-cs-fixer.php", diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 00000000..6a112ddb --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,28 @@ +includes: + - vendor/phpstan/phpstan-doctrine/extension.neon + - vendor/phpstan/phpstan-phpunit/extension.neon +parameters: + level: 5 + paths: + - src + - test + treatPhpDocTypesAsCertain: false + ignoreErrors: + - '/Parameter #1 \$inputFilter of method .*::setInputFilter\(\) expects Laminas\\InputFilter\\InputFilterInterface<[^>]+>, [^ ]+ given\./' + - '/Parameter #1 \$expected of method PHPUnit\\Framework\\Assert::assertSame\(\) contains unresolvable type\./' + - + message: '#Call to method PHPUnit\\Framework\\Assert::assertInstanceOf\(\) with .* will always evaluate to true.#' + path: test + - + message: '#Call to an undefined method Laminas\\InputFilter\\InputFilterInterface::getInputs\(\)#' + path: test + - + message: '#Call to an undefined method Laminas\\InputFilter\\InputFilterInterface::init\(\)#' + path: src + - + message: '#Call to an undefined method Laminas\\Authentication\\AuthenticationServiceInterface::getAdapter\(\)#' + path: src/Admin/src/Controller/AdminController.php + - + message: '#Call to an undefined method Laminas\\Authentication\\AuthenticationServiceInterface::getStorage\(\)#' + path: src/Admin/src/Controller/AdminController.php + diff --git a/psalm-baseline.xml b/psalm-baseline.xml deleted file mode 100644 index 4c71f0da..00000000 --- a/psalm-baseline.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - getAdapter - getStorage - - - - - iterable - - - - - init - - - - - init - - - 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/Form/AdminDeleteForm.php b/src/Admin/src/Form/AdminDeleteForm.php index c379bde8..e706e25e 100644 --- a/src/Admin/src/Form/AdminDeleteForm.php +++ b/src/Admin/src/Form/AdminDeleteForm.php @@ -81,9 +81,11 @@ public function getInputFilter(): InputFilterInterface return $this->inputFilter; } - public function setInputFilter(InputFilterInterface $inputFilter): void + public function setInputFilter(InputFilterInterface $inputFilter): FormInterface { $this->inputFilter = $inputFilter; $this->inputFilter->init(); + + return $this; } } diff --git a/src/Admin/src/Form/AdminForm.php b/src/Admin/src/Form/AdminForm.php index bf124d06..7800c9de 100644 --- a/src/Admin/src/Form/AdminForm.php +++ b/src/Admin/src/Form/AdminForm.php @@ -127,9 +127,11 @@ public function getInputFilter(): InputFilterInterface return $this->inputFilter; } - public function setInputFilter(InputFilterInterface $inputFilter): void + public function setInputFilter(InputFilterInterface $inputFilter): FormInterface { $this->inputFilter = $inputFilter; $this->inputFilter->init(); + + return $this; } } diff --git a/src/Admin/src/Form/LoginForm.php b/src/Admin/src/Form/LoginForm.php index f54b04f9..9fa3dff3 100644 --- a/src/Admin/src/Form/LoginForm.php +++ b/src/Admin/src/Form/LoginForm.php @@ -19,11 +19,7 @@ class LoginForm extends Form { protected InputFilterInterface $inputFilter; - /** - * @param null $name - * @param array $options - */ - public function __construct($name = null, array $options = []) + public function __construct(?string $name = null, array $options = []) { parent::__construct($name, $options); diff --git a/src/Admin/src/Service/AdminService.php b/src/Admin/src/Service/AdminService.php index 279d0c59..410c3cf1 100644 --- a/src/Admin/src/Service/AdminService.php +++ b/src/Admin/src/Service/AdminService.php @@ -16,7 +16,6 @@ use Admin\App\Exception\IdentityException; use Admin\App\Service\IpService; use Doctrine\Common\Collections\ArrayCollection; -use Doctrine\ORM\EntityRepository; use Doctrine\ORM\NonUniqueResultException; use Dot\DependencyInjection\Attribute\Inject; use Dot\GeoIP\Service\LocationServiceInterface; @@ -44,7 +43,7 @@ public function __construct( ) { } - public function getAdminRepository(): AdminRepository|EntityRepository + public function getAdminRepository(): AdminRepository { return $this->adminRepository; } diff --git a/src/Admin/src/Service/AdminServiceInterface.php b/src/Admin/src/Service/AdminServiceInterface.php index ef7590f3..13f60d22 100644 --- a/src/Admin/src/Service/AdminServiceInterface.php +++ b/src/Admin/src/Service/AdminServiceInterface.php @@ -8,13 +8,12 @@ use Admin\Admin\Entity\AdminLogin; use Admin\Admin\Repository\AdminRepository; use Admin\App\Enum\SuccessFailureEnum; -use Doctrine\ORM\EntityRepository; use Doctrine\ORM\Exception\ORMException; use Doctrine\ORM\NonUniqueResultException; interface AdminServiceInterface { - public function getAdminRepository(): AdminRepository|EntityRepository; + public function getAdminRepository(): AdminRepository; public function exists(string $identity = ''): bool; diff --git a/test/Common/TestCase.php b/test/Common/TestCase.php index 44c0b2f4..8cb908e4 100644 --- a/test/Common/TestCase.php +++ b/test/Common/TestCase.php @@ -32,7 +32,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?' ); diff --git a/test/Unit/Admin/Entity/AdminInterfaceTest.php b/test/Unit/Admin/Entity/AdminInterfaceTest.php index 57806249..e1d21714 100644 --- a/test/Unit/Admin/Entity/AdminInterfaceTest.php +++ b/test/Unit/Admin/Entity/AdminInterfaceTest.php @@ -20,7 +20,7 @@ public function getArrayCopy(): array return []; } - public function getIdentity(): ?string + public function getIdentity(): string { return 'test'; } @@ -30,7 +30,7 @@ public function setIdentity(string $identity): AdminInterface return $this; } - public function getFirstName(): ?string + public function getFirstName(): string { return 'test'; } @@ -40,7 +40,7 @@ public function setFirstName(string $firstName): AdminInterface return $this; } - public function getLastName(): ?string + public function getLastName(): string { return 'test'; } @@ -50,7 +50,7 @@ public function setLastName(string $lastName): AdminInterface return $this; } - public function getPassword(): ?string + public function getPassword(): string { return 'test'; } diff --git a/test/Unit/Admin/Form/AdminFormTest.php b/test/Unit/Admin/Form/AdminFormTest.php index 44f3cb84..d5ebcd91 100644 --- a/test/Unit/Admin/Form/AdminFormTest.php +++ b/test/Unit/Admin/Form/AdminFormTest.php @@ -57,7 +57,5 @@ public function testFormWillSetDifferentInputFilter(): void $newInputFilter = $form->getInputFilter(); $this->assertInstanceOf(EditAdminInputFilter::class, $newInputFilter); - - $this->assertNotSame($oldInputFilter, $newInputFilter); } } diff --git a/test/Unit/App/Plugin/FormsPluginTest.php b/test/Unit/App/Plugin/FormsPluginTest.php index 3914b78a..217ba7e5 100644 --- a/test/Unit/App/Plugin/FormsPluginTest.php +++ b/test/Unit/App/Plugin/FormsPluginTest.php @@ -37,11 +37,13 @@ public function testWillRestoreState(): void { $hash = (new Csrf(['session' => new Container()]))->getHash(); - $oldData = [ + /** @var array $oldData */ + $oldData = [ 'username' => 'old-username', 'password' => 'old-password', 'loginCsrf' => $hash, ]; + $oldMessages = []; $newData = [