eps90/req2cmd-bundle

从HTTP请求中提取命令并发送到命令总线,如Tactician

安装: 154

依赖关系: 0

建议者: 0

安全: 0

星标: 2

关注者: 2

分支: 0

开放问题: 5

类型:symfony-bundle

v1.0.0-rc8 2017-12-26 15:54 UTC

This package is not auto-updated.

Last update: 2024-09-29 03:36:28 UTC


README

从HTTP请求中提取命令并发送到命令总线,如Tactician

Latest Stable Version Latest Unstable Version License

Build Status Coverage Status Scrutinizer Code Quality Codacy Badge

SensioLabsInsight

动机

最近我一直在用框架无关的代码和一些项目配合CQRS方法,这样我就可以以干净和易于阅读的方式编写所有用例的单独类。当我开始将其与Symfony框架集成时,我发现每个控制器的操作看起来都一样:从请求创建命令,发送到命令总线,并从操作返回Response

我创建了这个库,以便将请求转换为命令并自动将其发送到命令总线,如Tactician。多亏了Symfony Router组件和Symfony Event Dispatcher的内核事件监听器,应用程序能够识别路由参数中的命令并将其转换为命令实例,这一切都归功于这个组件。

这个组件是可工作的并且已经准备好使用。但是,它可能需要一些工作才能适应每种情况。我希望完全框架无关的代码很快就会可用,这样您就可以使用您喜欢的任何框架。仍然需要从Symfony的Request类中分离出来并使用PSR7的RequestInterface实现。对其他命令总线的支持也是一个很好的功能。每个贡献都受欢迎!

需求

  • PHP 7.1+
  • Symfony 框架组件(或Symfony 标准版)- 2.3+|3.0+

根据使用情况,可能需要以下内容

  • Tactician 组件 0.4+
  • Symfony 序列化器(包含在Symfony 框架组件中)

安装

步骤 1: 打开命令控制台,进入项目的根目录并运行以下命令以使用Composer安装包

composer require eps90/req2cmd-bundle

步骤 2: 通过将其添加到app/AppKernel.php文件中注册的组件列表来启用组件

<?php

// ...
class AppKernel extends Kernel
{
    public function registerBundles(): array
    {
        $bundles = [
            // ...
            new Eps\Req2CmdBundle\Req2CmdBundle(),
            // ...
        ];
        
        // ...
    }
    
    // ...
}

用法

(文档正在编写中)

将路由转换为命令

此组件使用Symfony Router的能力来匹配配置命令的路由。在正常情况下,您只需在您的路由中设置一个_command_class参数

app.add_post:
  path: /add_post.{_format}
  methods: ['POST']
  defaults:
    _command_class: AppBundle\Command\AddPostCommand
    _format: ~

在这种情况下,一个事件监听器将尝试使用CommandExtractor(默认提取器是Symfony 序列化器)将请求内容转换为命令实例。结果命令实例将被保存在请求中的_command参数中。

<?php

// ...

final class PostController
{
    // ...
    public function addPostAction(Request $request): Response
    {
        // ...
        $command = $request->attributes->get('_command');
        // ...
    }
}

唯一的要求是在触发ExtractCommandFromRequestListener之前提供请求的格式(使用Request::setRequestFormat)。这可以通过已可用的组件完成,如FOSRestBundle,但我希望这样的监听器很快也会包含在这个组件中。

动作!

如果您不在路由中添加 _controller 参数,您的请求将自动发送到 ApiResponderAction,该动作负责从请求中提取命令并将其发送到命令总线。此外,关于请求发送的方法,它将返回适当的响应状态码。例如,对于成功的 POST 请求,您可以期望看到 201 状态码(201:已创建)。

自定义控制器

当然,您可以使用自己的控制器,带有标准的 _controller 参数。如果该参数已经定义,此包的监听器不会覆盖此参数。

反序列化命令

如果您的命令复杂且使用了一些嵌套类型,默认的 Symfony Serializer 可能无法将请求反序列化为您的命令。

此包包含一个反序列化器,它查找 DeserializableCommandInterface 实现并调用其上的 fromArray 构造函数。

<?php

use Eps\Req2CmdBundle\Command\DeserializableCommandInterface;

final class AddPost implements DeserializableCommandInterface
{
    // ...
    
    public function __construct(PostId $postId, PostContents $contents) 
    {
        $this->postId = $postId;
        $this->contents = $contents;
    }
    
    // ... getters
    
    public static function fromArray(array $commandProps): self 
    {
        return new self(
            new PostId($commandProps['id']),
            new PostContents(
                $commandProps['title'],
                $commandProps['author'],
                $commandProps['contents']
            )
         );
    }
}

然后,您的命令可以无缝地使用 CommandExtractor 进行反序列化。您可以自由地注册自己的反序列化器。

如果您不想使用默认的反序列化器,您可以在配置中禁用它。

# app/config.yml
# ...
req2cmd:
  extractor:
    use_cmd_denormalizer: false
# ...

您还可以设置一个 JMSSerializerCommandExtractor 作为您的提取器,并使用方便的类映射进行反序列化。

# src/AppBundle/Resources/config/jms_serializer/Command.AddPost.yml
AppBundle\Command\AddPost:
  properties:
    postId:
      type: AppBundle\Identity\PostId
    postContents:
      type: AppBundle\ValueObject\PostContents
# app/config.yml
# ...
req2cmd:
  extractor: jms_serializer
# ...

将路径参数附加到命令中

您可以将路由参数附加到命令反序列化中,就像它从客户端发送一样。假设您有一个映射到如下命令的路由

app.update_post_title:
  path: /posts/{id}.{_format}
  methods: ['PUT']
  defaults:
    _command_class: AppBundle\Command\UpdatePostTitle

并且您有如下命令

<?php

final class UpdatePostTitle
{
    private $postId;
    private $postTitle;
    
    public function __construct(int $postId, string $postTitle) 
    {
        $this->postId = $postId;
        $this->postTitle = $postTitle;
    }
    
    // ...
}

如您所见,UpdatePost 命令需要一个 id 以及一些字符串,以便允许更新帖子标题。

为了正确序列化该命令,请求内容中需要这两个参数。当然,您可以发送以下请求将您的命令发送到事件总线

PUT http://example.com/api/posts/4234.json
{
    "id": 4234,
    "title": "Updated title"
}

如您所见,id 属性存在于路径和请求体中。为了消除这种重复,您可以指定一个 路由参数 在反序列化中包含

app.update_post_title:
  path: /posts/{id}.{_format}
  defaults:
    _command_class: AppBundle\Command\UpdatePostTitle
    _command_properties:
      path:
        id: ~

然后,路由中的 id 将像请求体的一部分一样传递,并正确创建您的命令。然后,您的请求可能如下所示

PUT http://example.com/api/posts/4234.json
{
    "title": "Updated title"
}

然后一切都将按预期工作。

我所说的 路由参数 意味着 所有路由参数,因此如果您想附加,例如,一个 _format(是的,我知道,这是一个愚蠢的例子),您可以以同样的方式做。

更改路由参数名称

您可能希望在它到达提取器之前更改参数名称。鉴于上面的示例,序列化器可能需要在请求内容中有一个 post_id 而不是 id。名称可以通过在路由定义中传递参数名称的值来更改

app.update_post_title:
  path: /posts/{id}.{_format}
  defaults:
    _command_class: AppBundle\Command\UpdatePostTitle
    _command_properties:
      path:
        id: post_id

然后以下代码将工作

<?php

use Eps\Req2CmdBundle\Command\DeserializableCommandInterface;

final class UpdatePostTitle implements DeserializableCommandInterface
{
    // ...
    
    public static function fromArray(array $commandProps): self 
    {
        return new self($commandProps['post_id'], $commandProps['title']); 
    }   
}

必需的路由参数

PathParamsMapper 可以识别配置参数是否必需且不为空。为了允许它,在参数名称前加上一个感叹号

app.update_post_title:
  path: /posts/{id}.{_format}
  defaults:
    _format: ~
    _command_class: AppBundle\Command\UpdatePostTitle
    _command_properties:
      path:
        !_format: requested_format

在这种情况下,当 _format 参数等于 null 时,映射器将抛出 ParamMapperException

注册自定义参数映射器

默认参数映射器是 PathParamsMapper 类实例,它只负责提取路由参数。当然,您可以自由地注册自己的映射器,通过实现 ParamMapperInterface

完成之后,将其注册为服务并添加 req2cmd.param_mapper 标签。可选地,您可以设置一个优先级,以确保此映射器将尽早执行。优先级越高,服务越重要。

services:
# ...
  app.param_mapper.my_awesome_mapper:
    class: AppBundle\ParamMapper\MyAwesomeMapper
    tags:
      - { name: 'req2cmd.param_mapper', priority: 128 }

但我想要使用不同的提取器!

当然,为什么不呢!您需要创建一个实现 CommandExtractorInterface 接口的类。该接口只包含一个方法,extractFromRequest,您可以在其中访问 Request 和命令类。例如

<?php

use Eps\Req2CmdBundle\CommandExtractor\CommandExtractorInterface;
// ...

class DummyExtractor implements CommandExtractorInterface
{
    public function extractorFromRequest(Request $request, string $commandName)
    {
        // get the requested format from the Request object 
        if ($request->getRequestFormat() === 'json') {
            // decode contents
            $contents = json_decode($request->getContents(), true); 
        }
        
        // and return command instance
        return new $commandName($contents); 
    }
}

然后,在服务映射中注册此服务

services:
# ...
  app.extractor.my_extractor: 
    class: AppBundle\Extractor\DummyExtractor

通过设置 extractor.service_id 的值来调整项目配置

# ...
req2cmd:
  # ...
   extractor:
      service_id: app.extractor.my_extractor
# ...

注意:req2cmd.extractor 配置属性定义字符串值仅适用于内置提取器。目前只允许 serializerjms_serializer

...我还想有其他的命令总线!

你可以使用任何你想要的命令总线。唯一的要求是,你需要编写一个实现 CommandBusInterface 的适配器。

然后你可以将其注册为服务并调整配置

# app/config/config.yml
# ...
req2cmd:
  # ...
  command_bus:
    service_id: app.command_bus.my_command_bus
  # ...
# ...

注意:战术家(Tactician)是默认的命令总线,因此你不需要手动配置它。实际上,以下配置与缺失配置等效

# ...
req2cmd:
  # Short version:
  command_bus: tactician
  # Verbose version:
  command_bus:
    service_id: eps.req2cmd.command_bus.tactician
    name: default
# ...

配置命令总线

默认命令总线是 战术家命令总线(Tactician command bus),它允许你声明多个适应你需求的命令总线。在不更改配置的情况下,此包使用 tactician.commandbus.default 命令总线,这对于大多数情况来说已经足够了。然而,如果你需要设置不同的命令总线名称,你可以通过传递一个 name 参数到配置中来实现。

# app/config/config.yml
# ...
req2cmd:
  # ...
  command_bus:
    name: 'queued'
  # ...

在这种情况下将使用 tactician.commandbus.queued

设置监听器优先级

默认情况下,ExtractCommandFromRequestListener 将以优先级 0 在你的项目中注册。这意味着所有其他优先级设置为高于 0 的监听器将比 ExtractCommandFromRequestListener 先执行。

幸运的是,你可以通过在配置中设置适当的值轻松地更改这一点。

# app/config/config.yml
# ...
req2cmd:
  # ...
  listeners:
    extractor:
      priority: 128
  # ...

使用此类配置,此监听器将以 kernel.event_listener 的形式注册,并具有 priority 值 128。

禁用监听器

你可能想禁用监听器。要做到这一点,你需要将监听器的 enabled 属性设置为 false

# app/config/config.yml
# ...
req2cmd:
  listeners:
    extractor:
      enabled: false

或者甚至更简单

# app/config/config.yml
# ...
req2cmd:
  listeners:
    extractor: false

注意:你必须意识到,如果你禁用了 extractor 监听器,在亚洲的某个地方,一只可爱的小熊猫会死去。你不想这样,对吧?没有人想。每个人都爱熊猫。请记住这一点。

异常

此包中所有异常都实现了 Req2CmdExceptionInterface。目前配置了以下异常

  • ParamMapperException
    • ::noParamFound(代码 101)- 当在请求中找不到所需属性时
    • ::paramEmpty(代码 102)- 当找到所需属性但为空时

测试和贡献

此项目由 PHPUnit 测试覆盖。要运行它们,请输入

bin/phpunit