PHPArkitect helps you to keep your PHP codebase coherent and solid, by permitting to add some architectural constraint check to your workflow. You can express the constraint that you want to enforce, in simple and readable PHP code, for example:
Rule::allClasses() ->that(newResideInOneOfTheseNamespaces('App\Controller')) ->should(newHaveNameMatching('*Controller')) ->because('it\'s a symfony naming convention');composer require --dev phparkitect/phparkitectSometimes 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].
To use this tool you need to launch a command via Bash:
phparkitect check With this command phparkitect will search all rules in the root of your project the default config file called phparkitect.php. 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.
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 is present a baseline file with the default name 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 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 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.
Example of configuration file phparkitect.php
<?phpdeclare(strict_types=1); useArkitect\ClassSet; useArkitect\CLI\Config; useArkitect\Expression\ForClasses\HaveNameMatching; useArkitect\Expression\ForClasses\NotHaveDependencyOutsideNamespace; useArkitect\Expression\ForClasses\ResideInOneOfTheseNamespaces; useArkitect\Rules\Rule; returnstaticfunction (Config$config): void{$mvcClassSet = ClassSet::fromDir(__DIR__.'/mvc', __DIR__.'/lib/my-lib/src'); $rules = []; $rules[] = Rule::allClasses() ->that(newResideInOneOfTheseNamespaces('App\Controller')) ->should(newHaveNameMatching('*Controller')) ->because('we want uniform naming'); $rules[] = Rule::allClasses() ->that(newResideInOneOfTheseNamespaces('App\Domain')) ->should(newNotHaveDependencyOutsideNamespace('App\Domain')) ->because('we want 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();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:
$rules[] = Rule::allClasses() ->that(newResideInOneOfTheseNamespaces('App\Domain')) ->should(newDependsOnlyOnTheseNamespaces(['App\Domain', 'Ramsey\Uuid'], ['App\Excluded'])) ->because('we want to protect our domain from external dependencies except for Ramsey\Uuid');$rules[] = Rule::allClasses() ->that(newResideInOneOfTheseNamespaces('App\Domain\Events')) ->should(newContainDocBlockLike('@psalm-immutable')) ->because('we want to enforce immutability');$rules[] = Rule::allClasses() ->that(newResideInOneOfTheseNamespaces('App\Controller')) ->should(newNotContainDocBlockLike('@psalm-immutable')) ->because('we don\'t want to enforce immutability');$rules[] = Rule::allClasses() ->that(newResideInOneOfTheseNamespaces('App\Controller')) ->should(newExtend('App\Controller\AbstractController')) ->because('we want to be sure that all controllers extend AbstractController'); You can add multiple parameters, the violation will happen when none of them match$rules[] = Rule::allClasses() ->that(newResideInOneOfTheseNamespaces('App\Controller')) ->should(newHaveAttribute('Symfony\Component\HttpKernel\Attribute\AsController')) ->because('it configures the service container');$rules[] = Rule::allClasses() ->that(newResideInOneOfTheseNamespaces('App\Service')) ->should(newHaveNameMatching('*Service')) ->because('we want uniform naming for services');$rules[] = Rule::allClasses() ->that(newResideInOneOfTheseNamespaces('App\Controller')) ->should(newImplement('ContainerAwareInterface')) ->because('all controllers should be container aware');$rules[] = Rule::allClasses() ->that(newResideInOneOfTheseNamespaces('App\Infrastructure\RestApi\Public')) ->should(newNotImplement('ContainerAwareInterface')) ->because('all public controllers should not be container aware');$rules[] = Rule::allClasses() ->that(newResideInOneOfTheseNamespaces('App\Customer\Service')) ->should(newIsAbstract()) ->because('we want to be sure that classes are abstract in a specific namespace');$rules[] = Rule::allClasses() ->that(newResideInOneOfTheseNamespaces('App\Customer\Service\Traits')) ->should(newIsTrait()) ->because('we want to be sure that there are only traits in a specific namespace');$rules[] = Rule::allClasses() ->that(newResideInOneOfTheseNamespaces('App\Domain\Aggregates')) ->should(newIsFinal()) ->because('we want to be sure that aggregates are final classes');$rules[] = Rule::allClasses() ->that(newResideInOneOfTheseNamespaces('App\Domain\ValueObjects')) ->should(newIsReadonly()) ->because('we want to be sure that value objects are readonly classes');$rules[] = Rule::allClasses() ->that(newResideInOneOfTheseNamespaces('App\Interfaces')) ->should(newIsInterface()) ->because('we want to be sure that all interfaces are in one directory');$rules[] = Rule::allClasses() ->that(newResideInOneOfTheseNamespaces('App\Enum')) ->should(newIsEnum()) ->because('we want to be sure that all classes are enum');$rules[] = Rule::allClasses() ->that(newResideInOneOfTheseNamespaces('App\Domain')) ->should(newIsNotAbstract()) ->because('we want to avoid abstract classes into our domain');$rules[] = Rule::allClasses() ->that(newResideInOneOfTheseNamespaces('App\Domain')) ->should(newIsNotTrait()) ->because('we want to avoid traits in our codebase');$rules[] = Rule::allClasses() ->that(newResideInOneOfTheseNamespaces('App\Infrastructure\Doctrine')) ->should(newIsNotFinal()) ->because('we want to be sure that our adapters are not final classes');$rules[] = Rule::allClasses() ->that(newResideInOneOfTheseNamespaces('App\Domain\Entity')) ->should(newIsNotReadonly()) ->because('we want to be sure that there are no readonly entities');$rules[] = Rule::allClasses() ->that(newResideInOneOfTheseNamespaces('Tests\Integration')) ->should(newIsNotInterface()) ->because('we want to be sure that we do not have interfaces in tests');$rules[] = Rule::allClasses() ->that(newResideInOneOfTheseNamespaces('App\Controller')) ->should(newIsNotEnum()) ->because('we want to be sure that all classes are not enum');$rules[] = Rule::allClasses() ->that(newResideInOneOfTheseNamespaces('App\Application')) ->should(newNotDependsOnTheseNamespaces(['App\Infrastructure'], ['App\Infrastructure\Repository'])) ->because('we want to avoid coupling between application layer and infrastructure layer');$rules[] = Rule::allClasses() ->that(newResideInOneOfTheseNamespaces('App\Controller\Admin')) ->should(newNotExtend('App\Controller\AbstractController')) ->because('we want to be sure that all admin controllers not extend AbstractController for security reasons'); You can add multiple parameters, the violation will happen when one of them match$rules[] = Rule::allClasses() ->that(newResideInOneOfTheseNamespaces('App\Domain')) ->should(newNotHaveDependencyOutsideNamespace('App\Domain', ['Ramsey\Uuid'], true)) ->because('we want protect our domain except for Ramsey\Uuid');$rules[] = Rule::allClasses() ->that(newResideInOneOfTheseNamespaces('App')) ->should(newNotHaveNameMatching('*Manager')) ->because('*Manager is too vague in naming classes');$rules[] = Rule::allClasses() ->that(newHaveNameMatching('*Handler')) ->should(newResideInOneOfTheseNamespaces('App\Application')) ->because('we want to be sure that all CommandHandlers are in a specific namespace');$rules[] = Rule::allClasses() ->that(newExtend('App\Domain\Event')) ->should(newNotResideInTheseNamespaces('App\Application', 'App\Infrastructure')) ->because('we want to be sure that all events not reside in wrong layers');You can also define components and ensure that a component:
- should not depend on any component
- may depend on specific components
- may depend on any component
Check out this demo project to get an idea on how write rules.
PHPArkitect offers some builders that enable you to implement more readable rules for specific contexts.
Thanks to this builder you can define components and enforce dependency constraints between them in a more readable fashion.
<?phpdeclare(strict_types=1); useArkitect\ClassSet; useArkitect\CLI\Config; useArkitect\Expression\ForClasses\HaveNameMatching; useArkitect\Expression\ForClasses\ResideInOneOfTheseNamespaces; useArkitect\RuleBuilders\Architecture\Architecture; useArkitect\Rules\Rule; returnstaticfunction (Config$config): void{$classSet = ClassSet::fromDir(__DIR__.'/src'); $layeredArchitectureRules = Architecture::withComponents() ->component('Controller')->definedBy('App\Controller\*') ->component('Service')->definedBy('App\Service\*') ->component('Repository')->definedBy('App\Repository\*') ->component('Entity')->definedBy('App\Entity\*') ->component('Domain')->definedBy('App\Domain\*') ->where('Controller')->mayDependOnComponents('Service', 'Entity') ->where('Service')->mayDependOnComponents('Repository', 'Entity') ->where('Repository')->mayDependOnComponents('Entity') ->where('Entity')->shouldNotDependOnAnyComponent() ->where('Domain')->shouldOnlyDependOnComponents('Domain') ->rules(); // Other rule definitions...$config->add($classSet, $serviceNamingRule, $repositoryNamingRule, ...$layeredArchitectureRules)};If you want to exclude some classes from the parser you can use the except function inside your config file like this:
$rules[] = Rule::allClasses() ->except('App\Controller\FolderController\*') ->that(newResideInOneOfTheseNamespaces('App\Controller')) ->should(newHaveNameMatching('*Controller')) ->because('we want uniform naming');You can use wildcards or the exact name of a class.
You can add parameters when you launch the tool. At the moment you can add these parameters and options:
-v: with this option you launch Arkitect with the verbose mode to see every parsed file--config: with this parameter, you can specify your config file instead of the default. like this:
phparkitect check --config=/project/yourConfigFile.php --target-php-version: With this parameter, you can specify which PHP version should use the parser. This can be useful to debug problems and to understand if there are problems with a different PHP version. Supported PHP versions are: 7.4, 8.0, 8.1, 8.2 8.3--stop-on-failure: With this option the process will end immediately after the first violation.--autoload: specify the path of an autoload file to be loaded when running phparkitect.
For some reasons, you might want to run only a specific rule, you can do it using runOnlyThis like this:
$rules[] = Rule::allClasses() ->except('App\Controller\FolderController\*') ->that(newResideInOneOfTheseNamespaces('App\Controller')) ->should(newHaveNameMatching('*Controller')) ->because('we want uniform naming') ->runOnlyThis();If you plan to use Arkitect with Laravel, smortexa wrote a nice wrapper with some predefined rules for laravel: https://github.com/smortexa/laravel-arkitect