Last active
October 18, 2018 07:21
-
-
Save nicolas-grekas/1d08b6d5ecbb2504b788e378583920a0 to your computer and use it in GitHub Desktop.
A middleware stack that doesn't double the stack size
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
error_reporting(-1); | |
require 'vendor/autoload.php'; | |
/** | |
* This is the core interface. It already allows building dispatchers *and middlewareHandlers*. | |
*/ | |
interface MessageBusInterface | |
{ | |
public function dispatch($message): void; | |
} | |
/** | |
* This allows writting *stackable* middlewareHandlers that are *still* dispatchers. | |
* | |
* (there is no reason to force all middlewareHandlers be stackable BTW | |
* so that implementing this is not a requirement to write a middleware) | |
* | |
* It *extends* MessageBusInterface, which is the core requirement all this is built around. | |
*/ | |
interface MiddlewareInterface extends MessageBusInterface | |
{ | |
/** | |
* @return static | |
*/ | |
public function withBus(BusInterface $bus): self; | |
} | |
/** | |
* This interface is mostly internal - only one implementation of it is needed really, see below. | |
* | |
* It's purpose is to chain stackable middlewareHandlers together at runtime. | |
*/ | |
interface BusInterface | |
{ | |
public function next(): MessageBusInterface; | |
} | |
/** | |
* Nothing fancy here, just the null design pattern. | |
*/ | |
class NullBus implements MessageBusInterface | |
{ | |
/** | |
* {@inheritdoc} | |
*/ | |
public function dispatch($message): void | |
{ | |
// no-op | |
} | |
} | |
/** | |
* This is the typical implementation of a BusMiddlewareInterface. | |
* | |
* By contract, the injected bus must be consumed once and can thus be | |
* unset after usage. *Internally*, a next() method is provided. | |
*/ | |
abstract class AbstractMiddleware implements MiddlewareInterface | |
{ | |
protected $bus; | |
/** | |
* @return static | |
*/ | |
public function withBus(BusInterface $bus): MiddlewareInterface | |
{ | |
$self = clone $this; | |
$self->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. | |
*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment