digital-craftsman/cqs-routing

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

安装: 115

依赖项: 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