Skip to content

Commit

Permalink
feature #27128 [Messenger] Middleware factories support in config (og…
Browse files Browse the repository at this point in the history
…izanagi)

This PR was squashed before being merged into the 4.1 branch (closes #27128).

Discussion
----------

[Messenger] Middleware factories support in config

| Q             | A
| ------------- | ---
| Branch?       | master <!-- see below -->
| Bug fix?      | no
| New feature?  | yes <!-- don't forget to update src/**/CHANGELOG.md files -->
| BC breaks?    | no  <!-- see https://symfony.com/bc -->
| Deprecations? | no <!-- don't forget to update UPGRADE-*.md and src/**/CHANGELOG.md files -->
| Tests pass?   | yes    <!-- please add some, will be required by reviewers -->
| Fixed tickets | N/A   <!-- #-prefixed issue number(s), if any -->
| License       | MIT
| Doc PR        | todo

Following #26864, this would allow to configure easily the middlewares by using an abstract factory definition to which are provided simple arguments (just scalars, no services references).

For instance, here is how the DoctrineBundle would benefit from such a feature (also solving the wiring of the `DoctrineTransactionMiddleware` reverted in #26684):

```yaml
framework:
    messenger:
      buses:
        default:
          middleware:
            - logger
            - doctrine_transaction_middleware: ['entity_manager_name']
```

where `doctrine_transaction_middleware` would be an abstract factory definition provided by the doctrine bundle:

```yml
services:

    doctrine.orm.messenger.middleware_factory.transaction:
      class: Symfony\Bridge\Doctrine\Messenger\DoctrineTransactionMiddlewareFactory
      arguments: ['@doctrine']

    doctrine_transaction_middleware:
      class: Symfony\Bridge\Doctrine\Messenger\DoctrineTransactionMiddleware
      factory: ['@doctrine.orm.messenger.middleware_factory.transaction', 'createMiddleware']
      abstract: true
      # the default arguments to use when none provided from config.
      # i.e:
      #   middlewares:
      #     - doctrine_transaction_middleware: ~
      arguments: ['default']
```

and is interpreted as:

```yml
buses:
    default:
        middleware:
            -
                id: logger
                arguments: {  }
            -
                id: doctrine_transaction_middleware
                arguments:
                    - entity_manager_name
        default_middleware: true
```

---

<details>

<summary>Here is the whole config reference with these changes: </summary>

```yaml
# Messenger configuration
messenger:
    enabled:              true
    routing:

        # Prototype
        message_class:
            senders:              []
    serializer:
        enabled:              true
        format:               json
        context:

            # Prototype
            name:                 ~
    encoder:              messenger.transport.serializer
    decoder:              messenger.transport.serializer
    adapters:

        # Prototype
        name:
            dsn:                  ~
            options:              []
    default_bus:          null
    buses:

        # Prototype
        name:
            default_middleware:  true
            middleware:

                # Prototype
                -
                    id:                   ~ # Required
                    arguments:            []
```

</details>

Commits
-------

f5ef421 [Messenger] Middleware factories support in config
  • Loading branch information
sroze committed May 14, 2018
2 parents 5f0e2d6 + f5ef421 commit f59ce97
Show file tree
Hide file tree
Showing 13 changed files with 222 additions and 24 deletions.
@@ -0,0 +1,37 @@
<?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\Bridge\Doctrine\Messenger;

use Symfony\Bridge\Doctrine\ManagerRegistry;

/**
* Create a Doctrine ORM transaction middleware to be used in a message bus from an entity manager name.
*
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
*
* @experimental in 4.1
* @final
*/
class DoctrineTransactionMiddlewareFactory
{
private $managerRegistry;

public function __construct(ManagerRegistry $managerRegistry)
{
$this->managerRegistry = $managerRegistry;
}

public function createMiddleware(string $managerName): DoctrineTransactionMiddleware
{
return new DoctrineTransactionMiddleware($this->managerRegistry, $managerName);
}
}
Expand Up @@ -1061,7 +1061,36 @@ function ($a) {
})
->end()
->defaultValue(array())
->prototype('scalar')->end()
->prototype('array')
->beforeNormalization()
->always()
->then(function ($middleware): array {
if (!\is_array($middleware)) {
return array('id' => $middleware);
}
if (isset($middleware['id'])) {
return $middleware;
}
if (\count($middleware) > 1) {
throw new \InvalidArgumentException(sprintf('There is an error at path "framework.messenger" in one of the buses middleware definitions: expected a single entry for a middleware item config, with factory id as key and arguments as value. Got "%s".', json_encode($middleware)));
}

return array(
'id' => key($middleware),
'arguments' => current($middleware),
);
})
->end()
->fixXmlConfig('argument')
->children()
->scalarNode('id')->isRequired()->cannotBeEmpty()->end()
->arrayNode('arguments')
->normalizeKeys(false)
->defaultValue(array())
->prototype('variable')
->end()
->end()
->end()
->end()
->end()
->end()
Expand Down
Expand Up @@ -1468,12 +1468,17 @@ private function registerMessengerConfiguration(array $config, ContainerBuilder
$config['default_bus'] = key($config['buses']);
}

$defaultMiddleware = array('before' => array('logging'), 'after' => array('route_messages', 'call_message_handler'));
$defaultMiddleware = array(
'before' => array(array('id' => 'logging')),
'after' => array(array('id' => 'route_messages'), array('id' => 'call_message_handler')),
);
foreach ($config['buses'] as $busId => $bus) {
$middleware = $bus['default_middleware'] ? array_merge($defaultMiddleware['before'], $bus['middleware'], $defaultMiddleware['after']) : $bus['middleware'];

if (!$validationConfig['enabled'] && \in_array('messenger.middleware.validation', $middleware, true)) {
throw new LogicException('The Validation middleware is only available when the Validator component is installed and enabled. Try running "composer require symfony/validator".');
foreach ($middleware as $middlewareItem) {
if (!$validationConfig['enabled'] && 'messenger.middleware.validation' === $middlewareItem['id']) {
throw new LogicException('The Validation middleware is only available when the Validator component is installed and enabled. Try running "composer require symfony/validator".');
}
}

$container->setParameter($busId.'.middleware', $middleware);
Expand Down
Expand Up @@ -391,9 +391,16 @@

<xsd:complexType name="messenger_bus">
<xsd:sequence>
<xsd:element name="middleware" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="middleware" type="messenger_middleware" minOccurs="0" maxOccurs="unbounded" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"/>
<xsd:attribute name="default-middleware" type="xsd:boolean"/>
</xsd:complexType>

<xsd:complexType name="messenger_middleware">
<xsd:sequence>
<xsd:element name="argument" type="xsd:anyType" minOccurs="0" maxOccurs="unbounded" />
</xsd:sequence>
<xsd:attribute name="id" type="xsd:string" use="required"/>
</xsd:complexType>
</xsd:schema>
@@ -0,0 +1,16 @@
<?php

$container->loadFromExtension('framework', array(
'messenger' => array(
'buses' => array(
'command_bus' => array(
'middleware' => array(
array(
'foo' => array('qux'),
'bar' => array('baz'),
),
),
),
),
),
));
Expand Up @@ -7,6 +7,7 @@
'messenger.bus.commands' => null,
'messenger.bus.events' => array(
'middleware' => array(
array('with_factory' => array('foo', true, array('bar' => 'baz'))),
'allow_no_handler',
),
),
Expand Down
Expand Up @@ -9,12 +9,19 @@
<framework:messenger default-bus="messenger.bus.commands">
<framework:bus name="messenger.bus.commands" />
<framework:bus name="messenger.bus.events">
<framework:middleware>allow_no_handler</framework:middleware>
<framework:middleware id="with_factory">
<framework:argument>foo</framework:argument>
<framework:argument>true</framework:argument>
<framework:argument>
<framework:bar>baz</framework:bar>
</framework:argument>
</framework:middleware>
<framework:middleware id="allow_no_handler" />
</framework:bus>
<framework:bus name="messenger.bus.queries" default-middleware="false">
<framework:middleware>route_messages</framework:middleware>
<framework:middleware>allow_no_handler</framework:middleware>
<framework:middleware>call_message_handler</framework:middleware>
<framework:middleware id="route_messages" />
<framework:middleware id="allow_no_handler" />
<framework:middleware id="call_message_handler" />
</framework:bus>
</framework:messenger>
</framework:config>
Expand Down
@@ -0,0 +1,7 @@
framework:
messenger:
buses:
command_bus:
middleware:
- foo: ['qux']
bar: ['baz']
Expand Up @@ -5,6 +5,7 @@ framework:
messenger.bus.commands: ~
messenger.bus.events:
middleware:
- with_factory: [foo, true, { bar: baz }]
- "allow_no_handler"
messenger.bus.queries:
default_middleware: false
Expand Down
Expand Up @@ -604,18 +604,41 @@ public function testMessengerWithMultipleBuses()

$this->assertTrue($container->has('messenger.bus.commands'));
$this->assertSame(array(), $container->getDefinition('messenger.bus.commands')->getArgument(0));
$this->assertEquals(array('logging', 'route_messages', 'call_message_handler'), $container->getParameter('messenger.bus.commands.middleware'));
$this->assertEquals(array(
array('id' => 'logging'),
array('id' => 'route_messages'),
array('id' => 'call_message_handler'),
), $container->getParameter('messenger.bus.commands.middleware'));
$this->assertTrue($container->has('messenger.bus.events'));
$this->assertSame(array(), $container->getDefinition('messenger.bus.events')->getArgument(0));
$this->assertEquals(array('logging', 'allow_no_handler', 'route_messages', 'call_message_handler'), $container->getParameter('messenger.bus.events.middleware'));
$this->assertEquals(array(
array('id' => 'logging'),
array('id' => 'with_factory', 'arguments' => array('foo', true, array('bar' => 'baz'))),
array('id' => 'allow_no_handler', 'arguments' => array()),
array('id' => 'route_messages'),
array('id' => 'call_message_handler'),
), $container->getParameter('messenger.bus.events.middleware'));
$this->assertTrue($container->has('messenger.bus.queries'));
$this->assertSame(array(), $container->getDefinition('messenger.bus.queries')->getArgument(0));
$this->assertEquals(array('route_messages', 'allow_no_handler', 'call_message_handler'), $container->getParameter('messenger.bus.queries.middleware'));
$this->assertEquals(array(
array('id' => 'route_messages', 'arguments' => array()),
array('id' => 'allow_no_handler', 'arguments' => array()),
array('id' => 'call_message_handler', 'arguments' => array()),
), $container->getParameter('messenger.bus.queries.middleware'));

$this->assertTrue($container->hasAlias('message_bus'));
$this->assertSame('messenger.bus.commands', (string) $container->getAlias('message_bus'));
}

/**
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage There is an error at path "framework.messenger" in one of the buses middleware definitions: expected a single entry for a middleware item config, with factory id as key and arguments as value. Got "{"foo":["qux"],"bar":["baz"]}"
*/
public function testMessengerMiddlewareFactoryErroneousFormat()
{
$this->createContainerFromFile('messenger_middleware_factory_erroneous_format');
}

public function testTranslator()
{
$container = $this->createContainerFromFile('full');
Expand Down
Expand Up @@ -27,4 +27,9 @@ public function testAssetsHelperIsRemovedWhenPhpTemplatingEngineIsEnabledAndAsse
{
$this->markTestSkipped('The assets key cannot be set to false using the XML configuration format.');
}

public function testMessengerMiddlewareFactoryErroneousFormat()
{
$this->markTestSkipped('XML configuration will not allow eeroneous format.');
}
}
Expand Up @@ -248,24 +248,37 @@ private function registerBusToCollector(ContainerBuilder $container, string $bus
$container->getDefinition('messenger.data_collector')->addMethodCall('registerBus', array($busId, new Reference($tracedBusId)));
}

private function registerBusMiddleware(ContainerBuilder $container, string $busId, array $middleware)
private function registerBusMiddleware(ContainerBuilder $container, string $busId, array $middlewareCollection)
{
$container->getDefinition($busId)->replaceArgument(0, array_map(function (string $name) use ($container, $busId) {
if (!$container->has($messengerMiddlewareId = 'messenger.middleware.'.$name)) {
$messengerMiddlewareId = $name;
$middlewareReferences = array();
foreach ($middlewareCollection as $middlewareItem) {
$id = $middlewareItem['id'];
$arguments = $middlewareItem['arguments'] ?? array();
if (!$container->has($messengerMiddlewareId = 'messenger.middleware.'.$id)) {
$messengerMiddlewareId = $id;
}

if (!$container->has($messengerMiddlewareId)) {
throw new RuntimeException(sprintf('Invalid middleware "%s": define such service to be able to use it.', $name));
throw new RuntimeException(sprintf('Invalid middleware "%s": define such service to be able to use it.', $id));
}

if ($container->getDefinition($messengerMiddlewareId)->isAbstract()) {
if (($definition = $container->findDefinition($messengerMiddlewareId))->isAbstract()) {
$childDefinition = new ChildDefinition($messengerMiddlewareId);
$count = \count($definition->getArguments());
foreach (array_values($arguments ?? array()) as $key => $argument) {
// Parent definition can provide default arguments.
// Replace each explicitly or add if not set:
$key < $count ? $childDefinition->replaceArgument($key, $argument) : $childDefinition->addArgument($argument);
}

$container->setDefinition($messengerMiddlewareId = $busId.'.middleware.'.$name, $childDefinition);
$container->setDefinition($messengerMiddlewareId = $busId.'.middleware.'.$id, $childDefinition);
} elseif ($arguments) {
throw new RuntimeException(sprintf('Invalid middleware factory "%s": a middleware factory must be an abstract definition.', $id));
}

return new Reference($messengerMiddlewareId);
}, $middleware));
$middlewareReferences[] = new Reference($messengerMiddlewareId);
}

$container->getDefinition($busId)->replaceArgument(0, $middlewareReferences);
}
}
Expand Up @@ -13,6 +13,7 @@

use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\Compiler\ResolveChildDefinitionsPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ServiceLocator;
Expand Down Expand Up @@ -359,14 +360,42 @@ public function testRegistersMiddlewareFromServices()
$container = $this->getContainerBuilder();
$container->register($fooBusId = 'messenger.bus.foo', MessageBusInterface::class)->setArgument(0, array())->addTag('messenger.bus');
$container->register('messenger.middleware.allow_no_handler', AllowNoHandlerMiddleware::class)->setAbstract(true);
$container->register('middleware_with_factory', UselessMiddleware::class)->addArgument('some_default')->setAbstract(true);
$container->register('middleware_with_factory_using_default', UselessMiddleware::class)->addArgument('some_default')->setAbstract(true);
$container->register(UselessMiddleware::class, UselessMiddleware::class);

$container->setParameter($middlewareParameter = $fooBusId.'.middleware', array(UselessMiddleware::class, 'allow_no_handler'));
$container->setParameter($middlewareParameter = $fooBusId.'.middleware', array(
array('id' => UselessMiddleware::class),
array('id' => 'middleware_with_factory', 'arguments' => array('foo', 'bar')),
array('id' => 'middleware_with_factory_using_default'),
array('id' => 'allow_no_handler'),
));

(new MessengerPass())->process($container);
(new ResolveChildDefinitionsPass())->process($container);

$this->assertTrue($container->hasDefinition($childMiddlewareId = $fooBusId.'.middleware.allow_no_handler'));
$this->assertEquals(array(new Reference(UselessMiddleware::class), new Reference($childMiddlewareId)), $container->getDefinition($fooBusId)->getArgument(0));

$this->assertTrue($container->hasDefinition($factoryChildMiddlewareId = $fooBusId.'.middleware.middleware_with_factory'));
$this->assertEquals(
array('foo', 'bar'),
$container->getDefinition($factoryChildMiddlewareId)->getArguments(),
'parent default argument is overridden, and next ones appended'
);

$this->assertTrue($container->hasDefinition($factoryWithDefaultChildMiddlewareId = $fooBusId.'.middleware.middleware_with_factory_using_default'));
$this->assertEquals(
array('some_default'),
$container->getDefinition($factoryWithDefaultChildMiddlewareId)->getArguments(),
'parent default argument is used'
);

$this->assertEquals(array(
new Reference(UselessMiddleware::class),
new Reference($factoryChildMiddlewareId),
new Reference($factoryWithDefaultChildMiddlewareId),
new Reference($childMiddlewareId),
), $container->getDefinition($fooBusId)->getArgument(0));
$this->assertFalse($container->hasParameter($middlewareParameter));
}

Expand All @@ -378,7 +407,25 @@ public function testCannotRegistersAnUndefinedMiddleware()
{
$container = $this->getContainerBuilder();
$container->register($fooBusId = 'messenger.bus.foo', MessageBusInterface::class)->setArgument(0, array())->addTag('messenger.bus');
$container->setParameter($middlewareParameter = $fooBusId.'.middleware', array('not_defined_middleware'));
$container->setParameter($middlewareParameter = $fooBusId.'.middleware', array(
array('id' => 'not_defined_middleware', 'arguments' => array()),
));

(new MessengerPass())->process($container);
}

/**
* @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException
* @expectedExceptionMessage Invalid middleware factory "not_an_abstract_definition": a middleware factory must be an abstract definition.
*/
public function testMiddlewareFactoryDefinitionMustBeAbstract()
{
$container = $this->getContainerBuilder();
$container->register('not_an_abstract_definition', UselessMiddleware::class);
$container->register($fooBusId = 'messenger.bus.foo', MessageBusInterface::class)->setArgument(0, array())->addTag('messenger.bus', array('name' => 'foo'));
$container->setParameter($middlewareParameter = $fooBusId.'.middleware', array(
array('id' => 'not_an_abstract_definition', 'arguments' => array('foo')),
));

(new MessengerPass())->process($container);
}
Expand Down

0 comments on commit f59ce97

Please sign in to comment.