NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
Packagist · #2109 most downloaded on Packagist
Configurable PHP architecture guards — define your layers and rules, then keep them enforced
Last release 4 days ago
04 Oct 2026
Ships on a steady schedule
a new release about every 9 days
Most releases are documented
notes for 50 of the last 60 stable releases
Nothing withdrawn
no release was ever pulled
5 months old
225 releases · first in 2026
One column per month.
Reduce temporary memory usage when grouping analysis cache nodes by @samsonasik in #493
Full Changelog: 0.18.10...0.18.11
Released: StructArmed 0.18.11 Latest
Latest
Compare
Improve analyser performance by reducing repeated function name normalization by @samsonasik in #491
Full Changelog: 0.18.9...0.18.10
perf: Optimize ruleset expansion and simplify dependency checks by @samsonasik in #489
Full Changelog: 0.18.8...0.18.9
Fix MayNotUseClassRule to match class names case insensitively by @samsonasik in #485
Full Changelog: 0.18.7...0.18.8
perf: Preserve function analysis performance while retaining namespace fallback fixes by @samsonasik in #483
Full Changelog: 0.18.6...0.18.7
Fix MayNotCallFunctionRule missing unqualified calls to same namespace functions declared in another file by @samsonasik in #482
Full Changelog: 0.18.5...0.18.6
Fix conditional namespaced function suppressing global function fallback usage by @samsonasik in #481
Full Changelog: 0.18.4...0.18.5
fix: Resolve unqualified function calls without short name collisions by @samsonasik in #480
Full Changelog: 0.18.3...0.18.4
fix: Recognise global function fallback in namespaced top level calls by @samsonasik in #479
Full Changelog: 0.18.2...0.18.3
perf: Clean up redundant call type checks in AnalysisNodeCollector by @samsonasik in #470
Full Changelog: 0.18.1...0.18.2
perf: Skip garbage collection during analyse() by @samsonasik in #466
Full Changelog: 0.18.0...0.18.1
StructArmed 0.18.0 improves CI integration and expands YAGNI analysis to named functions.
StructArmed 0.18.0 improves CI integration and expands YAGNI analysis to named functions.
This release introduces:
--report=github;Architecture::layer();Boundwize\StructArmed\Rule\UsedFunctionAwareRuleInterface and the new fixable MustBeUsedFunctionRule.StructArmed can now report violations directly as GitHub Actions annotations:
vendor/bin/structarmed analyse --report=githubEach violation is emitted as a GitHub workflow error annotation, so violations appear directly on affected files and lines in pull requests.
The regular console report is appended after the annotations, so View details still opens a readable StructArmed report in the job log.
For example:
- name: Run StructArmed
run: vendor/bin/structarmed analyse --report=githubThe GitHub report intentionally exits with 0, allowing violations to be surfaced as annotations without failing the job itself.
Use the regular console or json report when violations should fail the job.
The github and json reports also disable the progress bar automatically.
Architecture::layer() now supports excluding one or more nested paths from a layer.
For example:
return Architecture::define()
->layer(
'Logger',
'src/Logger/',
excludePath: 'src/Logger/Factory/',
)
->layer('Factory', 'src/Logger/Factory/');Files under src/Logger/Factory/ now belong to Factory without also belonging to Logger.
Multiple paths can be excluded:
->layer(
'Logger',
'src/Logger/',
excludePath: [
'src/Logger/Factory/',
'src/Logger/Formatter/',
],
)The exclusion only applies to that particular layer. A file may still resolve to another matching path layer, namespace-pattern layer, or the automatically registered Source layer.
This mirrors the existing exclusion support in layerPattern() and makes nested architecture boundaries easier to express without changing the directory structure.
The YAGNI preset can now detect unused named functions.
A new rule:
MustBeUsedFunctionRulechecks whether a function is called or otherwise referenced within the scanned code.
For example:
-function legacy_helper(): string
-{
- return 'unused';
-}The rule understands usages including:
'App\helper';A function that only recursively calls itself still counts as unused.
MustBeUsedFunctionRule supports --fix, removing the unused function automatically. It can also clean up an empty surrounding if (! function_exists(...)) guard and remove the file when only boilerplate remains.
The rule is enabled automatically by:
Preset::YAGNI()As with the other YAGNI rules, only usages inside the scanned paths are known. Functions intended as public extension points for external consumers can be excluded with skipRule() or skip paths.
Custom function rules can opt into function-usage analysis through the new:
Boundwize\StructArmed\Rule\UsedFunctionAwareRuleInterfaceIt extends FunctionRuleInterface and makes the usage state available through:
$functionNode->isReferencedFor example:
final readonly class CustomRule implements UsedFunctionAwareRuleInterface
{
public function appliesTo(FunctionNode $functionNode): bool
{
return $functionNode->isInLayer('Source');
}
public function evaluate(FunctionNode $functionNode): ?RuleViolation
{
if ($functionNode->isReferenced) {
return null;
}
// ...
}
}Usage analysis is opt-in: StructArmed performs the additional work only when at least one active rule implements UsedFunctionAwareRuleInterface.
The PHPUnit extension is now easier to control in CI.
Set:
STRUCTARMED_DISABLED=1 vendor/bin/phpunitStructArmed remains configured in phpunit.xml, but the extension skips architecture analysis for that run.
This is useful when StructArmed already runs as a dedicated CI step.
PHPUnit's own --no-progress now also disables the StructArmed progress bar:
vendor/bin/phpunit --no-progressTo disable only StructArmed's progress output while preserving PHPUnit's progress display:
<extensions>
<bootstrap class="Boundwize\StructArmed\PHPUnit\StructArmedExtension">
<parameter name="progress" value="false"/>
</bootstrap>
</extensions>StructArmed now requires:
"nikic/php-parser": "^5.9"--report=github in #445^5.9 in #448excludePath in Architecture::layer() in #450STRUCTARMED_DISABLED env to disable the PHPUnit extension in #459UsedFunctionAwareRuleInterface and MustBeUsedFunctionRule for YAGNI preset in #462Full Changelog: 0.17.12...0.18.0
Full Changelog: 0.17.12...0.18.0
perf: Merge same layer check into allowed layers loop by @samsonasik in #455
Full Changelog: 0.17.11...0.17.12
Fix enum protected-to-private fixer producing invalid "final private" members by @samsonasik in #453
Full Changelog: 0.17.10...0.17.11
fix: Prefer specific layer over equally specific Source layer in NamespaceLayerResolver by @samsonasik in #449
Full Changelog: 0.17.9...0.17.10
refactor: Merge loadAnalysisNodesWithFileAnalysis into loadAnalysisNodes via $withFileAnalysis flag by @samsonasik in #440
Full Changelog: 0.17.8...0.17.9
fix: Resolve same-file function calls only against unconditionally declared functions by @samsonasik in #439
Full Changelog: 0.17.7...0.17.8
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
perf: Optimize object-bound anonymous-function detection hot path by @samsonasik in #433
Full Changelog: 0.17.5...0.17.6
perf: Optimize baseline generation and filtering performance by @samsonasik in #431
Full Changelog: 0.17.4...0.17.5
chore: Bump nikic/php-parser version to ^5.8 by @samsonasik in #429
Full Changelog: 0.17.3...0.17.4
refactor: Extract shared nameEndsWith()/nameStartsWith() into NameQueryTrait for ClassNode and FunctionNode by @samsonasik in #424
Full Changelog: 0.17.2...0.17.3
refactor: Pair each class-like with its analysis instead of keying by spl_object_id() by @samsonasik in #421
Full Changelog: 0.17.1...0.17.2
perf: Compact FileAnalysis cache scalars into a positional list by @samsonasik in #414
Full Changelog: 0.17.0...0.17.1
StructArmed 0.17.0 expands architecture analysis beyond named classes.
StructArmed 0.17.0 expands architecture analysis beyond named classes.
This release introduces dedicated analysis nodes and rule interfaces for:
It also introduces the new PER Coding Style and Code Quality presets, expands the MVC and DDD presets, adds several fixable coding-style rules.
Three new interfaces allow custom rules to target a specific kind of PHP declaration:
Boundwize\StructArmed\Rule\FunctionRuleInterfaceBoundwize\StructArmed\Rule\AnonymousFunctionRuleInterfaceBoundwize\StructArmed\Rule\AnonymousClassRuleInterfaceEach 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:
static;$this;Anonymous-class nodes include:
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.
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 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.
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 OrderStatus
{
- case pending_payment;
+ case PendingPayment;
}Enums cannot be extended, so protected methods should be private:
enum OrderStatus
{
- protected function label(): string
+ private function label(): string
{
return $this->name;
}
} enum OrderStatus
{
- protected const DEFAULT_LABEL = 'Unknown';
+ private const DEFAULT_LABEL = 'Unknown';
}-$object = new class () {};
+$object = new class {};The enum visibility and anonymous-class-parentheses rules support --fix.
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.
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(),
);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.
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.
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',
),
);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(),
);Global skip paths, rule-scoped skip() paths, and skipRule() also apply to function, anonymous-function, and anonymous-class rules.
Layer-aware rules now extend AbstractLayerAwareRule instead of implementing LayerAwareRuleInterface.
-use Boundwize\StructArmed\Rule\LayerAwareRuleInterface;
+use Boundwize\StructArmed\Rule\AbstractLayerAwareRule;
-final class DomainDependencyRule implements RuleInterface, LayerAwareRuleInterface
+final class DomainDependencyRule extends AbstractLayerAwareRule implements RuleInterface
{
}The base class provides the class-node map injection and getDependencyNode() lookup used to inspect the layer of another scanned class.
Token-aware fixer visitors similarly use AbstractTokenAwareVisitor instead of TokenAwareVisitorInterface.
Several internal extractor and parallel-worker classes were also renamed from ClassNode* to AnalysisNode* to reflect their expanded support for functions and anonymous classes.
Full Changelog: 0.16.31...0.17.0
Fix: normalise reported composer.json path in PSR-4 rules for consistent violation display on Windows by @samsonasik in #381
Full Changelog: 0.16.30...0.16.31
dx: use 💡 icon in Hint for --fix usage on ConsoleReport by @samsonasik in #377
Full Changelog: 0.16.29...0.16.30
perf: Remove fflush call from WorkerProgressHandler::advance() method by @samsonasik in #372
Full Changelog: 0.16.28...0.16.29
perf: Reduce redundant key-scan passes when decoding cached nodes in AnalysisResultCache by @samsonasik in #362
Full Changelog: 0.16.27...0.16.28
perf: Single-pass source path matching in PhpFileFinder::filesFromScope() by @samsonasik in #361
Full Changelog: 0.16.26...0.16.27
chore: Clean up no longer used method sourcePathsFor() on Psr4SourcePathsRule by @samsonasik in #357
Full Changelog: 0.16.25...0.16.26
chore: clean up rector skip config by @samsonasik in #354
Full Changelog: 0.16.24...0.16.25
perf: collect deferred instantiation metadata in a single pass by @samsonasik in #353
Full Changelog: 0.16.23...0.16.24
refactor: Separate file discovery from cached file analysis by @samsonasik in #352
Full Changelog: 0.16.22...0.16.23
perf: Reduce redundant skip-path checks in Analyser by @samsonasik in #351
Full Changelog: 0.16.21...0.16.22
fix: avoid symlink file collection by @samsonasik in #350
Full Changelog: 0.16.20...0.16.21
perf: avoid redundant canonicalised path in Analyser by @samsonasik in #347
Full Changelog: 0.16.19...0.16.20
fix: skip Enum on ClassConstantNameMustBeUpperCaseRule by @samsonasik in #345
Full Changelog: 0.16.18...0.16.19
[test] Add more tests on ArchitectureTest by @samsonasik in #342
Full Changelog: 0.16.17...0.16.18
fix: Avoid Source layer defined via layerPattern() by @samsonasik in #339
Full Changelog: 0.16.16...0.16.17
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Fix skipRule() treating unregistered rule keys as skip paths by @samsonasik in #330
Full Changelog: 0.16.10...0.16.11
Released: StructArmed 0.16.11 Latest
Latest
Compare
Fix cyclomatic complexity counting for NullsafePropertyFetch by @samsonasik in #329
Full Changelog: 0.16.9...0.16.10
Fix cyclomatic complexity counting for null coalescing operators by @samsonasik in #328
Full Changelog: 0.16.8...0.16.9
fix: Auto-resolve "Source" layer from composer PSR-4 paths when no Source layer is defined by @samsonasik in #326
Full Changelog: 0.16.7...0.16.8
fix: Recognize leading-backslash class-name strings (eg: '\App\Contract') as references and instantiations by @samsonasik in #325
Full Changelog: 0.16.6...0.16.7
fix: Report exit code and stderr when a parallel worker dies before writing its payload by @samsonasik in #324
Full Changelog: 0.16.5...0.16.6
Fix new self() / new static() instantiation tracking to follow PHP binding in traits and class hierarchies by @samsonasik in #323
Full Changelog: 0.16.4...0.16.5
fix: Resolve new parent() in traits as instantiation of the using class's parent when trait is used by @samsonasik in #322
Full Changelog: 0.16.3...0.16.4
Add 'yagni' to the list of composer.json keywords by @samsonasik in #317
Full Changelog: 0.16.2...0.16.3
perf: Reorder String_ checks to skip strtolower on non-class-like strings on ClassCollector by @samsonasik in #315
Full Changelog: 0.16.1...0.16.2
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Your coding agent can read these notes before it upgrades. Set up the MCP server →