modulith / arch-check
在您的PHP应用程序中强制实施架构约束
Requires
- php: ^7.1|^8
- ext-json: *
- nikic/php-parser: ~4
- ondram/ci-detector: ^4.1
- phpstan/phpdoc-parser: ^1.2
- symfony/console: ^3.0|^4.0|^5.0|^6.0
- symfony/event-dispatcher: ^3.0|^4.0|^5.0|^6.0
- symfony/finder: ^3.0|^4.0|^5.0|^6.0
- symfony/polyfill-php80: ^1.20
- webmozart/assert: ^1.9
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.0
- mikey179/vfsstream: ^1.6
- phpspec/prophecy: ^1.10
- phpunit/phpunit: ^7.5|^9.0|^10.0
- roave/security-advisories: dev-master
- symfony/var-dumper: ^3.0|^4.0|^5.0|^6.0
- vimeo/psalm: ^4.6
This package is auto-updated.
Last update: 2024-08-29 22:58:16 UTC
README
索引
简介
Archcheck可以帮助您保持PHP代码库的连贯性和稳固性,通过允许您在您的工流程中添加一些架构约束检查。您可以用简单的可读PHP代码表达您想要实施的约束,例如
Rule::allClasses()
->that(new ResideInOneOfTheseNamespaces('App\Controller'))
->should(new HaveNameMatching('*Controller'))
->because('it\'s a symfony naming convention');
安装
使用Composer
composer require --dev archcheck/archcheck
使用
要使用此工具,您需要通过Bash启动一个命令
archcheck check
使用此命令,archcheck将在您的项目根目录中搜索所有规则,默认配置文件名为archcheck.php。您也可以使用--config选项指定配置文件,如下所示
archcheck check --config=/project/yourConfigFile.php
默认情况下,进度条将显示正在进行分析的状态。
使用基线文件
如果您的代码库中有许多违规行为,您现在无法修复,您可以使用基线功能来指示工具忽略过去的违规行为。
要创建基线文件,运行带有generate-baseline参数的check命令,如下所示
archcheck check --generate-baseline
这将创建一个archcheck-baseline.json文件,如果您想使用不同的文件名,可以使用以下方法
archcheck check --generate-baseline=my-baseline.json
它将生成包含当前违规列表的JSON文件。
如果存在默认名称的基线文件,它将自动使用。
要使用不同的基线文件,运行带有use-baseline参数的check命令,如下所示
archcheck check --use-baseline=my-baseline.json
要避免使用默认基线文件,您可以使用skip-baseline选项
archcheck check --skip-baseline
基线中的行号
默认情况下,基线检查也会查看已知违规的行号。当违反规定的行之前的行更改时,行号会更改,并且即使有基线,检查也会失败。
使用可选标志ignore-baseline-linenumbers,您可以忽略违规的行号
archcheck check --ignore-baseline-linenumbers
警告:在忽略行号时,archcheck将无法发现规则是否在同一个文件中违反了额外的次数。
配置
配置文件archcheck.php的示例
<?php
declare(strict_types=1);
use Modulith\ArchCheck\ClassSet;
use Modulith\ArchCheck\CLI\Config;
use Modulith\ArchCheck\Expression\ForClasses\HaveNameMatching;
use Modulith\ArchCheck\Expression\ForClasses\NotHaveDependencyOutsideNamespace;
use Modulith\ArchCheck\Expression\ForClasses\ResideInOneOfTheseNamespaces;
use Modulith\ArchCheck\Rules\Rule;
return static function (Config $config): void {
$mvcClassSet = ClassSet::fromDir(__DIR__.'/mvc');
$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 protect our domain');
$config
->add($mvcClassSet, ...$rules);
};
Archcheck还可以检测文档块自定义注释(如@Assert\NotBlank或@Serializer\Expose)上的违规行为。如果您想禁用此功能,您可以添加以下简单配置
$config->skipParsingCustomAnnotations();
可用规则
提示:如果您想测试规则的功能,您可以使用类似archcheck debug:expression <RuleName> <arguments>的命令来检查您的当前文件夹中哪些类满足规则。
例如:archcheck debug:expression ResideInOneOfTheseNamespaces App
目前,您可以检查以下内容
在给定的映射中是否被引用
这很有用,例如,确保DTO(如命令和事件)总是设置在映射中,这样我们就可以确保序列化器知道如何序列化和反序列化它们。
$map = [
'a' => 'App\Core\Component\MyComponent\Command\MyCommand',
'b' => 'App\Core\Component\MyComponent\Event\MyEvent',
];
$rules = Rule::allClasses()
->that(new ResideInOneOfTheseNamespaces('App\Core\Component\**\Command', 'App\Core\Component\**\Event'))
->should(new IsMapped($map))
->because('we want to ensure our serializer can serialize/deserialize all commands and events');
在另一个命名空间中是否有对应的代码单元
这将允许我们确保某些类始终有一个测试,或者每个测试都有一个匹配的类,并且它们的命名空间是正确的。
$rules = Rule::allClasses()
->that(new ResideInOneOfTheseNamespaces('App\Core\Component\**\Command\*'))
->should(new HaveCorrespondingUnit(
// This will assert that class `App\Core\Component\MyComponent\Command\MyCommand`
// has a test class in `Tests\App\Core\Component\MyComponent\Command\MyCommandTest`
function ($fqcn) {
return 'Tests\\'.$fqcn.'Test';
}
)
)
->because('we want all our command handlers to have a test');
依赖于一个命名空间
$rules = Rule::allClasses()
->that(new ResideInOneOfTheseNamespaces('App\Domain'))
->should(new DependsOnlyOnTheseNamespaces('App\Domain', 'Ramsey\Uuid'))
->because('we want to protect our domain from external dependencies except for Ramsey\Uuid');
文档块包含一个字符串
$rules = Rule::allClasses()
->that(new ResideInOneOfTheseNamespaces('App\Domain\Events'))
->should(new ContainDocBlockLike('@psalm-immutable'))
->because('we want to enforce immutability');
文档块不包含一个字符串
$rules = Rule::allClasses()
->that(new ResideInOneOfTheseNamespaces('App\Controller'))
->should(new NotContainDocBlockLike('@psalm-immutable'))
->because('we don\'t want to enforce immutability');
扩展另一个类
$rules = Rule::allClasses()
->that(new ResideInOneOfTheseNamespaces('App\Controller'))
->should(new Extend('App\Controller\AbstractController'))
->because('we want to be sure that all controllers extend AbstractController');
有一个属性(需要PHP >= 8.0)
$rules = Rule::allClasses()
->that(new ResideInOneOfTheseNamespaces('App\Controller'))
->should(new HaveAttribute('AsController'))
->because('it configures the service container');
有一个匹配的模式的名字
$rules = Rule::allClasses()
->that(new ResideInOneOfTheseNamespaces('App\Service'))
->should(new HaveNameMatching('*Service'))
->because('we want uniform naming for services');
实现了一个接口
$rules = Rule::allClasses()
->that(new ResideInOneOfTheseNamespaces('App\Controller'))
->should(new Implement('ContainerAwareInterface'))
->because('all controllers should be container aware');
没有实现一个接口
$rules = Rule::allClasses()
->that(new ResideInOneOfTheseNamespaces('App\Infrastructure\RestApi\Public'))
->should(new NotImplement('ContainerAwareInterface'))
->because('all public controllers should not be container aware');
是抽象的
$rules = Rule::allClasses()
->that(new ResideInOneOfTheseNamespaces('App\Customer\Service'))
->should(new IsAbstract())
->because('we want to be sure that classes are abstract in a specific namespace');
是特性
$rules = Rule::allClasses()
->that(new ResideInOneOfTheseNamespaces('App\Customer\Service\Traits'))
->should(new IsTrait())
->because('we want to be sure that there are only traits in a specific namespace');
是最终的
$rules = Rule::allClasses()
->that(new ResideInOneOfTheseNamespaces('App\Domain\Aggregates'))
->should(new IsFinal())
->because('we want to be sure that aggregates are final classes');
是接口
$rules = Rule::allClasses()
->that(new ResideInOneOfTheseNamespaces('App\Interfaces'))
->should(new IsInterface())
->because('we want to be sure that all interfaces are in one directory');
是枚举
$rules = Rule::allClasses()
->that(new ResideInOneOfTheseNamespaces('App\Enum'))
->should(new IsEnum())
->because('we want to be sure that all classes are enum');
不是抽象的
$rules = Rule::allClasses()
->that(new ResideInOneOfTheseNamespaces('App\Domain'))
->should(new IsNotAbstract())
->because('we want to avoid abstract classes into our domain');
不是特性
$rules = Rule::allClasses()
->that(new ResideInOneOfTheseNamespaces('App\Domain'))
->should(new IsNotTrait())
->because('we want to avoid traits in our codebase');
非最终版本
$rules = Rule::allClasses()
->that(new ResideInOneOfTheseNamespaces('App\Infrastructure\Doctrine'))
->should(new IsNotFinal())
->because('we want to be sure that our adapters are not final classes');
非接口
$rules = Rule::allClasses()
->that(new ResideInOneOfTheseNamespaces('Tests\Integration'))
->should(new IsNotInterface())
->because('we want to be sure that we do not have interfaces in tests');
非枚举
$rules = Rule::allClasses()
->that(new ResideInOneOfTheseNamespaces('App\Controller'))
->should(new IsNotEnum())
->because('we want to be sure that all classes are not enum');
不依赖于命名空间
$rules = Rule::allClasses()
->that(new ResideInOneOfTheseNamespaces('App\Application'))
->should(new NotDependsOnTheseNamespaces('App\Infrastructure'))
->because('we want to avoid coupling between application layer and infrastructure layer');
不扩展其他类
$rules = Rule::allClasses()
->that(new ResideInOneOfTheseNamespaces('App\Controller\Admin'))
->should(new NotExtend('App\Controller\AbstractController'))
->because('we want to be sure that all admin controllers not extend AbstractController for security reasons');
在命名空间外没有依赖
$rules = Rule::allClasses()
->that(new ResideInOneOfTheseNamespaces('App\Domain'))
->should(new NotHaveDependencyOutsideNamespace('App\Domain', ['Ramsey\Uuid']))
->because('we want protect our domain except for Ramsey\Uuid');
没有匹配模式的名称
$rules = Rule::allClasses()
->that(new ResideInOneOfTheseNamespaces('App'))
->should(new NotHaveNameMatching('*Manager'))
->because('*Manager is too vague in naming classes');
位于命名空间内
$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');
不在命名空间内
$rules = Rule::allClasses()
->that(new Extend('App\Domain\Event'))
->should(new NotResideInOneOfTheseNamespaces('App\Application', 'App\Infrastructure'))
->because('we want to be sure that all events not reside in wrong layers');
您还可以定义组件并确保组件
- 不应依赖任何组件
- 可能依赖于特定组件
- 可能依赖于任何组件
规则构建器
Archcheck提供了一些构建器,使您能够为特定环境实现更易读的规则。
组件架构规则构建器
感谢这个构建器,您可以以更易读的方式定义组件并在它们之间实施依赖约束。
<?php
declare(strict_types=1);
use Modulith\ArchCheck\ClassSet;
use Modulith\ArchCheck\CLI\Config;
use Modulith\ArchCheck\Expression\ForClasses\HaveNameMatching;
use Modulith\ArchCheck\Expression\ForClasses\ResideInOneOfTheseNamespaces;
use Modulith\ArchCheck\RuleBuilders\Architecture\Architecture;
use Modulith\ArchCheck\Rules\Rule;
return static function (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);
};
解析时排除类
如果您想从解析器中排除某些类,您可以在配置文件中使用 except 函数,如下所示
$rules[] = Rule::allClasses()
->except('App\Controller\FolderController\*')
->that(new ResideInOneOfTheseNamespaces('App\Controller'))
->should(new HaveNameMatching('*Controller'))
->because('we want uniform naming');
您可以使用通配符或类的确切名称。
可选参数和选项
您可以在启动工具时添加参数。目前您可以添加以下参数和选项
-v: 使用此选项以详细模式启动ArchCheck以查看每个解析的文件--config: 使用此参数,您可以指定您的配置文件而不是默认配置。如下所示archcheck check --config=/project/yourConfigFile.php--target-php-version: 使用此参数,您可以指定解析器应使用哪个PHP版本。这可以用于调试问题和了解是否存在不同PHP版本的问题。支持的PHP版本是:7.1,7.2,7.3,7.4,8.0,8.1,8.2--stop-on-failure: 使用此选项后,进程将在第一个违规后立即结束。
只运行特定规则
由于某些原因,您可能只想运行特定规则,您可以使用 runOnlyThis 如此操作
$rules[] = Rule::allClasses()
->except('App\Controller\FolderController\*')
->that(new ResideInOneOfTheseNamespaces('App\Controller'))
->should(new HaveNameMatching('*Controller'))
->because('we want uniform naming')
->runOnlyThis();
致谢
此项目是 phparkitect/arkitect 的克隆。
我们决定进行分支,因为我们的PR变得过时,这意味着原始项目可能没有得到维护,或者他们不希望项目按照我们需要的方向发展。
无论如何,初始工作值得称赞,非常感激。