Boundaries
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.
"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:
| Finding | Level | Meaning |
|---|---|---|
| Undeclared dependency | Error | The layer uses a class of a layer it does not require. Each file and line is listed. |
| Cycle | Error | Layers require each other through require: functional/orders -> functional/users -> functional/orders. |
| Unused requirement | Warning | A 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:
useimports, with groups (use Functional\Billing\{Models\Invoice, Models\Payment};), aliases,use functionanduse 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 torequire-devwhen only its tests use it. Then runcomposer updatefor 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:
'boundaries' => [
'ignore' => [
'Technical\\Shared\\Contracts\\',
['functional/orders', 'technical/payments'],
],
],
| Entry | Effect |
|---|---|
| A string: class prefix | References 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 names | functional/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.
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