modulith/arch-check

在您的PHP应用程序中强制实施架构约束

v0.1.0 2023-10-01 20:29 UTC

This package is auto-updated.

Last update: 2024-08-29 22:58:16 UTC


README

Software License GitLab tag (latest by SemVer)

build status coverage report

索引

  1. 简介
  2. 安装
  3. 使用
  4. 可用规则
  5. 规则构建器
  6. 致谢

简介

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变得过时,这意味着原始项目可能没有得到维护,或者他们不希望项目按照我们需要的方向发展。

无论如何,初始工作值得称赞,非常感激。