Login to bookmark this video
Buy Access to Course
10.

Servicios perezosos

|

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

Es hora de hablar de una de mis características favoritas de Symfony: los servicios perezosos. Muchas veces, inyectas un servicio, pero sólo se utiliza bajo ciertas condiciones. Aquí tienes un ejemplo:

public function calculateSomething(ExpensiveService $service)
{
    if ($this->isCached()) {
        return $this->getCachedValue();
    }

    return $service->doSomethingExpensive();
}

En este caso, la mayor parte del tiempo, el ExpensiveService no se utiliza. Pero, como está inyectado, siempre se instancia.

Un "servicio perezoso" se relaja en el sofá, comiendo patatas fritas, hasta que realmente se necesita.

Crear un nuevo botón

¡Hagamos uno! Crea una nueva clase PHP en src/Remote/ llamadaParentalControls. Esto nos enviará alertas cuando los niños hagan algo que saben perfectamente que no deben hacer, los muy granujas. Marca la clase como final y añade un constructor con: private MailerInterface $mailer para que podamos enviar las alertas por correo electrónico:

19 lines | src/Remote/ParentalControls.php
// ... lines 1 - 6
final class ParentalControls
// ... line 8
public function __construct(
private MailerInterface $mailer,
) {
}
// ... lines 13 - 17
}

Añade un nuevo método público llamado volumeTooHigh() con un tipo de retorno void. Dentro, para representar el envío del correo electrónico, sólo tienes que escribir dump('send volume alert email'):

19 lines | src/Remote/ParentalControls.php
// ... lines 1 - 6
final class ParentalControls
{
// ... lines 9 - 13
public function volumeTooHigh(): void
{
dump('send volume alert email');
}
}

A continuación, abre VolumeUpButton, añade un constructor aquí e inyectaprivate ParentalControls $parentalControls:

25 lines | src/Remote/Button/VolumeUpButton.php
// ... lines 1 - 8
final class VolumeUpButton implements ButtonInterface
{
public function __construct(
private ParentalControls $parentalControls,
) {
}
// ... lines 15 - 23
}

En el método press(), imagina que estamos detectando cuando el volumen es demasiado alto. Añade una declaración if (true) (con un comentario para recordarnos lo que representa), y luego $this->parentalControls->volumeTooHigh():

25 lines | src/Remote/Button/VolumeUpButton.php
// ... lines 1 - 8
final class VolumeUpButton implements ButtonInterface
{
// ... lines 11 - 15
public function press(): void
{
if (true) { // determine if volume is too high
$this->parentalControls->volumeTooHigh();
}
// ... lines 21 - 22
}
}

Vuelve a nuestra aplicación, actualízala, pulsa el botón "subir volumen" y comprueba el perfilador. ¡Podemos ver que nuestro servicio ParentalControls se está utilizando y funciona!

De vuelta en VolumeUpButton, cambia true por false para fingir que no detectamos un volumen alto. Debajo de la sentencia if, escribe dump($this->parentalControls):

27 lines | src/Remote/Button/VolumeUpButton.php
// ... lines 1 - 8
final class VolumeUpButton implements ButtonInterface
{
// ... lines 11 - 15
public function press(): void
{
if (false) { // determine if volume is too high
$this->parentalControls->volumeTooHigh();
}
// ... line 21
dump($this->parentalControls);
// ... lines 23 - 24
}
}

Vuelve atrás, actualiza, pulsa "subir volumen" y comprueba el perfilador. Aunque no utilizamos ParentalControls, ¡seguía instanciado! También lo estaba el servicio mailer del que depende, el transporte mailer, etc. Se trata de una larga cadena de dependencias instanciadas pero no utilizadas

#[Lazy] Atributo de clase

¿La solución? Haz de ParentalControls un servicio perezoso. Abre esa clase y añade el atributo #[Lazy]:

21 lines | src/Remote/ParentalControls.php
// ... lines 1 - 7
#[Lazy]
final class ParentalControls
// ... lines 10 - 21

Volvemos a nuestra aplicación, la actualizamos, y... ¡un error!

No se puede generar un proxy perezoso para el servicio ParentalControls.

Comprueba la excepción anterior para ver por qué:

La clase ParentalControls es final.

Éste es un pequeño inconveniente de utilizar servicios perezosos: la clase no puede ser final. Veremos por qué en un segundo.

Abre ParentalControls, elimina final...

21 lines | src/Remote/ParentalControls.php
// ... lines 1 - 8
class ParentalControls
// ... lines 10 - 21

y actualiza la aplicación. ¡Ya está!

Pulsa "subir volumen" y comprueba el perfilador.

Proxies fantasma

¡Guau! ¿Qué es esto? ¿ ParentalControlsGhost con una cadena aleatoria detrás? Se llama "proxy fantasma" y lo genera Symfony. Extiende nuestra clase ParentalControls(por eso no puede ser final) y, hasta que no se utiliza realmente, no se instancia completamente: ¡es un fantasma! ¡Espeluznante!

Pero, ¿y si no fuéramos "propietarios" de ParentalControls? ¿Y si formara parte de un paquete de terceros? ¿Cómo podríamos hacerlo perezoso? No podemos editar el código del proveedor, pero se puede añadir el atributo #[Lazy] a un argumento para hacerlo perezoso según su uso.

#[Lazy] Atributo Argumento

En ParentalControls, elimina #[Lazy]:

21 lines | src/Remote/ParentalControls.php
// ... lines 1 - 8
class ParentalControls
// ... lines 10 - 21

y en VolumeUpButton, añade #[Lazy] encima del argumento $parentalControls:

29 lines | src/Remote/Button/VolumeUpButton.php
// ... lines 1 - 9
final class VolumeUpButton implements ButtonInterface
{
public function __construct(
#[Lazy]
private ParentalControls $parentalControls,
) {
}
// ... lines 17 - 27
}

En nuestra aplicación, actualiza, pulsa "subir volumen" y comprueba el perfilador. ¡Sigue siendo perezoso!

Cuando añades el atributo #[Lazy] a una clase, todas las instancias de ese servicio son perezosas. Cuando lo añades a un argumento, sólo es perezoso cuando se utiliza en ese contexto.

¿Y si existiera en un paquete de terceros y fuera final? ¿Nos quedamos sin suerte?

No Symfony tiene otros trucos -y atributos- en la manga para ayudarnos. ¡Veámoslos a continuación!