Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Messenger] Middleware factories support in config #27128

Merged
merged 1 commit into from May 14, 2018
Merged

[Messenger] Middleware factories support in config #27128

merged 1 commit into from May 14, 2018

Conversation

ogizanagi
Copy link
Member

@ogizanagi ogizanagi commented May 2, 2018

Q A
Branch? master
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? no
Tests pass? yes
Fixed tickets N/A
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):

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:

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:

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

Here is the whole config reference with these changes:
# 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:            []

@sroze
Copy link
Contributor

sroze commented May 2, 2018

I don't see the point of the - doctrine_transaction_middleware: ['entity_manager_name'] syntax if we support - {id: doctrine_transaction_middleware, arguments: ['entity_manager_name']}. And the last one is more explicit.

@ogizanagi
Copy link
Member Author

ogizanagi commented May 2, 2018

I don't see the point of the - doctrine_transaction_middleware: ['entity_manager_name'] syntax if we support - {id: doctrine_transaction_middleware, arguments: ['entity_manager_name']}.

Same as for keeping the simple - logger syntax, which could be replaced by - id: logger.
It's less cumbersome to read & write.

@sroze
Copy link
Contributor

sroze commented May 2, 2018

To me, using these arguments is the "advanced" way of configuring the middleware: using the object syntax with the id is simple enough that we don't need to use another confusing syntax IMHO.

-             - doctrine_transaction_middleware: ['entity_manager_name']
+             - {id: 'doctrine_transaction_middleware', arguments: ['entity_manager_name']}

Or do I miss something?

@nicolas-grekas
Copy link
Member

nicolas-grekas commented May 2, 2018

@sroze or drop the {id: *} style altogether?

@nicolas-grekas nicolas-grekas added this to the 4.1 milestone May 2, 2018
@ogizanagi
Copy link
Member Author

ogizanagi commented May 3, 2018

@sroze : Obviously you didn't miss anything. I just like this syntax :) (and I expected end-users to have a preference for it too but that's just my guess).
So, let's have a little poll:

  1. Current (concrete services and factories short syntaxes)

    middleware:
      - logger
      - doctrine_transaction_middleware: ['entity_manager_name']
  2. Drop the factories short syntax:

    middleware:
      - logger
      - id: doctrine_transaction_middleware:
        arguments: ['entity_manager_name']
  3. Drop every short syntaxes:

    middleware:
      - id: logger
      - id: doctrine_transaction_middleware:
        arguments: ['entity_manager_name']
  4. Drop array syntax:

    middleware:
      logger: ~
      doctrine_transaction_middleware: ['entity_manager_name']

My own preference is 1. Perhaps 4.
I think I blindly excluded the last one at first because it requires unique keys, but actually I don't think it makes sense to configure the same middleware, even with a different config, in the very same bus. The very same limitation still exists anyway in the current internal implementation but the syntax would allow it later if we need it. Changed
The other reason is that it enforces providing ~ for concrete services (logger: ~) which are not configurable anyway but might let you think so.

@sroze
Copy link
Contributor

sroze commented May 3, 2018

Looking at it this way, 4 looks nicer but the “one only” limitation is too many assumptions IMHO. 3 is verbose and 4 not really coherent. Let’s keep 1 then, good point 😃

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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just using setArgument here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Due to the way child definitions arguments work, using replaceArgument saves an extra hard coded index_ prefix for the key here.

if (is_int($index)) {
$this->arguments['index_'.$index] = $value;

@@ -202,22 +202,35 @@ private function registerBusToCollector(ContainerBuilder $container, string $bus

private function registerBusMiddleware(ContainerBuilder $container, string $busId, array $middleware)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you rename to $middlewareCollection to make it more explicit?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, good idea 👍 (uncountable nouns are hell in programmation)

$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));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we put that first as a simple if so we don't have this extra indentation?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so, it'll still need the abstract check and won't save any indent, right?

$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, 'arguments' => array()),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As you deal with the fact that arguments might not be present, shouldn't we remove it here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The config adds the default [] value, but not the default middleware in https://github.com/ogizanagi/symfony/blob/a36dad010b7634bbe3c35f74490ced841d0fc2a1/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php#L1472-L1474

Should I update the extension and enforce in the pass? Or the config so it removes it if empty? Not really worth it IMHO.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NVM, understood. I thought this was the FrameworkExtensionTest class 😅

$arguments = reset($middleware);

return array(
'id' => key($middleware),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should throw an exception is we have multiple keys in here

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue is that beforeNormalization isn't really designed for throwing. It's the validate responsibility which would also prepend the path. But at this point we would have already ignored the extra keys.

So we could add manually something like:

if (\count($middleware) > 1) {
    throw new \InvalidArgumentException(sprintf('At path "framework.messenger.buses.middleware": expected a single entry for a middleware item array, with factory id as key and arguments as value. Got "%s".', json_encode($middleware)));
}

Do you think it is worth it?

Copy link
Contributor

@sroze sroze May 10, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’d say, it’s easy to end up with something like that:

middleware:
    - foo: 1
      bar: { baz: ~ }

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, added

Copy link
Contributor

@sroze sroze left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great. Needs to be in 4.1 because it changes the XML syntax for middlewares for the good 👍

@sroze
Copy link
Contributor

sroze commented May 11, 2018

Failure on deps=high is expected.

@ogizanagi
Copy link
Member Author

I've just created the PR on doctrine/DoctrineBundle#817 to wire the services when the component is enabled.

@sroze
Copy link
Contributor

sroze commented May 14, 2018

Thank you @ogizanagi.

@sroze sroze merged commit f5ef421 into symfony:4.1 May 14, 2018
sroze added a commit that referenced this pull request May 14, 2018
…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
@ogizanagi ogizanagi deleted the messenger/middleware_factories branch May 14, 2018 19:21
@fabpot fabpot mentioned this pull request May 21, 2018
javiereguiluz added a commit to symfony/symfony-docs that referenced this pull request May 24, 2018
…iereguiluz)

This PR was merged into the 4.1 branch.

Discussion
----------

[Messenger] Document middleware factories

<!--

If your pull request fixes a BUG, use the oldest maintained branch that contains
the bug (see https://symfony.com/roadmap for the list of maintained branches).

If your pull request documents a NEW FEATURE, use the same Symfony branch where
the feature was introduced (and `master` for features of unreleased versions).

-->

Refs:

- Symfony PR: symfony/symfony#27128
- DoctrineBundle PR: doctrine/DoctrineBundle#817

Commits
-------

c5ef7c8 Reowrd to restore the original meaning
a87f21b Minor rewords
2880027 [Messenger] Document middleware factories
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants