Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.
Symfony Messenger
Build your own Command | Query | Event Bus
Michał Kurzeja
@michalkurzeja
michal@accesto.com
CQS
Command Query Separation
Fetch value or change state, never both!
CQRS
Command
+handler
Query

+handler
Command Bus
Query Bus
Event Bus
Command | Query | Event Bus
Fundamental differences
Command Bus Query Bus Event Bus
# of handlers 1 1 0-N
Can return value No Yes No
Async? Might be Shouldn’t Might be
But this talk is not about CQRS….
Existing Message Bus solutions
Tactician
Simple Bus
Prooph Service Bus
Symfony Messenger
A PSR around the corner….
Symfony Messenger
composer require symfony/messenger
Unfortunately, a bit complicated approach
Example 1 - Simple command bus
Idea
Simple Todo list
class AddNewTodo
{
/**
* @var UuidInterface
*/
private $uuid;
/**
* @var string
*/
private $task;
/**
* @var DateTimeImmut...
class AddNewToDoHandler
{
/**
* @var ToDoRepository
*/
private $repository;
public function __invoke(AddNewTodo $addNewTod...
$c[AddNewToDoHandler::class] = function ($c) {
return new AddNewToDoHandler($c[ToDoRepository::class]);
};
DI config
$c['command_bus.handler_locator'] = function ($c) {
return new HandlersLocator([
AddNewTodo::class => [AddNewToDoHandler::...
$c['command_bus'] = function ($c) {
return new MessageBus(
[
new HandleMessageMiddleware($c['command_bus.handler_locator']...
$addNewTodo = AddNewTodo::add($uuid, $task, $deadline);
$this->container['command_bus']->dispatch($addNewTodo);
Middleware
Middleware 1 Middleware 2 Middleware 3
// logic
next() // logic
next() // logic
next()
// more logic
// more logic
// more...
Example 2 - additional middleware
Middleware - let’s add logging*
* Not really needed as it is already implemented
class LoggingMiddleware implements MiddlewareInterface
{
public function handle(Envelope $envelope, StackInterface $stack)...
Middleware use-cases
DB Transaction
Dispatch event
Validate command
….
Example 3 - Envelopes
Envelope - Request tracking
class RequestIdStamp implements StampInterface
{
/**
* @var string
*/
private $requestId;
public function __construct(stri...
$requestId = Uuid::uuid4()->toString();
$addNewTodo = AddNewTodo::add($uuid, $task, $deadline);
$this->container['command_...
$requestId = null;
$requestIdStamps = $envelope->all(RequestIdStamp::class);
if (count($requestIdStamps)) {
$requestId = $...
Example 4 - transports
Transport - csv file
class FileSender implements SenderInterface
{
public function send(Envelope $envelope): Envelope
{
$file = fopen($this->fi...
class FileReceiver implements ReceiverInterface
{
public function receive(callable $handler): void
{
$file = fopen($this->...
Query Bus
class ListTodo
{
}
class ListTodoHandler
{
/**
* @var ToDoRepository
*/
private $repository;
public function __construct(ToDoRepository $repo...
$c[ListTodoHandler::class] = function ($c) {
return new ListTodoHandler($c[ToDoRepository::class]);
};
$c['query_bus.handl...
/**
* @var $envelope Envelope
*/
$envelope = $this->container['query_bus']->dispatch(new ListTodo());
/**
* @var $handledS...
Event Bus
Send e-mail with new todo
class TodoAdded
{
/**
* @var UuidInterface
*/
private $uuid;
/**
* @var string
*/
private $task;
/**
* @var DateTimeImmuta...
class SendNewTodoToEmail
{
public function __invoke(TodoAdded $todoAdded)
{
printf("nNew todo e-mail notification: %sn", $...
class AddNewToDoHandler
{
/**
* @var MessageBus
*/
private $eventBus;
public function __invoke(AddNewTodo $addNewTodo)
{
(...
$c[SendNewTodoToEmail::class] = function ($c) {
return new SendNewTodoToEmail();
};
$c['event_bus.handler_locator'] = func...
Even easier with Symfony Framework
class DefaultController extends AbstractController
{
public function index(MessageBusInterface $bus)
{
$bus->dispatch(new ...
class SmsNotificationHandler implements MessageHandlerInterface
{
public function __invoke(SmsNotification $message)
{
// ...
Easier routing
Debugging
https://symfony.com/doc/current/messenger.html
And more
Michał Kurzeja
@michalkurzeja
michal@accesto.com
Questions?
Symfony messenger - PHPers Summit 2019
Upcoming SlideShare
Loading in …5
×

Symfony messenger - PHPers Summit 2019

74 views

Published on

Build your own message (command/query/event) bus on top of Symfony Messenger and learn all its extension points.

Published in: Software
  • Be the first to comment

  • Be the first to like this

Symfony messenger - PHPers Summit 2019

  1. 1. Symfony Messenger Build your own Command | Query | Event Bus Michał Kurzeja @michalkurzeja michal@accesto.com
  2. 2. CQS Command Query Separation Fetch value or change state, never both!
  3. 3. CQRS Command +handler Query
 +handler
  4. 4. Command Bus Query Bus Event Bus
  5. 5. Command | Query | Event Bus Fundamental differences
  6. 6. Command Bus Query Bus Event Bus # of handlers 1 1 0-N Can return value No Yes No Async? Might be Shouldn’t Might be
  7. 7. But this talk is not about CQRS….
  8. 8. Existing Message Bus solutions Tactician Simple Bus Prooph Service Bus Symfony Messenger A PSR around the corner….
  9. 9. Symfony Messenger composer require symfony/messenger
  10. 10. Unfortunately, a bit complicated approach
  11. 11. Example 1 - Simple command bus
  12. 12. Idea Simple Todo list
  13. 13. class AddNewTodo { /** * @var UuidInterface */ private $uuid; /** * @var string */ private $task; /** * @var DateTimeImmutable */ private $deadline; /** * @var bool */ private $done; Command
  14. 14. class AddNewToDoHandler { /** * @var ToDoRepository */ private $repository; public function __invoke(AddNewTodo $addNewTodo) { $todo = new ToDo( $addNewTodo->uuid(), $addNewTodo->task(), $addNewTodo->done(), $addNewTodo->deadline() ); $this->repository->save($todo); } } Handler
  15. 15. $c[AddNewToDoHandler::class] = function ($c) { return new AddNewToDoHandler($c[ToDoRepository::class]); }; DI config
  16. 16. $c['command_bus.handler_locator'] = function ($c) { return new HandlersLocator([ AddNewTodo::class => [AddNewToDoHandler::class => $c[AddNewToDoHandler::class]], ]); }; Map Command -> Handler
  17. 17. $c['command_bus'] = function ($c) { return new MessageBus( [ new HandleMessageMiddleware($c['command_bus.handler_locator']), ] ); };
  18. 18. $addNewTodo = AddNewTodo::add($uuid, $task, $deadline); $this->container['command_bus']->dispatch($addNewTodo);
  19. 19. Middleware
  20. 20. Middleware 1 Middleware 2 Middleware 3 // logic next() // logic next() // logic next() // more logic // more logic // more logic
  21. 21. Example 2 - additional middleware
  22. 22. Middleware - let’s add logging* * Not really needed as it is already implemented
  23. 23. class LoggingMiddleware implements MiddlewareInterface { public function handle(Envelope $envelope, StackInterface $stack): Envelope { $message = $envelope->getMessage(); $this->logger->debug(sprintf('Started handling of %s', get_class($message))); $result = $stack->next()->handle($envelope, $stack); $this->logger->debug(sprintf('Finished handling of %s', get_class($message))); return $result; } }
  24. 24. Middleware use-cases DB Transaction Dispatch event Validate command ….
  25. 25. Example 3 - Envelopes
  26. 26. Envelope - Request tracking
  27. 27. class RequestIdStamp implements StampInterface { /** * @var string */ private $requestId; public function __construct(string $requestId) { $this->requestId = $requestId; } public function requestId(): string { return $this->requestId; } }
  28. 28. $requestId = Uuid::uuid4()->toString(); $addNewTodo = AddNewTodo::add($uuid, $task, $deadline); $this->container['command_bus']->dispatch( (new Envelope($addNewTodo))->with(new RequestIdStamp($requestId)) );
  29. 29. $requestId = null; $requestIdStamps = $envelope->all(RequestIdStamp::class); if (count($requestIdStamps)) { $requestId = $requestIdStamps[0]->requestId(); } $this->logger->debug(sprintf('[%s] Started handling of %s', $requestId, get_class($message))); $result = $stack->next()->handle($envelope, $stack); $this->logger->debug(sprintf('[%s] Finished handling of %s', $requestId, get_class($message)));
  30. 30. Example 4 - transports
  31. 31. Transport - csv file
  32. 32. class FileSender implements SenderInterface { public function send(Envelope $envelope): Envelope { $file = fopen($this->filePath, 'a'); $message = $envelope->getMessage(); fputcsv( $file, [ $message->uuid()->toString(), $message->task(), $message->deadline() ? $message->deadline()->format('Y-m-d') : '', $message->done() ? 'Y' : 'N', ] ); fclose($file); return $envelope; } }
  33. 33. class FileReceiver implements ReceiverInterface { public function receive(callable $handler): void { $file = fopen($this->filePath, 'r'); while (!$this->shouldStop) { while (($data = fgetcsv($file)) !== false) { try { $deadline = new DateTimeImmutable($data[3]); } catch (Exception $e) { $deadline = null; } $addTodo = new AddNewTodo( Uuid::fromString($data[0]), $data[1], $data[2] === 'Y', $deadline ); $handler(new Envelope($addTodo)); } sleep(1); } fclose($file); } }
  34. 34. Query Bus
  35. 35. class ListTodo { }
  36. 36. class ListTodoHandler { /** * @var ToDoRepository */ private $repository; public function __construct(ToDoRepository $repository) { $this->repository = $repository; } public function __invoke(ListTodo $listTodo) { return $this->repository->findAll(); } }
  37. 37. $c[ListTodoHandler::class] = function ($c) { return new ListTodoHandler($c[ToDoRepository::class]); }; $c['query_bus.handler_locator'] = function ($c) { return new HandlersLocator([ ListTodo::class => [ListTodoHandler::class => $c[ListTodoHandler::class]] ]); }; $c['query_bus'] = function ($c) { return new MessageBus( [ new HandleMessageMiddleware($c['query_bus.handler_locator']), ] ); };
  38. 38. /** * @var $envelope Envelope */ $envelope = $this->container['query_bus']->dispatch(new ListTodo()); /** * @var $handledStamp HandledStamp */ $handledStamp = $envelope->last(HandledStamp::class); $todos = $handledStamp->getResult();
  39. 39. Event Bus
  40. 40. Send e-mail with new todo
  41. 41. class TodoAdded { /** * @var UuidInterface */ private $uuid; /** * @var string */ private $task; /** * @var DateTimeImmutable */ private $deadline; /** * @var bool */ private $done; public static function fromCommand(AddNewTodo $addNewTodo): TodoAdded { return new self($addNewTodo->uuid(), $addNewTodo->task(), $addNewTodo->deadline(), $addNewTodo->done()); }
  42. 42. class SendNewTodoToEmail { public function __invoke(TodoAdded $todoAdded) { printf("nNew todo e-mail notification: %sn", $todoAdded->task()); } }
  43. 43. class AddNewToDoHandler { /** * @var MessageBus */ private $eventBus; public function __invoke(AddNewTodo $addNewTodo) { (…) $todoAdded = TodoAdded::fromCommand($addNewTodo); $this->eventBus->dispatch($todoAdded); } }
  44. 44. $c[SendNewTodoToEmail::class] = function ($c) { return new SendNewTodoToEmail(); }; $c['event_bus.handler_locator'] = function ($c) { return new HandlersLocator([ TodoAdded::class => [SendNewTodoToEmail::class => $c[SendNewTodoToEmail::class]] ]); }; $c['event_bus'] = function ($c) { return new MessageBus( [ new HandleMessageMiddleware($c['event_bus.handler_locator'], true), ] ); }; Allow no handler
  45. 45. Even easier with Symfony Framework
  46. 46. class DefaultController extends AbstractController { public function index(MessageBusInterface $bus) { $bus->dispatch(new SmsNotification('A string to be sent...')); } }
  47. 47. class SmsNotificationHandler implements MessageHandlerInterface { public function __invoke(SmsNotification $message) { // do something with it. } } No extra DI config needed, even tagging!
  48. 48. Easier routing
  49. 49. Debugging
  50. 50. https://symfony.com/doc/current/messenger.html And more
  51. 51. Michał Kurzeja @michalkurzeja michal@accesto.com Questions?

×