Buy

Autowiring Controller Arguments

Our 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:

7 lines app/config/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!

143 lines src/AppBundle/Controller/GenusController.php
... 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:

143 lines src/AppBundle/Controller/GenusController.php
... 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:

46 lines app/config/services.yml
... 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():

46 lines app/config/services.yml
... 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:

142 lines src/AppBundle/Controller/GenusController.php
... 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!

6 lines app/config/legacy_aliases.yml
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:

40 lines app/config/security.yml
... lines 1 - 2
security:
... lines 4 - 14
firewalls:
... lines 16 - 20
main:
... line 22
guard:
authenticators:
- app.security.login_form_authenticator
... lines 26 - 40

81 lines src/AppBundle/Controller/UserController.php
... 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:

40 lines app/config/security.yml
... 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:

82 lines src/AppBundle/Controller/UserController.php
... 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:

42 lines app/config/services.yml
# 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:

46 lines app/config/services.yml
... 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:

97 lines src/AppBundle/Controller/Admin/GenusAdminController.php
... 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:

97 lines src/AppBundle/Controller/Admin/GenusAdminController.php
... 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:

42 lines app/config/services.yml
... 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.

Leave a comment!