Skip to content

[DependencyInjection] Autowire arguments using attributes #40406

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

Merged
merged 2 commits into from
Apr 13, 2021

Conversation

derrabus
Copy link
Member

@derrabus derrabus commented Mar 7, 2021

Q A
Branch? 5.x
Bug fix? no
New feature? yes
Deprecations? no
Tickets N/A
License MIT
Doc PR TODO

This PR allows us to bind arguments via attributes. This mechanism is enabled for tagged iterators and service locators for now.

To receive an iterator with all services tagged with my_tag:

use Symfony\Component\DependencyInjection\Attribute\TaggedIterator;

class MyService
{
    public function __construct(
        #[TaggedIterator('my_tag')]
        private iterator $taggedServices,
    ) {
    }
}

To receive a locator with all services tagged with my_tag:

use Psr\Container\ContainerInterface;
use Symfony\Component\DependencyInjection\Attribute\TaggedLocator;

class MyService
{
    public function __construct(
        #[TaggedLocator('my_tag')]
        private ContainerInterface $taggedServices,
    ) {
    }
}

@rvanlaak
Copy link
Contributor

rvanlaak commented Mar 8, 2021

Any ideas on an instanceof tagged iterator? That would prevent the need for tagging these services first.

@derrabus
Copy link
Member Author

derrabus commented Mar 8, 2021

Any ideas on an instanceof tagged iterator? That would prevent the need for tagging these services first.

$container->registerForAutoconfiguration(YourInterface::class)
    ->addTag('your_tag');

@nicolas-grekas
Copy link
Member

$container->registerForAutoconfiguration(YourInterface::class)->addTag('your_tag');

💯

or #39804 on 5.3+PHP8 :)

@nicolas-grekas nicolas-grekas added this to the 5.x milestone Mar 8, 2021
Copy link
Member

@nicolas-grekas nicolas-grekas left a comment

Choose a reason for hiding this comment

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

Thanks here are some comments.

This approach means that for arguments, we don't provide extensibility similar to registerAttributeForAutoconfiguration(). But maybe we don't need it. I can't think of an obvious use case anyway.

This mechanism is enabled for tagged iterators and service locators for now

What else should we support? I can think of:

  • #[Value('%foo% bar %env(baz)%')]
  • #[Name('my_cache')] (or #[Variant]? or #[Alias]?) <- this would override the "name" part when matching named autowiring aliases - aka replace the name of the argument itself

We could add a way to reference services by identifier, but I don't think it's a good idea, since autowiring provides better decoupling and the code should not know about service ids.

@derrabus
Copy link
Member Author

derrabus commented Mar 8, 2021

This approach means that for arguments, we don't provide extensibility similar to registerAttributeForAutoconfiguration(). But maybe we don't need it. I can't think of an obvious use case anyway.

Me neither. I started using the same mechanism first, but I wasn‘t happy with it.

In the configurators, I always accessed the bindings, fetched the parameter name from the reflector, prepended it with a $, configured an argument object, added it to the bindings as stuffed the new bindings arrays back into the definition. This felt like a lot of unnecessary, error-prone boilerplate.

And in the end, constructing the argument object was the only operation that is different in each configurator. So my next attempt was to reuse an existing data structure for arguments and I somewhat liked who easy it was to implement that. But I already noticed that you‘re not sharing my enthusiasm here and that‘s fine. I‘m going to build dedicated attributes for constructor parameters. 🙂

What else should we support? I can think of:

  • #[Value('%foo% bar %env(baz)%')]

Value is probably a good idea, though I‘d like to have simplified versions of it as well: Parameter and EnvVariable.

We could add a way to reference services by identifier, but I don't think it's a good idea, since autowiring provides better decoupling and the code should not know about service ids.

I don‘t think that it‘s inherently bad to have that attribute. However I can imagine more bad than good use-cases for it. But I‘ll leave it out of this PR for the moment.

@nicolas-grekas
Copy link
Member

nicolas-grekas commented Mar 9, 2021

Value is probably a good idea, though I‘d like to have simplified versions of it as well: Parameter and EnvVariable.

I'm mixed on this: it'd mean teaching something that would be a dead-end once ppl want to append/prepend any string next to a param/env. I think I'd prefer not to add them.

@derrabus derrabus force-pushed the feature/bind-arguments branch 3 times, most recently from 647c06d to 18499b8 Compare March 13, 2021 19:12
@derrabus
Copy link
Member Author

Please have another look. The PR now uses new dedicated attribute classes.

@derrabus derrabus force-pushed the feature/bind-arguments branch from 18499b8 to d9c3152 Compare March 13, 2021 23:59
@derrabus
Copy link
Member Author

I've switched the pass to AbstractRecursivePass and added tests for a nested definition and a factory service.

@derrabus derrabus force-pushed the feature/bind-arguments branch from d9c3152 to 35b3945 Compare March 14, 2021 00:08
Copy link
Member

@nicolas-grekas nicolas-grekas left a comment

Choose a reason for hiding this comment

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

Nice step :)

@nicolas-grekas
Copy link
Member

(please rebase to account for #40468)

@derrabus derrabus force-pushed the feature/bind-arguments branch 3 times, most recently from 9276b67 to e07e130 Compare April 2, 2021 18:54
@derrabus
Copy link
Member Author

derrabus commented Apr 2, 2021

I have explored a bit if we can extend that feature to setters, but I'm a bit afraid of the edge cases that we get here.

Is the idea to browse all public methods for those attributes? If yes:

  • If we find one of the attributes on a parameter of a public method other than the constructor: Do we add a method call for that method?
  • What if there already is a method call configured for that very method?
  • What if the method has more parameters than the one with the attribute?

Or should we only browse methods that are already configured to be called?

  • What if a call is added later, via a userland compiler pass?
  • Is this behavior comprehesible for a developer or would they expect the attribute to act as a marker for "call this method"?

Copy link
Member

@Nyholm Nyholm left a comment

Choose a reason for hiding this comment

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

Thank you.

I've done my best reviewing this. Im not super confident with PHP8 attributes nor the DI component.

I think it looks good, just some minor comments.

@nicolas-grekas
Copy link
Member

nicolas-grekas commented Apr 11, 2021

I submitted derrabus#1 on your fork to help implement this in a simpler yet more powerful way.
The core idea is that this is a matter related to autowiring - not to autoconfiguration.
Once we acknowledge this, the implementation becomes simpler and more powerful, have a look.

The implementation in AutowirePass also hints about two improvements that we could make after this:

  1. we could add a new attribute to tell where a decorated service should be injected when using decoration. The current logic in AutowirePass is a bit fragile (it can detect the injection point only when a single argument is compatible with the decorated class.)
  2. instead of harcoding how TaggedIterator/TaggedLocator are wired, we can make this generic thanks to a new $container->registerAttributeForAutowiring() method.

@derrabus derrabus requested a review from dunglas as a code owner April 11, 2021 20:50
@nicolas-grekas nicolas-grekas changed the title [DependencyInjection] Bind constructor arguments via attributes [DependencyInjection] Autowire arguments using attributes Apr 11, 2021
@nicolas-grekas
Copy link
Member

Thank you @derrabus.

@nicolas-grekas nicolas-grekas merged commit dd919a7 into symfony:5.x Apr 13, 2021
@derrabus derrabus deleted the feature/bind-arguments branch April 13, 2021 20:36
@fabpot fabpot mentioned this pull request Apr 18, 2021
@symfony symfony deleted a comment from zmitic Apr 18, 2021
@zmitic
Copy link

zmitic commented Jun 3, 2021

Can TaggedLocator get an attribute for property as well? It creates a problem when constructor promotion is used:

Example:

    public function __construct(
        #[TaggedLocator(tag: 'app.tag')] 
        private ServiceLocator $locator,
    )
    {
    }

works but psalm reports this error:

This attribute can not be used on a property (see https://psalm.dev/242)

#[TaggedLocator(tag: 'app.tag')]  private ServiceLocator $locator,

Simple fix:

#[\Attribute(\Attribute::TARGET_PARAMETER | \Attribute::TARGET_PROPERTY)]
class TaggedLocator {}

@nicolas-grekas
Copy link
Member

Does it work? If yes, that's a bug in psalm where it should be reported.

Okhoshi added a commit to Okhoshi/symfony that referenced this pull request Oct 19, 2021
Okhoshi added a commit to Okhoshi/symfony that referenced this pull request Oct 19, 2021
Okhoshi added a commit to Okhoshi/symfony that referenced this pull request Oct 19, 2021
Okhoshi added a commit to Okhoshi/symfony that referenced this pull request Oct 19, 2021
Okhoshi added a commit to Okhoshi/symfony that referenced this pull request Oct 19, 2021
Okhoshi added a commit to Okhoshi/symfony that referenced this pull request Oct 19, 2021
Okhoshi added a commit to Okhoshi/symfony that referenced this pull request Oct 19, 2021
Okhoshi added a commit to Okhoshi/symfony that referenced this pull request Oct 19, 2021
Okhoshi added a commit to Okhoshi/symfony that referenced this pull request Oct 19, 2021
Okhoshi added a commit to Okhoshi/symfony that referenced this pull request Oct 19, 2021
Okhoshi added a commit to Okhoshi/symfony that referenced this pull request Oct 19, 2021
Okhoshi added a commit to Okhoshi/symfony that referenced this pull request Oct 20, 2021
Okhoshi added a commit to Okhoshi/symfony that referenced this pull request Oct 20, 2021
Okhoshi added a commit to Okhoshi/symfony that referenced this pull request Oct 21, 2021
nicolas-grekas added a commit that referenced this pull request Oct 26, 2021
… attributes (Okhoshi)

This PR was merged into the 5.3 branch.

Discussion
----------

[DependencyInjection] Fix autowiring tagged arguments from attributes

| Q             | A
| ------------- | ---
| Branch?       | 5.3
| Bug fix?      | yes
| New feature?  | no
| Deprecations? | no
| Tickets       | Fix #43272
| License       | MIT
| Doc PR        | no

Reimplement #40406 with `AttributeAutoconfigurationPass` to avoid the BC following the change in `CompilerPass` ordering in `PassConfig`.

Also revert the various fix made in an attempt to recover the BC introduced by #40406.

~Note: `5.4` branch was needed because `AttributeAutoconfigurationPass` is not handling parameters in 5.3~

To-do:
- [x] Add a test to cover the breakage presented in #43272

Commits
-------

4c7566f [DependencyInjection] Fix autowiring tagged arguments from attributes
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.

6 participants