Autowiring Controller Arguments
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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeOur app is now fully using the new config features. It's time to start enjoying it a little!
Let's start by cleaning up legacy_aliases.yml
:
services: | |
app.markdown_transformer: '@AppBundle\Service\MarkdownTransformer' | |
app.markdown_extension: '@AppBundle\Twig\MarkdownExtension' | |
app.security.login_form_authenticator: '@AppBundle\Security\LoginFormAuthenticator' | |
app.doctrine.hash_password_listener: '@AppBundle\Doctrine\HashPasswordListener' | |
app.form.help_form_extenion: '@AppBundle\Form\TypeExtension\HelpFormExtension' |
Remember, we created this because these old service ids might still be used in our app. Let's eliminate these one-by-one.
Copy the first id and go see where it's used:
git grep app.markdown_transformer
Private Services and $container->get()
Ok! This is used in GenusController
. Open that up - there it is!
// ... lines 1 - 14 | |
class GenusController extends Controller | |
{ | |
// ... lines 17 - 77 | |
public function showAction(Genus $genus) | |
{ | |
// ... lines 80 - 81 | |
$markdownTransformer = $this->get('app.markdown_transformer'); | |
// ... lines 83 - 95 | |
} | |
// ... lines 97 - 141 | |
} |
Easy fix! Let's change this to use the new service id: MarkdownTransformer::class
:
// ... lines 1 - 14 | |
class GenusController extends Controller | |
{ | |
// ... lines 17 - 77 | |
public function showAction(Genus $genus) | |
{ | |
// ... lines 80 - 81 | |
$markdownTransformer = $this->get(MarkdownTransformer::class); | |
// ... lines 83 - 95 | |
} | |
// ... lines 97 - 141 | |
} |
Awesome! Let's try it: navigate to /genus
. That code was on the show page, so click any of the genuses and... explosion!
You have requested a non-existent service
AppBundle\Service\MarkdownTransformer
.
But... we know that's a service: it's even explicitly configured:
// ... lines 1 - 8 | |
services: | |
// ... lines 10 - 29 | |
AppBundle\Service\MarkdownTransformer: | |
arguments: ['@markdown.parser', '@doctrine_cache.providers.my_markdown_cache'] | |
// ... lines 32 - 46 |
What's going on!?
Remember, all of these services are private... which means that we cannot fetch them via $container->get()
:
// ... lines 1 - 8 | |
services: | |
_defaults: | |
// ... lines 11 - 12 | |
public: false | |
// ... lines 14 - 46 |
This is actually one of the big motivations behind all of these autowiring and auto-registration changes: we do not want you to fetch things out of the container directly anymore. Nope, we want you to use dependency injection.
Not only is dependency injection a better practice than calling $container->get()
, using it will actually give you better errors!
For example, if you use $container->get()
and accidentally fetch a service that doesn't exist, you'll only get an error if you visit a page that runs that code. But if you use dependency injection and reference a service that doesn't exist, you'll get a huge error when you access any page or try to do anything in your app. If you make all services private, the new config system is actually more stable than the old one.
Controller Action Injection
To fix our error, we need to use classic dependency injection. And actually, there are two ways to do this in a controller. First, we could of course, go to the top of our class, add a __construct
function, type-hint a MarkdownTransformer
argument set that on a property, and use it below. Thanks to autowiring, we wouldn't need to touch any config files.
But, because this is a bit tedious and so common to do in a controller, we've added a shortcut. In controllers only, you can autowire a service into an argument of your action method. We'll add MarkdownTransformer $markdownTransformer
:
// ... lines 1 - 14 | |
class GenusController extends Controller | |
{ | |
// ... lines 17 - 77 | |
public function showAction(Genus $genus, MarkdownTransformer $markdownTransformer) | |
{ | |
// ... lines 80 - 94 | |
} | |
// ... lines 96 - 140 | |
} |
Now, remove the $this->get()
line... which is a shortcut for $this->container->get()
.
This fixes things... because we've eliminated the $container->get()
call that does not work with private services.
Celebrate by removing the first alias! The rest are easy!
services: | |
app.markdown_extension: '@AppBundle\Twig\MarkdownExtension' | |
// ... lines 3 - 6 |
The app.markdown_extension
id isn't referenced anywhere, so remove that. The app.security.login_form_authenticator
is used in two places: security.yml
and also UserController
:
// ... lines 1 - 2 | |
security: | |
// ... lines 4 - 14 | |
firewalls: | |
// ... lines 16 - 20 | |
main: | |
// ... line 22 | |
guard: | |
authenticators: | |
- app.security.login_form_authenticator | |
// ... lines 26 - 40 |
// ... lines 1 - 11 | |
class UserController extends Controller | |
{ | |
// ... lines 14 - 16 | |
public function registerAction(Request $request) | |
{ | |
// ... lines 19 - 21 | |
if ($form->isValid()) { | |
// ... lines 23 - 30 | |
return $this->get('security.authentication.guard_handler') | |
->authenticateUserAndHandleSuccess( | |
// ... lines 33 - 34 | |
$this->get('app.security.login_form_authenticator'), | |
// ... line 36 | |
); | |
} | |
// ... lines 39 - 42 | |
} | |
// ... lines 44 - 79 | |
} |
Copy the new service id - the class name. In security.yml
, just replace the old with the new:
// ... lines 1 - 2 | |
security: | |
// ... lines 4 - 14 | |
firewalls: | |
// ... lines 16 - 20 | |
main: | |
// ... line 22 | |
guard: | |
authenticators: | |
- AppBundle\Security\LoginFormAuthenticator | |
// ... lines 26 - 40 |
Next, in UserController
, I'll search for "authenticator". Ah, we're fetching it out of the container directly! We know the fix: type-hint a new argument with LoginFormAuthenticator $authenticator
and use that below:
// ... lines 1 - 7 | |
use AppBundle\Security\LoginFormAuthenticator; | |
// ... lines 9 - 12 | |
class UserController extends Controller | |
{ | |
// ... lines 15 - 17 | |
public function registerAction(Request $request, LoginFormAuthenticator $authenticator) | |
{ | |
// ... lines 20 - 22 | |
if ($form->isValid()) { | |
// ... lines 24 - 31 | |
return $this->get('security.authentication.guard_handler') | |
->authenticateUserAndHandleSuccess( | |
// ... lines 34 - 35 | |
$authenticator, | |
// ... line 37 | |
); | |
} | |
// ... lines 40 - 43 | |
} | |
// ... lines 45 - 80 | |
} |
Almost done! The app.doctrine.hash_password_listener
service isn't being used anywhere, and neither is app.form.help_form_extension
.
And... that's it! At the top of services.yml
, remove the import:
# Learn more about services, parameters and containers at | |
# http://symfony.com/doc/current/book/service_container.html | |
parameters: | |
# parameter_name: value | |
services: | |
// ... lines 7 - 42 |
Then, delete legacy_aliases.yml
.
Our last public service is MessageManager
:
// ... lines 1 - 8 | |
services: | |
// ... lines 10 - 40 | |
AppBundle\Service\MessageManager: | |
arguments: | |
- ['You can do it!', 'Dude, sweet!', 'Woot!'] | |
- ['We are *never* going to figure this out', 'Why even try again?', 'Facepalm'] | |
public: true |
Now we know how to fix this. In GenusAdminController
, find editAction()
and add an argument: MessageManager $messageManager
:
// ... lines 1 - 6 | |
use AppBundle\Service\MessageManager; | |
// ... lines 8 - 16 | |
class GenusAdminController extends Controller | |
{ | |
// ... lines 19 - 64 | |
public function editAction(Request $request, Genus $genus, MessageManager $messageManager) | |
{ | |
// ... lines 67 - 95 | |
} | |
} |
Use that below in both places:
// ... lines 1 - 6 | |
use AppBundle\Service\MessageManager; | |
// ... lines 8 - 16 | |
class GenusAdminController extends Controller | |
{ | |
// ... lines 19 - 64 | |
public function editAction(Request $request, Genus $genus, MessageManager $messageManager) | |
{ | |
// ... lines 67 - 70 | |
if ($form->isSubmitted() && $form->isValid()) { | |
// ... lines 72 - 77 | |
$this->addFlash( | |
'success', | |
$messageManager->getEncouragingMessage() | |
); | |
// ... lines 82 - 85 | |
} elseif ($form->isSubmitted()) { | |
$this->addFlash( | |
'error', | |
$messageManager->getDiscouragingMessage() | |
); | |
} | |
// ... lines 92 - 95 | |
} | |
} |
Back in services.yml
, make that service private:
// ... lines 1 - 5 | |
services: | |
// ... lines 7 - 37 | |
AppBundle\Service\MessageManager: | |
arguments: | |
- ['You can do it!', 'Dude, sweet!', 'Woot!'] | |
- ['We are *never* going to figure this out', 'Why even try again?', 'Facepalm'] |
This is reason to celebrate! All our services are now private! We're using proper dependency injection on everything! Thanks to this, the container will optimize itself for performance and give us clear errors if we make a mistake... anywhere.
Next, we need to talk more about aliases: the key to unlocking the full potential of autowiring.
Hey Ryan,
What about the DataFixtures Service?