Skip to content

Released: StructArmed 0.16.0

Choose a tag to compare

@samsonasik samsonasik released this 20 Aug 02:05
· 599 commits to main since this release
0.16.0
d0334fb

ci build PHPStan

This release introduces 2 new interfaces:

  • Boundwize\StructArmed\Rule\UsedInterfaceAwareRuleInterface
  • Boundwize\StructArmed\Rule\UsedTraitAwareRuleInterface

They complete the existing Boundwize\StructArmed\Rule\ExtendedClassAwareRuleInterface, which was introduced in 0.15.0.

These interfaces are used by the new YAGNI preset to detect abstractions that are not actually used by the scanned codebase.

The preset checks for:

  • unused interfaces: Boundwize\StructArmed\Rule\Rules\Class_\MustBeUsedInterfaceRule
  • unused abstract classes: Boundwize\StructArmed\Rule\Rules\Class_\MustBeInterfaceRule
  • unused traits: Boundwize\StructArmed\Rule\Rules\Class_\MustBeUsedTraitRule
  • concrete classes that are extended but never instantiated: Boundwize\StructArmed\Rule\Rules\Class_\ExtendedClassMustBeAbstractOrInstantiatedRule

For the last case, the class can usually be made abstract.

-class SomeClassOnlyExtended
+abstract class SomeClassOnlyExtended

Usage detection also takes references into account, including type hints, instanceof, ::class, static calls, and class-name strings.

All YAGNI rules support --fix. Unused declarations can be removed, while classes that are only extended can be changed to abstract.

You can define YAGNI preset:

<?php

declare(strict_types=1);

use Boundwize\StructArmed\Architecture;
use Boundwize\StructArmed\Preset\Preset;
use Boundwize\StructArmed\Preset\Presets\Psr1Preset;
use Boundwize\StructArmed\Rule\Rules\Class_\MustBeFinalRule;

return Architecture::define()
     // ... other config ...
-    ->withPresets(Preset::PSR4(), Preset::PSR15());
+    ->withPresets(Preset::PSR4(), Preset::PSR15(), Preset::YAGNI());

Using The New Interfaces In A Custom Rule

The new interfaces are marker interfaces that custom rules can implement instead of the plain Boundwize\StructArmed\Rule\RuleInterface (each marker extends it, so no other change is needed). Collecting usage information costs extra analysis time, so the analyser only computes it when an active rule declares that it needs it via one of these markers:

Marker interface Flags populated on ClassNode
Boundwize\StructArmed\Rule\ExtendedClassAwareRuleInterface $isExtended, $isReferenced, $isInstantiated
Boundwize\StructArmed\Rule\UsedInterfaceAwareRuleInterface $isImplemented, $isReferenced
Boundwize\StructArmed\Rule\UsedTraitAwareRuleInterface $isReferenced

Without a matching marker on at least one active rule, the corresponding flags keep their default false.

For example, a custom rule that requires every interface in a Contracts layer to actually be implemented or referenced:

<?php

namespace App\Architecture\Rules;

use Boundwize\StructArmed\Analyser\ClassNode;
use Boundwize\StructArmed\Rule\RuleViolation;
use Boundwize\StructArmed\Rule\UsedInterfaceAwareRuleInterface;

use function sprintf;

final readonly class ContractMustBeImplementedRule implements UsedInterfaceAwareRuleInterface
{
    public function appliesTo(ClassNode $classNode): bool
    {
        return $classNode->isInterface
            && $classNode->isInLayer('Contracts');
    }

    public function evaluate(ClassNode $classNode): ?RuleViolation
    {
        if ($classNode->isImplemented || $classNode->isReferenced) {
            return null;
        }

        return new RuleViolation(
            message:   sprintf('Contract [%s] must be implemented or referenced', $classNode->className),
            file:      $classNode->file,
            line:      $classNode->line,
            className: $classNode->className,
            layer:     $classNode->layer,
        );
    }
}

The built-in YAGNI preset rules follow this same pattern: MustBeUsedInterfaceRule implements UsedInterfaceAwareRuleInterface, MustBeUsedTraitRule implements UsedTraitAwareRuleInterface, and MustBeUsedAbstractClassRule and ExtendedClassMustBeAbstractOrInstantiatedRule implement ExtendedClassAwareRuleInterface.

See the Reading Usage Flags In A Custom Rule documentation for more details.

YAGNI only knows about usage inside the scanned paths. If a library intentionally exposes interfaces or extension points for external consumers, the relevant rules or paths should be skipped.

What's Changed

Full Changelog: 0.15.44...0.16.0