该软件包已被弃用且不再维护。作者建议使用 https://github.com/digital-craftsman-de/cqs-routing 软件包。

通过 CQS 在 Symfony 中降低变更成本

安装数: 20,043

依赖项: 0

建议者: 0

安全性: 0

星标: 20

关注者: 1

分支: 4

开放问题: 1

类型:symfony-bundle

v1.0.0 2024-08-23 11:47 UTC

README

Latest Stable Version PHP Version Require codecov Packagist Downloads Packagist License

安装和配置

通过 composer 安装包

composer require digital-craftsman/cqs-routing

然后,将以下 cqs-routing.php 文件添加到您的 config/packages 中,并用您的接口实例替换它

<?php

declare(strict_types=1);

use DigitalCraftsman\CQSRouting\DTOConstructor\SerializerDTOConstructor;
use DigitalCraftsman\CQSRouting\RequestDecoder\JsonRequestDecoder;
use DigitalCraftsman\CQSRouting\ResponseConstructor\EmptyResponseConstructor;
use DigitalCraftsman\CQSRouting\ResponseConstructor\SerializerJsonResponseConstructor;
// Automatically generated by Symfony though a config builder (see https://symfony.com.cn/doc/current/configuration.html#config-config-builder).
use Symfony\Config\CqsRoutingConfig;

return static function (CqsRoutingConfig $cqsRoutingConfig) {
    $cqsRoutingConfig->queryController()
        ->defaultRequestDecoderClass(JsonRequestDecoder::class)
        ->defaultDtoConstructorClass(SerializerDTOConstructor::class)
        ->defaultResponseConstructorClass(SerializerJsonResponseConstructor::class);

    $cqsRoutingConfig->commandController()
        ->defaultRequestDecoderClass(JsonRequestDecoder::class)
        ->defaultDtoConstructorClass(SerializerDTOConstructor::class)
        ->defaultResponseConstructorClass(EmptyResponseConstructor::class);
};

您可以在以下位置找到完整的配置(包括用 yaml 配置的示例):完整配置

该软件包包含请求解码器、DTO 构造函数和响应构造函数的实例。有了这些,您就可以立即使用它。当您想使用这些实例时,只需要创建自己的 DTO 验证器、请求数据转换器和处理器包装器。

在哪里以及如何使用这些实例,以下将进行说明。

原因

使用 Symfony 构建CRUD和REST API非常容易。有像参数转换器这样的组件,都是为了将数据快速传入控制器以处理逻辑。遗憾的是,尽管以REST思维构建端点非常快,但处理业务逻辑使得变更变得容易、独立和安全却非常困难。简而言之,我们以较低的引入成本为代价,换取了较低的变更成本。

使用CQS意味着端点和数据模型之间的依赖性更少,因此在处理另一个端点时破坏一个端点的表面积也更小。Symfony最近增加了对在控制器中直接构造DTO的支持,但仍有一些缺失的部分。

CQS路由软件包填补了这一空白,并且通过仅略微增加引入成本,大大降低了变更成本。

总体目标

该软件包使使用CQS变得更加容易,并具有以下目标

  1. 从业务逻辑的角度来看,使了解正在发生的事情(非常快且易于理解)。
  2. 通过广泛使用值对象来提高代码的安全性。
  3. 通过广泛使用类型来提高重构的安全性。
  4. 在业务逻辑和应用/基础设施逻辑之间添加清晰的边界。

方法

该软件包由两个起点组成,即 CommandControllerQueryController,以及以下组件

  • 请求验证器 (示例)
    在应用级别验证请求。
  • 请求解码器 (示例)
    解码请求并将其转换为数组结构的请求数据。
  • 请求数据转换器 (示例)
    转换先前生成的请求数据。
  • DTO构造函数 (示例)
    从请求数据生成命令或查询。
  • DTO验证器 (示例)
    验证创建的命令或查询。
  • 处理器 (示例)
    包含业务逻辑的命令或查询处理器。
  • 处理器包装器 (示例)
    将处理器包装起来,以执行准备/尝试/捕获逻辑。
  • 响应构造器 (示例)
    将处理器收集到的数据转换为响应。

控制器处理请求的过程以及何时使用哪个组件可以在这里描述

路由

通过Symfony路由,我们定义哪些组件的实例(如果相关)用于哪个路由。我们使用PHP文件而不是默认的YAML来定义路由,以提高类型安全性,并使得通过IDE重命名组件更容易。

一个路由可能看起来像这样

return static function (RoutingConfigurator $routes) {

    RouteBuilder::addCommandRoute(
        $routes,
        path: '/api/news/create-news-article-command',
        dtoClass: CreateNewsArticleCommand::class,
        handlerClass: CreateNewsArticleCommandHandler::class,
        dtoValidatorClasses: [
            UserIdValidator::class => null,
        ],
    );
    
};

您只需要定义与在cqs-routing.php配置中配置的默认值不同的组件。有关路由的更多信息。

命令示例

命令和查询是强类型值对象,它们已经验证了它们能做的事情。以下是一个用于创建新闻文章的示例命令

<?php

declare(strict_types=1);

namespace App\Domain\News\WriteSide\CreateNewsArticle;

use App\Helper\HtmlHelper;
use App\ValueObject\UserId;
use Assert\Assertion;
use DigitalCraftsman\CQSRouting\Command\Command;

final readonly class CreateNewsArticleCommand implements Command
{
    public function __construct(
        public UserId $userId,
        public string $title,
        public string $content,
        public bool $isPublished,
    ) {
        Assertion::notBlank($this->title);
        Assertion::maxLength($this->title, 255);
        
        Assertion::notBlank($this->content);
        Assertion::maxLength($this->content, 1000);
        HtmlHelper::assertValidHtml($this->content);
    }
}

因此,结构验证已经通过命令的创建完成,命令处理器只需处理业务逻辑验证。一个命令处理器可能看起来像这样

<?php

declare(strict_types=1);

namespace App\Domain\News\WriteSide\CreateNewsArticle;

use App\DomainService\UserCollection;
use App\Entity\NewsArticle;
use App\Time\Clock\ClockInterface;
use App\ValueObject\NewsArticleId;
use DigitalCraftsman\CQSRouting\Command\Command;
use DigitalCraftsman\CQSRouting\Command\CommandHandlerInterface;
use Doctrine\ORM\EntityManagerInterface;

final readonly class CreateNewsArticleCommandHandler implements CommandHandlerInterface
{
    public function __construct(
        private ClockInterface $clock,
        private UserRepository $userRepository,
        private EntityManagerInterface $entityManager,
    ) {
    }

    public function __invoke(CreateProductNewsArticleCommand $command): void
    {
        $commandExecutedAt = $this->clock->now();

        // Validate
        $requestingUser = $this->userRepository->getOne($command->userId);
        $requestingUser->mustNotBeLocked();
        $requestingUser->mustHavePermissionToWriteArticle();

        // Apply
        $this->createNewsArticle(
            $command->title,
            $command->content,
            $command->isPublished,
            $commandExecutedAt,
        );
    }

    private function createNewsArticle(
        string $title,
        string $content,
        bool $isPublished,
        \DateTimeImmutable $commandExecutedAt,
    ): void {
        $newsArticle = new NewsArticle(
            NewsArticleId::generateRandom(),
            $title,
            $content,
            $isPublished,
            $commandExecutedAt,
        );

        $this->entityManager->persist($newsArticle);
        $this->entityManager->flush();
    }
}

赞助商

Blackfire