eps90 / req2cmd-bundle
从HTTP请求中提取命令并发送到命令总线,如Tactician
Requires
- php: >=7.1
- league/tactician-bundle: ^1.0
- symfony/framework-bundle: ~2.3|~3.0|~4.0
Requires (Dev)
- jms/serializer-bundle: ^2.0
- matthiasnoback/symfony-config-test: ^3.0
- matthiasnoback/symfony-dependency-injection-test: ^2.1
- phpunit/phpunit: ^6.2
- satooshi/php-coveralls: ^2.0@dev
- symfony/serializer: ~2.3|~3.0|~4.0
Suggests
- jms/serializer-bundle: To deserialize commands using JMS Serializer
- symfony/serializer: To deserialize commands using Symfony serializer
This package is not auto-updated.
Last update: 2024-09-29 03:36:28 UTC
README
从HTTP请求中提取命令并发送到命令总线,如Tactician。
动机
最近我一直在用框架无关的代码和一些项目配合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配置属性定义字符串值仅适用于内置提取器。目前只允许serializer和jms_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