Concepts

Boundaries

The rule osdd:check enforces between layers, what it reads, and how to fix or ignore a violation.

Each layer lists the layers it depends on in the require section of its composer.json. All layers share one Composer autoloader, though, so PHP lets functional/orders use a class of functional/billing even when functional/orders does not require it. osdd:check closes that gap.

The rule

A layer may only use the classes of the layers in its own require. Files under its tests/ folder may also use the layers in its require-dev.

functional/orders/composer.json
"require": {
    "lomkit/laravel-osdd": "*",
    "functional/users": "*"
},
"require-dev": {
    "technical/testing": "*"
}

With this file, functional/orders may use Functional\Users\... anywhere, and Technical\Testing\... in its tests. A class of functional/billing is a violation.

A layer reached through another requirement does not count: if functional/users requires technical/core, functional/orders must still require technical/core to use it. Removing a requirement from functional/users then never breaks functional/orders silently. Pass --transitive to allow these layers too.

Checking

php artisan osdd:check
  ERROR  functional/orders uses functional/billing but does not require it.

  functional/orders/src/Checkout.php:7 ........ Functional\Billing\Models\Invoice
  functional/orders/tests/CheckoutTest.php:5 .. Functional\Billing\Models\Invoice

   WARN  functional/billing requires technical/core but does not use it.

  ERROR  1 boundary violation in 4 layers (1 unused requirement).

The command reports:

FindingLevelMeaning
Undeclared dependencyErrorThe layer uses a class of a layer it does not require. Each file and line is listed.
CycleErrorLayers require each other through require: functional/orders -> functional/users -> functional/orders.
Unused requirementWarningA layer in require or require-dev that the layer's code never references.

It exits with 1 when there is at least one error. Warnings do not fail the command.

What is read

osdd:check reads the .php files of each layer's source directory, database/, routes/ and tests/ folders, never vendor/. It parses them with the PHP tokenizer and never loads them. It collects:

  • use imports, with groups (use Functional\Billing\{Models\Invoice, Models\Payment};), aliases, use function and use const;
  • fully qualified names: \Functional\Billing\Models\Invoice::class, new \..., attributes, type hints;
  • qualified names, resolved through the imports and the current namespace (Billing\Models\Invoice, namespace\Foo).

Comments, docblocks and strings are ignored, and an unqualified name only counts through its use import.

Each name belongs to the layer with the longest matching prefix among the autoload.psr-4 and autoload-dev.psr-4 entries of every layer. Names of the same layer, of vendor/ packages, of the framework or of App\ are never reported.

Fixing a violation

  • Add the used layer to the layer's require, or to require-dev when only its tests use it. Then run composer update for the layer.
  • Or move the shared code into a layer both may require.
  • Break a cycle by extracting the shared concept into a third layer, often under technical/.

Share code between layers walks through each option.

For a new layer, declare its requirements when you create it:

php artisan osdd:layer functional/invoices --requires=functional/orders --requires=technical/core

Ignoring a dependency

osdd.boundaries.ignore lists what osdd:check does not report. Set it in technical/osdd/config/osdd.php:

technical/osdd/config/osdd.php
'boundaries' => [
    'ignore' => [
        'Technical\\Shared\\Contracts\\',
        ['functional/orders', 'technical/payments'],
    ],
],
EntryEffect
A string: class prefixReferences to these classes are allowed from every layer. The prefix matches on namespace boundaries: Functional\Billing covers Functional\Billing\... but not Functional\BillingReports\....
A list of two layer namesfunctional/orders may use technical/payments without requiring it, its requirement on technical/payments is never reported as unused, and the edge is left out of the cycle detection.

Any other entry fails osdd:check with Invalid osdd.boundaries.ignore entry [...]. Ignored references still count as uses for the "unused requirement" warning.

Keep the list short. An ignored dependency is still a dependency: fix it in composer.json when it is a real one.

In CI

Run php artisan osdd:check --format=github in your pipeline: each violation becomes an annotation on the pull request. See Tests and CI.

Seeing the graph

osdd:graph draws the require edges between layers, in Mermaid, DOT or JSON:

php artisan osdd:graph > docs/layers.mmd