cooldevguys / phpstan-dhexpendency-rules
A set of rules for PHPStan based on the Hexagonal architecture layers dependency
Package info
github.com/CoolDevGuys/phpstan-dhexpendency-rules
Type:phpstan-extension
pkg:composer/cooldevguys/phpstan-dhexpendency-rules
Requires
- php: >=7.4.0
- phpstan/phpstan: ^2.0
Requires (Dev)
- phpunit/phpunit: ^9.6
- roave/security-advisories: dev-latest
Suggests
- phpstan/extension-installer: Allows automatic registration of the bundled rules.neon
README
A set of additional PHPStan rules that help enforce the
dependency flow of a hexagonal (ports & adapters) architecture โ so a stray use statement in
the wrong direction gets caught in CI, not in code review. ๐ฆ
๐ The rule this package enforces
These rules assume the typical hexagonal architecture with 3 layers: Infrastructure, Application and Domain, and that dependencies must only ever flow one way:
Infrastructure โก๏ธ Application โก๏ธ Domain
In other words:
| Layer | May depend on | May not depend on |
|---|---|---|
| ๐๏ธ Infrastructure | Application, Domain, external vendors | (nothing forbidden) |
| โ๏ธ Application | Domain only | Infrastructure |
| ๐ง Domain | itself only | Infrastructure, Application |
The Domain layer is the one that matters most to protect: it's your business logic, and it should never know that a database, an HTTP client, or a specific framework exists.
โจ What you get
LayersDependencyFlowRuleโ flags anyusestatement that points from a layer to a layer it isn't allowed to depend on (per the table above).NoExternalVendorsAllowedRule(opt-in viavendorStrictMode) โ flags anyusestatement in the Application or Domain layers that imports a class from outside your own project vendor namespace (e.g. a third-party library), so your business logic only talks to your own code โ never directly to a framework or SDK. Infrastructure is always exempt, since it's exactly the layer meant to talk to the outside world.
๐ฆ Installation
โ ๏ธ Dev-only package. This is a static analysis tool โ its rules only run while
phpstan analyseruns, never in your application's runtime code. Always install it with--dev(as below) so it lands inrequire-dev, notrequire, and never ships to production. If you forget the flag, Composer itself will notice (this package is taggedstatic analysis) and offer to re-run the command with--devfor you.
composer require --dev cooldevguys/phpstan-dhexpendency-rules
If you use PHPStan's extension installer,
you're all set. ๐ Otherwise, register the rules manually in your phpstan.neon:
includes: - vendor/cooldevguys/phpstan-dhexpendency-rules/rules.neon
Requirements: PHP >=7.4 and phpstan/phpstan ^2.0.
โ๏ธ Configuration
Add your project's values nested under a single dhexpendency key in phpstan.neon:
parameters: dhexpendency: myVendorName: CoolDevGuys infrastructureLayerName: Infra applicationLayerName: App domainLayerName: Dom vendorStrictMode: true ignoredExternalVendors: ['IgnoredVendor']
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
myVendorName |
string |
โ | โ | Your project's top-level namespace segment (e.g. CoolDevGuys in CoolDevGuys\App\...). Must be a valid PHP identifier. |
infrastructureLayerName |
string |
โ | โ | The namespace segment you use for the Infrastructure layer. |
applicationLayerName |
string |
โ | โ | The namespace segment you use for the Application layer. |
domainLayerName |
string |
โ | โ | The namespace segment you use for the Domain layer. |
vendorStrictMode |
bool |
โ | false |
Enables NoExternalVendorsAllowedRule (see above). |
ignoredExternalVendors |
string[] |
โ | [] |
Vendor namespaces exempted from vendorStrictMode. Only has an effect when vendorStrictMode is true. |
The three layer names must all be different from each other, and every name above must be a valid PHP namespace segment (letters, digits, underscores; can't start with a digit) โ an invalid value fails fast with a clear error instead of silently misbehaving.
โฌ๏ธ Upgrading from 1.x?
This is a breaking change (v2.0): parameters moved from the top level of
parameters:into a nesteddhexpendency:key, andNoExternalVendorsAllowedFromDomainRulewas renamed toNoExternalVendorsAllowedRule(it always checked Application and Domain, not just Domain โ the new name matches what it actually does). To migrate, just move your existing six keys underdhexpendency:as shown above; nothing else changes, and no error messages or rule identifiers were affected, so anyphpstan-baseline.neonyou already generated stays valid.
๐งฉ How your namespaces should look
Each layer is identified by a segment in the namespace, placed after your vendor name and an optional "context" segment โ for example, with the config above:
CoolDevGuys\Infra\... ๐๏ธ Infrastructure
CoolDevGuys\Billing\App\... โ๏ธ Application (context: "Billing")
CoolDevGuys\Billing\Dom\... ๐ง Domain
When a namespace could plausibly match more than one configured layer name at different depths, the leftmost (earliest, shallowest) match wins โ so a Domain module with an unrelated nested segment that happens to share a name with another layer is still correctly recognized as Domain. Files whose namespace doesn't start with your configured vendor name, or has no recognizable layer segment at all, are silently skipped by both rules (they're none of this package's business).
๐จ What a violation looks like
// src/CoolDevGuys/App/PlaceOrder.php namespace CoolDevGuys\App; use CoolDevGuys\Infra\PaymentGatewayClient; // โ Application โ Infrastructure
The layer <App> can not contain references to the layers: Infra.
Use statement: <CoolDevGuys\Infra\PaymentGatewayClient>
๐ชช dhexpendencyRules.forbiddenLayerReference
// src/CoolDevGuys/Dom/Order.php namespace CoolDevGuys\Dom; use Ramsey\Uuid\Uuid; // โ Domain โ external vendor (with vendorStrictMode: true)
The layer <Dom> can not contain references to the external vendor: Ramsey.
Use statement: <Ramsey\Uuid\Uuid>
๐ชช dhexpendencyRules.externalVendorReference
Both errors carry a stable ๐ชช identifier, so
if you ever need to allow a specific case, you can target it precisely in phpstan.neon:
parameters: ignoreErrors: - identifier: dhexpendencyRules.externalVendorReference path: src/CoolDevGuys/Dom/LegacyOrder.php
๐งญ Scope & limitations
This package covers the classic 3-layer hexagonal setup with a single project vendor namespace. It does not support additional layers (e.g. a separate Presentation/UI layer) or checking more than one vendor namespace at once.
๐งช Development
composer install # install dependencies vendor/bin/phpunit # run the test suite make tests # same, via the prerequisites target make docker-run # run the test suite inside php:7.4.33-cli-alpine (the minimum supported PHP)
Pull requests are welcome! Please make sure vendor/bin/phpunit passes before opening one.
๐ License
MIT ยฉ Alejandro Romero โ see LICENSE.md.