-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[DependencyInjection] Configure service tags via attributes #39776
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
Conversation
fac3e51
to
a4710cf
Compare
Having only read the nicely detailed PR description:
Btw, thanks for exploring the use-case of attributes in Symfony! I'm excited to see what things we can come up with for attributes :) |
49c0d81
to
7c63c43
Compare
They would be my personal preference as well. I wanted to provide a quick way to explore this feature. A way that always works, even if the bundle consuming a particular DI tag does not (yet) provide an implementation of |
Remark on the CI: I strongly disagree with fabbot and I'm a bit clueless why the type patching fails in Travis. 🤔 |
7c63c43
to
02c1ca3
Compare
I think I'm 👎 for the same reasons as before: DI configuration via annotations conflicts with configuration via other means, with no way to solve the conflict. |
Can you give an example of such a conflict? |
Sure: eg an eventlistener that is enabled only for some specific Symfony env. If one uses the annotation on such an event listener, there would be no way to prevent it from being registered without resorting to workarounds. The code (and the author of the code) should never be given the power to decide how such code is used. |
They way to disable this feature would be: $definition->setAutoconfigured(false); By the way, don't we have the same problem with event subscribers? What's our solution there?
This is why, as far as I understand it, you shouldn't enable autoconfiguration on code you don't own. |
I think I agree with @derrabus here. As we're only doing this on autoconfigured definitions, it makes sense to me to do this. They can be used to add autoconfiguration for those tags that can't work with interfaces (like event listeners). |
Hum, OK, I suppose I'm a bit overeactive here, since the topic of configuration via DI came quite often from the wrong end. If things are only enabled when autoconfiguration is enabled (which alleviates my concerns), then I think we should make it crystal clear in the name of the attributes. E.g. |
I've been thinking a bit more about this, and if we bind this to autoconfiguration (which is a good idea), then I think it should behave exactly the same way, especially regarding inheritance. In practice, this means the implementation should rely on This means the feature should not be limited to tags, since autoconfig allows configuring more than tags. I also repeat that the name of the attributes should be crystal clear about what they mean. My current proposal would be to make them all start with the E.g Alternatively, we could go with only one attribute: Actually, we might want to support both styles. |
im for a single |
See draft proposal in #39804 |
From an end user point of view, I prefer the syntax proposed by @derrabus. This is crystal clear and very elegant: #[EventListener]
class MyListener
{
public function __invoke(RequestEvent $event): void
{
// …
}
} I also like |
Prefer vs what? About the
I don't know what you're talking about. If you're comparing with what is found in #39804, the implementation in this PR is more complex (more code/more logic) and less powerful (new concepts that are not fully defined). In comparison, #39804 just builds on existing concepts and is thus on steroids (as the rest is already.)
Sorry, but that's totally wrong. Attributes should never have mandatory side effects. They should be purely descriptive. And that's exactly what the All in all, I don't understand your comment @dunglas. I feel like you maybe didn't read or understand my PR, if that's what you're referring to implicitly. |
VS the
I'm comparing about the proposed attributes vs existing YAML/XML/PHP configuration formats, from a user point of view (I wasn't talking about internals).
I know well how PHP attributes work, I know that there is no hard coupling, and it's exactly why I was talking about "polluting", and not about "coupling". Also, PHP attributes change nothing regarding this topic when compared to annotations. If we want to change this policy, I would prefer to assume the "leak" and go one step further: #[Autowire]
#[Service('service.name')] // the parameter is optional
#[Tag('my.tag')] // add a tag
class Foo
{
public function __construct(
private BarInterface $bar, // autowired
#[Service('foo.bar')]
private BarInterface $bar2, // inject a specific service
#[Parameter('stripe_api_key')
private string $stripeApiKey,
#(Tagged('another.tag')]
private iterable $tagger,
) {}
Sure they could, but as they don't use the same terms and the same concepts, it would be pointless. No DI container I'm aware of uses the "autoconfiguration" term, Laravel DI has a concept of tags too, but PHP-DI hasn't, bindings is something Symfony-specific... To summarize:
|
so |
Thanks for clarifying @dunglas, I wasn't able to grasp what you meant sorry. About the name, it all depends on what we have:
Note that I think that @derrabus' critical new idea here is linking autoconfiguration and attributes. But for now, I think I'd like to focus on autoconf+attributes, because that's quite simple and bounded, and quite powerful. It might provide us enough of the DX we target. @derrabus I feel like we might want to continue this work on #39804 and close this PR. WDYT? |
Sorry for being quiet on this PR this week. My day job and two home schooled kids took most of my time. :-/ I try to find some time to elaborate on my proposal a bit more. Right now, I think that this PR is on a good path and I'd like to keep it open. Sorry again for the delay. |
.github/patch-types.php
Outdated
@@ -44,6 +47,7 @@ | |||
case false !== strpos($file, '/src/Symfony/Component/VarDumper/Tests/Fixtures/NotLoadableClass.php'): | |||
case false !== strpos($file, '/src/Symfony/Component/VarDumper/Tests/Fixtures/Php74.php') && \PHP_VERSION_ID < 70400: | |||
case false !== strpos($file, '/src/Symfony/Component/VarDumper/Tests/Fixtures/RepeatableAttribute.php'): | |||
case false !== strpos($file, '/src/Symfony/Contracts/Service/Attribute/Tag.php'): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
all changes on this file can be reverted now
02c1ca3
to
40c5b02
Compare
Sorry again for delaying the whole discussion. It has been a work intense week for me and the fact that the schools are closed at the moment did not make it easier for me. And sorry for the upcoming wall of text, but I felt I needed to elaborate on my approach a bit, given that there is now a competing PR. Why I chose this implementationThe idea of autoconfiguration (as I understood it)Autoconfiguration is a feature that I was skeptical about at first, but I learned to like it over the years. What I like about it is that the framework (or a bundle as an extension to the framework) allows to define extension points for the application that are automagically configured if I place the right class in the right folder (and PSR-4 discovery and autowiring are enabled as well). A popular example are event subscribers: I can create a class implementing There are a few aspects that I would emphasize here:
Take Attaching tag attributes to autoconfigurationI've attached this feature to autoconfiguration because
If I'm bloating the autoconfiguration feature with this, we could introduce a new setting Autoconfiguration requires an interfaceThe autoconfiguration feature requires an interface it can listen on. This is not ideal for DI tags that we cannot build an interface for or that require additional configuration, e.g.:
Those are the cases I'd like to improve with tag attributes. Markers are meh, conventions can get ugly and telling users to redefine an auto-discovered service because they want to change the monolog channel feels unnecessarily complicated. DI tags and their properties are not discoverableNote: Our DI tags can have optional
So, for all the examples I've listed above, imagine you can attach the following attributes to your classes:
The DX is way better here:
The contractsThis PR adds an attribute and an interface to the service contracts package. That package has an implementation-agnostic approach: It contains interfaces that can be used to build a service container, without an opinion on how to implement it. The This PR attempts to follow the nature of this approach. It allows attaching metadata to a class. It does not mention autoconfiguration because that is a feature of the Symfony implementation. ReadabilitySometimes I have to earn money with my coding work and when I do, I usually help modernizing legacy software. Thus I get to read a lot of code. A fair share of it is code that has been written by inexperienced developers who didn't read the Symfony documentation as often as they should have. When I build a feature like that, I inevitably think about how those people would've written their code if my feature had existed back then. Would I enjoy reading that code? Regarding those attributes, I'd say I would. More than I enjoy reading their The generic The proposed
|
53e9294
to
d5d49c2
Compare
Thanks for your answer @derrabus. I'm trying a comparative answer here:
Same here. Except for one thing:
You know that's not the case, autoconfiguration is more powerful than just tags. That's important here because that's a difference between both proposals (this one is only about tags.)
Correct. I don't know if you make this point in relation to my proposal or not. I've been using the
100% The good idea of this proposal. Both build on it.
There is a significant difference thought: autoconfiguration works by looking at the base types of a class (aka looking at parent classes and interfaces), this one proposes to break this rule by making autoconfigure do something else that doesn't consider the type hierarchy, but applies only to the very class that uses the attributes. To be remembered for the comparison.
Indeed, that wouldn't make sense at all: a core design principle of the DI container is to never use reflection unless asked to. Asking to enable reflection by using reflection is a non-sense.
I agree we need only one way to opt-out from regular config (and opt-in for reflection-based config.) - and it already exists.
More accurately, autoconfiguration requires a base class. Take e.g. the
For sure. Did you read that's what my proposal was about? Hell no, that's not the case :)
100% aligned with this section, no diff here between both proposals.
Side note: if we go the way implemented in this PR, I will recommend removing the interface. Value objects should not have an interface, because they don't provide a behavior but a data structure.
Describing how a class should be autoconfigured is not something specific to Symfony at all, if that's the point you're making. All DI containers, no matter the implementation, need either explicit manual wiring or a description of how to auto-instantiate a class.
Locality for the win. We're on par, no diff here.
100%, no diff here.
I'm sure you don't, but our community has the need, that's why we built
Neither do I. We will document how to use individual attributes, that works already.
Sorry but this has no base. Neither the "will ignore", nor the "horrifies me" part. I cannot discuss this statement.
Not really fair ;) I can trivially write down the very same typo while describing the very same thing using the Did you see in my PR that I also built an attribute that will assist ppl in describing eg an event listener, and save them from typos, exactly like yours?
Yep, that's why you'll find also
Totally doable. And autoconfiguration would be the way to actually do the wiring under the hood, reusing existing implementations.
Sorry but I don't agree at all. There is actually zero implementation in these attributes, they are pure data structures (they should use public properties by the way, exactly for this reason.) And there are zero couplings to any implementation. The description provided by the attributes defined here or in my proposal can trivially be read by any DI container implementation, and they will trivially map to similar concepts - because all of them deal with instantiating classes. These attributes just give them a few useful hints.
Yes it should, see just above.
This very PR diverges from the current definition of autoconfiguration by introducing new behaviors. My PR builds on top of autoconfiguration without changing anything about the concept. Instead, it provides a new declarative way to define rules, where currently we have to resort to either My proposal is already more capable, just because it builds on what already works. If we go with this PR, we're going to have to cherry-pick new attributes at every Symfony version, and we'll build a system parallel to the already existing one. If you want just an example: bindings work with either constructor argument, service subscribers, or controller's actions. This works thanks to many compiler passes that work together with one single declarative source: autoconfiguration rules. Are we going to do this work again, to permeate new declarations based on attributes into all those passes? I think this is calling for a significant increase of complexity, for no reason. |
Please allow me to enter this discussion without any internal knowledge about the autoconfigure, but purely basing on the comments in this PR: The current state of autoconfigure in Symfony: you can configure tags, method calls and argument binds based on interfaces The big issue with both the PRs is that both are stating that promise, but are not fulfilling it. I believe this discussion is founded 100% by the mismatch between promise and implementation. As in fact, the PRs are very similar (autoconfigure as flag vs system is the only difference). Both PRs have a base attribute ( Why can't we drop this completely and focus on the low level: allow generic attributes to be used for service autoconfiguration? Example: There is an namespace Symfony\Component\EventListener\Attribute;
class EventListener
{
public function __construct(
public ?string $event = null
) {
}
} And we allow registering how to handle this attribute using a static call in the FrameworkBundle's AttributeAutoconfigurationPass::addAttribute(EventListener::class, function (EventListener $attribute) {
return [
'tags' => [
'name' => 'kernel.event_listener',
'event' => $attribute->event,
],
];
}); The compiler pass will then use the callable to transform a decoupled attribute into autoconfigure configuration, which is then passed to the autoconfigure system (see Nicolas' PR for an implementation using the yaml loader). This also fixes 3 of the pain points mentioned in the discussion:
|
@wouterj thanks for the proposal. Actually, I think my proposal does fulfill the promise: if one cares to read the If we go the way you propose, we totally fail the promise to me, as every single application could decide how to interpret a specific attribute, and any third party implementation would need to know about all variations of those attributes to reuse them. Attributes are a language (a DSL) in the language (PHP). Providing one single data structure to rule them all is how the promise is fulfilled to me. |
Fair enough, we currently have that one exception to the rule.
I did, to explain why I chose not to use that wording.
All right, so let's find the best way to make those attributes happen.
I disagree, but let's not open up that discussion as well now. 😃
My point here is that without understanding the use case, I cannot come up with a good solution. But the service tagging is a pain I share, that's why I concentrated on that aspect.
Okay, but why that generic base class then? I think that class is what bugs me the most. We can totally implement
I did. I merely explained why I'm already having my doubts on my own generic
Let me rephrase my statement: The property names are an enumeration of the DI component's feature set.
I don't understand this statement.
The already existing system is that we tag services for compiler passes to pick them up. This proposal blends in because it gives the developer a tool to tag services, nothing more.
You lost me completely here, sorry. All of that will continue to work without making the code aware of the new attributes. |
I very much like that idea as well. This way, the attribute does not know how it's mapped to a DI tag. That concern is moved to the bundle, where we configure autoconfiguration already. A small drawback is that the reflection API is a bit friendlier if you can specify a common base type for the attributes you're looking for. With your proposal we would have to loop either through a class' attributes or the list of attributes registered for autoconfiguration. But that does not have to be a problem.
Well, the compiler passes are already part of the component and leak the tag name (although it's configurable there). |
I think I understand what you're trying to achieve: you're trying to prevent typos no matter what. That's why you're not satisfied with the I don't share this concern, because to me, this save-ppl-from-typos-no-matter-what mantra leads to removing power from users. I like the With the approach proposed by @wouterj, we can remove the base Note that if we still decide to go this way, the static method (aka global state) is a no-go on my side. Instead, this should be a compiler pass - e.g. one per bundle - that could possibly extend a base class to allow factorizing some common boilerplate.
You're not alone, we're all here to share our experience and understanding. Mine here is that ppl don't need to configure only tags, even if they're quite common. My bet is that this PR is just the very beginning of autoconfiguration by attribute. That's what I meant when I wrote "If we go with this PR, we're going to have to cherry-pick new attributes at every Symfony version". Ppl will submit PRs to configure all the other things that they already configure. My PR solves all use cases that autoconfiguration already supports. |
At least for me (I know you're probably referring to @derrabus here, but since we're talking features, I think I can share my opinion), autocompletion and spelling check using language features a nice bonus (afaik, it's the main reason that Symfony moved to PHP configuration internally). However, the big issue with |
Please acknowledge the critical difference between this proposal and the previous ones: this one is opt-in, thanks to the autoconfigure flag. By doing so, it does not conflict with the other configuration formats (since that's the big no-go of previous proposals.) |
Yes, I know (which is also why this PR wasn't immediately rejected 😄 ). However, this comment by @dunglas is imho a significant blocker for attributes to relate directly to DI that isn't fixed (yet):
Hence my proposal of making declarative attributes from a component view (just like interface). And then using the FrameworkBundle to wire declarative attributes to DI configuration (if enabled for that service through autoconfiguration).
I think we can maybe also use the same idea as the |
I fail to understand how this makes any difference with @dunglas' objections? If using annotations is the issue, no matter how their interpretation logic is implemented, they still have all the listed issues. Also, I don't agree with these comments (in the context of this discussion). Declarative programming decouples everything so that no coupling exists because ppl are just free to ignore (part of) the declarations. |
Correct, the DI component would provide a framework registering attributes for autoconfiguration, just as it currently provides a framework for registering base types for autoconfiguration.
Yeah, no static method call. How about this? $container->registerForAutoconfiguration(EventSubscriberInterface::class)
->addTag('kernel.event_subscriber');
$container->registerAttributeForAutoconfiguration(
EventListener::class,
static function (Definition $definition, EventListener $attribute): void {
$definition->addTag('kernel.event_listener', ['event' => $attribute->event]);
}
); |
d5d49c2
to
3ff85f8
Compare
Implemented as #39897, please let me know what you think. |
Closing in favor of #39897. |
…efine autoconfiguration rules (nicolas-grekas) This PR was merged into the 5.3-dev branch. Discussion ---------- [DependencyInjection] Add `#[Autoconfigure]` to help define autoconfiguration rules | Q | A | ------------- | --- | Branch? | 5.x | Bug fix? | yes | New feature? | no | Deprecations? | no | Tickets | - | License | MIT | Doc PR | - Being inspired by the discussion with @derrabus in #39776. This PR allows declaring autoconfiguration rules using an attribute on classes/interfaces, eg: `#[Autoconfigure(bind: ['$foo' => 'bar'], tags: [...], calls: [...])]` This should typically be added on a base class/interface to tell *how* implementations of such a base type should be autoconfigured. The attribute is parsed when autoconfiguration is enabled, except when a definition has the `container.ignore_attributes` tag, which allows opting out from this behavior. As usual, the corresponding rules are applied only to services that have autoconfiguration enabled. In practice, this means that this enables auto-tagging of all implementations of this interface: ```php #[Autoconfigure(tags: ['my_tag'])] interface MyInterface {...} ``` Of course, all auto-configurable settings are handled (calls, bindings, etc.) This PR adds another attribute: `#[AutoconfigureTag()]`. It extends `#[Autoconfigure]` and allows for specifically defining tags to attach by autoconfiguration. The name of the tag is optional and defaults to the name of the tagged type (typically the FQCN of an interface). This should ease with writing locators/iterators of tagged services. ```php #[AutoconfigureTag()] interface MyInterface {...} ``` Commits ------- 64ab6a2 [DependencyInjection] Add `#[Autoconfigure]` to help define autoconfiguration rules
…rabus) This PR was merged into the 5.3-dev branch. Discussion ---------- [DependencyInjection] Autoconfigurable attributes | Q | A | ------------- | --- | Branch? | 5.x | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | N/A | License | MIT | Doc PR | TODO Alternative to #39776. Please have a look at that PR as well to see the full discussion. With this PR, I propose to introduce a way to autoconfigure services by using PHP Attributes. The feature is enabled on all autoconfigured service definitions. The heart of this feature is a new way to register autoconfiguration rules: ```php $container->registerAttributeForAutoconfiguration( MyAttribute::class, static function (ChildDefinition $definition, MyAttribute $attribute, \ReflectionClass $reflector): void { $definition->addTag('my_tag', ['some_property' => $attribute->someProperty]); } ); ``` An example for such an attribute is shipped with this PR with the `EventListener` attribute. This piece of code is a fully functional autoconfigurable event listener: ```php use Symfony\Component\EventDispatcher\Attribute\EventListener; use Symfony\Component\HttpKernel\Event\RequestEvent; #[EventListener] class MyListener { public function __invoke(RequestEvent $event): void { // … } } ``` What makes attributes interesting for this kind of configuration is that they can transport meta information that can be evaluated during autoconfiguration. For instance, if we wanted to change the priority of the listener, we can just pass it to the attribute. ```php #[EventListener(priority: 42)] ``` The attribute itself is a dumb data container and is unaware of the DI component. This PR provides applications and bundles with the necessary tools to build own attributes and autoconfiguration rules. Commits ------- 2ab3caf [DependencyInjection] Autoconfigurable attributes
With this PR, I propose to introduce a way to tag services by using PHP Attributes. The feature is enabled on all autoconfigured service definitions. The heart of this feature is a new attribute class
Tag
that I've added to the service contracts package.The above code has the same effect as manually adding the tag
kernel.reset
to the service definition ofMyResettableService
.While the generic
Tag
attribute will work for any tag that we're currently using in Symfony, its UX is not really a revelation. This is why I've added aTagInterface
as well that allows us to implement more specific attribute classes. This PR includes an implementation for thekernel.reset
tag:Bonus content: I've also included an
EventListener
attribute. This piece of code is a fully functional autoconfigurable event listener: