bus = $bus; return $self; } protected function next(): MessageBusInterface { try { return $this->bus ? $this->bus->next() : new NullBus(); } finally { $this->bus = null; } } } /** * Now we talk: this is an example implementation of a middleware that dumps something around a decorated dispatcher. */ class DumpMiddleware extends AbstractMiddleware { /** * {@inheritdoc} */ public function dispatch($message): void { dump(1); $this->next()->dispatch($message); dump(2); } } /** * This is an example dispatcher that does just dump() the passed message. */ class DumpBus implements MessageBusInterface { /** * {@inheritdoc} */ public function dispatch($message): void { dump($message); } } /** * This is the implementation of a message bus: it gets a list of middleware handlers as arguments. */ class MessageBus implements MessageBusInterface, BusInterface { private $middlewareAggregate; private $middlewareIterator; public function __construct(iterable $middlewareHandlers) { if ($middlewareHandlers instanceof \IteratorAggregate) { $this->middlewareAggregate = $middlewareHandlers; } elseif (\is_array($middlewareHandlers)) { $this->middlewareAggregate = new \ArrayObject($middlewareHandlers); } else { $this->middlewareAggregate = new class() { public $aggregate; public $iterator; public function getIterator() { return $this->aggregate = new \ArrayObject(iterator_to_array($this->iterator, false)); } }; $this->middlewareAggregate->aggregate = &$this->middlewareAggregate; $this->middlewareAggregate->iterator = $middlewareHandlers; } } /** * {@inheritdoc} */ public function dispatch($message): void { $bus = clone $this; $bus->middlewareIterator = $bus->middlewareAggregate->getIterator(); while ($bus->middlewareIterator instanceof \IteratorAggregate) { $bus->middlewareIterator = $bus->middlewareIterator->getIterator(); } foreach ($bus->middlewareIterator as $middleware) { if ($middleware instanceof MiddlewareInterface) { $middleware = $middleware->withBus($bus); } $middleware->dispatch($message); return; } } public function next(): MessageBusInterface { if (null === $middlewareIterator = $this->middlewareIterator) { throw new \LogicException('Cannot get next middleware on uninitialized message bus.'); } $middlewareIterator->next(); if (!$middlewareIterator->valid()) { return new NullBus(); } $middleware = $middlewareIterator->current(); return $middleware instanceof MiddlewareInterface ? $middleware->withBus($this) : $middleware; } } // Let's demo all this $bus = new MessageBus(new \ArrayObject(array( new DumpMiddleware(), new DumpBus(), ))); $bus->dispatch(123); $bus->dispatch(234); /** * Discussion * * This design opposes PSR15-style middleware, that build around two *separate* * interfaces: one for dispatchers and a separate one for stackable middleware. * * Here, we seek for a core requirement: a middleware *should* be first a dispatcher * and then it *could* be stackable. On the contrary, PSR15 requires putting stackable * middleware under a separate uncompatible interface than the core decorated one. * This is design overhead we'd better get rid of. * * On the DX side, PSR15-style middleware have to be built around a generic indirection * object/callback (their $next argument). This has the nasty side effect of polluting * stack traces, thus making things harder to debug. That's not a minor issue. * * The design in this gist fixes all these flaws: * - middleware *are* dispatchers * - they can be made stackable using an *optional* additional *extending* interface * - stack frames are clean, they embed no indirections * * In terms of implementation complexity, both solutions are similar: they both require * the same amount of logic to call stackable middleware. */