From f976b7fbd45214e860c32f8d46fb803ec47200d2 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Wed, 23 Sep 2026 18:09:55 +0700 Subject: [PATCH 1/5] Use StructArmed on QA Signed-off-by: Abdul Malik Ikhsan --- .../workflows/static-analysis-structarmed.yml | 49 +++++++++++++++++++ README.md | 1 + composer.json | 1 + .../ProblemDetailsDelegatorFactory.php | 2 +- src/App/src/Factory/UserIdentityFactory.php | 2 +- .../Middleware/AuthorizationMiddleware.php | 2 +- .../ContentNegotiationMiddleware.php | 2 +- .../src/Middleware/DeprecationMiddleware.php | 2 +- src/Core/src/Admin/src/Entity/Admin.php | 2 +- .../src/Admin/src/Enum/AdminStatusEnum.php | 2 +- .../App/src/DBAL/Types/AbstractEnumType.php | 2 +- src/Core/src/User/src/Entity/User.php | 2 +- src/Core/src/User/src/Enum/UserRoleEnum.php | 2 +- src/Core/src/User/src/Enum/UserStatusEnum.php | 4 +- structarmed.php | 31 ++++++++++++ test/Unit/App/Template/ParserTest.php | 2 +- test/Unit/App/Template/RendererTest.php | 2 +- 17 files changed, 96 insertions(+), 14 deletions(-) create mode 100644 .github/workflows/static-analysis-structarmed.yml create mode 100644 structarmed.php diff --git a/.github/workflows/static-analysis-structarmed.yml b/.github/workflows/static-analysis-structarmed.yml new file mode 100644 index 0000000..a12b6c8 --- /dev/null +++ b/.github/workflows/static-analysis-structarmed.yml @@ -0,0 +1,49 @@ +on: + - push + +name: Run StructArmed checks + +jobs: + mutation: + name: StructArmed ${{ matrix.php }}-${{ matrix.os }} + + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: + - ubuntu-latest + + php: + - "8.3" + - "8.4" + - "8.5" + + steps: + - name: Checkout + uses: actions/checkout@v7 + + - 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@v6 + 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: Run static analysis with StructArmed + run: vendor/bin/structarmed analyze diff --git a/README.md b/README.md index 3abf56f..173e148 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ Check out the full documentation available at https://docs.dotkernel.org/api-doc [![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=7.0)](https://github.com/dotkernel/api/actions/workflows/qodana_code_quality.yml) [![PHPStan](https://github.com/dotkernel/api/actions/workflows/static-analysis.yml/badge.svg?branch=7.0)](https://github.com/dotkernel/api/actions/workflows/static-analysis.yml) +[![StructArmed](https://github.com/dotkernel/api/actions/workflows/static-analysis-structarmed.yml/badge.svg?branch=7.0)](https://github.com/dotkernel/api/actions/workflows/static-analysis-structarmed.yml) ## Getting Started diff --git a/composer.json b/composer.json index e98ca96..ea9488c 100644 --- a/composer.json +++ b/composer.json @@ -83,6 +83,7 @@ "zircote/swagger-php": "^6.0.0" }, "require-dev": { + "boundwize/structarmed": "^0.17.12", "dotkernel/dot-maker": "^2.0.0", "laminas/laminas-coding-standard": "^3.0.1", "laminas/laminas-development-mode": "^3.13.0", diff --git a/src/App/src/Factory/ProblemDetailsDelegatorFactory.php b/src/App/src/Factory/ProblemDetailsDelegatorFactory.php index fb0a6c9..e8487d4 100644 --- a/src/App/src/Factory/ProblemDetailsDelegatorFactory.php +++ b/src/App/src/Factory/ProblemDetailsDelegatorFactory.php @@ -41,7 +41,7 @@ public function __invoke( $errorHandler = $container->get(ErrorHandlerInterface::class); assert($errorHandler instanceof LogErrorHandler); - $listener = function (Throwable $throwable, RequestInterface $request) use ($errorHandler) { + $listener = static function (Throwable $throwable, RequestInterface $request) use ($errorHandler) { assert($request instanceof ServerRequestInterface); $errorHandler->handleThrowable($throwable, $request); }; diff --git a/src/App/src/Factory/UserIdentityFactory.php b/src/App/src/Factory/UserIdentityFactory.php index f2c3a23..436c5b4 100644 --- a/src/App/src/Factory/UserIdentityFactory.php +++ b/src/App/src/Factory/UserIdentityFactory.php @@ -12,7 +12,7 @@ class UserIdentityFactory { public function __invoke(ContainerInterface $container): callable { - return function (string $identity, array $roles = [], array $details = []): UserInterface { + return static function (string $identity, array $roles = [], array $details = []): UserInterface { /** @var non-empty-string $identity */ return new UserIdentity($identity, $roles, $details); }; diff --git a/src/App/src/Middleware/AuthorizationMiddleware.php b/src/App/src/Middleware/AuthorizationMiddleware.php index fe35f5a..b8a8065 100644 --- a/src/App/src/Middleware/AuthorizationMiddleware.php +++ b/src/App/src/Middleware/AuthorizationMiddleware.php @@ -75,7 +75,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface } $defaultUser->setRoles( - array_map(fn (RoleInterface $role): string => (string) $role->getName()->value, $user->getRoles()) + array_map(static fn (RoleInterface $role): string => (string) $role->getName()->value, $user->getRoles()) ); $request = $request->withAttribute(UserInterface::class, $defaultUser); diff --git a/src/App/src/Middleware/ContentNegotiationMiddleware.php b/src/App/src/Middleware/ContentNegotiationMiddleware.php index 39590bc..f511d32 100644 --- a/src/App/src/Middleware/ContentNegotiationMiddleware.php +++ b/src/App/src/Middleware/ContentNegotiationMiddleware.php @@ -128,7 +128,7 @@ private function parseAcceptHeader(string $header): array } // Sort by quality in descending order - usort($types, fn ($a, $b) => $b['quality'] <=> $a['quality']); + usort($types, static fn ($a, $b) => $b['quality'] <=> $a['quality']); return $types; } diff --git a/src/App/src/Middleware/DeprecationMiddleware.php b/src/App/src/Middleware/DeprecationMiddleware.php index 1ae0018..b153653 100644 --- a/src/App/src/Middleware/DeprecationMiddleware.php +++ b/src/App/src/Middleware/DeprecationMiddleware.php @@ -86,7 +86,7 @@ private function getAttribute(array $attributes): ?array return array_values( array_filter( $attributes, - fn (array $attribute): bool => $attribute['deprecationType'] === self::RESOURCE_DEPRECATION_ATTRIBUTE + static fn (array $attribute): bool => $attribute['deprecationType'] === self::RESOURCE_DEPRECATION_ATTRIBUTE ) )[0] ?? null; } diff --git a/src/Core/src/Admin/src/Entity/Admin.php b/src/Core/src/Admin/src/Entity/Admin.php index fde0af4..4bb7069 100644 --- a/src/Core/src/Admin/src/Entity/Admin.php +++ b/src/Core/src/Admin/src/Entity/Admin.php @@ -241,7 +241,7 @@ public function getArrayCopy(): array 'firstName' => $this->firstName, 'lastName' => $this->lastName, 'status' => $this->status->value, - 'roles' => array_map(fn (RoleInterface $role): array => $role->getArrayCopy(), $this->roles->toArray()), + 'roles' => array_map(static fn (RoleInterface $role): array => $role->getArrayCopy(), $this->roles->toArray()), 'created' => $this->created, 'updated' => $this->updated, ]; diff --git a/src/Core/src/Admin/src/Enum/AdminStatusEnum.php b/src/Core/src/Admin/src/Enum/AdminStatusEnum.php index 9fd605a..eb44b15 100644 --- a/src/Core/src/Admin/src/Enum/AdminStatusEnum.php +++ b/src/Core/src/Admin/src/Enum/AdminStatusEnum.php @@ -25,7 +25,7 @@ public static function values(): array */ public static function toArray(): array { - return array_reduce(self::cases(), function (array $collector, self $enum): array { + return array_reduce(self::cases(), static function (array $collector, self $enum): array { $collector[$enum->value] = $enum->name; return $collector; diff --git a/src/Core/src/App/src/DBAL/Types/AbstractEnumType.php b/src/Core/src/App/src/DBAL/Types/AbstractEnumType.php index 4383697..795f4b8 100644 --- a/src/Core/src/App/src/DBAL/Types/AbstractEnumType.php +++ b/src/Core/src/App/src/DBAL/Types/AbstractEnumType.php @@ -26,7 +26,7 @@ public function getSQLDeclaration(array $column, AbstractPlatform $platform): st return 'TEXT'; } - $values = array_map(fn($case) => "'$case->value'", $this->getEnumCases()); + $values = array_map(static fn($case) => "'$case->value'", $this->getEnumCases()); return sprintf('ENUM(%s)', implode(', ', $values)); } diff --git a/src/Core/src/User/src/Entity/User.php b/src/Core/src/User/src/Entity/User.php index d6d9886..7b29190 100644 --- a/src/Core/src/User/src/Entity/User.php +++ b/src/Core/src/User/src/Entity/User.php @@ -361,7 +361,7 @@ public function getArrayCopy(): array 'hash' => $this->hash, 'identity' => $this->identity, 'status' => $this->status->value, - 'roles' => array_map(fn (RoleInterface $role): array => $role->getArrayCopy(), $this->roles->toArray()), + 'roles' => array_map(static fn (RoleInterface $role): array => $role->getArrayCopy(), $this->roles->toArray()), 'created' => $this->created, 'updated' => $this->updated, ]; diff --git a/src/Core/src/User/src/Enum/UserRoleEnum.php b/src/Core/src/User/src/Enum/UserRoleEnum.php index b2d89bd..80e91d9 100644 --- a/src/Core/src/User/src/Enum/UserRoleEnum.php +++ b/src/Core/src/User/src/Enum/UserRoleEnum.php @@ -17,7 +17,7 @@ enum UserRoleEnum: string */ public static function validCases(): array { - return array_filter(self::cases(), fn (self $value) => $value !== self::Guest); + return array_filter(self::cases(), static fn (self $value) => $value !== self::Guest); } /** diff --git a/src/Core/src/User/src/Enum/UserStatusEnum.php b/src/Core/src/User/src/Enum/UserStatusEnum.php index ecaa696..8ab061a 100644 --- a/src/Core/src/User/src/Enum/UserStatusEnum.php +++ b/src/Core/src/User/src/Enum/UserStatusEnum.php @@ -35,7 +35,7 @@ public static function validValues(): array */ public static function validCases(): array { - return array_filter(self::cases(), fn (self $enum) => $enum !== self::Deleted); + return array_filter(self::cases(), static fn (self $enum) => $enum !== self::Deleted); } /** @@ -43,7 +43,7 @@ public static function validCases(): array */ public static function toArray(): array { - return array_reduce(self::validCases(), function (array $collector, self $enum): array { + return array_reduce(self::validCases(), static function (array $collector, self $enum): array { $collector[$enum->value] = $enum->name; return $collector; diff --git a/structarmed.php b/structarmed.php new file mode 100644 index 0000000..27db0ae --- /dev/null +++ b/structarmed.php @@ -0,0 +1,31 @@ +withPresets(Preset::PSR4(), Preset::CODEQUALITY()) + ->layerPattern('CoreApp', '/^Core\\\\App\\\\.*$/', '/^Core\\\\App\\\\Fixture\\\\.*$/') + ->layer('CoreFixture', 'src/Core/src/App/src/Fixture') + ->layer('CoreSetting', 'src/Core/src/Setting/src') + ->layer('CoreAdmin', 'src/Core/src/Admin/src') + ->layer('CoreSecurity', 'src/Core/src/Security/src') + ->layer('CoreUser', 'src/Core/src/User/src') + ->layer('App', 'src/App/src') + ->layer('Security', 'src/Security/src') + ->layer('Admin', 'src/Admin/src') + ->layer('User', 'src/User/src') + ->ruleset([ + 'CoreApp' => ['CoreUser'], + 'CoreSetting' => ['+CoreApp', 'CoreAdmin'], + 'CoreAdmin' => ['+CoreSetting'], + 'CoreSecurity' => ['+CoreAdmin', 'CoreUser'], + 'CoreUser' => ['+CoreSecurity'], + 'CoreFixture' => ['+CoreUser'], + 'App' => ['+CoreUser'], + 'Security' => ['+App'], + 'Admin' => ['+App'], + 'User' => ['+App'], + ]); diff --git a/test/Unit/App/Template/ParserTest.php b/test/Unit/App/Template/ParserTest.php index 53e2036..d93938b 100644 --- a/test/Unit/App/Template/ParserTest.php +++ b/test/Unit/App/Template/ParserTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace App\Template; +namespace ApiTest\Unit\App\Template; use Api\App\Template\Parser; use Api\App\Template\ParserInterface; diff --git a/test/Unit/App/Template/RendererTest.php b/test/Unit/App/Template/RendererTest.php index e2122f7..87d359f 100644 --- a/test/Unit/App/Template/RendererTest.php +++ b/test/Unit/App/Template/RendererTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace App\Template; +namespace ApiTest\Unit\App\Template; use Api\App\Exception\RuntimeException; use Api\App\Template\Parser; From 0d777282734a385b7ed4b45ac41404c24d22c8cd Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Wed, 23 Sep 2026 18:17:02 +0700 Subject: [PATCH 2/5] final touch: no need define coverage on structarmed ci Signed-off-by: Abdul Malik Ikhsan --- .github/workflows/static-analysis-structarmed.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/static-analysis-structarmed.yml b/.github/workflows/static-analysis-structarmed.yml index a12b6c8..2098857 100644 --- a/.github/workflows/static-analysis-structarmed.yml +++ b/.github/workflows/static-analysis-structarmed.yml @@ -27,7 +27,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: "${{ matrix.php }}" - coverage: pcov + coverage: none ini-values: assert.exception=1, zend.assertions=1, error_reporting=-1, log_errors_max_len=0, display_errors=On tools: composer:v2, cs2pr From 75842b5770b017d98e8351b73ce13d4358bc8cd4 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Wed, 23 Sep 2026 18:29:33 +0700 Subject: [PATCH 3/5] cs fix Signed-off-by: Abdul Malik Ikhsan --- src/App/src/Middleware/DeprecationMiddleware.php | 3 ++- src/Core/src/Admin/src/Entity/Admin.php | 5 ++++- src/Core/src/User/src/Entity/User.php | 5 ++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/App/src/Middleware/DeprecationMiddleware.php b/src/App/src/Middleware/DeprecationMiddleware.php index b153653..1f47851 100644 --- a/src/App/src/Middleware/DeprecationMiddleware.php +++ b/src/App/src/Middleware/DeprecationMiddleware.php @@ -86,7 +86,8 @@ private function getAttribute(array $attributes): ?array return array_values( array_filter( $attributes, - static fn (array $attribute): bool => $attribute['deprecationType'] === self::RESOURCE_DEPRECATION_ATTRIBUTE + static fn (array $attribute): bool + => $attribute['deprecationType'] === self::RESOURCE_DEPRECATION_ATTRIBUTE ) )[0] ?? null; } diff --git a/src/Core/src/Admin/src/Entity/Admin.php b/src/Core/src/Admin/src/Entity/Admin.php index 4bb7069..9692c92 100644 --- a/src/Core/src/Admin/src/Entity/Admin.php +++ b/src/Core/src/Admin/src/Entity/Admin.php @@ -241,7 +241,10 @@ public function getArrayCopy(): array 'firstName' => $this->firstName, 'lastName' => $this->lastName, 'status' => $this->status->value, - 'roles' => array_map(static fn (RoleInterface $role): array => $role->getArrayCopy(), $this->roles->toArray()), + 'roles' => array_map( + static fn (RoleInterface $role): array => $role->getArrayCopy(), + $this->roles->toArray() + ), 'created' => $this->created, 'updated' => $this->updated, ]; diff --git a/src/Core/src/User/src/Entity/User.php b/src/Core/src/User/src/Entity/User.php index 7b29190..ece0b09 100644 --- a/src/Core/src/User/src/Entity/User.php +++ b/src/Core/src/User/src/Entity/User.php @@ -361,7 +361,10 @@ public function getArrayCopy(): array 'hash' => $this->hash, 'identity' => $this->identity, 'status' => $this->status->value, - 'roles' => array_map(static fn (RoleInterface $role): array => $role->getArrayCopy(), $this->roles->toArray()), + 'roles' => array_map( + static fn (RoleInterface $role): array => $role->getArrayCopy(), + $this->roles->toArray() + ), 'created' => $this->created, 'updated' => $this->updated, ]; From 280be417755f6629a8d037481d1afeabcb42b0b5 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Wed, 23 Sep 2026 18:37:32 +0700 Subject: [PATCH 4/5] clean up config: already on laminas continues integration 1.x Signed-off-by: Abdul Malik Ikhsan --- .../workflows/static-analysis-structarmed.yml | 49 ------------------- README.md | 1 - 2 files changed, 50 deletions(-) delete mode 100644 .github/workflows/static-analysis-structarmed.yml diff --git a/.github/workflows/static-analysis-structarmed.yml b/.github/workflows/static-analysis-structarmed.yml deleted file mode 100644 index 2098857..0000000 --- a/.github/workflows/static-analysis-structarmed.yml +++ /dev/null @@ -1,49 +0,0 @@ -on: - - push - -name: Run StructArmed checks - -jobs: - mutation: - name: StructArmed ${{ matrix.php }}-${{ matrix.os }} - - runs-on: ${{ matrix.os }} - - strategy: - matrix: - os: - - ubuntu-latest - - php: - - "8.3" - - "8.4" - - "8.5" - - steps: - - name: Checkout - uses: actions/checkout@v7 - - - name: Install PHP - uses: shivammathur/setup-php@v2 - with: - php-version: "${{ matrix.php }}" - coverage: none - 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@v6 - 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: Run static analysis with StructArmed - run: vendor/bin/structarmed analyze diff --git a/README.md b/README.md index 173e148..3abf56f 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,6 @@ Check out the full documentation available at https://docs.dotkernel.org/api-doc [![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=7.0)](https://github.com/dotkernel/api/actions/workflows/qodana_code_quality.yml) [![PHPStan](https://github.com/dotkernel/api/actions/workflows/static-analysis.yml/badge.svg?branch=7.0)](https://github.com/dotkernel/api/actions/workflows/static-analysis.yml) -[![StructArmed](https://github.com/dotkernel/api/actions/workflows/static-analysis-structarmed.yml/badge.svg?branch=7.0)](https://github.com/dotkernel/api/actions/workflows/static-analysis-structarmed.yml) ## Getting Started From 09c50172d62a4a50c2e44c7252f7e88839394e70 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Wed, 23 Sep 2026 18:57:59 +0700 Subject: [PATCH 5/5] final touch: clean up ruleset inheritance expansion Signed-off-by: Abdul Malik Ikhsan --- structarmed.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/structarmed.php b/structarmed.php index 27db0ae..dbbfc4e 100644 --- a/structarmed.php +++ b/structarmed.php @@ -21,7 +21,7 @@ 'CoreApp' => ['CoreUser'], 'CoreSetting' => ['+CoreApp', 'CoreAdmin'], 'CoreAdmin' => ['+CoreSetting'], - 'CoreSecurity' => ['+CoreAdmin', 'CoreUser'], + 'CoreSecurity' => ['+CoreAdmin'], 'CoreUser' => ['+CoreSecurity'], 'CoreFixture' => ['+CoreUser'], 'App' => ['+CoreUser'],