chief/chief

一款强大的独立命令总线包

2.0.0 2023-01-23 16:17 UTC

This package is auto-updated.

Last update: 2024-09-24 20:18:29 UTC


README

#Chief

Build Status Code Coverage Scrutinizer Code Quality SensioLabsInsight

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 个类组成

  1. Command: 一个包含一些数据(可能只是公共属性或 getter/setter)的小对象
  2. CommandHandler: 负责通过 handle($command) 方法执行命令
  3. 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\FooCommandHandlerCommands\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