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 SubscribeYou may have noticed that I seem to be avoiding "action" injection. For both QuestionRepository
and ChartBuilderInterface
, normally, when I'm in a controller, I'll like to be lazy and autowire them directly into the controller method.
Let's actually try that, at least for ChartBuilderInterface
. Remove ChartBuilderInterface
from the constructor... and, instead add it to the method: ChartBuilderInterface $chartBuilder
.
... lines 1 - 24 | |
class DashboardController extends AbstractDashboardController | |
{ | |
... lines 27 - 28 | |
public function __construct(QuestionRepository $questionRepository) | |
{ | |
$this->questionRepository = $questionRepository; | |
} | |
... lines 33 - 35 | |
public function index(ChartBuilderInterface $chartBuilder = null): Response | |
{ | |
... lines 38 - 49 | |
} | |
... lines 51 - 125 | |
} |
And now... I need to pass $chartBuilder
into createChart()
... because, down here we can't reference the property anymore. So add ChartBuilderInterface $chartBuilder
... and use that argument.
... lines 1 - 35 | |
public function index(ChartBuilderInterface $chartBuilder = null): Response | |
{ | |
... lines 38 - 44 | |
return $this->render('admin/index.html.twig', [ | |
... lines 46 - 47 | |
'chart' => $this->createChart($chartBuilder), | |
]); | |
} | |
... lines 51 - 99 | |
private function createChart(ChartBuilderInterface $chartBuilder): Chart | |
{ | |
$chart = $chartBuilder->createChart(Chart::TYPE_LINE); | |
... lines 103 - 124 | |
} | |
... lines 126 - 127 |
Cool. So in theory, this should work... because this is a normal controller and... this is how action injection works! But you might already notice that PhpStorm is pretty mad. And, it's right! If we refresh, huge error!
DashboardController::index
must be compatible withAbstractDashboardController::index
.
The problem is that our parent class - AbstractDashboardController
- has an index()
method with no arguments. So it's not legal for us to override that and add a required argument.
But if you do want action injection to work, there is a workaround: allow the argument to be optional. So add = null
.
That makes PHP happy and, in practice, even though it's optional, Symfony will pass the chart builder service. So this will work... but to code defensively just in case, I'm going to add a little assert()
function.
This may or may not be a function you're familiar with. It comes from PHP itself. You put an expression inside like null !== $chartBuilder
- and if that expression is false, an exception will be thrown.
... lines 1 - 35 | |
public function index(ChartBuilderInterface $chartBuilder = null): Response | |
{ | |
assert(null !== $chartBuilder); | |
... lines 39 - 49 | |
} | |
... lines 51 - 127 |
So now we can confidently know that if our code gets this far, we do have a ChartBuilderInterface
object.
Refresh now and... got it! So action injection does still work... but it's not as awesome as it normally is. Though, it does have one concrete advantage over constructor injection: the ChartBuilderInterface
service won't be instantiated unless the index()
method is called. So if you were in a normal Crud controller with multiple actions, action injection allows you to make sure that a service is only instantiated for the action that needs it, instead of in all situations.
Next: let's learn how to override templates, like EasyAdmin's layout template, or how an IdField
is rendered across our entire admin area.
Hey Yaroslavche,
Interesting idea, though as you said it's public property so it cannot be read-only and probably violates some SOLID principles. Also, in case you inject it directly into the method name - it's a method dependency. But if you make it a property - it probably would mean it's a class dependency now, so it seems you increase its scope.
But in general, it is just a matter of taste and mostly depends on the specific use case. :)
Cheers!
// composer.json
{
"require": {
"php": ">=8.1.0",
"ext-ctype": "*",
"ext-iconv": "*",
"composer/package-versions-deprecated": "^1.11", // 1.11.99.4
"doctrine/doctrine-bundle": "^2.1", // 2.5.5
"doctrine/doctrine-migrations-bundle": "^3.0", // 3.2.1
"doctrine/orm": "^2.7", // 2.10.4
"easycorp/easyadmin-bundle": "^4.0", // v4.0.2
"handcraftedinthealps/goodby-csv": "^1.4", // 1.4.0
"knplabs/knp-markdown-bundle": "dev-symfony6", // dev-symfony6
"knplabs/knp-time-bundle": "^1.11", // 1.17.0
"sensio/framework-extra-bundle": "^6.0", // v6.2.5
"stof/doctrine-extensions-bundle": "^1.4", // v1.7.0
"symfony/asset": "6.0.*", // v6.0.1
"symfony/console": "6.0.*", // v6.0.2
"symfony/dotenv": "6.0.*", // v6.0.2
"symfony/flex": "^2.0.0", // v2.0.1
"symfony/framework-bundle": "6.0.*", // v6.0.2
"symfony/mime": "6.0.*", // v6.0.2
"symfony/monolog-bundle": "^3.0", // v3.7.1
"symfony/runtime": "6.0.*", // v6.0.0
"symfony/security-bundle": "6.0.*", // v6.0.2
"symfony/stopwatch": "6.0.*", // v6.0.0
"symfony/twig-bundle": "6.0.*", // v6.0.1
"symfony/ux-chartjs": "^2.0", // v2.0.1
"symfony/webpack-encore-bundle": "^1.7", // v1.13.2
"symfony/yaml": "6.0.*", // v6.0.2
"twig/extra-bundle": "^2.12|^3.0", // v3.3.7
"twig/twig": "^2.12|^3.0" // v3.3.7
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.3", // 3.4.1
"symfony/debug-bundle": "6.0.*", // v6.0.2
"symfony/maker-bundle": "^1.15", // v1.36.4
"symfony/var-dumper": "6.0.*", // v6.0.2
"symfony/web-profiler-bundle": "6.0.*", // v6.0.2
"zenstruck/foundry": "^1.1" // v1.16.0
}
}
I have a kind of question. How about the Required attribute?
Personally, I prefer that way to inject services.
Pros:
Cons:
Have you other thoughts about DI via ctor arguments vs Required attribute?