Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Dependency Injection

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

Our MixRepository service is sort of working. We can autowire it into our controller and the container is instantiating the object and passing it to us. We prove that over here because, when we run the code, it successfully calls the findAll() method.

But.... then it explodes. That's because, inside MixRepository we have two undefined variables. In order for our class to do its job, it needs two services: the $cache service and the $httpClient service.

Autowiring to Methods is a Controller-Only Superpower

I keep saying that there are many services floating around inside of Symfony, waiting for us to use them. That's true. But, you can't just grab them out of thin air from anywhere in your code. For example, there's no Cache::get() static method that you can call whenever you want that will return the $cache service object. Nothing like that exists in Symfony. And that's good! Allowing us to grab objects out of thin air is a recipe for writing bad code.

So how can we get access to these services? Currently, we only know one way: by autowiring them into our controller. But that won't work here. Autowiring services into a method is a superpower that only works for controllers.

Watch: if we added a CacheInterface argument... then went over and refreshed, we'd see:

Too few arguments to function [...]findAll(), 0 passed [...] and exactly 1 expected.

That's because we are calling findAll(). So if findAll() needs an argument, it is our responsibility to pass them: there's no Symfony magic. My point is: autowiring works in controller methods, but don't expect it to work for any other methods.

Manually Passing Services to a Method?

But one way we might get this to work is by adding both services to the findAll() method and then manually passing them in from the controller. This won't be the final solution, but let's try it.

I already have a CacheInterface argument... so now add the HttpClientInterface argument and call it $httpClient:

... lines 1 - 5
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class MixRepository
{
public function findAll(HttpClientInterface $httpClient, CacheInterface $cache): array
{
... lines 13 - 18
}
}

Perfect! The code in this method is now happy.

Back over in our controller, for findAll(), pass $httpClient and $cache:

... lines 1 - 12
class VinylController extends AbstractController
{
... lines 15 - 33
public function browse(HttpClientInterface $httpClient, CacheInterface $cache, MixRepository $mixRepository, string $slug = null): Response
{
... lines 36 - 37
$mixes = $mixRepository->findAll($httpClient, $cache);
... lines 39 - 43
}
}

And now... it works!

"Dependencies" Versus "Arguments"

So, on a high level, this solution makes sense. We know that we can autowire services into our controller... and then we just pass them into MixRepository. But if you think a bit deeper, the $httpClient and $cache services aren't really input to the findAll() function. They don't really make sense as arguments.

Let's look at an example. Pretend that we decide to change the findAll() method to accept a string $genre argument so the method will only return mixes for that genre. This argument makes perfect sense: passing different genres changes what it returns. The argument controls how the method behaves.

But the $httpClient and $cache arguments don't control how the function behaves. In reality, we would pass these same two values every time we call the method... just so things work.

Instead of arguments, these are really dependencies that the service needs. They're just stuff that must be available so that findAll() can do its job!

Dependency Injection & The Constructor

For "dependencies" like this, whether they're service objects or static configuration that your service needs, instead of passing them to the methods, we pass them into the constructor. Delete that pretend $genre argument... then add a public function __construct(). Copy the two arguments, delete them, and move them up here:

... lines 1 - 8
class MixRepository
{
... lines 11 - 13
public function __construct(HttpClientInterface $httpClient, CacheInterface $cache)
{
... lines 16 - 17
}
... lines 19 - 28
}

Before we finish this, I need to tell you that autowiring works in two places. We already know that we can autowire arguments into our controller methods. But we can also autowire arguments into the __construct() method of any service. In fact, that's the main place that autowiring is meant to work! The fact that autowiring also works for controller methods is... kind of an "extra" just to make life nicer.

Anyways, autowiring works in the __construct() method of our services. So as long as we type-hint the arguments (and we have), when Symfony instantiates our service, it will pass us these two services. Yay!

And what do we do with these two arguments? We set them onto properties.

Create a private $httpClient property and a private $cache property. Then, down in the constructor, assign them: $this->httpClient = $httpClient, and $this->cache = $cache:

... lines 1 - 8
class MixRepository
{
private $httpClient;
private $cache;
public function __construct(HttpClientInterface $httpClient, CacheInterface $cache)
{
$this->httpClient = $httpClient;
$this->cache = $cache;
}
... lines 19 - 28
}

So when Symfony instantiates our MixRepository, it passes us these two arguments and we store them on properties so we can use them later.

Watch! Down here, instead of $cache, use $this->cache. And then we don't need this use ($httpClient) over here... because we can say $this->httpClient:

... lines 1 - 8
class MixRepository
{
... lines 11 - 19
public function findAll(): array
{
return $this->cache->get('mixes_data', function(CacheItemInterface $cacheItem) {
... line 23
$response = $this->httpClient->request('GET', 'https://raw.githubusercontent.com/SymfonyCasts/vinyl-mixes/main/mixes.json');
... lines 25 - 26
});
}
}

This service is now in perfect shape.

Back over in VinylController, now we can simplify! The findAll() method doesn't need any arguments... and so we don't even need to autowire $httpClient or $cache at all. I'm going to celebrate by removing those use statements on top:

... lines 1 - 10
class VinylController extends AbstractController
{
... lines 13 - 31
public function browse(MixRepository $mixRepository, string $slug = null): Response
{
... lines 34 - 35
$mixes = $mixRepository->findAll();
... lines 37 - 41
}
}

Look how much easier that is! We autowire the one service we need, call the method on it, and... it even works! This is how we write services. We add any dependencies to the constructor, set them onto properties, and then use them.

Hello Dependency Injection!

By the way, what we just did has a fancy schmmancy name: "Dependency injection". But don't run away! That may be a scary... or at least "boring sounding" term, but it's a very simple concept.

When you're inside of a service like MixRepository and you realize you need another service (or maybe some config like an API key), to get it, create a constructor, add an argument for the thing you need, set it onto a property, and then use it down in your code. Yep! That's dependency injection.

Put simply, dependency injection says:

If you need something, instead of grabbing it out of thin air, force Symfony to pass it to you via the constructor.

This is one of the most important concepts in Symfony... and we'll do this over and over again.

PHP 8 Property Promotion

Okay, unrelated to dependency injection and autowiring, there are two minor improvements that we can make to our service. The first is that we can add types to our properties: HttpClientInterface and CacheInterface:

... lines 1 - 8
class MixRepository
{
... lines 11 - 13
public function __construct(HttpClientInterface $httpClient, CacheInterface $cache)
{
$this->httpClient = $httpClient;
$this->cache = $cache;
}
... lines 19 - 28
}

That doesn't change how our code works... it's just a nice, responsible way to do things.

But we can go further! In PHP 8, there's a new, shorter syntax for creating a property and setting it in the constructor like we're doing. It looks like this. First, I'll move my arguments onto multiple lines... just to keep things organized. Now add the word private in front of each argument. Finish by deleting the properties... as well as the inside of the method.

That might look weird at first, but as soon as you add private, protected, or public in front of a __construct() argument, that creates a property with this name and sets the argument onto that property:

... lines 1 - 8
class MixRepository
{
public function __construct(
private HttpClientInterface $httpClient,
private CacheInterface $cache
) {}
... lines 15 - 24
}

So it looks different, but it's the exact same as what we had before.

When we try it... yup! It still works.

Next: I keep saying that the container holds services. That's true! But it also holds one other thing - simple configuration called "parameters".

Leave a comment!

19
Login or Register to join the conversation
t5810 Avatar

Hi.
Can anyone explain why B is wrong answer in the next challenge:
Which of the following is NOT a correct way to use dependency injection:

Reply

Hey t5810,

The correct answer is A because the CacheInterface $cache is just an argument of the method, which technically is a dependency of the class, but it's not a property, so only that method depends on it but not the whole class. You may ask, but we do the same to Symfony controllers, yes, but they're a special kind of service, the Symfony container takes care of "injecting" all of the controller's method arguments

I hope this makes sense to you :) Cheers!

Reply
t5810 Avatar

Hi MolloKhan
I re-watched the video again, I re-read the question 3 times and THEN I noticed the bold and capital NOT in the question....
Sorry for wasting your time...

Regards

Reply

haha, it happens! You're welcome :)

Reply
Alin-D Avatar
Alin-D Avatar Alin-D | posted 4 months ago | edited

Hello, I have a question: why in this case we do not also inject CacheItemInterface? Why HttpClientInterface and CacheInterface are injected as dependencies and CacheItemInterface is inserted into the findAll() method as an argument?

Reply

Hey @Alin-D

The $cacheItem object it's not a dependency of the MixRepository class. It's an argument you get passed by the callback function of the $this->cache->get() method call. In other words, the second argument of that method is a callback function, and it will pass to you an instance of CacheItemInterface. I hope it's more clear now

Cheers!

Reply
Stefaaan Avatar
Stefaaan Avatar Stefaaan | posted 6 months ago

In which cases I have to create my own services? Is there a guideline for that?

Reply

Hey Stefaaan,

I think this tutorial should answer this question :) In short, if you need some job to do, and you would like to be able to re-use it in a few places or test it - there is a good plan to create a service for this.

Cheers!

Reply
Alexander-S Avatar
Alexander-S Avatar Alexander-S | posted 9 months ago | 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 9 months ago

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
    }
}
userVoice