awd-studio / service-buses
An implementation of such kind of patterns as: Command Bus, Query Bus and Event Bus; in a single package; driven by a Dependency Injection Container
Requires
- php: ^8.2
- psr/container: ^1.0|^2.0
Requires (Dev)
- dg/bypass-finals: ^1.1
- php-coveralls/php-coveralls: ^2.1
- phpspec/prophecy-phpunit: ^2.0
- phpstan/phpstan: ^1.10
- phpstan/phpstan-phpunit: ^1.3
- phpunit/php-code-coverage: ^9.2
- phpunit/phpunit: ^9.0
- psalm/plugin-phpunit: ^0.18.4
- psalm/plugin-symfony: ^5.0
- vimeo/psalm: ^5.0
This package is auto-updated.
Last update: 2024-10-10 18:30:25 UTC
README
A simple library, to implement CQRS
-ish pattern on PHP projects.
Features:
- Neither messages nor handlers don't need to extend or implement any additional abstraction.
- A handler can be any of
callable
items. - Handlers can subscribe on any of parents or implementations of an event.
- Contains a decorator to register handles as services handled via
PSR-11
's container. - Contains a decorator to auto-subscribe handlers by a typehint on a message that it handles.
- Provides ready to go bus patterns such a
Command Bus
, aQuery Bus
and anEvent Bus
implementations.
Contents:
- Get started
- Handling messages
- Predefined buses
- Subscribe on parents
- Services as handlers
- Define custom bus
- Testing
Get started:
Requirenments:
- PHP 8.2+
- PSR-11 - compatible container (optional)
Install:
composer require awd-studio/service-buses
Handling messages:
A message, is nothing, but a simple PHP-object.
It can contain any data you need, but usually, it's better to provide some immutable messages, that can be serialized.
<?php class MyMessage {}
Anyway, you are able to extend or implement anything you need.
<?php interface MessageInterface {} abstract class ParentMessage {} final class MyMessage extends ParentMessage implements MessageInterface {}
A handler-locator is a repository for handlers.
With them, we can assign a handler to particular messages. Library provides some handler locators, for example - a locator to store handlers in memory:
<?php use AwdStudio\Bus\Handler\InMemoryHandlerLocator; $handlers = new InMemoryHandlerLocator(); // To assign a handler we can call a method `add`. // As a "messageId" we send the FCQN of a message that we assign on. // A handler must be any callable PHP-item. $handlers->add(\stdClass::class, static function (\stdClass $message): void {}); // Now, we've got a handler that handles a message of type "stdClass". // But, we can add more than one handler per message. // Actually, it's not limited, but keep in mind the patterns // such Command-bus or Query-bus that suppose to use the only one handler // per a message that they handle. // So, we can add more handlers to same message, for example a callable object: $handler = new class { public function __invoke(\stdClass $message): void {} }; $handlers->add(\stdClass::class, $handler); // So now, we have 2 handlers that are going to be released // when somebody tries get them: $handlers->get(\stdClass::class); // To check if there are some handlers for certain message // there is a method `has`: $handlers->has(\stdClass::class); // true|false
To handle a message, the bus needs to be called. For instance, we have a bus that extends provided SimpleBus.
We're gonna use a
<?php use AwdStudio\Bus\Handler\InMemoryHandlerLocator; // We need to use a handler locator, from which a bus will get handlers $bus = new class(new InMemoryHandlerLocator()) extends \AwdStudio\Bus\SimpleBus { // We need to provide a method that will handle our message public function handle(object $message): void { // Our parent allows us to iterate all handlers // that assigned to certain message foreach ($this->handleAll($message) as $result) { echo $result; } } }; // To use a bus, we call a provided method: $bus->handle(new \stdClass());
Predefined buses:
There are a few predefined buses:
-
\AwdStudio\Command\CommandBus
(The Command-bus pattern akkaC
inCQRS
)\AwdStudio\Command\SimpleCommandBus
- Handles a command, via single handler.
-
\AwdStudio\Query\QueryBus
(The Query-bus pattern akkaQ
inCQRS
)\AwdStudio\Query\SimpleQueryBus
- Handles a query, via single handler.
-
\AwdStudio\Event\EventBus
(Observer-subscriber pattern)\AwdStudio\Event\SimpleEventBus
- Dispatches an event, to each subscriber (can be>= 0
).
Command-bus:
<?php use AwdStudio\Bus\Handler\InMemoryHandlerLocator; use AwdStudio\Command\SimpleCommandBus; class MyCommand { // Messages might be any of PHP class. // No any of implementation or extending required. } $handlers = new InMemoryHandlerLocator(); // Register a handler. It can be any callable thing. $handlers->add(MyCommand::class, static function (MyCommand $command): void {}); $bus = new SimpleCommandBus($handlers); $bus->handle(new MyCommand());
Query-bus:
<?php use AwdStudio\Bus\Handler\InMemoryHandlerLocator; use AwdStudio\Query\SimpleQueryBus; class MyQuery { // Messages might be any of PHP class. // No any of implementation or extending required. } $handlers = new InMemoryHandlerLocator(); // Register a handler. It can be any callable thing. $handlers->add(MyQuery::class, static function (MyQuery $query): string { return 'foo'; }); $bus = new SimpleQueryBus($handlers); $result = $bus->handle(new MyQuery()); // Result will be: // -> prefix foo suffix
Event-bus:
<?php use AwdStudio\Bus\Handler\InMemoryHandlerLocator; use AwdStudio\Event\SimpleEventBus; class MyEvent { // Messages might be any of PHP class. // No any of implementation or extending required. } $subscribers = new InMemoryHandlerLocator(); // Register a handler. It can be any callable thing. $subscribers->add(MyEvent::class, static function (MyEvent $event): void {}); // As the event-bus pattern allows to provide any amount of subscribers // we cah add more of them: $subscribers->add(MyEvent::class, static function (MyEvent $event): void {}); $bus = new SimpleEventBus($subscribers); $bus->handle(new MyEvent()); // After that, the event is delivered to each subscriber.
Subscribe on parents
The library allows subscribing not only on a certain class, but on all of its parents - either a parent or an implementation from any level.
<?php use AwdStudio\Bus\Handler\ParentsAwareClassHandlerRegistry; use AwdStudio\Bus\Handler\PsrContainerClassHandlerRegistry; use Psr\Container\ContainerInterface; class MyPsr11Container implements ContainerInterface {} interface Foo {} abstract class Bar {} final class Baz extends Bar implements Foo {} class Handler { // You can subscribe on any of level public function __invoke(Foo $message): void {} // ..or public function __invoke(Bar $message): void {} // ..or public function __invoke(Baz $message): void {} } $handlerRegistry = new ParentsAwareClassHandlerRegistry(new PsrContainerClassHandlerRegistry(new MyPsr11Container()));
Services as handlers
Of course, to resolve the only callbacks as handlers is not such a convenient way to build projects.
Fortunately, we have standards as a PSR-11
for such common use-cases as implementation of DIP
.
And, the library provides ability to use those containers as service locators for resolving handlers as DI.
To use it, there is a decorator for a handler-locator, that can be used for registering handlers with just FCQN.
As a dependency it accepts any of Psr\Container\ContainerInterface
, that supposed to resolve handlers.
<?php use AwdStudio\Bus\Handler\PsrContainerClassHandlerRegistry; use AwdStudio\Bus\SimpleBus; use Psr\Container\ContainerInterface; class MyPsr11Container implements ContainerInterface { private $dependencies; public function __construct(array $dependencies) { $this->dependencies = $dependencies; } public function has($id): bool { return \in_array($id, $this->dependencies, true); } public function get($id): object { return $id(); } } class StdClassHandler { public function __invoke(\stdClass $message): void { $message->foo = 'foo'; } } $serviceLocator = new MyPsr11Container([StdClassHandler::class]); $handlerRegistry = new PsrContainerClassHandlerRegistry($serviceLocator); // To assign a handler use a defined method: $handlerRegistry->register(\stdClass::class, StdClassHandler::class); // And pass them as a handler-locator to a bus $bus = new class ($handlerRegistry) extends SimpleBus { public function handle(object $message): void { foreach ($this->handleAll($message) as $result) { echo $result; } } }; // After that, you can call handling as usual: $bus->handle(new \stdClass()); // The handler will be executed
Auto-register services
There is even a decorator to subscribe callbacks automatically, by their signature, that supposed to contain a type-hint as the very first parameter.
<?php use AwdStudio\Bus\Handler\AutoRegisterHandlersRegistryClass; use AwdStudio\Bus\Handler\PsrContainerClassHandlerRegistry; $psrRegistry = new PsrContainerClassHandlerRegistry(new MyPsr11Container()); $autoRegistry = new AutoRegisterHandlersRegistryClass($psrRegistry); // Now, you can add a callback to assign a handler automatically. // Just be sure, that it has a correct type-hint of a message that it handles. $handler = static function (\stdClass $message): void { }; $autoRegistry->autoAdd($handler); // It will be called within the stdClass' messages. // And this is not all it can! // If you use services as handlers - you also can register them automatically. // Suppose we have this handler, that can be resolved from our container: class Handler { public function __invoke(\stdClass $message): void { } } // We can register it like so: $autoRegistry->autoRegister(Handler::class); // That's all..
Using your own handling methods
If you don't like invokable services, or somehow need to use handlers that handle via different methods - this is not a problem at all.
Just pass the name of a method while registering:
<?php use AwdStudio\Bus\Handler\PsrContainerClassHandlerRegistry; class Handler { public function handle(\stdClass $message): void { } } // Any registry can manage with it out of the box $psrRegistry = new PsrContainerClassHandlerRegistry(new MyPsr11Container()); $psrRegistry->register(\stdClass::class, Handler::class, 'handle'); // The 3rd argument tells which method is in charge of handling.
Define custom bus
To define your own bus, you can extend one of predefined ones. You have 2 options:
<?php use AwdStudio\Bus\SimpleBus; class MyBus extends SimpleBus { public function handle(object $message): string { $result = ''; foreach ($this->handleAll($message) as $handled) { $result .= $handled; } return $result; } }
The SimpleBus provides you an ability to handle messages with only handles.
<?php use AwdStudio\Bus\SimpleBus; class MyBus extends SimpleBus { public function handle(object $message): string { $result = ''; foreach ($this->handleMessage($message) as $chain) { $result .= $chain(); } return $result; } }
Testing:
composer setup-dev
composer test