feature #29159 [Messenger] collect all stamps added on Envelope as collections (nicolas-grekas)

This PR was merged into the 4.2-dev branch.

Discussion
----------

[Messenger] collect all stamps added on Envelope as collections

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

Late small BC break for Messenger:
 * `Envelope::all()` takes a new optional `$stampFqcn` argument and returns the stamps for the specified FQCN, or all stamps by their class name
 * `Envelope::get()` has been renamed `Envelope::last()`

This fixes the current behavior where we replace any previous stamp with the same type, which is unexpected to me as it silently loses data and more importantly blocks interesting use cases we're going to need in the near future.
Basically, that's the same as HTTP headers being allowed to exist several times: most of them make no sense as collections, but some are really useful as collections.

Commits
-------

2e9885922a [Messenger] collect all stamps added on Envelope as collections
This commit is contained in:
Fabien Potencier 2018-11-12 13:35:06 +01:00
commit fb249f0b69
9 changed files with 43 additions and 44 deletions

View File

@ -33,6 +33,8 @@ CHANGELOG
* `ActivationMiddlewareDecorator` has been renamed `ActivationMiddleware`
* `AllowNoHandlerMiddleware` has been removed in favor of a new constructor argument on `HandleMessageMiddleware`
* The `ContainerHandlerLocator`, `AbstractHandlerLocator`, `SenderLocator` and `AbstractSenderLocator` classes have been removed
* `Envelope::all()` takes a new optional `$stampFqcn` argument and returns the stamps for the specified FQCN, or all stamps by their class name
* `Envelope::get()` has been renamed `Envelope::last()`
4.1.0
-----

View File

@ -36,7 +36,7 @@ final class Envelope
$this->message = $message;
foreach ($stamps as $stamp) {
$this->stamps[\get_class($stamp)] = $stamp;
$this->stamps[\get_class($stamp)][] = $stamp;
}
}
@ -48,22 +48,26 @@ final class Envelope
$cloned = clone $this;
foreach ($stamps as $stamp) {
$cloned->stamps[\get_class($stamp)] = $stamp;
$cloned->stamps[\get_class($stamp)][] = $stamp;
}
return $cloned;
}
public function get(string $stampFqcn): ?StampInterface
public function last(string $stampFqcn): ?StampInterface
{
return $this->stamps[$stampFqcn] ?? null;
return isset($this->stamps[$stampFqcn]) ? end($this->stamps[$stampFqcn]) : null;
}
/**
* @return StampInterface[] indexed by fqcn
* @return StampInterface[]|StampInterface[][] The stamps for the specified FQCN, or all stamps by their class name
*/
public function all(): array
public function all(string $stampFqcn = null): array
{
if (null !== $stampFqcn) {
return $this->stamps[$stampFqcn] ?? array();
}
return $this->stamps;
}

View File

@ -35,7 +35,7 @@ class SendMessageMiddleware implements MiddlewareInterface
*/
public function handle(Envelope $envelope, StackInterface $stack): Envelope
{
if ($envelope->get(ReceivedStamp::class)) {
if ($envelope->all(ReceivedStamp::class)) {
// it's a received message, do not send it back
return $stack->next()->handle($envelope, $stack);
}

View File

@ -38,7 +38,7 @@ class ValidationMiddleware implements MiddlewareInterface
$message = $envelope->getMessage();
$groups = null;
/** @var ValidationStamp|null $validationStamp */
if ($validationStamp = $envelope->get(ValidationStamp::class)) {
if ($validationStamp = $envelope->last(ValidationStamp::class)) {
$groups = $validationStamp->getGroups();
}

View File

@ -29,7 +29,7 @@ class EnvelopeTest extends TestCase
$this->assertSame($dummy, $envelope->getMessage());
$this->assertArrayHasKey(ReceivedStamp::class, $stamps = $envelope->all());
$this->assertSame($receivedStamp, $stamps[ReceivedStamp::class]);
$this->assertSame($receivedStamp, $stamps[ReceivedStamp::class][0]);
}
public function testWithReturnsNewInstance()
@ -39,13 +39,13 @@ class EnvelopeTest extends TestCase
$this->assertNotSame($envelope, $envelope->with(new ReceivedStamp()));
}
public function testGet()
public function testGetLast()
{
$receivedStamp = new ReceivedStamp();
$envelope = new Envelope($dummy = new DummyMessage('dummy'), $receivedStamp);
$this->assertSame($receivedStamp, $envelope->get(ReceivedStamp::class));
$this->assertNull($envelope->get(ValidationStamp::class));
$this->assertSame($receivedStamp, $envelope->last(ReceivedStamp::class));
$this->assertNull($envelope->last(ValidationStamp::class));
}
public function testAll()
@ -57,8 +57,8 @@ class EnvelopeTest extends TestCase
$stamps = $envelope->all();
$this->assertArrayHasKey(ReceivedStamp::class, $stamps);
$this->assertSame($receivedStamp, $stamps[ReceivedStamp::class]);
$this->assertSame($receivedStamp, $stamps[ReceivedStamp::class][0]);
$this->assertArrayHasKey(ValidationStamp::class, $stamps);
$this->assertSame($validationStamp, $stamps[ValidationStamp::class]);
$this->assertSame($validationStamp, $stamps[ValidationStamp::class][0]);
}
}

View File

@ -56,7 +56,7 @@ class TraceableMessageBusTest extends TestCase
$this->assertCount(1, $tracedMessages = $traceableBus->getDispatchedMessages());
$this->assertArraySubset(array(
'message' => $message,
'stamps' => array($stamp),
'stamps' => array(array($stamp)),
'caller' => array(
'name' => 'TraceableMessageBusTest.php',
'file' => __FILE__,

View File

@ -18,16 +18,13 @@ use Symfony\Component\Messenger\Stamp\ValidationStamp;
use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage;
use Symfony\Component\Messenger\Transport\Serialization\Serializer;
use Symfony\Component\Serializer as SerializerComponent;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
class SerializerTest extends TestCase
{
public function testEncodedIsDecodable()
{
$serializer = new Serializer(
new SerializerComponent\Serializer(array(new ObjectNormalizer()), array('json' => new JsonEncoder()))
);
$serializer = new Serializer();
$envelope = new Envelope(new DummyMessage('Hello'));
@ -36,9 +33,7 @@ class SerializerTest extends TestCase
public function testEncodedWithStampsIsDecodable()
{
$serializer = new Serializer(
new SerializerComponent\Serializer(array(new ObjectNormalizer()), array('json' => new JsonEncoder()))
);
$serializer = new Serializer();
$envelope = (new Envelope(new DummyMessage('Hello')))
->with(new SerializerStamp(array(ObjectNormalizer::GROUPS => array('foo'))))
@ -50,9 +45,7 @@ class SerializerTest extends TestCase
public function testEncodedIsHavingTheBodyAndTypeHeader()
{
$serializer = new Serializer(
new SerializerComponent\Serializer(array(new ObjectNormalizer()), array('json' => new JsonEncoder()))
);
$serializer = new Serializer();
$encoded = $serializer->encode(new Envelope(new DummyMessage('Hello')));
@ -81,11 +74,7 @@ class SerializerTest extends TestCase
public function testEncodedWithSymfonySerializerForStamps()
{
$serializer = new Serializer(
new SerializerComponent\Serializer(array(new ObjectNormalizer()), array('json' => new JsonEncoder())),
'json',
array()
);
$serializer = new Serializer();
$envelope = (new Envelope(new DummyMessage('Hello')))
->with($serializerStamp = new SerializerStamp(array(ObjectNormalizer::GROUPS => array('foo'))))
@ -102,7 +91,7 @@ class SerializerTest extends TestCase
$decoded = $serializer->decode($encoded);
$this->assertEquals($serializerStamp, $decoded->get(SerializerStamp::class));
$this->assertEquals($validationStamp, $decoded->get(ValidationStamp::class));
$this->assertEquals($serializerStamp, $decoded->last(SerializerStamp::class));
$this->assertEquals($validationStamp, $decoded->last(ValidationStamp::class));
}
}

View File

@ -42,10 +42,11 @@ class WorkerTest extends TestCase
public function testWorkerDoesNotWrapMessagesAlreadyWrappedWithReceivedMessage()
{
$envelope = (new Envelope(new DummyMessage('API')))->with(new ReceivedStamp());
$envelope = new Envelope(new DummyMessage('API'));
$receiver = new CallbackReceiver(function ($handler) use ($envelope) {
$handler($envelope);
});
$envelope = $envelope->with(new ReceivedStamp());
$bus = $this->getMockBuilder(MessageBusInterface::class)->getMock();
$bus->expects($this->at(0))->method('dispatch')->with($envelope)->willReturn($envelope);

View File

@ -17,6 +17,7 @@ use Symfony\Component\Messenger\Exception\LogicException;
use Symfony\Component\Messenger\Stamp\SerializerStamp;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Encoder\XmlEncoder;
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer as SymfonySerializer;
use Symfony\Component\Serializer\SerializerInterface as SymfonySerializerInterface;
@ -34,9 +35,9 @@ class Serializer implements SerializerInterface
private $format;
private $context;
public function __construct(SymfonySerializerInterface $serializer, string $format = 'json', array $context = array())
public function __construct(SymfonySerializerInterface $serializer = null, string $format = 'json', array $context = array())
{
$this->serializer = $serializer;
$this->serializer = $serializer ?? self::create()->serializer;
$this->format = $format;
$this->context = $context;
}
@ -48,7 +49,7 @@ class Serializer implements SerializerInterface
}
$encoders = array(new XmlEncoder(), new JsonEncoder());
$normalizers = array(new ObjectNormalizer());
$normalizers = array(new ArrayDenormalizer(), new ObjectNormalizer());
$serializer = new SymfonySerializer($normalizers, $encoders);
return new self($serializer);
@ -70,9 +71,8 @@ class Serializer implements SerializerInterface
$stamps = $this->decodeStamps($encodedEnvelope);
$context = $this->context;
/** @var SerializerStamp|null $serializerStamp */
if ($serializerStamp = $stamps[SerializerStamp::class] ?? null) {
$context = $serializerStamp->getContext() + $context;
if (isset($stamps[SerializerStamp::class])) {
$context = end($stamps[SerializerStamp::class])->getContext() + $context;
}
$message = $this->serializer->deserialize($encodedEnvelope['body'], $encodedEnvelope['headers']['type'], $this->format, $context);
@ -87,7 +87,7 @@ class Serializer implements SerializerInterface
{
$context = $this->context;
/** @var SerializerStamp|null $serializerStamp */
if ($serializerStamp = $envelope->get(SerializerStamp::class)) {
if ($serializerStamp = $envelope->last(SerializerStamp::class)) {
$context = $serializerStamp->getContext() + $context;
}
@ -107,7 +107,10 @@ class Serializer implements SerializerInterface
continue;
}
$stamps[] = $this->serializer->deserialize($value, substr($name, \strlen(self::STAMP_HEADER_PREFIX)), $this->format, $this->context);
$stamps[] = $this->serializer->deserialize($value, substr($name, \strlen(self::STAMP_HEADER_PREFIX)).'[]', $this->format, $this->context);
}
if ($stamps) {
$stamps = array_merge(...$stamps);
}
return $stamps;
@ -115,13 +118,13 @@ class Serializer implements SerializerInterface
private function encodeStamps(Envelope $envelope): array
{
if (!$stamps = $envelope->all()) {
if (!$allStamps = $envelope->all()) {
return array();
}
$headers = array();
foreach ($stamps as $stamp) {
$headers[self::STAMP_HEADER_PREFIX.\get_class($stamp)] = $this->serializer->serialize($stamp, $this->format, $this->context);
foreach ($allStamps as $class => $stamps) {
$headers[self::STAMP_HEADER_PREFIX.$class] = $this->serializer->serialize($stamps, $this->format, $this->context);
}
return $headers;