chief / chief
A powerful standalone command bus package
Installs: 30 086
Dependents: 0
Suggesters: 0
Security: 0
Stars: 51
Watchers: 2
Forks: 4
Open Issues: 1
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-10-24 20:29:42 UTC
README
#Chief
Chief is a powerful standalone command bus package for PHP 5.4+.
Contents
- What is a command bus
- Installation
- Usage
- Dependcy injection container integration
- License
- Contributing
- Author
Command Bus?
The most common style of interface to a module is to use procedures, or object methods. So if you want a module to calculate a bunch of charges for a contract, you might have a BillingService class with a method for doing the calculation, calling it like this
$billingService->calculateCharges($contract);
. A command oriented interface would have a command class for each operation, and be called with something like this$cmd = new CalculateChargesCommand($contract); $cmd->execute();
. Essentially you have one command class for each method that you would have in the method-oriented interface. A common variation is to have a separate command executor object that actually does the running of the command.$command = new CalculateChargesCommand($contract); $commandBus->execute($command);
-- From Martin Fowler's Blog (code samples haven ported to PHP):
That 'executor' Martin mentions is what we call the command bus. The pattern typically consists of 3 classes:
Command
: A tiny object containing some data (probably just some public properties or getters/setters)CommandHandler
: Responsible for running the command through ahandle($command)
methodCommandBus
: All commands are passed to the busexecute($command)
method, which is responsible for finding the rightCommandHandler
and calling thehandle($command)
method.
For every Command
in your application, there should be a corresponding CommandHandler
.
In the below example, we demonstrate how a command bus design could handle registering a new user in your system using 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);
Installation
Install the latest version with composer require chief/chief
, or see Packagist.
No further setup is required, however if you're using a framework and want to make sure that we play nicely (with DI Containers, Event handlers, etc), then use the bridges below.
Laravel
After installing via composer, add the below to the $providers
array in your app/config/app.php
:
'Chief\Bridge\Laravel\LaravelServiceProvider'
Usage
We'll use the below command/handler for the usage examples:
use Chief\Chief, Chief\Command; class MyCommand implements Command {} class MyCommandHandler { public function handle(MyCommand $command) { /* ... */ } }
Automatic handler resolution
When you pass a Command
to Chief::execute()
, Chief will automatically search for the relevant CommandHandler
and call the handle()
method:
$chief = new Chief; $chief->execute(new MyCommand);
By default, this will search for a CommandHandler
with the same name as your Command
, suffixed with 'Handler', in both the current namespace and in a nested Handlers
namespace.
So Commands\FooCommand
will automatically resolve to Commands\FooCommandHandler
or Commands\Handlers\FooCommandHandler
if either class exists.
Want to implement your own method of automatically resolving handlers from commands? Implement your own version of the Chief\CommandHandlerResolver
interface to modify the automatic resolution behaviour.
Handlers bound by class name
If your handlers don't follow a particular naming convention, you can explicitly bind a command to a handler by its class name:
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);
Handlers bound by object
Or, just pass your CommandHandler
instance:
$resolver->bindHandler('MyCommand', new MyCommandHandler); $chief->execute(new MyCommand);
Handlers as anonymous functions
Sometimes you might want to quickly write a handler for your Command
without having to write a new class. With Chief you can do this by passing an anonymous function as your handler:
$resolver->bindHandler('MyCommand', function (Command $command) { /* ... */ }); $chief->execute(new MyCommand);
Self-handling commands
Alternatively, you may want to simply allow a Command
object to execute itself. To do this, just ensure your Command
class also implements CommandHandler
:
class SelfHandlingCommand implements Command, CommandHandler { public function handle(Command $command) { /* ... */ } } $chief->execute(new SelfHandlingCommand);
Decorators
Imagine you want to log every command execution. You could do this by adding a call to your logger in every CommandHandler
, however a much more elegant solution is to use decorators.
Registering a decorator:
$chief = new Chief(new SynchronousCommandBus, [new LoggingDecorator($logger)]);
Now, whenever Chief::execute()
is called, the command will be passed to LoggingDecorator::execute()
, which will perform some log action, and then pass the command to the relevant CommandHandler
.
Chief provides you with two decorators out-the-box:
- LoggingDecorator: Log before and after all executions to a
Psr\Log\LoggerInterface
- EventDispatchingDecorator: Dispatch an event to a
Chief\Decorators\EventDispatcher
after every command execution. - CommandQueueingDecorator: Put the command into a Queue for later execution, if it implements
Chief\QueueableCommand
. (Read more under "Queued Commands") - TransactionalCommandLockingDecorator: Lock the command bus when a command implementing
Chief\TransactionalCommand
is being executed. (Read more under "Transactional Commands")
Registering multiple decorators:
// 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() ) ) ) ) ) );
Queued Commands
Commands are often used for 'actions' on your domain (eg. send an email, create a user, log an event, etc). For these type of commands where you don't need an immediate response you may wish to queue them to be executed later. This is where the CommandQueueingDecorator
comes in to play.
Firstly, to use the CommandQueueingDecorator
, you must first implement the CommandQueuer
interface with your desired queue package:
interface CommandQueuer { /** * Queue a Command for executing * * @param Command $command */ public function queue(Command $command); }
An implementation of
CommandQueuer
for illuminate/queue is included.
Next, attach the CommandQueueingDecorator
decorator:
$chief = new Chief(); $queuer = MyCommandBusQueuer(); $chief->pushDecorator(new CommandQueueingDecorator($queuer));
Then, implement QueueableCommand
in any command which can be queued:
MyQueueableCommand implements Chief\QueueableCommand {}
Then use Chief as normal:
$command = new MyQueueableCommand(); $chief->execute($command);
If you pass Chief any command which implements QueueableCommand
it will be added to the queue. Any commands which do not implement QueueableCommand
will be executed immediately as normal.
If your commands implement QueueableCommand
but you are not using the CommandQueueingDecorator
, then they will be executed immediately as normal. For this reason, it is good practice to implement QueueableCommand
for any commands which may be queued in the future, even if you aren't using the queueing decorator yet.
Cached Command Execution
The CachingDecorator
can be used to store the execution return value for a given command.
For example, you may have a FetchUerReportCommand
, and an associated handler which takes a significant time to generate the "UserReport". Rather than re-generating the report every time, simply make FetchUserReport
implement CacheableCommand
, and the return value will be cached.
Data is cached to a psr/cache
(PSR-6) compatible cache library.
Chief does not supply a cache library. You must require this yourself and pass it in as a consturctor argument to the
CachingDecorator
.
Example:
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
Transactional Commands
Using the TransactionalCommandLockingDecorator
can help to prevent more than 1 command being executed at any time. In practice, this means that you if you nest a command execution inside a command handler, the nested command will not be executed until the first command has completed.
Here's an example:
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);
So what's happening here? When $chief->execute(new RecordUserActivity('registered-user'))
is called, that command is actually dropped into an in-memory queue, which will not execute until RegisterCommandHandler::handle()
has finished. In this example, because we're showing that an Exception
is thrown before the method completes, the RecordUserActivity
command is never actually executed.
Dependency Injection Container Integration
Chief uses a CommandHandlerResolver
class which is responsible for finding and instantiating the relevant CommandHandler
for a given Command
.
If you want to use your own Dependency Injection Container to control the actual instantiation, just create your own class which implements Chief\Container
and pass it to the CommandHandlerResolver
which is consumed by SynchronousCommandBus
.
For example, if you're using 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);
Containers have already been provided for :
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));
Contributing
We welcome any contributions to Chief. They can be made via GitHub issues or pull requests.
License
Chief is licensed under the MIT License - see the LICENSE.txt
file for details
Author
Adam Nicholson - adamnicholson10@gmail.com