Decoración con el contenedor de Symfony
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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeAcabamos 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:
// ... 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 XpEarnedObserver
se 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
:
// ... 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 | |
} |
// ... 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 interfazXpCalculatorInterface
pero no existe tal servicio. Tal vez debas alias de esta interfaz a uno de estos servicios existentesOutputtingXpCalculator
oXpCalculator
.
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
:
// ... 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
->OutputtingXpCalculator
Oh! Symfony está autocableando OutputtingXpCalculator
en XpEarnedObserver
... pero también está autocableando OutputtingXpCalculator
en sí mismo:
// ... 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
:
// ... 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!