Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Decoración: Anular los servicios del núcleo y 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.

Start your All-Access Pass
Buy just this tutorial for $10.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

En 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 EventDispatcherpara que cada vez que se despache un evento, volquemos un mensaje de depuración.

Investigando el despachador de eventos

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á!

Creación de la clase decoradora

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.

AsDecorator: Haciendo que Symfony utilice nuestro servicio

¡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 original event_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!

Usando AsDecorator con OutputtingXpCalculator

¿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!

Decoración múltiple

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.

¿Decoración en la naturaleza?

¿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!

Problemas resueltos por el decorador

¿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!

Leave a comment!

22
Login or Register to join the conversation

Thanks! Maybe aditional videos for Bridge, Proxy, Chain of responsibility and Visitor designs patterns.

4 Reply

Thank you for the suggestions Vincent!

1 Reply
Ruslan Avatar

Ups. It's last chapter.
This theme is really interesting.
You have to add some patterns more! :)
Pattern Filter for example .

1 Reply

Hey Ruslan,

Thank you for your feedback! It may happen in the future ;) For now, we're focusing on other topics :)

Cheers!

Reply
Ruslan Avatar

Hi,
Thank you for OOP course.

Small note:

  • "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."

Looks like I don't see/find "the code block on this page".

1 Reply

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!

1 Reply

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!

Reply
Justin Avatar
Justin Avatar Justin | weaverryan | posted hace 9 meses | edited

Not sure if you know about this GitHub issue. Seems like an issue that is likely to keep coming up.

2 Reply

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);
    }
}

Reply

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!

Reply

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

Reply

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?

Reply
Andrey Avatar

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!

Reply

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!

1 Reply
Rockas Avatar

Very good course, I loved it. Looking forward for this course extension of more patterns.

Reply

Cheers! If there are any specific patterns you'd like to see, let us know!

Reply
Maxim-M Avatar

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.

Reply

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!

1 Reply

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

Reply

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!

Reply

Oh yeah, happy new year & Слава Україні!

Reply

Happy new year to you too! :) Героям слава!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

userVoice