Skip to content

Instantly share code, notes, and snippets.

@nicolas-grekas
Last active October 18, 2018 07:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nicolas-grekas/1d08b6d5ecbb2504b788e378583920a0 to your computer and use it in GitHub Desktop.
Save nicolas-grekas/1d08b6d5ecbb2504b788e378583920a0 to your computer and use it in GitHub Desktop.
A middleware stack that doesn't double the stack size
<?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