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 Symfony, decoration has a secret super-power: it allows us to customize nearly any service inside of Symfony. Woh.
For example, imagine that there's a core Symfony service and we need to extend its behavior with our own. How could we do that? Well, we could subclass the core service... and reconfigure things so that Symfony's container uses our class instead of the core one. That might work... but this is where decoration shines.
So, as a challenge, let's extend the behavior of Symfony's core EventDispatcher
service so that whenever an event is dispatched, we dump a debugging message.
The ID of the service that we want to decorate is event_dispatcher
php ./bin/console debug:container event_dispatcher
And, fortunately, this class does implement an interface. Over on GitHub... on the symfony/symfony
repository, hit t
and open EventDispatcher.php
.
And... yup! This implements EventDispatcherInterface
. Decoration will work!
Let's go make our decorator class. I'll create a new Decorator/
directory... and inside, a new PHP class called... how about DebugEventDispatcherDecorator
.
Step one, is always to implement the interface: EventDispatcherInterface
... though this is a little tricky because there are three of them! There's Psr
, which is the smallest... one from Contract
, and this one from Component
. The one from Component
extends the one from Contract
... which extends the one from Psr
.
Which do we want? The "biggest" one: the one from Symfony\Component
:
... lines 1 - 2 | |
namespace App\Decorator; | |
use Symfony\Component\EventDispatcher\EventDispatcherInterface; | |
class DebugEventDispatcherDecorator implements EventDispatcherInterface | |
{ | |
... lines 9 - 12 | |
} |
The reason is that, if our EventDispatcher
decorator is going to be passed around the system in place of the real one, it needs to implement the strongest interface: the interface that has the most methods on it.
Go to "Code"->"Generate" - or Command
+N
on a Mac - and select "Implement methods" to add the bunch we needed. Whew... there we go!
... lines 1 - 7 | |
class DebugEventDispatcherDecorator implements EventDispatcherInterface | |
{ | |
... lines 10 - 14 | |
public function dispatch(object $event, string $eventName = null): object | |
{ | |
... line 17 | |
} | |
public function addListener(string $eventName, $listener, int $priority = 0) | |
{ | |
... line 22 | |
} | |
public function addSubscriber(EventSubscriberInterface $subscriber) | |
{ | |
... line 27 | |
} | |
public function removeListener(string $eventName, $listener) | |
{ | |
... line 32 | |
} | |
public function removeSubscriber(EventSubscriberInterface $subscriber) | |
{ | |
... line 37 | |
} | |
public function getListeners(string $eventName = null): array | |
{ | |
... line 42 | |
} | |
public function getListenerPriority(string $eventName, $listener): ?int | |
{ | |
... line 47 | |
} | |
public function hasListeners(string $eventName = null): bool | |
{ | |
... line 52 | |
} | |
} |
The other thing we need to do is add a constructor where the inner EventDispatcherInterface
will be passed to us... and make that a property with private readonly
:
... lines 1 - 7 | |
class DebugEventDispatcherDecorator implements EventDispatcherInterface | |
{ | |
public function __construct( | |
private readonly EventDispatcherInterface $eventDispatcher | |
) { | |
} | |
... lines 14 - 53 | |
} |
Now that we have this, we need to call the inner dispatcher in all of these methods. This part is simple.... but boring. Say $this->eventDispatcher->addListener($eventName,
$listener, $priority):
... lines 1 - 7 | |
class DebugEventDispatcherDecorator implements EventDispatcherInterface | |
{ | |
... lines 10 - 19 | |
public function addListener(string $eventName, $listener, int $priority = 0) | |
{ | |
$this->eventDispatcher->addListener($eventName, $listener, $priority); | |
} | |
... lines 24 - 53 | |
} |
We also need to check whether or not the method should return a value. We don't need to return in this method... but there are methods down here that do have return values, like getListeners()
.
To avoid spending the next 3 minutes repeating what I just did 8 more times and putting you to sleep... bam! I'll just paste in the finished version:
... lines 1 - 7 | |
class DebugEventDispatcherDecorator implements EventDispatcherInterface | |
{ | |
... lines 10 - 14 | |
public function dispatch(object $event, string $eventName = null): object | |
{ | |
return $this->eventDispatcher->dispatch($event, $eventName); | |
} | |
public function addListener(string $eventName, $listener, int $priority = 0) | |
{ | |
$this->eventDispatcher->addListener($eventName, $listener, $priority); | |
} | |
public function addSubscriber(EventSubscriberInterface $subscriber) | |
{ | |
$this->eventDispatcher->addSubscriber($subscriber); | |
} | |
public function removeListener(string $eventName, $listener) | |
{ | |
$this->eventDispatcher->removeListener($eventName, $listener); | |
} | |
public function removeSubscriber(EventSubscriberInterface $subscriber) | |
{ | |
$this->eventDispatcher->removeSubscriber($subscriber); | |
} | |
public function getListeners(string $eventName = null): array | |
{ | |
return $this->eventDispatcher->getListeners($eventName); | |
} | |
public function getListenerPriority(string $eventName, $listener): ?int | |
{ | |
return $this->eventDispatcher->getListenerPriority($eventName, $listener); | |
} | |
public function hasListeners(string $eventName = null): bool | |
{ | |
return $this->eventDispatcher->hasListeners($eventName); | |
} | |
} |
You can copy this from the code block on this page. We're simply calling the inner dispatcher in every method.
Finally, now that our decorator is doing all the things it must do, we can add our custom stuff. Right before the inner dispatch()
method is called, I'll paste in two dump()
lines and also dump Dispatching event
, $event::class
:
... lines 1 - 7 | |
class DebugEventDispatcherDecorator implements EventDispatcherInterface | |
{ | |
... lines 10 - 14 | |
public function dispatch(object $event, string $eventName = null): object | |
{ | |
dump('--------------------'); | |
dump('Dispatching event: ' . $event::class); | |
dump('--------------------'); | |
return $this->eventDispatcher->dispatch($event, $eventName); | |
} | |
... lines 23 - 57 | |
} |
Ok! Our decorator class is done! But, there are many places in Symfony that rely on the service whose ID is event_dispatcher
. So here's the million dollar question: how can we replace that service with our own service... but still get the original event dispatcher passed to us?
Whelp, Symfony has a feature built specifically for this and you're going to love it! Go to the top of our decorator class, add a PHP 8 attribute called: #[AsDecorator()]
and pass the ID of the service that we want to decorate: event_dispatcher
:
... lines 1 - 4 | |
use Symfony\Component\DependencyInjection\Attribute\AsDecorator; | |
... lines 6 - 8 | |
'event_dispatcher') ( | |
class DebugEventDispatcherDecorator implements EventDispatcherInterface | |
{ | |
... lines 12 - 59 | |
} |
That's it. Seriously! This says:
Hey Symfony! Thanks for being so cool! Also, please make me the real
event_dispatcher
service, but still autowire the originalevent_dispatcher
service into me.
Let's try it! Run our app:
php ./bin/console app:game:play
And... it works! Look! You can see the event being dumped out! And there's our custom event too. And when I exit... another event at the bottom! We just replaced the core event_dispatcher
service with our own by creating a single class. That's bananas!
Could we have used this AsDecorator
trick earlier for our own XpCalculator
decoration situation? Yep! Here's how: In config/services.yaml
, remove the manual arguments:
... lines 1 - 7 | |
services: | |
... lines 9 - 27 | |
App\Service\OutputtingXpCalculator: | |
arguments: | |
$innerCalculator: '@App\Service\XpCalculator' |
And change the interface to point to the original, undecorated service: XpCalculator
:
... lines 1 - 7 | |
services: | |
... lines 9 - 25 | |
App\Service\XpCalculatorInterface: '@App\Service\XpCalculator' |
Basically, in the service config, we want to set things up the "normal" way, as if there were no decorators.
If we tried our app now, it would work, but it wouldn't be using our decorator. But now, go into OutputtingXpCalculator
add #[AsDecorator()]
and pass it XpCalculatorInterface::class
, since that's the ID of the service we want to replace:
... lines 1 - 6 | |
use Symfony\Component\DependencyInjection\Attribute\AsDecorator; | |
XpCalculatorInterface::class) ( | |
class OutputtingXpCalculator implements XpCalculatorInterface | |
{ | |
... lines 12 - 32 | |
} |
Donezo! If we try this now:
php ./bin/console app:game:play
No errors. An even faster way to prove this is working is by running:
php ./bin/console debug:container XpCalculatorInterface --show-arguments
And... check it out! It says that this is an alias for the service OutputtingXpCalculator
. So anyone that's autowiring this interface will actually get the OutputtingXpCalculator
service. And if you look down here at the arguments, the first argument passed to OutputtingXpCalculator
is the real XpCalculator
. That's amazing!
All right, the decorator pattern is done. What a cool pattern! One feature of the decorator pattern that we only mentioned is that you can decorate a service as many times as you want. Yep! If we created another class that implemented XpCalculatorInterface
and gave it this #AsDecorator()
attribute, there would now be two services decorating it. Which service would be on the outside? If you care enough, you could set a priority
option on one of the attributes to control that.
Where do we see decoration in the wild? The answer to that is... sort of all over! In API Platform, it's common to use decoration to extend core services like the ContextBuilder
. And Symfony itself uses decoration pretty commonly to add debugging features while we're in the dev
environment. For example, we know that this EventDispatcher
class would be used in the prod
environment. But in the dev environment - I'll hit t
to search for a "TraceableEventDispatcher" - assuming that you have some debugging tools installed, this is the actual class that represents the event_dispatcher
service. It decorates the real one!
I can prove it. Head back to your terminal and run:
php ./bin/console debug:container event_dispatcher --show-arguments
Scroll to the top and... check it out! The event_dispatcher
service is an alias to debug.event_dispatcher
... whose class is TraceableEventDispatcher
! And if you scroll down to its arguments, ha! It's passed our DebugEventDispatcherDecorator
as an argument. Yup, there are 3 event dispatchers in this case: Symfony's core TraceableEventDispatcher
is on the outside, it calls into our DebugEventDispatcherDecorator
... and then that ultimately calls the real event dispatcher. Inception!
And what problems does the decorator pattern solve? Simple: it allows us to extend the behavior of an existing class - like XpCalculator
- even if that class does not contain any other extension points. This means we can use it to override vendor services when all else fails. The only downside to the decorator pattern is that we can only run code before or after the core method. And the service we want to decorate must implement an interface.
Okay, team. We're done! There are many more patterns out there in the wild: this was a collection of some of our favorites. If we skipped one or several that you really want to hear about, let us know! Until then, see if you can spot these patterns in the wild and figure out where you can apply them to clean up your own code... and impress your friends.
Thanks for coding with me, and I'll see you next time!
Ups. It's last chapter.
This theme is really interesting.
You have to add some patterns more! :)
Pattern Filter for example .
Hey Ruslan,
Thank you for your feedback! It may happen in the future ;) For now, we're focusing on other topics :)
Cheers!
Hi,
Thank you for OOP course.
Small note:
Looks like I don't see/find "the code block on this page".
Weird enough, I got that error
#message: "App\Decorator\DebugEventDispatcherDecorator::addListener(): Argument #2 ($listener) must be of type callable, array given, called in /srv/app/vendor/symfony/event-dispatcher/Debug/TraceableEventDispatcher.php on line 59"
Digging into Symfony\Component\EventDispatcher\EventDispatcher
i found that addListener
's signature looks like this:
public function addListener(string $eventName, callable|array $listener, int $priority = 0)
but the interface has this:
public function addListener(string $eventName, callable $listener, int $priority = 0);
Is it a bug in Symfony?
I fixed it on my code changing the signature of my decorator's method to match the signature from the component's method.
public function addListener(string $eventName, callable|array $listener, int $priority = 0)
{
$this->eventDispatcher->addListener($eventName, $listener, $priority);
}
Cheers!
Hey @julien_bonnier!
That IS interesting! Hmm. So it's legal for a concrete implementation (e.g. EventDispatcher
) to widen (i.e. allow more types) the arguments for a method from an interface. If the interface has callable $listener
, it is legal for the concrete class to have callable|array $listener
.
However, in practice this means that if someone ever calls ->addListener()
and passes an array for the $listener
argument, that "someone" should be type-hinting EventDispatcher
and not EventDispatcherInterface
. In other words, doing this would work, but you're kind of breaking the rules:
public function something(EventDispatcher $dispatcher)
{
$dispatcher->dispatch('some_event', [$this, 'bar']);
}
The fact that you got this error makes me think that, in fact, something, somewhere is doing exactly that. The problem is that, wherever this is happening, if the type-hint changed from EventDispatcherInterface to EventDispatcher (so that it's more "honest"), that would totally break the ability to decorate the event dispatcher.
So... yes, something is "not right" here. But I'm not sure exactly what it is. It might simply be that, in reality, Symfony relies on $listener
being able to be an array in some cases... and that the "bug" is that we need this... but we haven't make the $listener
argument in the interface match this reality. I can't locate the history in Symfony where this changed, so I don't know if this was intentional or accidental. Interesting!
Cheers!
Not sure if you know about this GitHub issue. Seems like an issue that is likely to keep coming up.
Hi SymfonyCasts,
Great tutorial, as always.
I'm trying to decorate the HttpBasicAuthenticator class.
Because i want to change the support function.
But for some reason my decorator class is not reached (debugger does not stop there but it does stop in support function in original httpBasicAuthenticator class).
Could you help me out please?
Thanks in advance...
This is what i get for bin/console debug:container security.authenticator.http_basic
// This service is a private alias for the service App\Decorator\AddSupportHttpBasicAuthenticatorDecorator
Information for Service "App\Decorator\AddSupportHttpBasicAuthenticatorDecorator"
=================================================================================
---------------- ---------------------------------------------------------
Option Value
---------------- ---------------------------------------------------------
Service ID App\Decorator\AddSupportHttpBasicAuthenticatorDecorator
Class App\Decorator\AddSupportHttpBasicAuthenticatorDecorator
Tags monolog.logger (channel: security)
Public no
Synthetic no
Lazy no
Shared yes
Abstract no
Autowired yes
Autoconfigured yes
Usages security.authenticator.http_basic
---------------- ---------------------------------------------------------
! [NOTE] The "security.authenticator.http_basic" service or alias has been removed or inlined when the container was
! compiled.
This is my decorator class 'AddSupportHtppBasicAuthentication'
namespace App\Decorator;
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
#[AsDecorator(decorates: 'security.authenticator.http_basic')]
class AddSupportHttpBasicAuthenticatorDecorator implements AuthenticatorInterface, AuthenticationEntryPointInterface
{
public function __construct(
private readonly AuthenticatorInterface $authenticator,
private readonly AuthenticationEntryPointInterface $entryPoint)
{
}
public function start(Request $request, AuthenticationException $authException = null): void
{
$this->entryPoint->start($request, $authException);
}
public function supports(Request $request): ?bool
{
// custom code
// .......
return $this->authenticator->supports($request);
}
public function authenticate(Request $request): Passport
{
return $this->authenticator->authenticate($request);
}
public function createToken(Passport $passport, string $firewallName): TokenInterface
{
return $this->authenticator->createToken($passport, $firewallName);
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
return $this->authenticator->onAuthenticationSuccess($request, $token, $firewallName);
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
return $this->authenticator->onAuthenticationFailure($request, $exception);
}
}
Hey @Annemieke-B
I'm glad to know you like our tutorials. Your decorator it's fine, what you're missing is to add that new authenticator to the firewall
security:
firewalls:
main:
custom_authenticators:
- App\AddSupportHttpBasicAuthenticatorDecorator
...
Give it a try, it should work. Cheers!
Thankyou for the quick reply.
I have made some changes and it works now, but i'm not sure if i have done it the correct way.
It's nice that it's working but i want it to be correct. So could you check?
I've added this to my security yaml file.
firewalls:
...
main:
custom_authenticators:
- App\Decorator\AddSupportHttpBasicAuthenticatorDecorator
I have removed AsDecorator
in decorator class because i don't need it in this case, am i correct?
I am only implementing one interface instead of two in the decorator class. So only AuthenticatorInterface
and not AuthenticationEntryPointInterface
because i think i don't need both, you only need one common interface between decorator class and the vendor class, correct?
And i needed to do this in services.yml:
App\Decorator\AddSupportHttpBasicAuthenticatorDecorator:
arguments:
- "@security.authenticator.http_basic_ldap.main"
So it works, but it has some weird behaviour. With the debugger i can see, that when enduser logs in, de support function in my decorator class is called twice and also the support function in vendor class.
Thanks in advance for helping me (again) .
Annemieke
Hey!
I have removed AsDecorator in decorator class because i don't need it in this case, am i correct?
Yes, it's not required to decorate it, if you need to reuse some of its code, you could use inheritance
I am only implementing one interface instead of two in the decorator class. So only AuthenticatorInterface and not AuthenticationEntryPointInterface because i think i don't need both, you only need one common interface between decorator class and the vendor class, correct?
Correct! The AuthenticationEntryPointInterface
it's needed when you specify the "entry point" in your security.yaml
file
With the debugger i can see, that when enduser logs in, de support function in my decorator class is called twice and also the support function in vendor class.
Hmm, that's a bit unexpected. Do both calls come from the same authenticator? Is something in the request triggering another request? Could you debug further and find out who's making the extra call?
Hey Ryan, what PHPStorm plugin gives you these icons in the project view and in the gutter? They seem to be related to Symfony, but I have the Symfony plugin installed, and nothing like that shows up...
Sorry for asking a question that you've probable answered a thousand times somewhere, but nonetheless – there's no way I'd be able to find the answer in this bottomless pit of Symfony courses that you produce😅 Keep up the great workl!
Hey Andrey,
That's another PhpStorm plugin that give you such icons, it's called "Atom Material Icons". Install it if you want to the same icons style :)
And don't worry, that's not something we've talked in the course, so the valid question ;)
Cheers!
Very good course, I loved it. Looking forward for this course extension of more patterns.
Hello Ryan,
Thanks for the tutorial :)
I have a question about the Decorator pattern. One of the features of this pattern is that you can decorate objects at runtime simply using the set of decorators, manually instantiating them and passing the previous as argument. And as you explained in Multiple decoration section, there is a way for multiple decoration in Symfony, but it is not clear how can we apply just some of decorators depending on runtime conditions? Thanks.
Hey Maxim!
Ah, cool question! The easiest way to do this is on an environment-by-environment basis. For example, to do something in the dev
environment only, it would be as simple as (in services.yaml)
when@dev:
# ... now add the decoration config
I think you can also do this with PHP attributes:
#[AsDecorator(decorates: ...)]
#[When(env: 'dev')]
class MyDecorator implements SomethingInterface
{
}
If you need to do something more complex, like via the value of an environment variable, I'm not sure if that is solvable simply with some config. It might be, but I don't know. What I would try to do is:
A) register your decorator service via services.yaml
B) But, set that decorator to be created via a factory service https://symfony.com/doc/current/service_container/factories.html
C) In that factory service, read the env var value to determine if you should instantiate the true decorator service or some "dummy"/blank decorator.
Cheers!
Design patterns through Symfony - many thanks to your team, that was like a Christmas gift for me! Please, add Adapter, Command or any other pattern, which could participate in Clean Architecture. Thank you in advance
Hey Alexander,
Thank you for your feedback! We're really happy to hear you liked the tutorial! :) We will add those additional patters to our idea pool for future tutorials. ;)
Happy new year!
Cheers!
Thanks! Maybe aditional videos for Bridge, Proxy, Chain of responsibility and Visitor designs patterns.