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
ButtonRemote
service, give themLoggerRemote
instead.
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"!
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 injectingRemoteInterface
intoLoggerRemote
. That's clear.Now, we mark
LoggerRemote
with#[AsDecorator]
, so anything asks for theButtonRemote
service, give themLoggerRemote
instead.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 injectLoggerRemote
instead. Service self injection?