Skip to content

Releases: boundwize/structarmed

Released: StructArmed 0.17.9

Choose a tag to compare

@samsonasik samsonasik released this 16 Sep 10:23
0.17.9
1e536cd

ci build PHPStan

What's Changed

  • refactor: Merge loadAnalysisNodesWithFileAnalysis into loadAnalysisNodes via $withFileAnalysis flag by @samsonasik in #440
  • perf: iterate pending workers directly during polling by @samsonasik in #441
  • perf: reuse collector's declared function names in FileAnalysisProvider by @samsonasik in #442
  • fix: Handle missing files gracefully in FileHashProvider by @samsonasik in #443

Full Changelog: 0.17.8...0.17.9

Released: StructArmed 0.17.8

Choose a tag to compare

@samsonasik samsonasik released this 15 Sep 15:05
0.17.8
052fdbe

ci build PHPStan

What's Changed

  • fix: Resolve same-file function calls only against unconditionally declared functions by @samsonasik in #439

Full Changelog: 0.17.7...0.17.8

Released: StructArmed 0.17.7

Choose a tag to compare

@samsonasik samsonasik released this 15 Sep 10:12
0.17.7
882e8da

ci build PHPStan

What's Changed

  • fix: Resolve names before PSR-1 file-state analysis, reusing the extractor's AST by @samsonasik in #438

Full Changelog: 0.17.6...0.17.7

Released: StructArmed 0.17.6

Choose a tag to compare

@samsonasik samsonasik released this 14 Sep 15:42
0.17.6
a306ac9

ci build PHPStan

What's Changed

  • perf: Optimize object-bound anonymous-function detection hot path by @samsonasik in #433
  • [docs] Update documentation about usage of ExtendedClassAwareRuleInterface by @samsonasik in #435
  • perf: Skip file path in analysis node cache metadata by @samsonasik in #436
  • fix: Resolve same-namespace function calls case-insensitively by @samsonasik in #437

Full Changelog: 0.17.5...0.17.6

Released: StructArmed 0.17.5

Choose a tag to compare

@samsonasik samsonasik released this 13 Sep 19:24
0.17.5
37bb9d7

ci build PHPStan

What's Changed

  • perf: Optimize baseline generation and filtering performance by @samsonasik in #431
  • fix: Prevent static conversion of object-bound anonymous functions by @samsonasik in #432

Full Changelog: 0.17.4...0.17.5

Released: StructArmed 0.17.4

Choose a tag to compare

@samsonasik samsonasik released this 13 Sep 13:57
0.17.4
fab65dc

ci build PHPStan

What's Changed

  • chore: Bump nikic/php-parser version to ^5.8 by @samsonasik in #429
  • fix: Strip base path from baseline messages that spell the file path differently by @samsonasik in #430

Full Changelog: 0.17.3...0.17.4

Released: StructArmed 0.17.3

Choose a tag to compare

@samsonasik samsonasik released this 10 Sep 17:45
0.17.3
b6a6876

ci build PHPStan

What's Changed

  • refactor: Extract shared nameEndsWith()/nameStartsWith() into NameQueryTrait for ClassNode and FunctionNode by @samsonasik in #424
  • chore: Refine logo by @samsonasik in #426
  • docs: Improve inline code colors in documentation by @samsonasik in #427
  • Fix rule-specific glob skips to include matching directories’ descendants by @samsonasik in #428

Full Changelog: 0.17.2...0.17.3

Released: StructArmed 0.17.2

Choose a tag to compare

@samsonasik samsonasik released this 07 Sep 14:35
0.17.2
4c9b8d1

ci build PHPStan

What's Changed

  • refactor: Pair each class-like with its analysis instead of keying by spl_object_id() by @samsonasik in #421
  • [yagni] fix: Skip PHPUnit with suffix Test classes in ExtendedClassMustBeAbstractOrInstantiatedRule by @samsonasik in #422
  • Fix stray 0 in test print by @samsonasik in #423

Full Changelog: 0.17.1...0.17.2

Released: StructArmed 0.17.1

Choose a tag to compare

@samsonasik samsonasik released this 06 Sep 06:13
0.17.1
b5bea9d

ci build PHPStan

What's Changed

  • perf: Compact FileAnalysis cache scalars into a positional list by @samsonasik in #414
  • refactor: simplify worker payload merge in ParallelAnalysisNodeExtractor with array_push() by @samsonasik in #417
  • perf: Split cache hydration across parallel workers by @samsonasik in #419

Full Changelog: 0.17.0...0.17.1

Released: StructArmed 0.17.0

Choose a tag to compare

@samsonasik samsonasik released this 04 Sep 02:28
0.17.0
5536c6d

ci build PHPStan

StructArmed 0.17.0 expands architecture analysis beyond named classes.

This release introduces dedicated analysis nodes and rule interfaces for:

  • named functions;
  • closures and arrow functions;
  • anonymous classes.

It also introduces the new PER Coding Style and Code Quality presets, expands the MVC and DDD presets, adds several fixable coding-style rules.

New Rule Interfaces

Three new interfaces allow custom rules to target a specific kind of PHP declaration:

  • Boundwize\StructArmed\Rule\FunctionRuleInterface
  • Boundwize\StructArmed\Rule\AnonymousFunctionRuleInterface
  • Boundwize\StructArmed\Rule\AnonymousClassRuleInterface

Each interface uses the same appliesTo() and evaluate() method names as the existing RuleInterface, but receives a node containing information specific to that declaration type.

Interface Node Analyses
FunctionRuleInterface FunctionNode Named functions
AnonymousFunctionRuleInterface AnonymousFunctionNode Closures and arrow functions
AnonymousClassRuleInterface AnonymousClassNode Anonymous classes

These nodes expose information such as their source file, line, layer, dependencies, function calls, superglobal access, language constructs, parameters, return types, complexity, and line count.

Anonymous-function nodes additionally report whether the declaration:

  • is a closure or arrow function;
  • is already static;
  • accesses $this;
  • belongs to a named class or function.

Anonymous-class nodes include:

  • their extended class and implemented interfaces;
  • traits and members;
  • transitive parent classes and interfaces;
  • constructor parameter count;
  • readonly status;
  • whether empty constructor parentheses were written.

Named Function Rules

The new MustHaveReturnTypeFunctionRule requires named functions in a configured layer to declare a return type.

It is enabled for the MVC preset's Helper layer.

-function format_price(int $amount)
+function format_price(int $amount): string
 {
     return number_format($amount);
 }

This complements the existing method return-type rules: standalone helper functions can now be checked independently from class methods.

Closures And Arrow Functions

The new MustBeStaticAnonymousFunctionRule detects closures and arrow functions that do not access $this but have not been declared static.

-$activeUsers = array_filter($users, function (User $user): bool {
+$activeUsers = array_filter($users, static function (User $user): bool {
     return $user->isActive();
 });

Arrow functions are supported as well:

-$ids = array_map(fn (User $user): int => $user->id, $users);
+$ids = array_map(static fn (User $user): int => $user->id, $users);

Closures that read $this, directly or through a nested closure, are skipped because PHP does not allow $this inside a static closure.

This rule supports --fix.

Anonymous Class Analysis

Anonymous classes now have their own AnonymousClassNode representation and rule interface.

Their class members, dependencies, traits, readonly status, and parent hierarchy are collected just like those of named classes. Consequently, methods such as extendsClass() and implementsInterface() work across direct and transitive parents.

The new fixable AnonymousClassMayNotHaveEmptyParenthesesRule implements the PER convention that an anonymous class passing no constructor arguments should omit empty parentheses:

-$handler = new class () implements Handler {
+$handler = new class implements Handler {
     public function handle(): void
     {
     }
 };

Parentheses containing actual constructor arguments are unaffected.

New PER Coding Style Preset

The new Preset::PER() implements additional rules from the PER Coding Style and includes the existing PSR-12 rules.

Enable it in structarmed.php:

 return Architecture::define()
-    ->withPresets(Preset::PSR4(), Preset::PSR12());
+    ->withPreset(Preset::PER());

In addition to PSR-12, the PER preset checks the following conventions.

Enum Cases Must Use PascalCase

 enum OrderStatus
 {
-    case pending_payment;
+    case PendingPayment;
 }

Enum Methods May Not Be Protected

Enums cannot be extended, so protected methods should be private:

 enum OrderStatus
 {
-    protected function label(): string
+    private function label(): string
     {
         return $this->name;
     }
 }

Enum Constants May Not Be Protected

 enum OrderStatus
 {
-    protected const DEFAULT_LABEL = 'Unknown';
+    private const DEFAULT_LABEL = 'Unknown';
 }

Anonymous Classes May Not Have Empty Parentheses

-$object = new class () {};
+$object = new class {};

The enum visibility and anonymous-class-parentheses rules support --fix.

Lowercase PHP Keyword Constants

The PSR-12 preset now includes the fixable MustUseLowercaseKeywordConstantRule.

It requires the PHP keyword constants true, false, and null to use their canonical lowercase spelling:

-$enabled = TRUE;
-$disabled = FALSE;
-$value = NULL;
+$enabled = true;
+$disabled = false;
+$value = null;

Only the spelling is changed. For example, a fully qualified \TRUE becomes \true.

New Code Quality Preset

The new Preset::CODEQUALITY() provides readability rules that are independent of a particular architecture style or coding standard.

Enable it alongside other presets:

 return Architecture::define()
     ->withPresets(
         Preset::DDD(),
+        Preset::CODEQUALITY(),
     );

Anonymous Functions Must Be Static

Closures and arrow functions that do not use $this must be declared static.

-$names = array_map(fn (User $user) => $user->name, $users);
+$names = array_map(static fn (User $user) => $user->name, $users);

Declaring these functions static makes it explicit that they do not capture the enclosing object.

Large Numeric Literals Must Use Separators

Plain decimal numeric literals of at least 1_000_000 must group their digits using _ separators:

-$maximumUploadSize = 10000000;
+$maximumUploadSize = 10_000_000;

Decimal fractions retain their fractional portion:

-$amount = 1000500.75;
+$amount = 1_000_500.75;

The default threshold can be customized by replacing the preset rule:

<?php

use Boundwize\StructArmed\Architecture;
use Boundwize\StructArmed\Preset\Preset;
use Boundwize\StructArmed\Preset\Presets\CodeQualityPreset;
use Boundwize\StructArmed\Rule\Rules\File\LargeNumericLiteralMustUseSeparatorRule;

return Architecture::define()
    ->withPreset(Preset::CODEQUALITY())
    ->replaceRule(
        CodeQualityPreset::LARGE_NUMERIC_LITERALS_MUST_USE_SEPARATOR,
        new LargeNumericLiteralMustUseSeparatorRule(minimum: 1_000),
    );

Both Code Quality rules support --fix.

DDD Preset: Prevent Infrastructure Inheritance

The new MayNotExtendClassRule prevents classes in a layer from extending a configured class, either directly or through a parent class.

The DDD preset uses it to prevent Domain classes from extending Doctrine's infrastructure-oriented EntityRepository:

 namespace App\Domain\Repository;

-use Doctrine\ORM\EntityRepository;
-
-final class OrderRepository extends EntityRepository
+interface OrderRepository
 {
 }

A custom rule can enforce the same boundary for another framework base class:

use Boundwize\StructArmed\Rule\Rules\Class_\MayNotExtendClassRule;

return Architecture::define()
    ->layer('Domain', 'src/Domain/')
    ->rule(
        'domain.must_not_extend_eloquent_model',
        new MayNotExtendClassRule(
            layer: 'Domain',
            class: 'Illuminate\Database\Eloquent\Model',
        ),
    );

Writing A Custom Function Rule

For example, the following rule prevents named functions in the Domain layer from reading PHP superglobals:

<?php

namespace App\Architecture\Rules;

use Boundwize\StructArmed\Analyser\FunctionNode;
use Boundwize\StructArmed\Rule\FunctionRuleInterface;
use Boundwize\StructArmed\Rule\RuleViolation;

use function sprintf;

final readonly class FunctionsMustNotAccessSuperglobalsRule implements FunctionRuleInterface
{
    public function appliesTo(FunctionNode $functionNode): bool
    {
        return $functionNode->isInLayer('Domain');
    }

    public function evaluate(FunctionNode $functionNode): ?RuleViolation
    {
        if (! $functionNode->accessesSuperglobals()) {
            return null;
        }

        return new RuleViolation(
            message: sprintf(
                'Function [%s()] must not access superglobals',
                $functionNode->functionName,
            ),
            file:         $functionNode->file,
            line:         $functionNode->line,
            className:    $functionNode->functionName,
            layer:        $functionNode->layer,
            functionName: $functionNode->functionName,
        );
    }
}

Register it like any other rule:

return Architecture::define()
    ->layer('Domain', 'src/Domain/')
    ->rule(
        'domain.functions_must_not_access_superglobals',
        new FunctionsMustNotAccessSuperglobalsRule(),
    );
...
Read more