Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion docs/available-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,14 @@ Namespace: `Boundwize\StructArmed\Rule\Rules\Class_`.
| `ClassNameMustBeStudlyCapsRule` | `new ClassNameMustBeStudlyCapsRule(layer: 'Source')` | Class names use StudlyCaps. |
| `ClassNameMustHaveSuffixRule` | `new ClassNameMustHaveSuffixRule(layer: 'Controller', suffix: 'Controller')` | Classes in a layer have the required suffix. |
| `ClassNameMustNotHavePrefixRule` | `new ClassNameMustNotHavePrefixRule(layer: 'Model', prefix: 'Model')` | Classes in a layer do not use a forbidden prefix. |
| `ExtendedClassMustBeAbstractOrInstantiatedRule` | `new ExtendedClassMustBeAbstractOrInstantiatedRule(layer: 'Source')` | Classes another scanned class extends are declared `abstract` unless they are also instantiated (`new X`, a `new self`/`new static`/`new parent` resolving to them, a constant class expression such as `new (X::class)` or `new ('App\X')`, or a chained `(new ReflectionClass(X::class))->newInstance*()`). Type hints, `instanceof`, and `::class` keep working on an abstract class, so they do not count. Runtime-fed construction (`new $class` from a parameter, `unserialize()`, container factories) is outside the scanned-code boundary — exclude such factories' targets with rule-scoped `skip()` or `skipRule()`. Supports `--fix` by adding the `abstract` modifier. |
| `MaxDependencyCountRule` | `new MaxDependencyCountRule(layer: 'Controller', maxCount: 5)` | Constructor dependency count stays below the configured limit. |
| `MayNotImplementInterfaceRule` | `new MayNotImplementInterfaceRule(layer: 'Domain', interface: JsonSerializable::class)` | Classes in a layer do not implement a forbidden interface. |
| `MustBeFinalRule` | `new MustBeFinalRule(layer: 'Domain', classNamePattern: '/Entity$/')` | Matching classes in a layer are declared `final`. Classes extended by another scanned class are skipped (making them `final` would break the child). Supports `--fix`. |
| `MustBeUsedInterfaceRule` | `new MustBeUsedInterfaceRule(layer: 'Source')` | Interfaces are implemented by a scanned class (directly or through inheritance), extended by another scanned interface, or referenced as a dependency (type hint, `instanceof`, `::class`, a class-name string, ...). Supports `--fix` by removing the unused interface (and deleting its file when only boilerplate remains). |
| `MustBeInterfaceRule` | `new MustBeInterfaceRule(layer: 'Contract', classNamePattern: '/Interface$/')` | Matching declarations in a layer are interfaces. |
| `MustBeUsedAbstractClassRule` | `new MustBeUsedAbstractClassRule(layer: 'Source')` | Abstract classes are extended by a scanned class or referenced as a dependency (type hint, `instanceof`, `::class`, static call, a class-name string, ...). Supports `--fix` by removing the unused abstract class (and deleting its file when only boilerplate remains). |
| `MustBeUsedTraitRule` | `new MustBeUsedTraitRule(layer: 'Source')` | Traits are used by a scanned class, trait, or enum, or referenced as a dependency (`::class`, static call, a class-name string, ...). Supports `--fix` by removing the unused trait (and deleting its file when only boilerplate remains). |
| `MustDeclareConstantVisibilityRule` | `new MustDeclareConstantVisibilityRule(layer: 'Source')` | Class constants declare `public`, `protected`, or `private`. Supports `--fix`. |
| `MustDeclareMethodVisibilityRule` | `new MustDeclareMethodVisibilityRule(layer: 'Source')` | Methods declare `public`, `protected`, or `private`. Supports `--fix`. |
| `MustDeclarePropertyVisibilityRule` | `new MustDeclarePropertyVisibilityRule(layer: 'Source')` | Properties declare `public`, `protected`, or `private`. Supports `--fix`. |
Expand All @@ -91,7 +95,7 @@ Namespace: `Boundwize\StructArmed\Rule\Rules\Class_`.

`classNamePattern` and `excludePattern` are regular expressions matched against the fully-qualified class name.

`Psr4DirectoryExistsRule`, `Psr1PhpTagsRule`, `Psr1Utf8WithoutBomRule`, `MustBeFinalRule`, `MustDeclareConstantVisibilityRule`, `MustDeclareMethodVisibilityRule`, and `MustDeclarePropertyVisibilityRule` implement `Boundwize\StructArmed\Rule\FixableInterface`, so StructArmed can automatically remove PSR-4 mappings for missing directories, normalize invalid PHP opening tags, remove UTF-8 byte order marks, add the `final` class modifier, and add missing constant, method, or property visibility modifiers when you run `vendor/bin/structarmed analyse --fix`.
`Psr4DirectoryExistsRule`, `Psr1PhpTagsRule`, `Psr1Utf8WithoutBomRule`, `ExtendedClassMustBeAbstractOrInstantiatedRule`, `MustBeFinalRule`, `MustBeUsedInterfaceRule`, `MustBeUsedAbstractClassRule`, `MustBeUsedTraitRule`, `MustDeclareConstantVisibilityRule`, `MustDeclareMethodVisibilityRule`, and `MustDeclarePropertyVisibilityRule` implement `Boundwize\StructArmed\Rule\FixableInterface`, so StructArmed can automatically remove PSR-4 mappings for missing directories, normalize invalid PHP opening tags, remove UTF-8 byte order marks, add the `final` or `abstract` class modifier, remove unused interfaces, abstract classes, and traits (deleting their file when only `declare`/`namespace`/`use` boilerplate remains), and add missing constant, method, or property visibility modifiers when you run `vendor/bin/structarmed analyse --fix`.

## Layer Rules

Expand Down
61 changes: 61 additions & 0 deletions docs/custom-rules-and-presets.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,67 @@ return Architecture::define()
);
```

## Reading Usage Flags In A Custom Rule

Every `ClassNode` carries four usage flags describing how the class-like is used elsewhere in the scanned paths:

| Flag | Meaning |
| --- | --- |
| `$classNode->isExtended` | Another scanned class (or anonymous class) extends this class, directly or through inheritance |
| `$classNode->isImplemented` | A scanned class implements this interface (directly or through inheritance), or another scanned interface extends it |
| `$classNode->isReferenced` | Another scanned scope references it as a dependency: a type hint, an `instanceof` check, a `::class` constant, a static call, a trait use, a class-name string, and so on |
| `$classNode->isInstantiated` | Another scanned scope instantiates it: `new X`, `new self`/`static`/`parent`, a constant class expression such as `new (X::class)`, or a resolvable `ReflectionClass` construction |

Collecting this usage information costs extra analysis time, so the analyser only computes it when an active rule declares that it needs it. A custom rule declares that by implementing one of three marker interfaces instead of the plain `Boundwize\StructArmed\Rule\RuleInterface` (each marker extends it, so no other change is needed):

| Marker interface | Flags populated |
| --- | --- |
| `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` — reading them from a rule that only implements `RuleInterface` reports every class-like as unused.

```php
<?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](../presets/) rules follow this pattern: `MustBeUsedInterfaceRule` implements `UsedInterfaceAwareRuleInterface`, `MustBeUsedTraitRule` implements `UsedTraitAwareRuleInterface`, and `MustBeUsedAbstractClassRule` and `ExtendedClassMustBeAbstractOrInstantiatedRule` implement `ExtendedClassAwareRuleInterface`.

Trade-off: only usage within the scanned paths is known. A class-like used solely by a consumer outside the scan — a vendor package, an unscanned directory, runtime-fed dynamic construction — is reported as if unused. Widen the scan, or use `skipRule()` and skip paths where such consumers exist.

## Making A Custom Rule Fixable

Use `Boundwize\StructArmed\Rule\FixableInterface` when a custom rule can safely rewrite the offending source file.
Expand Down
3 changes: 3 additions & 0 deletions docs/presets.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ StructArmed ships with presets for common PHP standards and architecture styles.
| `Preset::PSR4()` | Verifies configured source paths exist in composer.json `autoload` or `autoload-dev` PSR-4 mappings |
| `Preset::DDD()` | Layer isolation, entity/VO/repository/event/service conventions |
| `Preset::MVC()` | Layer isolation, thin controllers, model/view/service rules |
| `Preset::YAGNI()` | Speculative-abstraction cleanup: interfaces must be implemented by a class or extended by another interface, abstract classes must be extended, traits must be used, and extended classes that are never instantiated must be abstract — a dependency reference (type hint, `instanceof`, `::class`, static call, a class-name string, ...) also counts as usage within the scanned paths, while only instantiation (`new X`, `new self`/`static`/`parent`, or a constant class expression such as `new (X::class)`) keeps an extended class concrete. All rules support `--fix`, removing the unused declaration or adding the `abstract` modifier |

## Initialize Presets

Expand All @@ -35,6 +36,7 @@ vendor/bin/structarmed init --preset=psr12
vendor/bin/structarmed init --preset=psr15
vendor/bin/structarmed init --preset=mvc
vendor/bin/structarmed init --preset=ddd
vendor/bin/structarmed init --preset=yagni
vendor/bin/structarmed init --preset=all
```

Expand All @@ -49,6 +51,7 @@ return Architecture::define()
Preset::PSR15(),
Preset::MVC(),
Preset::DDD(),
Preset::YAGNI(),
);
```

Expand Down
Loading
Loading