Decorate a Service with AsDecorator
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeIn the last chapter, we used #[AsAlias] to alias RemoteInterface to ButtonRemote so that when we type-hint RemoteInterface, it gives us the ButtonRemote service. But, this broke our logging! We need to tell Symfony to give us LoggerRemote instead, but to pass the ButtonRemote service to LoggerRemote.
#[AsDecorator]
Basically, we need to tell Symfony that ButtonRemote is being decorated by LoggerRemote. To do this, in LoggerRemote, use another attribute: #[AsDecorator] passing in the service it decorates: ButtonRemote::class:
| // ... lines 1 - 5 | |
| use Symfony\Component\DependencyInjection\Attribute\AsDecorator; | |
| // ... line 7 | |
| (ButtonRemote::class) | |
| final class LoggerRemote implements RemoteInterface | |
| // ... lines 10 - 38 |
This tells Symfony:
Hey, if anything asks for the
ButtonRemoteservice, give themLoggerRemoteinstead.
Symfony essentially swaps the services and then makes ButtonRemote the "inner" service to LoggerRemote. This solidifies the need for the RemoteInterface we created earlier. If we tried to type-hint ButtonRemote directly, we'd get a type error because Symfony would be trying to inject LoggerRemote.
Service Decoration
So, follow me on this: we autowire RemoteInterface. That's aliased to ButtonRemote, so Symfony tries to give us that. But then, thanks to #[AsDecorator], it swaps that out for LoggerRemote... but passes ButtonRemote to LoggerRemote. In short, AsDecorator allows us to decorate an existing service with another.
Spin back to the app, refresh and... press "volume up". Check the "Logs" profiler panel and... we're logging again!
Multiple Decorators
Using #[AsDecorator] makes it super easy to add multiple decorators. Maybe we want to add a rate limiting decorator to prevent the kids from mashing buttons. We'd just need to create a RateLimitingRemote class that implements RemoteInterface and add #[AsDecorator(ButtonRemote::class)].
#[AsDecorator(ButtonRemote::class)]
class RateLimitingRemote implements RemoteInterface
{
public function __construct(
private RateLimiter $rateLimiter,
private RemoteInterface $inner,
) {
}
// ...
}
Next: We'll add a custom logging channel and explore "named autowiring"!
9 Comments
Hi everyone,
First thanks for your great course !
I'm trying to implement another decorator class (RateLimiterButton) in addition to LoggerRemote (with priority 5):
but i'm facing a Circular reference error:
I'm i missing something ?
Thanks
Hey @RobC25!
Small thing, instead of:
Inject the interface instead:
ButtonRemoterefers to the fully decorated service, in this caseRateLimiterRemoteso Symfony is trying to inject it into itself. When usingRemoteInterface, Symfony is smart enough to inject the service it's decorating.Hope that helps!
--Kevin
Thanks a lot, it's more clear now.
Hi All,
Thank you
I would add it's possible to add decorateor like that
#[AsDecorator(RemoteInterface::class)]
Hey @Amine!
You're absolutely right. I should have done that!
-Kevin
Hi there,
One thing is confusing to me here. In the previous chapter we said that we use
#[AsAlias]to tell Symfony which of our two services to use when we type-hintRemoteInterface. This was done while injectingRemoteInterfaceintoLoggerRemote. That's clear.Now, we mark
LoggerRemotewith#[AsDecorator], so anything asks for theButtonRemoteservice, give themLoggerRemoteinstead.So why doesn't it loop in
LoggerRemote? There we injectRemoteInterface, and via#[AsAlias]we expect and getButtonRemote, but via#[AsDecorator]Symfony should try to injectLoggerRemoteinstead. Service self injection?Hey @KamilS!
Yeah, this part can be a bit tough to conceptualize. Let's see if I can explain it a different way:
In the last chapter, when we added
#[AsAlias]toButtonRemote, this told Symfony to injectButtonRemotewhenever we autowireRemoteInterface.Now, when we add
#[AsDecorator(ButtonRemote::class)]toLoggerRemote, Symfony, behind the scenes, swaps theRemoteInterfacealias to now point toLoggerRemote.If we added a new service:
RateLimiterRemotewith#[AsDecorator(ButtonRemote::class)], Symfony would again swap the alias so thatRemoteInterfacepoints toRateLimiterRemote. It always swaps it to the top-level of the decoration stack.I hope that helps but let me know if it's still confusing.
--Kevin
Thanks @kbond!
But the question is: Why is
LoggerRemotestill injected withButtonRemoteand notLoggerRemotewhich is its decorator (or in the future withRateLimiterRemote)? Is it because it is the same class definition and at this stage Symfony does not know about decorating yet, and the current service implementingRemoteInterfaceis stillButtonRemote?Because
LoggerRemotehas theAsDecorator(ButtonRemote::class)attribute, this changes how theLoggerRemoteis wired up. For$inner, a new service is created internally (LoggerRemote.inner), and this is injected for$inner. This new service is the realButtonRemote. Additionally, the decorator logic changes theRemoteInterfacealias to point toLoggerRemoteinstead ofButtonRemote.It might help to use
bin/console debug:container remoteto see these services.I hope this helps but if not, please let me know and I can try and explain differently!
"Houston: no signs of life"
Start the conversation!