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 SubscribeOur 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.
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.
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!
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!
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.
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.
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".
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!
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
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?
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!
In which cases I have to create my own services? Is there a guideline for that?
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!
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!
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!
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.
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!
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!
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 : )
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!
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 :)
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!
// 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
}
}
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: