Más atributos de pereza
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 SubscribeLo bueno de los "servicios perezosos" es que no requieren cambios en tu código (siempre que los servicios no sean finales). Pero, ¿y si el servicio ParentalControlsviviera dentro de un paquete de terceros y fuera final? ¡Difícil! Pero tenemos algunas opciones.
#[AutowireServiceClosure]
Imagina que ParentalControls es final y vive en un paquete de terceros. En VolumeUpButton, sustituye #[Lazy] por #[AutowireServiceClosure] pasandoParentalControls::class:
| // ... lines 1 - 9 | |
| final class VolumeUpButton implements ButtonInterface | |
| { | |
| // ... lines 12 - 14 | |
| public function __construct( | |
| #[AutowireServiceClosure(ParentalControls::class)] | |
| private \Closure $parentalControls, | |
| ) { | |
| } | |
| // ... lines 20 - 28 | |
| } |
Esto inyectará un cierre que devuelve una instancia de ParentalControls cuando se invoca (y sólo se instanciará cuando se invoque).
Para ayudar a nuestro IDE, añade un docblock encima del constructor:@param \Closure():ParentalControls $parentalControls:
| // ... lines 1 - 9 | |
| final class VolumeUpButton implements ButtonInterface | |
| { | |
| /** | |
| * @param \Closure():ParentalControls $parentalControls | |
| */ | |
| public function __construct( | |
| // ... lines 16 - 17 | |
| ) { | |
| } | |
| // ... lines 20 - 28 | |
| } |
Ahora, abajo en la sentencia if del método press(), cambia false por true para que siempre detectemos que el volumen es demasiado alto. Como $parentalControls es un cierre, tenemos que envolver $this->parentalControls entre llaves e invocarlo con () antes de llamar a ->volumeTooHigh():
| // ... lines 1 - 9 | |
| final class VolumeUpButton implements ButtonInterface | |
| { | |
| // ... lines 12 - 20 | |
| public function press(): void | |
| { | |
| if (true) { // determine if volume is too high | |
| ($this->parentalControls)()->volumeTooHigh(); | |
| } | |
| dump('Change the volume up'); | |
| } | |
| } |
¡Compruébalo! Como hemos añadido el docblock, nuestro IDE proporciona autocompletado y nos permite hacer clic (con CMD+clic) en el método volumeTooHigh(). ¡Genial!
Quita el dump(), gira hasta nuestra aplicación, actualiza y pulsa el botón "subir volumen". Salta al perfilador. Vemos que se está llamando a la lógica volumeTooHigh(). ¡Estupendo! El servicio ParentalControls sólo se instancia cuando se invoca el cierre, y sólo lo invocamos cuando es necesario.
#[AutowireCallable]
Veamos otra forma de hacer lo mismo. En VolumeUpButton, sustituye #[AutowireServiceClosure] por #[AutowireCallable]. ManténParentalControls::class como primer argumento, pero anteponle service:
| // ... lines 1 - 9 | |
| final class VolumeUpButton implements ButtonInterface | |
| { | |
| // ... lines 12 - 14 | |
| public function __construct( | |
| #[AutowireCallable( | |
| service: ParentalControls::class, | |
| // ... lines 18 - 19 | |
| )] | |
| private \Closure $parentalControls, | |
| ) { | |
| } | |
| // ... lines 24 - 32 | |
| } |
#[AutowireCallable] también inyecta un cierre. Pero en lugar de devolver el objeto de servicio completo, instanciará el servicio, llamará a un único método y devolverá el resultado.
Hazlo multilínea para tener más espacio. Añade un segundo argumento:method: 'volumeTooHigh':
| // ... lines 1 - 14 | |
| public function __construct( | |
| #[AutowireCallable( | |
| // ... line 17 | |
| method: 'volumeTooHigh', | |
| // ... line 19 | |
| )] | |
| private \Closure $parentalControls, | |
| ) { | |
| } | |
| // ... lines 24 - 34 |
Cuando Symfony instancie un servicio que utilice #[AutowireCallable], por defecto, instanciará su servicio. ¡Es un castor ansioso! Para evitarlo, añade un tercer argumento: lazy: true:
| // ... lines 1 - 9 | |
| final class VolumeUpButton implements ButtonInterface | |
| { | |
| // ... lines 12 - 14 | |
| public function __construct( | |
| #[AutowireCallable( | |
| // ... lines 17 - 18 | |
| lazy: true, | |
| )] | |
| private \Closure $parentalControls, | |
| ) { | |
| } | |
| // ... lines 24 - 32 | |
| } |
Ahora, ParentalControls sólo se instanciará cuando se invoque el cierre.
En el docblock anterior, cambia el tipo de retorno del cierre a void para que coincida con el tipo de retorno de volumeTooHigh():
| // ... lines 1 - 9 | |
| final class VolumeUpButton implements ButtonInterface | |
| { | |
| /** | |
| * @param \Closure():void $parentalControls | |
| */ | |
| public function __construct( | |
| // ... lines 16 - 21 | |
| ) { | |
| } | |
| // ... lines 24 - 32 | |
| } |
Abajo, en press(), elimina la llamada a ->volumeTooHigh():
| // ... lines 1 - 9 | |
| final class VolumeUpButton implements ButtonInterface | |
| { | |
| // ... lines 12 - 24 | |
| public function press(): void | |
| // ... line 26 | |
| if (true) { // determine if volume is too high | |
| ($this->parentalControls)(); | |
| } | |
| // ... lines 30 - 31 | |
| } | |
| } |
Ahora el cierre lo llama cuando se invoca.
Vuelve a la aplicación, actualízala, pulsa el botón "subir volumen" y salta al perfilador. La lógica ParentalControls::volumeTooHigh() sigue siendo llamada. ¡Perfecto!
#[AutowireCallable] es ciertamente genial, pero para la mayoría de los casos, prefiero utilizar#[AutowireServiceClosure] porque:
- Es perezoso por defecto.
- Es más flexible porque devuelve el objeto de servicio completo.
- Y, con docblocks adecuados, obtenemos: autocompletado, navegación por métodos, soporte de refactorización y un mejor análisis estático con herramientas como PhpStan.
Bien equipo, ¡eso es todo por este curso! Pon un atributo #[TimeForVacation] en tu código y ¡relájate!
La configuración de servicios YAML no va a desaparecer del todo, pero estos atributos mejoran tu experiencia como desarrollador al mantener juntos tu código y la configuración de servicios.
Casi en cada nueva versión de Symfony se añaden más atributos. ¡Sigue el blog de Symfony para estar al día! Mira esto, en Symfony 7.2, ¡hay un nuevo atributo #[WhenNot]! Es básicamente lo contrario del atributo #[When] del que hablamos antes. ¡Genial!
Consulta la sección "Inyección de dependencia" del documento Descripción general de los atributos de Symfony para ver una lista de todos los atributos de inyección de dependencia disponibles actualmente y cómo funcionan.
¡Hasta la próxima! ¡Feliz programación!
11 Comments
Hello,
In the script, using
method: 'volumeToHigh', gives the same error for me: Cannot create lazy closure because its corresponding callable is invalid.I replaced it with volumeTooHigh and it worked !
Hey alpernuage,
Yes, it should be
volumeTooHighinstead ofvolumeToHigh, you have a type in 1 char in the first version. I don't see we usevolumeToHighsomewhere in the video or scripts. If you see we use it somewhere - please, refer me to the exact code block or a specific time in the video and I will fix it :)Cheers!
Hi Victor,
Thanks for your reply!
Actually, I didn’t follow the video since I don’t have a SymfonyCasts subscription🙈 — I was just following the code shown on the script page. That’s where I noticed the volumeToHigh typo.
Cheers,
Alper
Hey @alpernuage ,
Hm, I can't find any
volumeToHighin the scripts or code 🙃 Ok, if you find one - definitely point me to that and I will fix. Otherwise, it either was already fixed or you just made a typo I guess?Cheers!
Sorry for not being clear earlier — I wanted to share a screenshot but couldn’t upload it before.
Here it is: https://ibb.co/YFwhC5St
After the sentence
“Make this multiline to give us some more room. Add a second argument: method: 'volumeTooHigh':”
the usage is correct: method: 'volumeTooHigh',
However, 4 code blocks on this page contain the wrong version volumeToHigh: in this section
👉 https://symfonycasts.com/screencast/dependency-injection-attributes/more-laziness-attributes#code-autowirecallable-code
They appear after expanding the code sections (with the two arrows) in these parts:
Hope that helps clarify it! 🙈
Hey @alpernuage,
I see it now, thanks for sharing more details on it. I fixed the affected code blocks on that page. Yes, as you figured out, the correct version should be
volumeTooHigh:)Cheers!
Hello:
Everything was going well until class 11, when I finished coding according to the instructions, and when I tried to run the application, the following error appeared:
Cannot create lazy closure because its corresponding callable is invalid.
Hey @giorgiocba,
Ak, I did see this error a while ago and made a note to come back and check it out but now I can't re-recreate it! Let's see if we can figure this out.
startdirectory?composer.locklike runcomposer update?lazy: truedoes it work?--Kevin
If you remove lazy: true does it work? This is the solution
Thanks for the update! I think somehow there was a bug with some combination of Symfony/PHP versions. I've tried a bunch of permutations but can't re-create.
Thanks, Kevin. I'll check out all the options.
"Houston: no signs of life"
Start the conversation!