[Messenger] ease testing and allow forking the middleware stack

This commit is contained in:
Nicolas Grekas 2019-04-23 13:15:09 +02:00
parent 7e56ef1a3d
commit 3bdf4b0e0f
4 changed files with 122 additions and 29 deletions

View File

@ -14,6 +14,8 @@ namespace Symfony\Component\Messenger\Middleware;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* Implementations must be cloneable, and each clone must unstack the stack independently.
*
* @experimental in 4.2
*/
interface StackInterface

View File

@ -20,27 +20,42 @@ use Symfony\Component\Messenger\Envelope;
*/
class StackMiddleware implements MiddlewareInterface, StackInterface
{
private $middlewareIterator;
private $stack;
private $offset = 0;
public function __construct(\Iterator $middlewareIterator = null)
/**
* @param iterable|MiddlewareInterface[]|MiddlewareInterface|null $middlewareIterator
*/
public function __construct($middlewareIterator = null)
{
$this->middlewareIterator = $middlewareIterator;
$this->stack = new MiddlewareStack();
if (null === $middlewareIterator) {
return;
}
if ($middlewareIterator instanceof \Iterator) {
$this->stack->iterator = $middlewareIterator;
} elseif ($middlewareIterator instanceof MiddlewareInterface) {
$this->stack->stack[] = $middlewareIterator;
} elseif (!\is_iterable($middlewareIterator)) {
throw new \TypeError(sprintf('Argument 1 passed to %s() must be iterable of %s, %s given.', __METHOD__, MiddlewareInterface::class, \is_object($middlewareIterator) ? \get_class($middlewareIterator) : \gettype($middlewareIterator)));
} else {
$this->stack->iterator = (function () use ($middlewareIterator) {
yield from $middlewareIterator;
})();
}
}
public function next(): MiddlewareInterface
{
if (null === $iterator = $this->middlewareIterator) {
return $this;
}
$iterator->next();
if (!$iterator->valid()) {
$this->middlewareIterator = null;
if (null === $next = $this->stack->next($this->offset)) {
return $this;
}
return $iterator->current();
++$this->offset;
return $next;
}
public function handle(Envelope $envelope, StackInterface $stack): Envelope
@ -48,3 +63,31 @@ class StackMiddleware implements MiddlewareInterface, StackInterface
return $envelope;
}
}
/**
* @internal
*/
class MiddlewareStack
{
public $iterator;
public $stack = [];
public function next(int $offset): ?MiddlewareInterface
{
if (isset($this->stack[$offset])) {
return $this->stack[$offset];
}
if (null === $this->iterator) {
return null;
}
$this->iterator->next();
if (!$this->iterator->valid()) {
return $this->iterator = null;
}
return $this->stack[] = $this->iterator->current();
}
}

View File

@ -15,6 +15,7 @@ use PHPUnit\Framework\TestCase;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Middleware\MiddlewareInterface;
use Symfony\Component\Messenger\Middleware\StackInterface;
use Symfony\Component\Messenger\Middleware\StackMiddleware;
/**
* @author Nicolas Grekas <p@tchwork.com>
@ -25,23 +26,26 @@ abstract class MiddlewareTestCase extends TestCase
{
protected function getStackMock(bool $nextIsCalled = true)
{
if (!$nextIsCalled) {
$stack = $this->createMock(StackInterface::class);
$stack
->expects($this->never())
->method('next')
;
return $stack;
}
$nextMiddleware = $this->getMockBuilder(MiddlewareInterface::class)->getMock();
$nextMiddleware
->expects($nextIsCalled ? $this->once() : $this->never())
->expects($this->once())
->method('handle')
->willReturnCallback(function (Envelope $envelope, StackInterface $stack): Envelope {
return $envelope;
})
;
$stack = $this->createMock(StackInterface::class);
$stack
->expects($nextIsCalled ? $this->once() : $this->never())
->method('next')
->willReturn($nextMiddleware)
;
return $stack;
return new StackMiddleware($nextMiddleware);
}
protected function getThrowingStackMock(\Throwable $throwable = null)
@ -53,13 +57,6 @@ abstract class MiddlewareTestCase extends TestCase
->willThrowException($throwable ?? new \RuntimeException('Thrown from next middleware.'))
;
$stack = $this->createMock(StackInterface::class);
$stack
->expects($this->once())
->method('next')
->willReturn($nextMiddleware)
;
return $stack;
return new StackMiddleware($nextMiddleware);
}
}

View File

@ -0,0 +1,51 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Tests\Middleware;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\MessageBus;
use Symfony\Component\Messenger\Middleware\MiddlewareInterface;
use Symfony\Component\Messenger\Middleware\StackInterface;
class StackMiddlewareTest extends TestCase
{
public function testClone()
{
$middleware1 = $this->getMockBuilder(MiddlewareInterface::class)->getMock();
$middleware1
->expects($this->once())
->method('handle')
->willReturnCallback(function (Envelope $envelope, StackInterface $stack): Envelope {
$fork = clone $stack;
$stack->next()->handle($envelope, $stack);
$fork->next()->handle($envelope, $fork);
return $envelope;
})
;
$middleware2 = $this->getMockBuilder(MiddlewareInterface::class)->getMock();
$middleware2
->expects($this->exactly(2))
->method('handle')
->willReturnCallback(function (Envelope $envelope, StackInterface $stack): Envelope {
return $envelope;
})
;
$bus = new MessageBus([$middleware1, $middleware2]);
$bus->dispatch(new \stdClass());
}
}