Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Inyección de dependencia

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 $12.00

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

Login Subscribe

Nuestro servicio MixRepository está más o menos funcionando. Podemos autoinstalarlo en nuestro controlador y el contenedor está instanciando el objeto y pasándonoslo. Lo comprobamos aquí porque, cuando ejecutamos el código, llama con éxito al método findAll().

Pero .... luego explota. Eso es porque, dentro de MixRepository tenemos dos variables indefinidas. Para que nuestra clase haga su trabajo, necesita dos servicios: el servicio$cache y el servicio $httpClient.

La autoconexión con los métodos es un superpoder exclusivo de los controladores

Sigo diciendo que hay muchos servicios flotando dentro de Symfony, esperando que los usemos. Eso es cierto. Pero, no puedes cogerlos de la nada desde cualquier parte de tu código. Por ejemplo, no hay ningún método estático de Cache::get() que puedas llamar cuando quieras y que devuelva el objeto de servicio $cache. No existe nada así en Symfony. ¡Y eso es bueno! Permitirnos coger objetos de la nada es una receta para escribir mal código.

Entonces, ¿cómo podemos acceder a estos servicios? Actualmente, sólo conocemos una forma: autocableándolos en nuestro controlador. Pero eso no funcionará aquí. El autocableado de servicios en un método es un superpoder que sólo funciona para los controladores.

Fíjate: si añadimos un argumento CacheInterface... y luego pasamos y refrescamos, veríamos:

Demasiados argumentos para la función [...]findAll(), 0 pasados [...] y exactamente 1 esperado.

Eso es porque estamos llamando a findAll(). Así que si findAll() necesita un argumento, es nuestra responsabilidad pasarlo: no hay magia de Symfony. Lo que quiero decir es que el autocableado funciona en los métodos del controlador, pero no esperes que funcione en ningún otro método.

¿Pasar manualmente los servicios a un método?

Una forma de conseguir que esto funcione es añadir ambos servicios al métodofindAll() y luego pasarlos manualmente desde el controlador. Esta no será la solución definitiva, pero vamos a probarla.

Ya tengo un argumento CacheInterface... así que ahora añade el argumentoHttpClientInterface y llámalo $httpClient.

¡Perfecto! El código de este método está ahora contento.

De vuelta a nuestro controlador, para findAll(), pasa $httpClient y $cache.

Y ahora... ¡funciona!

"Dependencias" frente a "Argumentos"

Así que, a alto nivel, esta solución tiene sentido. Sabemos que podemos autoconectar servicios en nuestro controlador... y luego simplemente los pasamos a MixRepository. Pero si piensas un poco más en profundidad, los servicios $httpClient y $cache no son realmente una entrada para la función findAll(). No tienen realmente sentido como argumentos.

Veamos un ejemplo. Imagina que decidimos cambiar el método findAll() para que acepte un argumento string $genre para que el método sólo devuelva mezclas de ese género. Este argumento tiene mucho sentido: al pasar diferentes géneros cambia lo que devuelve. El argumento controla el comportamiento del método.

Pero los argumentos $httpClient y $cache no controlan el comportamiento de la función. En realidad, pasaríamos estos dos mismos valores cada vez que llamemos al método... para que las cosas funcionen.

En lugar de argumentos, son realmente dependencias que el servicio necesita. ¡Son cosas que deben estar disponibles para que findAll() pueda hacer su trabajo!

Inyección de dependencias y el constructor

Para las "dependencias" como ésta, ya sean objetos de servicio o configuración estática que necesita tu servicio, en lugar de pasarlas a los métodos, las pasamos al constructor. Elimina ese supuesto argumento de $genre... y añade un public function __construct(). Copia los dos argumentos, bórralos y muévelos hasta aquí.

Antes de terminar esto, tengo que decirte que el autocableado funciona en dos sitios. Ya sabemos que podemos autoconectar argumentos en los métodos de nuestro controlador. Pero también podemos autoconectar argumentos en el método __construct() de cualquier servicio. De hecho, ¡éste es el lugar principal en el que se supone que funciona la autoconexión! El hecho de que la autoconexión también funcione en los métodos del controlador es... una especie de "extra" para hacer la vida más agradable.

En cualquier caso, la autoconexión funciona en el método __construct() de nuestros servicios. Así que, siempre que indiquemos los argumentos (y lo hemos hecho), cuando Symfony instancie nuestro servicio, nos pasará estos dos servicios. ¡Sí!

¿Y qué hacemos con estos dos argumentos? Los establecemos en propiedades.

Creamos una propiedad private $httpClient y una propiedad private $cache. Luego, abajo, en el constructor, les asignamos: $this->httpClient = $httpClient, y$this->cache = $cache.

Así, cuando Symfony instancie nuestro MixRepository, nos pasará estos dos argumentos y los almacenaremos en propiedades para poder utilizarlos después.

¡Observa! Aquí abajo, en lugar de $cache, utiliza $this->cache. Y entonces no necesitamos este use ($httpClient) de aquí... porque podemos decir $this->httpClient.

Este servicio está ahora en perfecto estado.

De vuelta a VinylController, ¡ahora podemos simplificar! El método findAll()no necesita ningún argumento... y así ni siquiera necesitamos autoconducir$httpClient o $cache en absoluto. Voy a celebrarlo eliminando esas declaraciones usede la parte superior.

¡Mira qué fácil es! Autocableamos el único servicio que necesitamos, llamamos al método en él, y... ¡hasta funciona! Así es como escribimos servicios. Añadimos las dependencias al constructor, las establecemos en las propiedades y luego las utilizamos.

¡Hola a la inyección de dependencias!

Por cierto, lo que acabamos de hacer tiene un nombre extravagante: "Inyección de dependencias", ¡pero no huyas! Puede que sea un término que asuste... o que al menos suene "aburrido", pero es un concepto muy sencillo.

Cuando estás dentro de un servicio como MixRepository y te das cuenta de que necesitas otro servicio (o tal vez alguna configuración como una clave de API), para obtenerlo, crea un constructor, añade un argumento para lo que necesitas, ponlo en una propiedad, y luego úsalo abajo en tu código. Sí Eso es la inyección de dependencia.

En pocas palabras, la inyección de dependencia dice

Si necesitas algo, en lugar de cogerlo de la nada, obliga a Symfony a te lo pase a través del constructor.

Este es uno de los conceptos más importantes de Symfony... y lo haremos una y otra vez.

Promoción de propiedades en PHP 8

Bien, sin relación con la inyección de dependencia y el autocableado, hay dos pequeñas mejoras que podemos hacer a nuestro servicio. La primera es que podemos añadir tipos a nuestras propiedades: HttpClientInterface y CacheInterface. Eso no cambia el funcionamiento de nuestro código... es sólo una forma agradable y responsable de hacer las cosas.

¡Pero podemos ir más allá! En PHP 8, hay una nueva sintaxis más corta para crear una propiedad y establecerla en el constructor como estamos haciendo. Tiene el siguiente aspecto. En primer lugar, moveré mis argumentos a varias líneas... sólo para mantener las cosas organizadas. Ahora añade la palabra private delante de cada argumento. Termina borrando las propiedades... así como el interior del método.

Esto puede parecer raro al principio, pero en cuanto añades private, protected, opublic delante de un argumento __construct(), se crea una propiedad con este nombre y se fija el argumento en esa propiedad. Así que parece diferente, pero es exactamente lo mismo que teníamos antes.

Cuando lo probamos... ¡sí! Sigue funcionando.

Siguiente: Sigo diciendo que el contenedor contiene servicios. Es cierto Pero también contiene otra cosa: una simple configuración llamada "parámetros".

Leave a comment!

11
Login or Register to join the conversation
Alexander-S Avatar
Alexander-S Avatar Alexander-S | posted hace 2 meses | edited

I've been following along with these great tutorials and find myself a bit stuck. For this example I have 3 classes:

class One extends AbstractClassTwo {}
abstract class AbstractClassTwo extends AbstractClassThree {
public function __construct(array $arr = []){
        parent::__construct();
        $this->arr = $arr;
}
abstract class AbstractClassThree {
    public function __construct(protected string $injectedVar) {}
    public function doSomething() {
        echo $this->injectedVar;
}

services.yaml:

services:
    _defaults:
        autowire: true
        autoconfigure: true
        bind:
            'string $injectedVar': '%env(TO_INJECT)%'`
// .env.local:

TO_INJECT=astringgoeshere

When I try to execute the code I get:
Uncaught Error: Too few arguments to function App\AbstractClassThree::__construct(), 0 passed in /src/AbstractClassTwo on line 35 and exactly 1expected.

Any help much appreciated!

Reply
Jesse-Rushlow Avatar
Jesse-Rushlow Avatar Jesse-Rushlow | SFCASTS | Alexander-S | posted hace 2 meses

Howdy Alexander!

I edited your post just to make it a tad bit easier to read the classes and what they are doing. Anywho, I spent a few minutes playing around with this use case and I think I see what the problem is.

Symfony is great managing parent services when they only extended one level. E.g. Class One extends Abstract Class Two. But when we throw in a second abstraction level into the mix, we start hitting limitations with Symfony's auto-wiring and PHP's inheritance.

Because AbstractClassTwo extends AbstractClassThree and both of them have constructors(), I believe you will need to pass in the arguments for Three into Two's constructor like so:

abstract class Two extends Three
{
    public array $arr;

    public function __construct(string $injectedVar, array $arr = [])
    {
        parent::__construct($injectedVar);

        $this->arr = $arr;
    }
}

Otherwise Three's constructor is never called. Even when I tried your example using the "Common Dependency Method" shown in the docs: https://symfony.com/doc/current/service_container/parent_services.html - I had the same issue.

I Hope this helps!

Reply
Alexander-S Avatar

Thanks for the response and the time to dig in to find a solution. I also noticed in the docs (just above this line: https://symfony.com/doc/current/service_container/injection_types.html#immutable-setter-injection):

These advantages do mean that constructor injection is not suitable for working with optional dependencies. It is also more difficult to use in combination with class hierarchies: if a class uses constructor injection then extending it and overriding the constructor becomes problematic.
Emphasis mine.

I had also gotten your solution to work, it just seemed a little ugly compared with the way dependency injection usually seems to work.

Reply
Kaan-G Avatar

after the one years of struggling with Symfony first time i start to understand what Symfony is? and how it works?
many thanks for your work!

Reply

Hey Kaan,

Is it a question? :) Like do you want to know if SymfonyCasts tutorials will help you to understand Symfony framework better after a year of its learning on our platform? Or are you just leaving feedback about SymfonyCasts?

Cheers!

Reply
Kaan-G Avatar

it was actually a compliment sir..
i watched plenty of videos like 'symfony for beginners' or 'creating a blog system with symfony' but none of them explain what Symfony is. before watch your videos i was know to how to code with Symfony but i had no idea about how that machine works : )

thank you for your effort and forgive my terrible English..
i hope to made myself clear : )

Reply

Hey Kaan,

Ah, I see! The question marks in your first message confused me a bit :)

Thank you for your kind words about SymfonyCasts - that's exactly what our mission is, we're trying to explain complex things in a simple and understandable way for everyone, even newcomers :) So, we're really happy to hear our service is useful for you ;)

Cheers!

Reply

Bonjour!!
If I would like to use loop in my navbar (dropdown-menu) to show all the availble elements how I can declare a global variable in this case?
`I did that way:

                    {% for activity in activitys %}
                        <ul class="dropdown-menu">
                            <li><a class="dropdown-item" href="">{{ activity.titleA}}</a></li>
                        </ul>
                    {% endfor %}`

I have difficulty because I can't use findAll() method easily outside the repository and my project is very big so I can't declare it in every single page.

Any idea or soloution about that please?
Many thanks for your help :)

Reply

Hey Lubna,

If you have a static value for the global variable or when you need to make some env var global in twig - you can use this way: https://symfony.com/doc/current/templating/global_variables.html

But if you need a data from your repository - probably that global Twig var won't help you. In this case, you can create a custom Twig function that will return whatever you want, we're talking about custom Twig functions here: https://symfonycasts.com/screencast/symfony4-doctrine/twig-extension

Or you can check the Symfony's Twig docs about it.

Cheers!

1 Reply

Hi! Thanks for understanding my problem correctly.
Gonna try your suggestion!

Best,
Lubna

Reply

Hey Lubna,

You're welcome! Good luck with it ;)

Cheers!

1 Reply
Cat in space

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

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "knplabs/knp-time-bundle": "^1.18", // v1.19.0
        "symfony/asset": "6.1.*", // v6.1.0-RC1
        "symfony/console": "6.1.*", // v6.1.0-RC1
        "symfony/dotenv": "6.1.*", // v6.1.0-RC1
        "symfony/flex": "^2", // v2.1.8
        "symfony/framework-bundle": "6.1.*", // v6.1.0-RC1
        "symfony/http-client": "6.1.*", // v6.1.0-RC1
        "symfony/monolog-bundle": "^3.0", // v3.8.0
        "symfony/runtime": "6.1.*", // v6.1.0-RC1
        "symfony/twig-bundle": "6.1.*", // v6.1.0-RC1
        "symfony/ux-turbo": "^2.0", // v2.1.1
        "symfony/webpack-encore-bundle": "^1.13", // v1.14.1
        "symfony/yaml": "6.1.*", // v6.1.0-RC1
        "twig/extra-bundle": "^2.12|^3.0", // v3.4.0
        "twig/twig": "^2.12|^3.0" // v3.4.0
    },
    "require-dev": {
        "symfony/debug-bundle": "6.1.*", // v6.1.0-RC1
        "symfony/maker-bundle": "^1.41", // v1.42.0
        "symfony/stopwatch": "6.1.*", // v6.1.0-RC1
        "symfony/web-profiler-bundle": "6.1.*" // v6.1.0-RC1
    }
}