Arkitect
Put your architectural rules under test!
Install / Use
/learn @phparkitect/ArkitectREADME
📐 PHPArkitect
Introduction
PHPArkitect is a tool to enforce architectural constraints in your PHP codebase. It helps you maintain clean architecture by preventing violations of your design rules during development and in CI/CD pipelines.
Why PHPArkitect?
As projects grow, maintaining architectural consistency becomes challenging. PHPArkitect helps you:
- Prevent architectural drift: Ensure your Domain layer doesn't depend on Infrastructure code
- Enforce naming conventions: Make sure all Controllers end with "Controller", all Services with "Service", etc.
- Maintain layered architecture: Keep your application, domain, and infrastructure layers properly separated
- Catch violations early: Get immediate feedback in your IDE or CI pipeline before code review
- Document architecture as code: Your architectural rules become executable and self-documenting
Example
You can express architectural constraints in simple, readable PHP code:
Rule::allClasses()
->that(new ResideInOneOfTheseNamespaces('App\Controller'))
->should(new HaveNameMatching('*Controller'))
->because('it\'s a symfony naming convention');
Rule::allClasses()
->that(new ResideInOneOfTheseNamespaces('App\Domain'))
->should(new NotHaveDependencyOutsideNamespace('App\Domain'))
->because('we want to protect our domain from external dependencies');
Since selecting classes by namespace is very common, there's a convenient shortcut:
Rule::namespace('App\Controller')
->should(new HaveNameMatching('*Controller'))
->because('it\'s a symfony naming convention');
You can also specify multiple namespaces: Rule::namespace('App\Controller', 'App\Service').
Quick Start
Get started with PHPArkitect in 3 simple steps:
1. Install via Composer
composer require --dev phparkitect/phparkitect
2. Create a configuration file
Create a phparkitect.php file in your project root:
<?php
declare(strict_types=1);
use Arkitect\ClassSet;
use Arkitect\CLI\Config;
use Arkitect\Expression\ForClasses\HaveNameMatching;
use Arkitect\Expression\ForClasses\ResideInOneOfTheseNamespaces;
use Arkitect\Rules\Rule;
return static function (Config $config): void {
$classSet = ClassSet::fromDir(__DIR__.'/src');
$rules = [];
$rules[] = Rule::allClasses()
->that(new ResideInOneOfTheseNamespaces('App\Controller'))
->should(new HaveNameMatching('*Controller'))
->because('we want uniform naming for controllers');
$config->add($classSet, ...$rules);
};
3. Run the check
vendor/bin/phparkitect check
That's it! PHPArkitect will analyze your code and report any architectural violations.
Next steps: Check out the Available rules section to explore all the constraints you can enforce.
Installation
Using Composer
composer require --dev phparkitect/phparkitect
Using a Phar
Sometimes your project can conflict with one or more of PHPArkitect's dependencies. In that case you may find the Phar (a self-contained PHP executable) useful.
The Phar can be downloaded from GitHub:
wget https://github.com/phparkitect/arkitect/releases/latest/download/phparkitect.phar
chmod +x phparkitect.phar
./phparkitect.phar check
When you run phparkitect as phar and you have custom rules in need of autoloading the project classes you'll need to specify the option --autoload=[AUTOLOAD_FILE].
Usage
To use this tool you need to launch a command via Bash:
phparkitect check
With this command phparkitect will search for the default config file called phparkitect.php in the root of your project.
You can also specify your configuration file using --config option like this:
phparkitect check --config=/project/yourConfigFile.php
By default, a progress bar will show the status of the ongoing analysis.
Using a baseline file
If there are a lot of violations in your codebase and you can't fix them now, you can use the baseline feature to instruct the tool to ignore past violations.
To create a baseline file, run the check command with the generate-baseline parameter as follows:
phparkitect check --generate-baseline
This will create a phparkitect-baseline.json, if you want a different file name you can do it with:
phparkitect check --generate-baseline=my-baseline.json
It will produce a json file with the current list of violations.
If a baseline file with the default name is present, it will be used automatically.
To use a different baseline file, run the check command with the use-baseline parameter as follows:
phparkitect check --use-baseline=my-baseline.json
To avoid using the default baseline file, you can use the skip-baseline option:
phparkitect check --skip-baseline
Line numbers in baseline
By default, the baseline check also looks at line numbers of known violations. When a line before the offending line changes, the line numbers change and the check fails despite the baseline.
With the optional flag ignore-baseline-linenumbers, you can ignore the line numbers of violations:
phparkitect check --ignore-baseline-linenumbers
Warning: When ignoring line numbers, phparkitect can no longer discover if a rule is violated additional times in the same file.
Output format
Output format can be controlled using the parameter format=[FORMAT]. There are two available output formats
text: the default onejson: this format allows custom report using github action or another platform as Sonarqube and so on... Note that this will suppress any output apart from the violation reporting.gitlab: this follows Gitlab's code quality format. Note that this will suppress any output apart from the violation reporting.
Configuration
Example of configuration file phparkitect.php
<?php
declare(strict_types=1);
use Arkitect\ClassSet;
use Arkitect\CLI\Config;
use Arkitect\Expression\ForClasses\HaveNameMatching;
use Arkitect\Expression\ForClasses\NotHaveDependencyOutsideNamespace;
use Arkitect\Expression\ForClasses\ResideInOneOfTheseNamespaces;
use Arkitect\Rules\Rule;
return static function (Config $config): void {
$mvcClassSet = ClassSet::fromDir(__DIR__.'/mvc', __DIR__.'/lib/my-lib/src');
$rules = [];
$rules[] = Rule::allClasses()
->that(new ResideInOneOfTheseNamespaces('App\Controller'))
->should(new HaveNameMatching('*Controller'))
->because('we want uniform naming');
$rules[] = Rule::allClasses()
->that(new ResideInOneOfTheseNamespaces('App\Domain'))
->should(new NotHaveDependencyOutsideNamespace('App\Domain'))
->because('we want to protect our domain');
$config
->add($mvcClassSet, ...$rules);
};
PHPArkitect can detect violations also on DocBlocks custom annotations (like @Assert\NotBlank or @Serializer\Expose).
If you want to disable this feature you can add this simple configuration:
$config->skipParsingCustomAnnotations();
Available rules
Hint: If you want to test how a Rule work, you can use the command like phparkitect debug:expression <RuleName> <arguments> to check which class satisfy the rule in your current folder.
For example: phparkitect debug:expression ResideInOneOfTheseNamespaces App
Currently, you can check if a class:
Namespace
Reside in a namespace / Not reside in a namespace
// Enforce that all handlers live in the application layer
$rules[] = Rule::allClasses()
->that(new HaveNameMatching('*Handler'))
->should(new ResideInOneOfTheseNamespaces('App\Application'))
->because('we want to be sure that all CommandHandlers are in a specific namespace');
// Ensure domain events do not leak into other layers
$rules[] = Rule::allClasses()
->that(new Extend('App\Domain\Event'))
->should(new NotResideInTheseNamespaces('App\Application', 'App\Infrastructure'))
->because('we want to be sure that all events not reside in wrong layers');
Reside in a namespace exactly / Not reside in a namespace exactly
These rules check namespace membership without matching child namespaces. Unlike ResideInOneOfTheseNamespaces which matches recursively, these rules only match classes directly in the given namespace.
// Only allow entity classes at the root Entity namespace, not in subdirectories
$rules[] = Rule::allClasses()
->that(new HaveNameMatching('*Entity'))
->should(new ResideInOneOfTheseNamespacesExactly('App\Domain\Entity'))
->because('we want entity classes only in the root Entity namespace, not in subdirectories');
// Prevent classes from sitting directly at the Legacy namespace root
$rules[] = Rule::allClasses()
->that(new ResideInOneOfTheseNamespaces('App\Legacy'))
->should(new NotResideInOneOfTheseNamespacesExactly('App\Legacy'))
->because('we want to avoid classes directly in the Legacy namespace root');
For example, with namespace App\Domain\Entity:
App\Domain\Entity\User✅ matchesResideInOneOfTheseNamespacesExactly- `App\Domain\Entity\ValueObject\Email
