Taming Command Bus Krzysztof Menżyk @kmenzyk
Taming
Command Bus
Krzysztof Menżyk @kmenzyk
About me
Technical Leader at Test infectedBelieves that software is a craftLoves domain modelling
Homebrewer & squash player
This talk is not
This talk is not
DDD*
This talk is not
CQRS*
Back to the
PAST
BEGANIt all
with ...
class ProductController extends Controller{ //...
public function updateAction($id) { if (false === $this->get('security.context')->isGranted('ROLE_ADMIN')) { throw new AccessDeniedException(); }
$em = $this->getDoctrine()->getManager(); $product = $em->getRepository('AcmeDemoBundle:Product')->find($id);
if (!$product) { throw $this->createNotFoundException('Unable to find Product entity.'); }
$request = $this->getRequest(); $editForm = $this->createForm(new ProductType(), $product); $editForm->bind($request);
if ($editForm->isValid()) { $product->setLastUpdated(new \DateTime); $em->flush();
$message = \Swift_Message::newInstance() ->setSubject('Product updated') ->setFrom($this->container->getParameter('acme.product_email.from')) ->setTo($this->container->getParameter('acme.product_email.to')) ->setBody($this->renderView( 'AcmeDemoBundle:Product:email.txt.twig', array('product' => $product)) ) ; $this->get('mailer')->send($message);
return $this->redirect( $this->generateUrl('product', array('id' => $id)) ); }
return new Response( $this->renderView( 'AcmeDemoBundle:Product:edit.html.twig', array( 'product' => $product, 'edit_form' => $editForm->createView(), ) ) ); }}
Controller Driven Design
„approach”
class ProductController extends Controller{ //...
public function updateAction($id) { if (false === $this->get('security.context')->isGranted('ROLE_ADMIN')) { throw new AccessDeniedException(); }
$em = $this->getDoctrine()->getManager(); $product = $em->getRepository('AcmeDemoBundle:Product')->find($id);
if (!$product) { throw $this->createNotFoundException('Unable to find Product entity.'); }
$request = $this->getRequest(); $editForm = $this->createForm(new ProductType(), $product); $editForm->bind($request);
if ($editForm->isValid()) { $product->setLastUpdated(new \DateTime); $em->flush();
$message = \Swift_Message::newInstance() ->setSubject('Product updated') ->setFrom($this->container->getParameter('acme.product_email.from')) ->setTo($this->container->getParameter('acme.product_email.to')) ->setBody($this->renderView( 'AcmeDemoBundle:Product:email.txt.twig', array('product' => $product)) ) ; $this->get('mailer')->send($message);
return $this->redirect( $this->generateUrl('product', array('id' => $id)) ); }
return new Response( $this->renderView( 'AcmeDemoBundle:Product:edit.html.twig', array( 'product' => $product, 'edit_form' => $editForm->createView(), ) ) ); }}
class ProductController extends Controller{ //...
public function updateAction($id) { if (false === $this->get('security.context')->isGranted('ROLE_ADMIN')) { throw new AccessDeniedException(); }
$em = $this->getDoctrine()->getManager(); $product = $em->getRepository('AcmeDemoBundle:Product')->find($id);
if (!$product) { throw $this->createNotFoundException('Unable to find Product entity.'); }
$request = $this->getRequest(); $editForm = $this->createForm(new ProductType(), $product); $editForm->bind($request);
if ($editForm->isValid()) { $product->setLastUpdated(new \DateTime); $em->flush();
$message = \Swift_Message::newInstance() ->setSubject('Product updated') ->setFrom($this->container->getParameter('acme.product_email.from')) ->setTo($this->container->getParameter('acme.product_email.to')) ->setBody($this->renderView( 'AcmeDemoBundle:Product:email.txt.twig', array('product' => $product)) ) ; $this->get('mailer')->send($message);
return $this->redirect( $this->generateUrl('product', array('id' => $id)) ); }
return new Response( $this->renderView( 'AcmeDemoBundle:Product:edit.html.twig', array( 'product' => $product, 'edit_form' => $editForm->createView(), ) ) ); }}
LAYERS
Presentation
Presentation
Domain
Presentation
Service
Domain
Presentation
Service
Domain
Infrastructure
Presentation
Service
Domain
Infrastructure
Defines an application's boundary with a layer of services.
Service Layer
Establishes set of available operations.
Service Layer
Coordinates the application's response in each operation.
Service Layer
USE Cases
USER STORIES
Application Layer
class Library{ public function borrowBook($bookId, $readerId) { // ... }}
class LibraryController{ public function borrowAction(ServerRequestInterface $request) { // Forms, etc.
$this->library->borrowBook($bookId, $readerId); // Response }}
class Library{ public function borrowBook($bookId, $readerId) { if (null === $bookId || null === $readerId) { throw new \InvalidArgumentException(); }
$this->logging->debug('A reader is borrowing a book');
$this->connection->beginTransaction(); try { $book = $this->books->get($bookId); $book->borrowBy($readerId); $this->books->add($book); $this->connection->commit(); } catch (\Exception $e) { $this->connection->rollBack(); $this->logging->debug('Ooops! Bad luck.'); throw $e; }
$this->logging->debug('Success!'); }}
class Library{ public function borrowBook($bookId, $readerId) { if (null === $bookId || null === $readerId) { throw new \InvalidArgumentException(); }
$this->logging->debug('A reader is borrowing a book');
$this->connection->beginTransaction(); try { $book = $this->books->get($bookId); $book->borrowBy($readerId); $this->books->add($book); $this->connection->commit(); } catch (\Exception $e) { $this->connection->rollBack(); $this->logging->debug('Ooops! Bad luck.'); throw $e; }
$this->logging->debug('Success!'); }}
class Library{ public function borrowBook($bookId, $readerId) { if (null === $bookId || null === $readerId) { throw new \InvalidArgumentException(); }
$this->logging->debug('A reader is borrowing a book');
$this->connection->beginTransaction(); try { $book = $this->books->get($bookId); $book->borrowBy($readerId); $this->books->add($book); $this->connection->commit(); } catch (\Exception $e) { $this->connection->rollBack(); $this->logging->debug('Ooops! Bad luck.'); throw $e; }
$this->logging->debug('Success!'); }}
class Library{ public function borrowBook($bookId, $readerId) { if (null === $bookId || null === $readerId) { throw new \InvalidArgumentException(); }
$this->logging->debug('A reader is borrowing a book');
$this->connection->beginTransaction(); try { $book = $this->books->get($bookId); $book->borrowBy($readerId); $this->books->add($book); $this->connection->commit(); } catch (\Exception $e) { $this->connection->rollBack(); $this->logging->debug('Ooops! Bad luck.'); throw $e; }
$this->logging->debug('Success!'); }}
class Library{ public function borrowBook($bookId, $readerId) { if (null === $bookId || null === $readerId) { throw new \InvalidArgumentException(); }
$this->logging->debug('A reader is borrowing a book');
$this->connection->beginTransaction(); try { $book = $this->books->get($bookId); $book->borrowBy($readerId); $this->books->add($book); $this->connection->commit(); } catch (\Exception $e) { $this->connection->rollBack(); $this->logging->debug('Ooops! Bad luck.'); throw $e; }
$this->logging->debug('Success!'); }}
borrowBook($bookId, $readerId)
new BorrowBook($bookId, $readerId)
COMMAND
not a class/objectMESSAGE
Messaging Flavors
by @mathiasverraes
Messaging Flavors
by @mathiasverraes
Imperative
Messaging Flavors
by @mathiasverraes
Imperative
Command
Messaging Flavors
by @mathiasverraes
Imperative Interrogatory
Command
Messaging Flavors
by @mathiasverraes
Imperative Interrogatory
Command QUERY
Messaging Flavors
by @mathiasverraes
Imperative Interrogatory Informational
Command QUERY
Messaging Flavors
by @mathiasverraes
Imperative Interrogatory Informational
Command QUERY EVENT
Messaging Flavors
by @mathiasverraes
Imperative Interrogatory Informational
Command QUERY EVENT
Captures
THE INTENTof the user
Supports
UbiquitousLANGUAGE
Borrow BookGive Book BackRegister User
Start SubscriptionHire Employee
Contains
THE INPUTto carry out the task
final class BorrowBook{ public $bookId; public $readerId;
public function __construct($bookId, $readerId) { $this->bookId = $bookId; $this->readerId = $readerId; }}
Should be
IMMUTABLE
final class BorrowBook{ private $bookId; private $readerId;
public function __construct($bookId, $readerId) { $this->bookId = $bookId; $this->readerId = $readerId; }
public function getBookId() { return $this->bookId; }
public function getReaderId() { return $this->readerId; }}
Must be
VALID
final class BorrowBook{ private $bookId; private $readerId;
public function __construct($bookId, $readerId) { Assertion::notNull($bookId); Assertion::notNull($readerId); $this->bookId = $bookId; $this->readerId = $readerId; }
public function getBookId() { return $this->bookId; }
public function getReaderId() { return $this->readerId; }}
INTERPRETHow to
a command?
COMMANDHandler
per commandONE HANDLER
Returns
NO VALUE
final class BorrowBookHandler{ public function handle(BorrowBook $command) { $this->logging->debug('A reader is borrowing a book');
$this->connection->beginTransaction();
try { $book = $this->books->get($command->getBookId()); $book->borrowBy($command->getReaderId());
$this->books->add($book);
$this->connection->commit(); } catch (\Exception $e) { $this->connection->rollBack();
$this->logging->debug('Ooops! Bad luck.');
throw $e; }
$this->logging->debug('Success!'); }}
final class BorrowBookHandler{ public function handle(BorrowBook $command) { $this->logging->debug('A reader is borrowing a book');
$this->connection->beginTransaction();
try { $book = $this->books->get($command->getBookId()); $book->borrowBy($command->getReaderId());
$this->books->add($book);
$this->connection->commit(); } catch (\Exception $e) { $this->connection->rollBack();
$this->logging->debug('Ooops! Bad luck.');
throw $e; }
$this->logging->debug('Success!'); }}
One more thing ...
The BUS
COMMANDBUS
Hands over
THE COMMANDto
THE HANDLEr
Handler C
CommandBus
Handler BHandler A
Handler C
Command C
CommandBus
Handler BHandler A
Handler C
CommandBus
Handler BHandler A
Command C
Handler C
CommandBus
Handler BHandler A Command C
Handler C
CommandBus
Handler BHandler A
Single
ENTRY POINTto the application
interface CommandBus{ public function handle($command);}
class LibraryController{ public function borrowAction(ServerRequestInterface $request) { // Forms, etc.
$this->commandBus->handle(new BorrowBook($bookId, $readerId));
// Response }}
class SomeOtherController{ public function someOtherAction(ServerRequestInterface $request) { // Forms, etc.
$command = $form->getData(); $this->commandBus->handle($command);
// Response }}
class SomeOtherController{ public function someOtherAction(ServerRequestInterface $request) { // Forms, etc.
$command = $form->getData(); $this->commandBus->handle($command);
// Response }}
class BorrowBookCli{ protected function execute(InputInterface $input, OutputInterface $output) { // get $command from the input somehow $this->commandBus->handle($command);
// ... }}
IMPLEMENTLet's
the stupid thing
class SimpleCommandBus implements CommandBus{ private $handlers;
public function __construct(array $handlers) { $this->handlers = $handlers; }
public function handle($command) { $commandName = get_class($command);
if (!isset($this->handlers[$commandName])) { throw new \InvalidArgumentException('No handler'); } $this->handlers[$commandName]->handle($command); }}
new SimpleCommandBus([ BorrowBook::class => $borrowBookHandler, GiveBookBack::class => $giveBookBackHandler,]);
That's it?
Do not
TRY ITat home
class SimpleCommandBus implements CommandBus{ private $handlers;
public function __construct(array $handlers) { $this->handlers = $handlers; }
public function handle($command) { $commandName = get_class($command);
if (!isset($this->handlers[$commandName])) { throw new \InvalidArgumentException('No handler'); } $this->handlers[$commandName]->handle($command); }}
Cross cutting concerns
interface CommandBus{ public function handle($command);}
DECORATOR
class CommandBusWithAddedBehavior implements CommandBus{ public function __construct(CommandBus $originalCommandBus) { $this->originalCommandBus = $originalCommandBus; }
public function handle($command) { // do anything you want
$this->originalCommandBus->handle($command);
// do even more }}
new CommandBusWithAddedBehavior( new SimpleCommandBus([ BorrowBook::class => $borrowBookHandler, GiveBookBack::class => $giveBookBackHandler, ]));
Command Bus
Decorator
Decorator
Command
final class BorrowBookHandler{ public function handle(BorrowBook $command) { $this->logging->debug('A reader is borrowing a book');
$this->connection->beginTransaction();
try { $book = $this->books->get($command->getBookId()); $book->borrowBy($command->getReaderId());
$this->books->add($book);
$this->connection->commit(); } catch (\Exception $e) { $this->connection->rollBack();
$this->logging->debug('Ooops! Bad luck.');
throw $e; }
$this->logging->debug('Success!'); }}
class TransactionalCommandBus implements CommandBus{ public function __construct($innerCommandBus, $connection) { $this->innerCommandBus = $innerCommandBus; $this->connection = $connection; }
public function handle($command) { $this->connection->beginTransaction(); try { $this->innerCommandBus->handle($command); $this->connection->commit(); } catch (\Exception $exception) { $this->connection->rollBack();
throw $e; } }}
final class BorrowBookHandler{ public function handle(BorrowBook $command) { $this->logging->debug('A reader is borrowing a book');
$this->connection->beginTransaction();
try { $book = $this->books->get($command->getBookId()); $book->borrowBy($command->getReaderId());
$this->books->add($book);
$this->connection->commit(); } catch (\Exception $e) { $this->connection->rollBack();
$this->logging->debug('Ooops! Bad luck.');
throw $e; }
$this->logging->debug('Success!'); }}
class LoggingCommandBus implements CommandBus{ public function __construct($innerCommandBus, $logger) { $this->innerCommandBus = $innerCommandBus; $this->logger = $logger; }
public function handle($command) { $commandClass = get_class($command);
$this->logger->debug('Started handling ' . $commandClass);
try { $this->innerCommandBus->handle($command); } catch (\Exception $exception) { $this->logger->debug('Error while handling ' . $commandClass);
throw $e; }
$this->logger->debug('Finished handling ' . $commandClass); }}
final class BorrowBookHandler{ public function handle(BorrowBook $command) { $this->logging->debug('A reader is borrowing a book');
$this->connection->beginTransaction();
try { $book = $this->books->get($command->getBookId()); $book->borrowBy($command->getReaderId());
$this->books->add($book);
$this->connection->commit(); } catch (\Exception $e) { $this->connection->rollBack();
$this->logging->debug('Ooops! Bad luck.');
throw $e; }
$this->logging->debug('Success!'); }}
final class BorrowBookHandler{ public function handle(BorrowBook $command) { $book = $this->books->get($command->getBookId()); $book->borrowBy($command->getReaderId());
$this->books->add($book);'Success!'); }}
Easy to
TEST
But...
new LoggingCommandBus( new TransactionalCommandBus( new SimpleCommandBus([ BorrowBook::class => $borrowBookHandler, GiveBookBack::class => $giveBookBackHandler, ]), $connection ), $logger);
Can we do better?
Yes
We can!
CHAIN OF RESPONSIBILITY
interface CommandBusMiddleware{ public function handle($command, callable $next);}
Command Handler
Middleware
Command
Middleware
Middleware
class AddedBehaviorMiddleware implements CommandBusMiddleware{ public function handle($command, callable $next) { // do anything you want
$next($command);
// do even more }}
new CommandBusSupportingMiddleware([ new AddedBehaviorMiddleware(), new CommandHandlerMiddleware([ BorrowBook::class => $borrowBookHandler, GiveBookBack::class => $giveBookBackHandler, ])]);
new CommandBusSupportingMiddleware([ new LoggingMiddleware($logger), new TransactionalMiddleware($connection), new CommandHandlerMiddleware([ BorrowBook::class => $borrowBookHandler, GiveBookBack::class => $giveBookBackHandler, ])]);
TransactionsLoggingSecurity
Performance MetricsAudit log
You name it
Command Bus libraries
by @matthiasnobackSimple Bus
by @rosstuckTACTICIAN
by @qandidate-labsBROADWAY
Use them and contribute
Like we
DID
Krzysztof Menżyk @kmenzyk
Thanks!
Taming Command Bus
PHOTO CREDITShttps://flic.kr/p/F2qhc7https://flic.kr/p/zMzVzWhttps://flic.kr/p/F4j2fHhttps://flic.kr/p/5a5d3bhttps://flic.kr/p/vXAK3D