The rule says Domain must not depend on Infrastructure.
Code review misses this reference:
final class Order extends \Illuminate\Database\Eloquent\Model
{
}A quick substring scan catches it. The same scan also rejects a harmless
docblock which names EloquentOrderRepository while explaining what Domain
must not do.
The first check is unreliable because a person must notice every occurrence. The second is unreliable because text is not a PHP dependency. A token-aware source check catches the executable reference and ignores the prose.
Then the service container binds the right application port to the wrong adapter. The source graph remains legal, so the architecture check passes. A composition test fails.
An architecture rule belongs at the cheapest layer which can observe its violation without unacceptable false results. The layer changes with the property.
Begin with one owned rule
The synthetic SVC-04 fixture uses a small PHP application:
Delivery -> Application, framework
Infrastructure -> Application, Domain, framework
Application -> Domain
Domain -> PHPThree rules have stable IDs, owners, scopes, and repair directions:
ARCH-DEP-01keeps Domain independent of Application, Infrastructure, Delivery, Laravel, and Eloquent;ARCH-DEP-02lets Application use Domain but not Infrastructure, Delivery, Laravel, or Eloquent; andARCH-COMP-01binds the application-ownedOrderRepositoryport to the declared Eloquent adapter.
The first two describe source direction. The third describes runtime
selection. Giving all three an ARCH prefix does not make them observable by
one tool.
This is where architecture tests should start: not with a fashionable preset, but with a decision somebody owns. The rule needs an included namespace, a forbidden target, an explicit exception, and a reason to exist. “All classes must be final” is not an architecture decision unless the system has actually chosen and justified that constraint.
PHP tokens separate code from commentary
The fixture’s source check uses PHP’s token_get_all with TOKEN_PARSE. The
PHP manual says the function uses the engine’s lexical scanner
and returns identifiers for the language tokens it sees.
That distinction is enough for the declared rules. The check inspects qualified names in owned Domain and Application paths. It catches:
- imports and aliased imports;
- fully qualified class references;
- inheritance and interface implementation;
- trait use; and
- attributes.
Comments, docblocks, and string literals arrive as different tokens and do not become dependencies. A documentation sentence can name Infrastructure without making Domain execute it.
The check also ignores one exact generated/ path. It does not ignore every
file containing Generated, every proxy namespace, or the entire Domain layer.
A narrow exception is visible debt. A broad exclusion is an architecture
policy with the interesting parts removed.
Pest’s current architecture-testing documentation offers namespace, usage, inheritance, trait, and explicit ignoring mechanics. Deptrac’s documentation goes further with owned layers, rule sets, violations, exceptions, and uncovered dependencies. Either may be a better maintained choice in a real project. The fixture uses the lower-level tokenizer so its exact observation boundary remains inspectable without assuming a Pest version or adding a package.
A useful failure points toward repair
Seed an Infrastructure import in Domain and the fixture exits non-zero with:
ARCH-DEP-01 violation:
src/Domain/Order.php references App\Infrastructure\EloquentOrderRepository;
repair: move the dependency behind a Domain-owned contractThe message names the rule, source, target, and repair direction. A developer
can disagree with the rule or fix the dependency without first reverse
engineering a generic architecture test failed message.
The same contract catches a fully qualified Eloquent base class, an Infrastructure base class, a Delivery interface, an Infrastructure trait, and a framework attribute. Application receives its own message and repair: depend on an Application-owned port, then bind it at composition.
Legal outward dependencies remain legal. Delivery can extend a framework controller. Infrastructure can use a database connection. Architecture rules which reject adapters for adapting have confused inward policy with universal framework abstinence.
Cheap execution can still be expensive enforcement
The reproduction retains two deliberately naive scans.
An import-only regular expression is quick and quiet. It misses a fully
qualified reference because no use statement exists. A raw substring scan
finds that reference but rejects the same name in a harmless string.
Both commands are cheaper to run than a parser. Neither is cheaper to operate. One allows the forbidden dependency. The other creates false alarms which teach developers to add exclusions or ignore the check.
“Cheapest” therefore includes:
- whether the observer can see the defect;
- whether legal code stays green;
- how much source and configuration it scans;
- whether its failure explains the repair; and
- which properties still require another check.
Runtime alone is not a useful comparison. A ten-millisecond check which cannot observe the violation costs ten milliseconds and leaves the rule unenforced.
A legal graph can still compose the wrong application
The fixture changes the composition map from
EloquentOrderRepository to NullOrderRepository.
Both dependency rules stay green. Application still owns the port and Infrastructure still supplies an adapter. Static direction cannot decide which adapter the running application selected.
The focused composition check fails instead:
ARCH-COMP-01 violation:
App\Application\OrderRepository expected
App\Infrastructure\EloquentOrderRepository,
selected App\Infrastructure\NullOrderRepository;
repair: bind the application port to the declared infrastructure adapterLaravel’s service-container documentation makes this runtime concern explicit: service providers register bindings, and the container can map an interface to a chosen implementation. A source dependency test can validate the direction of that interface. It cannot prove the binding loaded for this environment.
The narrow integration test is cheaper than booting an end-to-end request and more reliable than reading the provider by eye. It asks the container one composition question and reports the actual selection.
Each property needs an observer which can see it
The final matrix is short because it refuses to make architecture testing mean everything:
| Property | Cheapest reliable observer | Declared limitation |
|---|---|---|
| Source dependency direction | Tokenizer architecture test | Runtime selection |
| Container composition | Focused integration test | Business behavior |
| Accepted business behavior | Behavior test | Production conditions |
| Production resilience | Operational signal | Source direction |
An architecture test can prove that Domain source does not name Eloquent under the syntax and paths its parser covers. It cannot prove that saving an order is transactional, that a queue retries safely, that a query meets its latency budget, or that production fails over correctly.
A dynamic class name assembled from configuration creates the same boundary. The tokenizer cannot infer the resulting runtime target. The fixture records that limitation and routes the property to composition. Calling it statically verified would make the report more impressive and less true.
Behavior and operational checks work in the opposite direction. A passing request may show that one configured path works. It does not prove that Domain source stayed independent. A production metric can reveal saturation. It does not identify the forbidden import which made a later extraction harder.
The layers complement one another because they observe different failures.
Enforcement should retire with the decision
The rule inventory includes reversal conditions. A source rule changes when the architecture changes, the owned scan moves, supported syntax outgrows the analyzer, or false results show that the observer is no longer reliable.
That is not permission to delete a failing test until the build turns green. It is a requirement to keep policy and enforcement in the same conversation. A test which survives after its decision disappears becomes ceremony. A decision which survives after its test stops observing reality becomes wishful documentation.
Review remains useful for the parts which resist a reliable binary check: whether a boundary is still valuable, whether an exception is justified, and whether the repair cost exceeds the risk. Automation preserves the narrower decision between those reviews.
Make one violation executable
The synthetic reproduction retains three rules, 31 passing tests, four verified primary sources, legal framework and generated-code controls, a wrong-binding integration failure, an observer matrix, and 18 isolated counterfactuals. The counterfactuals hide supported syntax, naive-scan errors, broad exclusions, legal entry points, runtime composition, dynamic limits, actionable output, or replace the matrix with one universal layer and a score. All eighteen fail.
The clean export reproduces from commit
abdc3bbb5c01aef14dc185c24068e4ab231c8791. It proves the declared fixture,
analyzer, CI, and failure controls. It does not prove every PHP syntax form,
framework configuration, runtime behavior, or production property in Brian’s
systems.
Take one architecture sentence from the next review and finish four fields:
rule:
owned scope and exception:
representative violation:
cheapest observer which reliably fails:If the violation cannot be made to fail without rejecting legal code, keep the decision in review and say why. If it can, make the failure readable and put it in CI. The architecture document can then spend its words on the decision, instead of hoping somebody remembers to police the syntax.