Buy Access to Course
13.

Decoración con el contenedor de Symfony

|

Share this awesome video!

|

Keep on Learning!

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

Login Subscribe

Acabamos de implementar el patrón decorador, en el que básicamente envolvimos elXpCalculator original en un cálido abrazo con nuestro OutputtingXpCalculator. Luego... lo introducimos silenciosamente en el sistema en lugar del original... sin que nadie más -como XpEarnedObserver - sepa o se preocupe de que lo hemos hecho:

107 lines | src/Command/GameCommand.php
// ... lines 1 - 17
class GameCommand extends Command
{
// ... lines 20 - 26
protected function execute(InputInterface $input, OutputInterface $output): int
{
$xpCalculator = new XpCalculator();
$xpCalculator = new OutputtingXpCalculator($xpCalculator);
$this->game->subscribe(new XpEarnedObserver($xpCalculator));
// ... lines 32 - 47
}
// ... lines 49 - 105
}

Pero para configurar la decoración, estoy instanciando los objetos manualmente, lo que no es muy realista en una aplicación Symfony. Lo que realmente queremos es que XpEarnedObserverse autoconecte a XpCalculatorInterface de forma normal, sin que tengamos que hacer nada de esta instanciación manual. Pero necesitamos que el contenedor le pase nuestro servicio decoradorOutputtingXpCalculator, no el original XpCalculator. ¿Cómo podemos conseguirlo? ¿Cómo podemos decirle al contenedor que cada vez que alguien haga una sugerencia de tipo XpCalculatorInterface, debe pasarle nuestro servicio de decorador?

Para responder a esto, empecemos por deshacer nuestro código manual: Tanto en GameCommand... como en Kernel... vuelve a poner el código de fantasía que adjunta el observador aGameApplication:

100 lines | src/Command/GameCommand.php
// ... lines 1 - 14
class GameCommand extends Command
{
// ... lines 17 - 23
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$io->text('Welcome to the game where warriors fight against each other for honor and glory... and 🍕!');
// ... lines 29 - 40
}
// ... lines 42 - 98
}

31 lines | src/Kernel.php
// ... lines 1 - 11
class Kernel extends BaseKernel implements CompilerPassInterface
{
// ... lines 14 - 21
public function process(ContainerBuilder $container)
{
// ... lines 24 - 25
foreach ($taggedObservers as $id => $tags) {
$definition->addMethodCall('subscribe', [new Reference($id)]);
}
}
}

Si ahora probamos el comando

php ./bin/console app:game:play

Falla:

No se puede autoconectar el servicio XpEarnedObserver: el argumento $xpCalculator hace referencia interfaz XpCalculatorInterface pero no existe tal servicio. Tal vez debas alias de esta interfaz a uno de estos servicios existentes OutputtingXpCalculator o XpCalculator.

Cablear manualmente la decoración del servicio: Alias

Ese es un gran error... y tiene sentido. Dentro de nuestro observador, estamos haciendo un guiño a la interfaz en lugar de a una clase concreta. Y, a menos que hagamos un poco más de trabajo, Symfony no sabe qué servicio de XpCalculatorInterface debe pasarnos. ¿Cómo se lo decimos? Creando un alias de servicio.

En config/services.yaml, digamos que App\Service\XpCalculatorInterface se convierte en@App\Service\OutputtingXpCalculator:

27 lines | config/services.yaml
// ... lines 1 - 7
services:
// ... lines 9 - 25
App\Service\XpCalculatorInterface: '@App\Service\OutputtingXpCalculator'

Esto crea un servicio cuyo id es App\Service\XpCalculatorInterface... pero en realidad es sólo un "puntero", o "alias" al servicio OutputtingXpCalculator. Y recuerda que, durante el autocableado, cuando Symfony ve un argumento indicado conXpCalculatorInterface, para saber qué servicio pasar, simplemente busca en el contenedor un servicio cuyo id coincida con ese, por lo queApp\Service\XpCalculatorInterface. Y ahora, ¡encuentra uno!

Así que vamos a intentarlo de nuevo.

php ./bin/console app:game:play

Y... sigue sin funcionar. ¡Estamos de enhorabuena!

Referencia circular detectada para el servicio OutputtingXpCalculator, ruta: OutputtingXpCalculator -&gt OutputtingXpCalculator

Oh! Symfony está autocableando OutputtingXpCalculator en XpEarnedObserver... pero también está autocableando OutputtingXpCalculator en sí mismo:

32 lines | src/Service/OutputtingXpCalculator.php
// ... lines 1 - 7
class OutputtingXpCalculator implements XpCalculatorInterface
{
public function __construct(
private readonly XpCalculatorInterface $innerCalculator
)
{
}
// ... lines 15 - 30
}

¡Ups! Queremos que OutputtingXpCalculator se utilice en todas las partes del sistema que autocablean XpCalculatorInterface... excepto en sí mismo.

Para conseguirlo, de nuevo en services.yaml, podemos configurar manualmente el servicio. Aquí abajo, añade App\Service\OutputtingXpCalculator con arguments,$innerCalculator (ese es el nombre de nuestro argumento) establecido en@App\Service\XpCalculator:

31 lines | config/services.yaml
// ... lines 1 - 7
services:
// ... lines 9 - 27
App\Service\OutputtingXpCalculator:
arguments:
$innerCalculator: '@App\Service\XpCalculator'

Esto anulará el argumento sólo para este caso. Y ahora...

php ./bin/console app:game:play

¿Funciona? Quiero decir, ¡claro que funciona! Si jugamos unas cuantas rondas y avanzamos rápidamente... ¡sí! ¡Ahí está el mensaje de "has subido de nivel"! ¡Sí que pasó por nuestro decorador!

Esta forma de cablear el decorador no es nuestra solución definitiva. Pero antes de llegar ahí, tengo un reto aún mayor: vamos a sustituir completamente un servicio principal de Symfony por el nuestro a través del decorador. ¡Eso a continuación!