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 SubscribeEn Symfony, la decoración tiene un superpoder secreto: nos permite personalizar casi cualquier servicio dentro de Symfony.
Por ejemplo, imagina que hay un servicio del núcleo de Symfony y necesitas ampliar su comportamiento con el tuyo propio. ¿Cómo podrías hacerlo? Bueno, podríamos subclasificar el servicio del núcleo... y reconfigurar las cosas para que el contenedor de Symfony utilice nuestra clase en lugar de la del núcleo. Eso podría funcionar, pero aquí es donde brilla la decoración.
Así que, como reto, vamos a ampliar el comportamiento del servicio central de Symfony EventDispatcher
para que cada vez que se despache un evento, volquemos un mensaje de depuración.
El ID del servicio que queremos decorar es event_dispatcher
. Y, afortunadamente, esta clase implementa una interfaz. En GitHub... en el repositoriosymfony/symfony
, pulsa "t" y abre EventDispatcher.php
.
Y... ¡sí! Esto implementa EventDispatcherInterface
. ¡La decoración funcionará!
Así que vamos a crear nuestra clase decoradora personalizada. Crearé un nuevo directorio Decorator/
... y dentro, una nueva clase PHP llamada... ¿qué talDebugEventDispatcherDecorator
.
El primer paso, es siempre implementar la interfaz: EventDispatcherInterface
... ¡aunque esto es un poco complicado porque hay tres! Está Psr
, que es la más pequeña.. la de Contract
, y esta otra de Component
. La de Component
extiende la de Contact
... que extiende la de Psr
.
En realidad, el más "grande": el de Symfony/Component
. La razón es que, si nuestro decorador de EventDispatcher
va a pasar por el sistema en lugar del real, necesita implementar la interfaz más fuerte: la que tiene más métodos.
Ve a Generar Código -o "comando" + "N" en un Mac- y selecciona "Implementar Métodos" para implementar el montón que necesitábamos. Uf... ¡ya está!
Lo otro que tenemos que hacer es añadir un constructor al que se le pasará elEventDispatcherInterface
interior... y hacer que sea una propiedad conprivate readonly
.
¡Perfecto! Ahora que tenemos esto, tenemos que llamar al despachador interno en todos estos métodos. Esta parte es sencilla.... pero aburrida. Digamos que$this->eventDispatcher->addListener($eventName, $listener,
$priority).
También tenemos que comprobar si el método debe devolver un valor o no. No necesitamos devolver en este método... pero hay métodos aquí abajo que sí tienen valores de retorno, como getListeners()
.
Para no pasarme los próximos 3 meses repitiendo lo que acabo de hacer 8 veces más, voy a borrar todo esto y a pegar una versión acabada: puedes copiarlo del bloque de código de esta página. Simplemente estamos llamando al despachador interno en cada método.
Por último, ahora que nuestro decorador está haciendo todo lo que debe hacer, podemos añadir funcionalidad personalizada. Justo antes de llamar al método interno dispatch()
, pegaré dos llamadas a dump()
y también volcaré Dispatching event
, $event::class
.
¡Muy bien! ¡Nuestra clase decoradora está hecha! Pero, hay muchos lugares en Symfony que dependen del servicio cuyo ID es event_dispatcher
. Así que aquí está la pregunta del millón: ¿cómo podemos reemplazar ese servicio con nuestro propio servicio... pero seguir obteniendo el despachador de eventos original que nos han pasado?
Pues bien, Symfony tiene una función creada específicamente para esto, ¡y te va a encantar! Ve a la parte superior de nuestra clase decoradora y añade un atributo de PHP 8 llamado:#[AsDecorator()]
y pasa el id del servicio que queremos decorar:event_dispatcher
.
Eso es todo. Esto dice:
Oye Symfony, por favor, hazme el servicio real
event_dispatcher
, pero sigue autocablea el servicio originalevent_dispatcher
en mí.
¡Vamos a probarlo! Ejecuta nuestra aplicación:
php bin/console app:game:play
Y... ¡funciona! Mira! ¡Puedes ver cómo se vierte el evento! Y también está nuestro evento personalizado. Y si salgo... ¡otro evento al final! Acabamos de sustituir el servicio central de event_dispatcher
por el nuestro creando una sola clase. ¡Esto es una maravilla!
¿Podríamos haber utilizado antes este truco de AsDecorator
para nuestra propia situación de decoración de XpCalculator
? Sí He aquí cómo: En config/services.yaml
, elimina los argumentos manuales y cambia la interfaz para que apunte al servicio original no decorado:XpCalculator
. Básicamente, en la configuración del servicio, queremos configurar las cosas de la manera "normal", como si no hubiera decoradores.
Si probáramos ahora nuestra aplicación, funcionaría, pero no utilizaría nuestro decorador. Pero ahora, entra en OutputtingXpCalculator
añade #[AsDecorator()]
y pásaleXpCalculatorInterface::class
, ya que ése es el ID del servicio que queremos decorar.
¡Donezo! Si probamos esto ahora:
php bin/console app:game:play
No hay errores. Una forma aún más rápida de comprobar que esto funciona es ejecutando
php bin/console debug:container XpCalculatorInterface --show-arguments
Si ejecutamos esto... ¡compruébalo! Dice que esto es un alias del servicioOutputtingXpCalculator
. Así que cualquiera que esté autocableando esto está recibiendo realmente el servicioOutputtingXpCalculator
. Y si miras aquí abajo los argumentos, el primer argumento que se pasa a OutputtingXpCalculator
es el verdadero XpCalculator
. ¡Esto es increíble!
Muy bien, el patrón decorador está hecho. ¡Qué patrón más chulo! Una característica del patrón decorador que sólo hemos mencionado es que puedes decorar un servicio tantas veces como quieras. ¡Sí! Si creamos otra clase que implementeXpCalculatorInterface
y le damos este atributo AsDecorator
, ahora habría dos servicios que la decorarían. ¿Qué servicio estaría en el exterior? Si te importa lo suficiente, podrías establecer una opción priority
en uno de los atributos para controlarlo.
La mayor limitación del patrón decorador es simplemente que sólo puedes ejecutar código antes o después de un método. No podemos, por ejemplo, entrar en las tripas de ninguno de los métodos del núcleo EventDispatcher
y cambiar su comportamiento. La decoración tiene un poder limitado.
¿Dónde vemos la decoración en la naturaleza? La respuesta es... más o menos por todas partes. En la Plataforma API, es muy común utilizar la decoración para ampliar los comportamientos del núcleo, como el ContextBuilder. Y el propio Symfony utiliza la decoración con bastante frecuencia para añadir funciones de depuración mientras estamos en el entorno dev
. Por ejemplo, sabemos que esta clase EventDispatcher
se utilizaría en el entorno prod
. Pero en el entorno dev - voy a darle a la "T" para buscar un "TraceableEventDispatcher" - suponiendo que tengas algunas herramientas de depuración instaladas, esta es la clase real que representa el event_dispatcher
. ¡Decora el real!
Puedo demostrarlo. Vuelve a tu terminal y ejecuta
./bin/console debug:container event_dispatcher --show-arguments
Desplázate hasta la parte superior y... ¡compruébalo! El servicio event_dispatcher
es un alias de debug.event_dispatcher
... ¡cuya clase es TraceableEventDispatcher
! Y si te desplazas hasta sus argumentos, ¡ja! Ha pasado nuestro DebugEventDispatcherDecorator
. como argumento. Sí, en este caso hay 3 despachadores de eventos: El núcleo de Symfony TraceableEventDispatcher
está en el exterior, llama a nuestroDebugEventDispatcherDecorator
... y éste, en última instancia, llama al despachador de eventos real. ¡Inicio!
¿Y qué problemas resuelve el patrón decorador? Sencillo: nos permite ampliar el comportamiento de una clase existente -como XpCalculator
- aunque esa clase no contenga ningún otro punto de ampliación. Esto significa que podemos utilizarlo para anular los servicios del proveedor cuando todo lo demás falla. El único inconveniente del patrón decorador es que sólo podemos ejecutar código antes o después del método principal. Y el servicio que queremos decorar debe implementar una interfaz.
Bien, equipo. ¡Ya hemos terminado! Hay muchos más patrones por ahí en la naturaleza: ésta ha sido una recopilación de algunos de nuestros favoritos. Si nos hemos saltado uno o varios de los que realmente quieres oír hablar, ¡háznoslo saber! Hasta entonces, comprueba si puedes detectar estos patrones en la naturaleza y averiguar dónde puedes aplicarlos para limpiar tu propio código... e impresionar a tus amigos.
Gracias por codificar conmigo, ¡y nos vemos la próxima vez!
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.