modulith-php / enforcer
A package to enforce sets of architecture requirements
Requires
- php: ^8.2
Requires (Dev)
- ergebnis/composer-normalize: ^2.47.0
- ergebnis/phpunit-slow-test-detector: ^2.16
- friendsofphp/php-cs-fixer: ^3.75
- phparkitect/phparkitect: ^1.0.0
- phpstan/phpstan: ^2.1.17
- phpunit/phpunit: ^11.5.21
- rector/rector: ^2.0.16
- roave/security-advisories: dev-master
README
This is a tool to enforce sets of predefined architecture templates.
It uses a superset of PHPArkitect to make it possible to have very dynamic sets of architecture templates.
What this repository provides
This repository provides several architecture templates that can be applied as is, using PHPArkitect, or if you don't like something in them, you can just copy/paste to your project and change them as you please.
However, you will need to use a fully backwards compatible version of PHPArkitect. See below how to install it.
- Enforce higher level architecture, for example:
- Layers (Onion Architecture & Ports & Adapters)
- Dependencies must go inwards & downwards
- Core must not implement or extend from ports (allow for exceptions like commands and events)
- Classes must only have sensible dependencies
- Slices (Modular Monolith)
- Adapters must be decoupled
- Core components must be decoupled
- Core components tests must be decoupled
- Code style
- Classes must be suffixed
- Classes must not be suffixed
- Quality
- Exceptions must inherit from project exception tree
- Classes must not rely on reflection (there are exceptions like Symfony compiler passes)
- Classes must be in map (ie a serialization map, so we are sure they can go ito a message queue)
- Layers (Onion Architecture & Ports & Adapters)
Installation
Since we need to use a superset of PHPArkitect, you need to specify the repository of the alternative PHPArkitect.
It is completely backwards compatible, so no other changes are needed.
Add this to your composer.json
:
{
"repositories": [
{
"type": "vcs",
"url": "git@github.com:hgraca/arkitect.git"
}
]
}
Then install PHPArkitect as usual:
composer require --dev phparkitect/phparkitect modulith-php/enforcer
How to use
LayerDependenciesGoInwardsAndDownwards
declare(strict_types=1);
use Arkitect\ClassSet;
use Arkitect\CLI\Config;
use Arkitect\Expression\Boolean\Andx;
use Arkitect\Expression\Boolean\Not;
use Arkitect\Expression\Boolean\Orx;
use Arkitect\Expression\ForClasses\IsA;
use Arkitect\Expression\ForClasses\NonePass;
use Arkitect\Expression\ForClasses\NotResideInTheseNamespaces;
use Arkitect\Expression\ForClasses\ResideInOneOfTheseNamespaces;
use ModulithPhp\Enforcer\Architecture\Layers\LayerDependenciesGoInwardsAndDownwards;
return static function (Config $config): void {
$rootPath = \realpath(__DIR__);
$classSet = ClassSet::fromDir("{$rootPath}/app", "{$rootPath}/tests");
$srcNamespace = 'MyVendorName\\MyProject';
$testsNamespace = "{$srcNamespace}\\Test";
// Code that is quite assetic, we consider it as a thin layer above PHP itself, and depend on it as of PHP itself
$phpOverlay = new ResideInOneOfTheseNamespaces(
'MyVendorName\\PhpOverlay',
);
// Code that is kind of config, namely mappers which are part of an adapter but need to depend directly on the core
$codeConfig = new NonePass();
// List of external namespaces we choose to conform with (AKA ignore list)
$conformistDependencies = new Orx(
new ResideInOneOfTheseNamespaces(
'Assert\\*',
'Carbon\\*',
'MyVendorName\\MyOtherProject\\*',
'Psr\\*',
'Ramsey\\Uuid\\*',
),
new Andx( // all code in legacy namespaces
new ResideInOneOfTheseNamespaces(
"{$srcNamespace}\\*",
),
new NotResideInTheseNamespaces(
"{$srcNamespace}\\Core\\*",
"{$srcNamespace}\\Infrastructure\\*",
"{$srcNamespace}\\Presentation\\*",
),
),
);
$domain = new ResideInOneOfTheseNamespaces("{$srcNamespace}\Core\Component\*\Domain\*");
$useCase = new IsA(Command::class);
$application = new ResideInOneOfTheseNamespaces("{$srcNamespace}\Core\Component\*\Application\*");
$sharedKernel = new Andx(
new Orx(
// new IsA(Event::class), // Use your own Event interface
// new IsA(Id::class), // Use your own entities ID interface
),
new Not($conformistDependencies),
);
$port = new ResideInOneOfTheseNamespaces("{$srcNamespace}\Core\Port\*");
$adapter = new Andx(
new ResideInOneOfTheseNamespaces("{$srcNamespace}\Infrastructure\*"),
new Not($codeConfig),
);
$vendor = new Andx(
new NotResideInTheseNamespaces("{$srcNamespace}\*", "{$testsNamespace}\*"),
new Not($phpOverlay),
new Not($conformistDependencies),
);
$presentation = new ResideInOneOfTheseNamespaces("{$srcNamespace}\Presentation\*");
$tests = new ResideInOneOfTheseNamespaces("{$testsNamespace}\*");
$rules = [
...(new LayerDependenciesGoInwardsAndDownwards(
$domain,
$useCase,
$application,
$sharedKernel,
$port,
$adapter,
$phpOverlay,
$codeConfig,
$conformistDependencies,
$vendor,
$presentation,
$tests,
))->build(),
];
$config->add($classSet, ...$rules);
};
ModulesAreDecoupledFromEachOther
declare(strict_types=1);
use Arkitect\ClassSet;
use Arkitect\CLI\Config;
use ModulithPhp\Enforcer\Architecture\Slices\ModulesAreDecoupledFromEachOther;
return static function (Config $config): void {
$rootPath = \realpath(__DIR__);
$classSet = ClassSet::fromDir("{$rootPath}/app", "{$rootPath}/tests");
$srcNamespace = 'MyVendorName\\MyProject';
$testsNamespace = "{$srcNamespace}\\Test";
$adaptersBaseNamespace = "{$srcNamespace}\\Infrastructure";
$adaptersBasePath = "{$rootPath}/src/Infrastructure";
$componentsBaseNamespace = "{$srcNamespace}\\Core\\Component";
$componentsBasePath = "{$rootPath}/src/Core/Component";
$adaptersUnitTestsBaseNamespace = "{$testsNamespace}\\Unit\\Infrastructure";
$adaptersUnitTestsBasePath = "{$rootPath}/tests/src/Unit/Infrastructure";
$adaptersIntegrationTestsBaseNamespace = "{$testsNamespace}\\Integration\\Infrastructure";
$adaptersIntegrationTestsBasePath = "{$rootPath}/tests/src/Integration/Infrastructure";
$adaptersFunctionalTestsBaseNamespace = "{$testsNamespace}\\Functional\\Infrastructure";
$adaptersFunctionalTestsBasePath = "{$rootPath}/tests/src/Functional/Infrastructure";
$componentsUnitTestsBaseNamespace = "{$testsNamespace}\\Unit\\Core\\Component";
$componentsUnitTestsBasePath = "{$rootPath}/tests/src/Unit/Core/Component";
$componentsIntegrationTestsBaseNamespace = "{$testsNamespace}\\Integration\\Core\\Component";
$componentsIntegrationTestsBasePath = "{$rootPath}/tests/src/Integration/Core/Component";
$componentsFunctionalTestsBaseNamespace = "{$testsNamespace}\\Functional\\Core\\Component";
$componentsFunctionalTestsBasePath = "{$rootPath}/tests/src/Functional/Core/Component";
$rules = [
...(new ModulesAreDecoupledFromEachOther(
$adaptersBaseNamespace,
$adaptersBasePath,
basePathGlob: '/*/*',
))->build(),
...(new ModulesAreDecoupledFromEachOther(
$adaptersUnitTestsBaseNamespace,
$adaptersUnitTestsBasePath,
basePathGlob: '/*/*',
))->build(),
...(new ModulesAreDecoupledFromEachOther(
$adaptersIntegrationTestsBaseNamespace,
$adaptersIntegrationTestsBasePath,
basePathGlob: '/*/*',
))->build(),
...(new ModulesAreDecoupledFromEachOther(
$adaptersFunctionalTestsBaseNamespace,
$adaptersFunctionalTestsBasePath,
basePathGlob: '/*/*',
))->build(),
...(new ModulesAreDecoupledFromEachOther(
$componentsBaseNamespace,
$componentsBasePath,
))->build(),
...(new ModulesAreDecoupledFromEachOther(
$componentsUnitTestsBaseNamespace,
$componentsUnitTestsBasePath,
))->build(),
...(new ModulesAreDecoupledFromEachOther(
$componentsIntegrationTestsBaseNamespace,
$componentsIntegrationTestsBasePath,
))->build(),
...(new ModulesAreDecoupledFromEachOther(
$componentsFunctionalTestsBaseNamespace,
$componentsFunctionalTestsBasePath,
))->build(),
];
$config->add($classSet, ...$rules);
};