Skip to content

Commit

Permalink
feature #31204 [Messenger] ease testing and allow forking the middlew…
Browse files Browse the repository at this point in the history
…are stack (nicolas-grekas)

This PR was merged into the 4.3-dev branch.

Discussion
----------

[Messenger] ease testing and allow forking the middleware stack

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #31179
| License       | MIT
| Doc PR        | -

A less radical alternative than #31185 that preserves laziness and addresses the linked issue.

Commits
-------

3bdf4b0 [Messenger] ease testing and allow forking the middleware stack
  • Loading branch information
nicolas-grekas committed Apr 26, 2019
2 parents be80868 + 3bdf4b0 commit 77f642e
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 28 deletions.
2 changes: 2 additions & 0 deletions src/Symfony/Component/Messenger/Middleware/StackInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* Implementations must be cloneable, and each clone must unstack the stack independently.
*
* @experimental in 4.2
*/
interface StackInterface
Expand Down
65 changes: 54 additions & 11 deletions src/Symfony/Component/Messenger/Middleware/StackMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,31 +20,74 @@
*/
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) {
if (null === $next = $this->stack->next($this->offset)) {
return $this;
}
$iterator->next();

if (!$iterator->valid()) {
$this->middlewareIterator = null;
++$this->offset;

return $this;
}

return $iterator->current();
return $next;
}

public function handle(Envelope $envelope, StackInterface $stack): Envelope
{
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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
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>
Expand All @@ -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)
Expand All @@ -53,13 +57,6 @@ protected function getThrowingStackMock(\Throwable $throwable = null)
->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);
}
}
Original file line number Diff line number Diff line change
@@ -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());
}
}

0 comments on commit 77f642e

Please sign in to comment.