feature #30628 Making the serializer configurable by transport (weaverryan)

This PR was merged into the 4.3-dev branch.

Discussion
----------

Making the serializer configurable by transport

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | yes
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #30293 (already closed, but I don't think this was reasonably possible before)
| License       | MIT
| Doc PR        | TODO - as many of the new messenger PRs will be done at once

Use cases:
* #30293
* Transport A will be consumed by your Symfony app (so native php serialization is cool) but transport B will be consumed by another app, so you want to serialize as JSON
* Upgrading from Symfony 4.2 to 4.3. The change to the `PhpSerialize` means that messages that were sent to the queue on 4.2, will fail on 4.3. The solution is to use the old serializer in your config. This would allow you to make your existing transport use the old serializer, then migrate to a new transport using the new serializer (then remove the old one later).

Thanks!

Commits
-------

ef6f23e8b9 Making the serializer configurable by transport
This commit is contained in:
Fabien Potencier 2019-03-31 16:55:57 +02:00
commit 88042a317a
29 changed files with 75 additions and 174 deletions

View File

@ -8,11 +8,18 @@ CHANGELOG
* Not passing the project directory to the constructor of the `AssetsInstallCommand` is deprecated. This argument will
be mandatory in 5.0.
* Deprecated the "Psr\SimpleCache\CacheInterface" / "cache.app.simple" service, use "Symfony\Contracts\Cache\CacheInterface" / "cache.app" instead
* Added the ability to specify a custom `serializer` option for each
transport under`framework.messenger.transports`.
* [BC Break] When using Messenger, the default transport changed from
using Symfony's serializer service to use `PhpSerializer`, which uses
PHP's native `serialize()` and `unserialize()` functions. To use the
original serialization method, set the `framework.messenger.serializer.id`
config option to `messenger.transport.symfony_serializer`.
original serialization method, set the `framework.messenger.defaut_serializer`
config option to `messenger.transport.symfony_serializer`. Or set the
`serializer` option under one specific `transport`.
* [BC Break] The `framework.messenger.serializer` config key changed to
`framework.messenger.default_serializer`, which holds the string service
id and `framework.messenger.symfony_serializer`, which configures the
options if you're using Symfony's serializer.
* Added information about deprecated aliases in `debug:autowiring`
* Added php ini session options `sid_length` and `sid_bits_per_character`
to the `session` section of the configuration

View File

@ -1107,29 +1107,19 @@ class Configuration implements ConfigurationInterface
->end()
->end()
->end()
->arrayNode('serializer')
->scalarNode('default_serializer')
->defaultValue('messenger.transport.native_php_serializer')
->info('Service id to use as the default serializer for the transports.')
->end()
->arrayNode('symfony_serializer')
->addDefaultsIfNotSet()
->beforeNormalization()
->always()
->then(function ($config) {
if (false === $config) {
return ['id' => null];
}
if (\is_string($config)) {
return ['id' => $config];
}
return $config;
})
->end()
->children()
->scalarNode('id')->defaultValue('messenger.transport.native_php_serializer')->end()
->scalarNode('format')->defaultValue('json')->end()
->scalarNode('format')->defaultValue('json')->info('Serialization format for the messenger.transport.symfony_serializer service (which is not the serializer used by default).')->end()
->arrayNode('context')
->normalizeKeys(false)
->useAttributeAsKey('name')
->defaultValue([])
->info('Context array for the messenger.transport.symfony_serializer service (which is not the serializer used by default).')
->prototype('variable')->end()
->end()
->end()
@ -1146,6 +1136,7 @@ class Configuration implements ConfigurationInterface
->fixXmlConfig('option')
->children()
->scalarNode('dsn')->end()
->scalarNode('serializer')->defaultNull()->info('Service id of a custom serializer to use.')->end()
->arrayNode('options')
->normalizeKeys(false)
->defaultValue([])

View File

@ -78,7 +78,6 @@ use Symfony\Component\Mailer\Mailer;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
use Symfony\Component\Messenger\MessageBus;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
use Symfony\Component\Messenger\Transport\TransportFactoryInterface;
use Symfony\Component\Messenger\Transport\TransportInterface;
use Symfony\Component\Mime\MimeTypeGuesserInterface;
@ -1604,28 +1603,6 @@ class FrameworkExtension extends Extension
$loader->load('messenger.xml');
if (empty($config['transports'])) {
$container->removeDefinition('messenger.transport.symfony_serializer');
$container->removeDefinition('messenger.transport.amqp.factory');
} else {
if ('messenger.transport.symfony_serializer' === $config['serializer']['id']) {
if (!$this->isConfigEnabled($container, $serializerConfig)) {
throw new LogicException('The Messenger serializer cannot be enabled as the Serializer support is not available. Try enabling it or running "composer require symfony/serializer-pack".');
}
$container->getDefinition('messenger.transport.symfony_serializer')
->replaceArgument(1, $config['serializer']['format'])
->replaceArgument(2, $config['serializer']['context']);
}
if ($config['serializer']['id']) {
$container->setAlias('messenger.transport.serializer', $config['serializer']['id']);
} else {
$container->removeDefinition('messenger.transport.amqp.factory');
$container->removeDefinition(SerializerInterface::class);
}
}
if (null === $config['default_bus'] && 1 === \count($config['buses'])) {
$config['default_bus'] = key($config['buses']);
}
@ -1677,16 +1654,24 @@ class FrameworkExtension extends Extension
}
}
if (empty($config['transports'])) {
$container->removeDefinition('messenger.transport.symfony_serializer');
$container->removeDefinition('messenger.transport.amqp.factory');
} else {
$container->getDefinition('messenger.transport.symfony_serializer')
->replaceArgument(1, $config['symfony_serializer']['format'])
->replaceArgument(2, $config['symfony_serializer']['context']);
$container->setAlias('messenger.default_serializer', $config['default_serializer']);
}
$senderAliases = [];
$transportRetryReferences = [];
foreach ($config['transports'] as $name => $transport) {
if (0 === strpos($transport['dsn'], 'amqp://') && !$container->hasDefinition('messenger.transport.amqp.factory')) {
throw new LogicException('The default AMQP transport is not available. Make sure you have installed and enabled the Serializer component. Try enabling it or running "composer require symfony/serializer-pack".');
}
$serializerId = $transport['serializer'] ?? 'messenger.default_serializer';
$transportDefinition = (new Definition(TransportInterface::class))
->setFactory([new Reference('messenger.transport_factory'), 'createTransport'])
->setArguments([$transport['dsn'], $transport['options']])
->setArguments([$transport['dsn'], $transport['options'], new Reference($serializerId)])
->addTag('messenger.receiver', ['alias' => $name])
;
$container->setDefinition($transportId = 'messenger.transport.'.$name, $transportDefinition);

View File

@ -26,7 +26,7 @@
<argument /> <!-- Format -->
<argument type="collection" /> <!-- Context -->
</service>
<service id="Symfony\Component\Messenger\Transport\Serialization\SerializerInterface" alias="messenger.transport.serializer" />
<service id="Symfony\Component\Messenger\Transport\Serialization\SerializerInterface" alias="messenger.default_serializer" />
<service id="messenger.transport.native_php_serializer" class="Symfony\Component\Messenger\Transport\Serialization\PhpSerializer" />
@ -64,7 +64,6 @@
<service id="messenger.transport.amqp.factory" class="Symfony\Component\Messenger\Transport\AmqpExt\AmqpTransportFactory">
<tag name="messenger.transport_factory" />
<argument type="service" id="messenger.transport.serializer" />
</service>
<service id="messenger.transport.sync.factory" class="Symfony\Component\Messenger\Transport\Sync\SyncTransportFactory">

View File

@ -402,7 +402,8 @@
<xsd:complexType name="messenger">
<xsd:sequence>
<xsd:element name="serializer" type="messenger_serializer" minOccurs="0" />
<xsd:element name="default-serializer" type="xsd:string" minOccurs="0" />
<xsd:element name="symfony-serializer" type="messenger_symfony_serializer" minOccurs="0" />
<xsd:element name="encoder" type="xsd:string" minOccurs="0" />
<xsd:element name="decoder" type="xsd:string" minOccurs="0" />
<xsd:element name="routing" type="messenger_routing" minOccurs="0" maxOccurs="unbounded" />
@ -412,12 +413,11 @@
<xsd:attribute name="default-bus" type="xsd:string" />
</xsd:complexType>
<xsd:complexType name="messenger_serializer">
<xsd:complexType name="messenger_symfony_serializer">
<xsd:sequence>
<xsd:element name="context" type="metadata" minOccurs="0" maxOccurs="unbounded" />
</xsd:sequence>
<xsd:attribute name="format" type="xsd:string" />
<xsd:attribute name="id" type="xsd:string" />
</xsd:complexType>
<xsd:complexType name="messenger_routing">
@ -437,6 +437,7 @@
<xsd:element name="options" type="metadata" minOccurs="0" maxOccurs="unbounded" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="serializer" type="xsd:string" />
<xsd:attribute name="dsn" type="xsd:string" />
</xsd:complexType>

View File

@ -325,8 +325,8 @@ class ConfigurationTest extends TestCase
'enabled' => !class_exists(FullStack::class) && interface_exists(MessageBusInterface::class),
'routing' => [],
'transports' => [],
'serializer' => [
'id' => 'messenger.transport.native_php_serializer',
'default_serializer' => 'messenger.transport.native_php_serializer',
'symfony_serializer' => [
'format' => 'json',
'context' => [],
],

View File

@ -5,7 +5,7 @@ use Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\FooMessage;
$container->loadFromExtension('framework', [
'messenger' => [
'serializer' => false,
'default_serializer' => false,
'routing' => [
FooMessage::class => ['sender.bar', 'sender.biz'],
BarMessage::class => 'sender.foo',

View File

@ -1,10 +0,0 @@
<?php
$container->loadFromExtension('framework', [
'messenger' => [
'serializer' => false,
'transports' => [
'default' => 'amqp://localhost/%2f/messages',
],
],
]);

View File

@ -3,7 +3,7 @@
$container->loadFromExtension('framework', [
'serializer' => true,
'messenger' => [
'serializer' => 'messenger.transport.symfony_serializer',
'default_serializer' => 'messenger.transport.symfony_serializer',
'routing' => [
'Symfony\Component\Messenger\Tests\Fixtures\DummyMessage' => ['amqp', 'audit'],
'Symfony\Component\Messenger\Tests\Fixtures\SecondMessage' => [

View File

@ -3,8 +3,8 @@
$container->loadFromExtension('framework', [
'serializer' => true,
'messenger' => [
'serializer' => [
'id' => 'messenger.transport.symfony_serializer',
'default_serializer' => 'messenger.transport.symfony_serializer',
'symfony_serializer' => [
'format' => 'csv',
'context' => ['enable_max_depth' => true],
],

View File

@ -1,13 +0,0 @@
<?php
$container->loadFromExtension('framework', [
'serializer' => [
'enabled' => false,
],
'messenger' => [
'serializer' => 'messenger.transport.symfony_serializer',
'transports' => [
'default' => 'amqp://localhost/%2f/messages',
],
],
]);

View File

@ -3,12 +3,13 @@
$container->loadFromExtension('framework', [
'serializer' => true,
'messenger' => [
'serializer' => 'messenger.transport.symfony_serializer',
'default_serializer' => 'messenger.transport.symfony_serializer',
'transports' => [
'default' => 'amqp://localhost/%2f/messages',
'customised' => [
'dsn' => 'amqp://localhost/%2f/messages?exchange_name=exchange_name',
'options' => ['queue' => ['name' => 'Queue']],
'serializer' => 'messenger.transport.native_php_serializer',
],
],
],

View File

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:framework="http://symfony.com/schema/dic/symfony"
xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
<framework:config>
<framework:messenger>
<framework:serializer id="null" />
<framework:transport name="default" dsn="amqp://localhost/%2f/messages" />
</framework:messenger>
</framework:config>
</container>

View File

@ -8,7 +8,7 @@
<framework:config>
<framework:serializer enabled="true" />
<framework:messenger>
<framework:serializer id="messenger.transport.symfony_serializer" />
<framework:default-serializer>messenger.transport.symfony_serializer</framework:default-serializer>
<framework:routing message-class="Symfony\Component\Messenger\Tests\Fixtures\DummyMessage">
<framework:sender service="amqp" />
<framework:sender service="audit" />

View File

@ -8,11 +8,12 @@
<framework:config>
<framework:serializer enabled="true" />
<framework:messenger>
<framework:serializer id="messenger.transport.symfony_serializer" format="csv">
<framework:default-serializer>messenger.transport.symfony_serializer</framework:default-serializer>
<framework:symfony-serializer format="csv">
<framework:context>
<framework:enable_max_depth>true</framework:enable_max_depth>
</framework:context>
</framework:serializer>
</framework:symfony-serializer>
<framework:transport name="default" dsn="amqp://localhost/%2f/messages" />
</framework:messenger>
</framework:config>

View File

@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:framework="http://symfony.com/schema/dic/symfony"
xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
<framework:config>
<framework:serializer enabled="false" />
<framework:messenger>
<framework:serializer id="messenger.transport.symfony_serializer" />
<framework:transport name="default" dsn="amqp://localhost/%2f/messages" />
</framework:messenger>
</framework:config>
</container>

View File

@ -8,9 +8,9 @@
<framework:config>
<framework:serializer enabled="true" />
<framework:messenger>
<framework:serializer id="messenger.transport.symfony_serializer" />
<framework:default-serializer>messenger.transport.symfony_serializer</framework:default-serializer>
<framework:transport name="default" dsn="amqp://localhost/%2f/messages" />
<framework:transport name="customised" dsn="amqp://localhost/%2f/messages?exchange_name=exchange_name">
<framework:transport name="customised" dsn="amqp://localhost/%2f/messages?exchange_name=exchange_name" serializer="messenger.transport.native_php_serializer">
<framework:options>
<framework:queue>
<framework:name>Queue</framework:name>

View File

@ -1,6 +1,6 @@
framework:
messenger:
serializer: false
default_serializer: false
routing:
'Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\FooMessage': ['sender.bar', 'sender.biz']
'Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\BarMessage': 'sender.foo'

View File

@ -1,5 +0,0 @@
framework:
messenger:
serializer: false
transports:
default: 'amqp://localhost/%2f/messages'

View File

@ -1,7 +1,7 @@
framework:
serializer: true
messenger:
serializer: messenger.transport.symfony_serializer
default_serializer: messenger.transport.symfony_serializer
routing:
'Symfony\Component\Messenger\Tests\Fixtures\DummyMessage': [amqp, audit]
'Symfony\Component\Messenger\Tests\Fixtures\SecondMessage':

View File

@ -1,8 +1,8 @@
framework:
serializer: true
messenger:
serializer:
id: messenger.transport.symfony_serializer
default_serializer: messenger.transport.symfony_serializer
symfony_serializer:
format: csv
context:
enable_max_depth: true

View File

@ -1,7 +0,0 @@
framework:
serializer:
enabled: false
messenger:
serializer: messenger.transport.symfony_serializer
transports:
default: 'amqp://localhost/%2f/messages'

View File

@ -1,7 +1,7 @@
framework:
serializer: true
messenger:
serializer: messenger.transport.symfony_serializer
default_serializer: messenger.transport.symfony_serializer
transports:
default: 'amqp://localhost/%2f/messages'
customised:
@ -9,3 +9,4 @@ framework:
options:
queue:
name: Queue
serializer: 'messenger.transport.native_php_serializer'

View File

@ -661,15 +661,18 @@ abstract class FrameworkExtensionTest extends TestCase
$this->assertTrue($container->hasDefinition('messenger.transport.default'));
$this->assertTrue($container->getDefinition('messenger.transport.default')->hasTag('messenger.receiver'));
$this->assertEquals([['alias' => 'default']], $container->getDefinition('messenger.transport.default')->getTag('messenger.receiver'));
$transportArguments = $container->getDefinition('messenger.transport.default')->getArguments();
$this->assertEquals(new Reference('messenger.default_serializer'), $transportArguments[2]);
$this->assertTrue($container->hasDefinition('messenger.transport.customised'));
$transportFactory = $container->getDefinition('messenger.transport.customised')->getFactory();
$transportArguments = $container->getDefinition('messenger.transport.customised')->getArguments();
$this->assertEquals([new Reference('messenger.transport_factory'), 'createTransport'], $transportFactory);
$this->assertCount(2, $transportArguments);
$this->assertCount(3, $transportArguments);
$this->assertSame('amqp://localhost/%2f/messages?exchange_name=exchange_name', $transportArguments[0]);
$this->assertSame(['queue' => ['name' => 'Queue']], $transportArguments[1]);
$this->assertEquals(['queue' => ['name' => 'Queue']], $transportArguments[1]);
$this->assertEquals(new Reference('messenger.transport.native_php_serializer'), $transportArguments[2]);
$this->assertTrue($container->hasDefinition('messenger.transport.amqp.factory'));
}
@ -693,29 +696,11 @@ abstract class FrameworkExtensionTest extends TestCase
], $sendersMapping[DummyMessage::class]->getValues());
}
/**
* @expectedException \Symfony\Component\DependencyInjection\Exception\LogicException
* @expectedExceptionMessage The Messenger serializer cannot be enabled as the Serializer support is not available. Try enabling it or running "composer require symfony/serializer-pack".
*/
public function testMessengerTransportConfigurationWithoutSerializer()
{
$this->createContainerFromFile('messenger_transport_no_serializer');
}
/**
* @expectedException \Symfony\Component\DependencyInjection\Exception\LogicException
* @expectedExceptionMessage The default AMQP transport is not available. Make sure you have installed and enabled the Serializer component. Try enabling it or running "composer require symfony/serializer-pack".
*/
public function testMessengerAMQPTransportConfigurationWithoutSerializer()
{
$this->createContainerFromFile('messenger_amqp_transport_no_serializer');
}
public function testMessengerTransportConfiguration()
{
$container = $this->createContainerFromFile('messenger_transport');
$this->assertSame('messenger.transport.symfony_serializer', (string) $container->getAlias('messenger.transport.serializer'));
$this->assertSame('messenger.transport.symfony_serializer', (string) $container->getAlias('messenger.default_serializer'));
$serializerTransportDefinition = $container->getDefinition('messenger.transport.symfony_serializer');
$this->assertSame('csv', $serializerTransportDefinition->getArgument(1));

View File

@ -4,10 +4,12 @@ CHANGELOG
4.3.0
-----
* [BC BREAK] The `TransportFactoryInterface::createTransport()` signature
changed: a required 3rd `SerializerInterface` argument was added.
* Added a new `SyncTransport` along with `ForceCallHandlersStamp` to
explicitly handle messages synchronously.
* Added optional parameter `prefetch_count` in connection configuration,
to setup channel prefetch count
to setup channel prefetch count.
* New classes: `RoutableMessageBus`, `AddBusNameStampMiddleware`
and `BusNameStamp` were added, which allow you to add a bus identifier
to the `Envelope` then find the correct bus when receiving from

View File

@ -21,9 +21,7 @@ class AmqpTransportFactoryTest extends TestCase
{
public function testSupportsOnlyAmqpTransports()
{
$factory = new AmqpTransportFactory(
$this->getMockBuilder(SerializerInterface::class)->getMock()
);
$factory = new AmqpTransportFactory();
$this->assertTrue($factory->supports('amqp://localhost', []));
$this->assertFalse($factory->supports('sqs://localhost', []));
@ -32,12 +30,11 @@ class AmqpTransportFactoryTest extends TestCase
public function testItCreatesTheTransport()
{
$factory = new AmqpTransportFactory(
$serializer = $this->getMockBuilder(SerializerInterface::class)->getMock()
);
$factory = new AmqpTransportFactory();
$serializer = $this->createMock(SerializerInterface::class);
$expectedTransport = new AmqpTransport(Connection::fromDsn('amqp://localhost', ['foo' => 'bar']), $serializer);
$this->assertEquals($expectedTransport, $factory->createTransport('amqp://localhost', ['foo' => 'bar']));
$this->assertEquals($expectedTransport, $factory->createTransport('amqp://localhost', ['foo' => 'bar'], $serializer));
}
}

View File

@ -11,7 +11,6 @@
namespace Symfony\Component\Messenger\Transport\AmqpExt;
use Symfony\Component\Messenger\Transport\Serialization\PhpSerializer;
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
use Symfony\Component\Messenger\Transport\TransportFactoryInterface;
use Symfony\Component\Messenger\Transport\TransportInterface;
@ -23,16 +22,9 @@ use Symfony\Component\Messenger\Transport\TransportInterface;
*/
class AmqpTransportFactory implements TransportFactoryInterface
{
private $serializer;
public function __construct(SerializerInterface $serializer = null)
public function createTransport(string $dsn, array $options, SerializerInterface $serializer): TransportInterface
{
$this->serializer = $serializer ?? new PhpSerializer();
}
public function createTransport(string $dsn, array $options): TransportInterface
{
return new AmqpTransport(Connection::fromDsn($dsn, $options), $this->serializer);
return new AmqpTransport(Connection::fromDsn($dsn, $options), $serializer);
}
public function supports(string $dsn, array $options): bool

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\Messenger\Transport;
use Symfony\Component\Messenger\Exception\InvalidArgumentException;
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
/**
* @author Samuel Roze <samuel.roze@gmail.com>
@ -30,11 +31,11 @@ class TransportFactory implements TransportFactoryInterface
$this->factories = $factories;
}
public function createTransport(string $dsn, array $options): TransportInterface
public function createTransport(string $dsn, array $options, SerializerInterface $serializer): TransportInterface
{
foreach ($this->factories as $factory) {
if ($factory->supports($dsn, $options)) {
return $factory->createTransport($dsn, $options);
return $factory->createTransport($dsn, $options, $serializer);
}
}

View File

@ -11,6 +11,8 @@
namespace Symfony\Component\Messenger\Transport;
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
/**
* Creates a Messenger transport.
*
@ -20,7 +22,7 @@ namespace Symfony\Component\Messenger\Transport;
*/
interface TransportFactoryInterface
{
public function createTransport(string $dsn, array $options): TransportInterface;
public function createTransport(string $dsn, array $options, SerializerInterface $serializer): TransportInterface;
public function supports(string $dsn, array $options): bool;
}