Layers And Rulesets

Layers tell StructArmed where classes belong. Rulesets tell StructArmed which layer dependencies are allowed.

Contents

  1. Path-Based Layers
  2. Namespace-Based Layers
  3. Declarative Rulesets
  4. Inheriting Allowed Layers
  5. Multiple Layer Patterns
  6. Excluding From Layer Patterns
  7. Skipping Class-Level Violations
  8. Excluding Paths From Ruleset Checks Only

Path-Based Layers

Each class is assigned a layer based on which registered layer() path its file falls under. No class attributes are needed.

// Files under src/Domain/ resolve to layer 'Domain'.
->layer('Domain', 'src/Domain/')

// Files under src/Application/ resolve to layer 'Application'.
->layer('Application', 'src/Application/')

// Files under src/Infrastructure/ resolve to layer 'Infrastructure'.
->layer('Infrastructure', 'src/Infrastructure/')

Namespace-Based Layers

When your architecture is expressed through namespace conventions rather than directory structure, use layerPattern() to resolve layers by matching the fully-qualified class name against a regex.

return Architecture::define()
    ->layerPattern('API',    '/^App\\\\API\\\\.*$/')
    ->layerPattern('HTTP',   '/^App\\\\HTTP\\\\.*$/')
    ->layerPattern('Router', '/^App\\\\Router\\\\.*$/');

Declarative Rulesets

Once layers are defined, declare which layers each layer is allowed to depend on. Any dependency that resolves to a layer outside the allowed list is a violation.

return Architecture::define()
    ->layerPattern('API',      '/^App\\\\API\\\\.*$/')
    ->layerPattern('HTTP',     '/^App\\\\HTTP\\\\.*$/')
    ->layerPattern('Database', '/^App\\\\Database\\\\.*$/')
    ->ruleset([
        'API'      => ['HTTP'],     // API may only depend on HTTP.
        'HTTP'     => ['Database'], // HTTP may only depend on Database.
        'Database' => [],           // Database may not depend on any layer.
    ]);

Layers absent from the ruleset keys are not checked. Dependencies on external, non-registered classes are always allowed.

Same-layer dependencies are always allowed regardless of the ruleset.

Inheriting Allowed Layers

Use the + prefix to merge a layer’s allowed dependencies into another layer’s allowed list.

->ruleset([
    'API'        => ['Format'],
    'Controller' => ['Validation'],
    'RESTful'    => ['+API', '+Controller'],
])

Each +LayerName entry expands to that layer itself plus all layers it is allowed to depend on.

-'RESTful' => ['API', 'Format', 'Controller', 'Validation'],
+'RESTful' => ['+API', '+Controller'],

When the allowed layers of API or Controller change, RESTful picks them up automatically. Unknown + references expand to nothing silently.

Multiple Layer Patterns

You can assign a layer using multiple regexes.

return Architecture::define()
    ->layerPattern('Service', [
        '/^App\\\\Service\\\\.*$/',
        '/^App\\\\Application\\\\.*Service$/',
    ]);

Excluding From Layer Patterns

An optional third argument excludes classes whose FQN matches one or more regexes, even when the layer pattern matches.

Use a single exclude regex when one class or namespace branch should not belong to a broader layer. In this example, App\HTTP\URI matches the broad HTTP pattern, but it is excluded from HTTP and then registered as its own URI layer.

// HTTP layer: includes everything under App\HTTP\, except App\HTTP\URI.
->layerPattern('HTTP', '/^App\\\\HTTP\\\\.*$/', '/^App\\\\HTTP\\\\URI$/')
->layerPattern('URI',  '/^App\\\\HTTP\\\\URI$/')

Use an array of exclude regexes when several class-name patterns should be omitted from the same broad layer.

->layerPattern('HTTP', '/^App\\\\HTTP\\\\.*$/', [
    '/Exception$/',
    '/^App\\\\HTTP\\\\URI$/',
])

With the configuration above, classes such as App\HTTP\Request still resolve to HTTP, while App\HTTP\RequestException and App\HTTP\URI do not.

Skipping Class-Level Violations

When a specific class-to-class dependency is a known exception, suppress it without disabling the whole layer rule.

->skipClassViolation('App\\HTTP\\ResponseTrait', [
    'App\\Pager\\PagerInterface',
])
->skipClassViolation('App\\Log\\ChromeLoggerHandler', 'App\\HTTP\\ResponseInterface')

The first argument is the fully-qualified violating class name. The second argument is one or more dependency FQNs to ignore for that class.

Excluding Paths From Ruleset Checks Only

Test files often cross layer boundaries by design. Use skipPathsForRuleset() to exclude paths from ruleset evaluation while still scanning them for all other rules, such as PSR-4 namespace checks.

return Architecture::define()
    ->withPresets(Preset::PSR4())
    ->layerPattern('HTTP',     '/^App\\\\HTTP\\\\.*$/')
    ->layerPattern('Database', '/^App\\\\Database\\\\.*$/')
    ->ruleset(['HTTP' => [], 'Database' => ['HTTP']])
    ->skipPathsForRuleset(['*tests*', '*fixtures*']);

This is different from skipPaths() and skipPath(), which exclude files from all analysis.