Buy Access to Course
12.

Dependency Injection

|

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

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:

21 lines | src/Service/MixRepository.php
// ... 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:

46 lines | src/Controller/VinylController.php
// ... 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:

30 lines | src/Service/MixRepository.php
// ... 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:

30 lines | src/Service/MixRepository.php
// ... 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:

30 lines | src/Service/MixRepository.php
// ... 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:

44 lines | src/Controller/VinylController.php
// ... 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:

30 lines | src/Service/MixRepository.php
// ... 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:

26 lines | src/Service/MixRepository.php
// ... 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".