chief / chief
一款强大的独立命令总线包
Requires
- php: >=8.0
- psr/cache: ^1.0 || ^2.0 || 3.0
Requires (Dev)
- illuminate/container: *@dev
- illuminate/events: *@dev
- league/container: ~1.3
- phpspec/prophecy: ^1.16
- phpspec/prophecy-phpunit: ^2.0
- phpunit/phpunit: ^9.5
- psr/log: *@dev
This package is auto-updated.
Last update: 2024-09-24 20:18:29 UTC
README
#Chief
Chief 是一款为 PHP 5.4+ 设计的强大独立命令总线包。
内容
命令总线?
模块最常用的接口风格是使用过程,或对象方法。所以如果你想让模块计算一个合同的各项费用,你可能会有一个 BillingService 类,其中有一个执行计算的方法,调用方式如下
$billingService->calculateCharges($contract);
。面向命令的接口将会有每个操作的命令类,并且可以用类似这样的方式调用$cmd = new CalculateChargesCommand($contract); $cmd->execute();
。本质上,对于方法导向接口中的每个方法,你都有一个命令类。一个常见的变体是有一个独立的命令执行者对象,它实际运行命令。$command = new CalculateChargesCommand($contract); $commandBus->execute($command);
-- 来自 Martin Fowler 的博客 (代码示例已移植到 PHP)
Martin 提到的 'executor' 就是我们所称的命令总线。这个模式通常由 3 个类组成
Command
: 一个包含一些数据(可能只是公共属性或 getter/setter)的小对象CommandHandler
: 负责通过handle($command)
方法执行命令CommandBus
: 所有命令都通过总线execute($command)
方法传递,该方法是负责找到正确的CommandHandler
并调用handle($command)
方法的。
对于你应用程序中的每个 Command
,都应该有一个相应的 CommandHandler
。
以下示例演示了如何使用 Chief 处理注册新用户
use Chief\Chief, Chief\Command; class RegisterUserCommand implements Command { public $email; public $name; } class RegisterUserCommandHandler { public function handle(RegisterUserCommand $command) { Users::create([ 'email' => $command->email, 'name' => $command->name ]); Mailer::sendWelcomeEmail($command->email); } } $chief = new Chief; $registerUserCommand = new RegisterUserCommand; $registerUserCommand->email = 'adamnicholson10@gmail.com'; $registerUserCommand->name = 'Adam Nicholson'; $chief->execute($registerUserCommand);
安装
使用 composer require chief/chief
安装最新版本,或查看 Packagist。
不需要进一步设置,但如果你在使用框架,并希望确保我们能够很好地配合(与 DI 容器、事件处理器等),则可以使用以下桥梁。
Laravel
通过 composer 安装后,将以下内容添加到你的 app/config/app.php
文件中的 $providers
数组中
'Chief\Bridge\Laravel\LaravelServiceProvider'
使用
以下是我们将用于使用示例的命令/处理器
use Chief\Chief, Chief\Command; class MyCommand implements Command {} class MyCommandHandler { public function handle(MyCommand $command) { /* ... */ } }
自动处理器解析
当你将一个 Command
传递给 Chief::execute()
时,Chief 会自动搜索相关的 CommandHandler
并调用 handle()
方法
$chief = new Chief; $chief->execute(new MyCommand);
默认情况下,这将搜索与你的 Command
具有相同名称的 CommandHandler
,后缀为 'Handler',在当前命名空间和嵌套的 Handlers
命名空间中。
因此,Commands\FooCommand
将自动解析为 Commands\FooCommandHandler
或 Commands\Handlers\FooCommandHandler
,如果这两个类都存在。
想要实现自己的自动从命令解析处理器的机制?实现你自己的 Chief\CommandHandlerResolver
接口以修改自动解析行为。
通过类名绑定的处理器
如果您的处理器不遵循特定的命名约定,您可以通过类名显式地将命令绑定到处理器。
use Chief\Chief, Chief\NativeCommandHandlerResolver, Chief\Busses\SynchronousCommandBus; $resolver = new NativeCommandHandlerResolver(); $bus = new SynchronousCommandBus($resolver); $chief = new Chief($bus); $resolver->bindHandler('MyCommand', 'MyCommandHandler'); $chief->execute(new MyCommand);
通过对象绑定的处理器
或者,直接传递您的CommandHandler
实例
$resolver->bindHandler('MyCommand', new MyCommandHandler); $chief->execute(new MyCommand);
处理器作为匿名函数
有时您可能想快速编写一个处理器来处理您的Command
,而无需编写新的类。使用Chief,您可以通过传递匿名函数作为处理器来实现这一点。
$resolver->bindHandler('MyCommand', function (Command $command) { /* ... */ }); $chief->execute(new MyCommand);
自我处理的命令
或者,您可能希望简单地允许Command
对象自行执行。为此,只需确保您的Command
类也实现了CommandHandler
。
class SelfHandlingCommand implements Command, CommandHandler { public function handle(Command $command) { /* ... */ } } $chief->execute(new SelfHandlingCommand);
装饰器
假设您想记录每次命令执行。您可以在每个CommandHandler
中添加对日志记录器的调用来完成此操作,但一个更优雅的解决方案是使用装饰器。
注册装饰器
$chief = new Chief(new SynchronousCommandBus, [new LoggingDecorator($logger)]);
现在,每次调用Chief::execute()
时,命令将被传递给LoggingDecorator::execute()
,它将执行一些日志操作,然后将命令传递给相关的CommandHandler
。
Chief为您提供两个内置装饰器
- LoggingDecorator:将所有执行的日志记录到
Psr\Log\LoggerInterface
。 - EventDispatchingDecorator:在每次命令执行后向
Chief\Decorators\EventDispatcher
发送事件。 - CommandQueueingDecorator:如果命令实现了
Chief\QueueableCommand
,则将其放入队列以供稍后执行。(更多信息请参阅“队列命令”)。 - TransactionalCommandLockingDecorator:当执行实现
Chief\TransactionalCommand
的命令时锁定命令总线。(更多信息请参阅“事务命令”)。
注册多个装饰器
// Attach decorators when you instantiate $chief = new Chief(new SynchronousCommandBus, [ new LoggingDecorator($logger), new EventDispatchingDecorator($eventDispatcher) ]); // Or attach decorators later $chief = new Chief(); $chief->pushDecorator(new LoggingDecorator($logger)); $chief->pushDecorator(new EventDispatchingDecorator($eventDispatcher)); // Or manually stack decorators $chief = new Chief( new EventDispatchingtDecorator($eventDispatcher, new LoggingDecorator($logger, $context, new CommandQueueingDecorator($queuer, new TransactionalCommandLockingDecorator( new CommandQueueingDecorator($queuer, new SynchronousCommandBus() ) ) ) ) ) );
队列命令
命令通常用于对域进行“操作”(例如,发送电子邮件、创建用户、记录事件等)。对于这些不需要立即响应的命令类型,您可能希望将它们排队以供稍后执行。这就是CommandQueueingDecorator
发挥作用的地方。
首先,要使用CommandQueueingDecorator
,您必须首先使用您想要的队列包实现CommandQueuer
接口。
interface CommandQueuer { /** * Queue a Command for executing * * @param Command $command */ public function queue(Command $command); }
对于illuminate/queue的
CommandQueuer
实现包含在此处。
接下来,附加CommandQueueingDecorator
装饰器
$chief = new Chief(); $queuer = MyCommandBusQueuer(); $chief->pushDecorator(new CommandQueueingDecorator($queuer));
然后,在可以排队执行的任何命令中实现QueueableCommand
MyQueueableCommand implements Chief\QueueableCommand {}
然后正常使用Chief
$command = new MyQueueableCommand(); $chief->execute($command);
如果您向Chief传递任何实现QueueableCommand
的命令,它将被添加到队列中。任何不实现QueueableCommand
的命令将像往常一样立即执行。
如果您的命令实现了QueueableCommand
但您没有使用CommandQueueingDecorator
,则它们将像往常一样立即执行。因此,对于任何可能排队执行的命令,实现QueueableCommand
是良好的实践,即使您还没有使用队列装饰器也是如此。
缓存命令执行
CachingDecorator
可用于存储给定命令的执行返回值。
例如,您可能有一个FetchUerReportCommand
,以及一个关联的处理程序,该处理程序需要较长时间来生成“UserReport”。而不是每次都重新生成报告,只需让FetchUserReport
实现CacheableCommand
即可,其返回值将被缓存。
数据被缓存到psr/cache
(PSR-6)兼容的缓存库。
Chief不提供缓存库。您必须自己引入它,并将其作为构造函数参数传递给
CachingDecorator
。
示例
use Chief\CommandBus, Chief\CacheableCommand, Chief\Decorators\CachingDecorator; $chief = new Chief(); $chief->pushDecorator(new CachingDecorator( $cache, // Your library of preference implementing PSR-6 CacheItemPoolInterface. 3600 // Time in seconds that values should be cached for. 3600 = 1 hour. )); class FetchUserReportCommand implements CacheableCommand { } class FetchUserReportCommahdHandler { public function handle(FetchUserReportCommand $command) { return 'foobar'; } } $report = $chief->execute(new FetchUserReportCommand); // (string) "foo" handle() is called $report = $chief->execute(new FetchUserReportCommand); // (string) "foo" Value taken from cache $report = $chief->execute(new FetchUserReportCommand); // (string) "foo" Value taken from cache
事务命令
使用 TransactionalCommandLockingDecorator
可以帮助防止同时执行多个命令。在实践中,这意味着如果你在一个命令处理程序内部嵌套命令执行,嵌套命令将不会执行,直到第一个命令完成。
下面是一个例子
use Chief\CommandBus; use Chief\Command; use Chief\Decorators\TransactionalCommandLockingDecorator; class RegisterUserCommandHandler { public function __construct(CommandBus $bus, Users $users) { $this->bus = $bus; } public function handle(RegisterUserCommand $command) { $this->bus->execute(new RecordUserActivity('this-will-never-be-executed')); Users::create([ 'email' => $command->email, 'name' => $command->name ]); throw new Exception('Something unexpected; could not create user'); } } $chief = new Chief(); $chief->pushDecorator(new TransactionalCommandLockingDecorator()); $command = new RegisterUserCommand; $command->email = 'foo@example.com'; $command->password = 'password123'; $chief->execute($command);
这里发生了什么?当调用 $chief->execute(new RecordUserActivity('registered-user'))
时,该命令实际上被放入一个内存队列中,它将不会执行,直到 RegisterCommandHandler::handle()
完成。在这个例子中,由于我们展示了方法完成前抛出了异常,因此 RecordUserActivity
命令实际上从未被执行。
依赖注入容器集成
Chief 使用一个 CommandHandlerResolver
类,该类负责为给定的 Command
查找和实例化相关的 CommandHandler
。
如果你想使用自己的依赖注入容器来控制实际的实例化,只需创建一个实现 Chief\Container
的类,并将其传递给由 SynchronousCommandBus
消费的 CommandHandlerResolver
。
例如,如果你在使用 Laravel
use Chief\Resolvers\NativeCommandHandlerResolver, Chief\Chief, Chief\Busses\SynchronousCommandBus, Chief\Container; class IlluminateContainer implements Container { public function make($class) { return \App::make($class); } } $resolver = new NativeCommandHandlerResolver(new IlluminateContainer); $chief = new Chief(new SynchronousCommandBus($resolver)); $chief->execute(new MyCommand);
已经为以下容器提供了支持
Illuminate\Container
:
$container = new \Illuminate\Container\Container; $resolver = new NativeCommandHandlerResolver(new \Chief\Bridge\Laravel\IlluminateContainer($container)); $chief = new Chief(new \Chief\Busses\SynchronousCommandBus($resolver));
League\Container
:
$container = new \League\Container\Container; $resolver = new NativeCommandHandlerResolver(new \Chief\Bridge\League\LeagueContainer($container)); $chief = new Chief(new \Chief\Busses\SynchronousCommandBus($resolver));
贡献
我们欢迎对 Chief 的任何贡献。可以通过 GitHub 问题或拉取请求进行贡献。
许可
Chief 采用 MIT 许可证 - 详细信息请参阅 LICENSE.txt
文件
作者
Adam Nicholson - adamnicholson10@gmail.com