Scroll down to the script below, click on any sentence (including terminal blocks!) to jump to that spot in the video!
In 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
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. And, fortunately,
this class does implement an interface. Over on GitHub... on the
symfony/symfony repository, hit "t" and open
And... yup! This implements
EventDispatcherInterface. Decoration will work!
Let's go make our decorator class. I'll create a new
and inside, a new PHP class called... how about
Step one, is always to implement the interface:
this is a little tricky because there are three of them! There's
is the smallest... one from
Contract, and this one from
Component. The one
Component extends the one from
Contract... which extends the one from
Which do we want? The "biggest" one: the one from
Symfony\Component. 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!
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
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).
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
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: 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
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:
That's it. Seriously! This says:
Hey Symfony. That's for being so cool! Also, please make me the real
event_dispatcherservice, but still autowire the original
event_dispatcherservice 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
event_dispatcher service with our own by creating a single class.
Could we have used this
AsDecorator trick earlier for our own
decoration situation? Yep! Here's how: In
config/services.yaml, remove the manual
arguments and change the interface to point to the original, undecorated 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
#[AsDecorator()] and pass it
XpCalculatorInterface::class, since that's the ID of the service we want to
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
OutputtingXpCalculator service. And if you look down here at the arguments,
the first argument passed to
OutputtingXpCalculator is the real
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
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
event_dispatcher service. It decorates the real one!
I can prove it. Head back to your terminal and run:
./bin/console debug:container event_dispatcher --show-arguments
Scroll to the top and... check it out! The
event_dispatcher service is an alias
debug.event_dispatcher... whose class is
TraceableEventDispatcher! And if
you scroll down to its arguments, ha! It's passed our
as an argument. Yup, there are 3 event dispatchers in this case: Symfony's
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!