Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

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.

Start your All-Access Pass
Buy just this tutorial for $6.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

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:

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.

Leave a comment!

4
Login or Register to join the conversation

Hey Ryan,
What about the DataFixtures Service?

Reply

Hey DaveKnifeman!

Excellent question! Back when we made this tutorial, your fixtures were *not* services: the DoctrineFixturesBundle "found" them by looking in the DataFixtures/ORM directory if your bundle and then it instantiated them all on its own. This mean that you were not allowed to have any constructor arguments (quite like how controllers were done before these changes). However, in the latest version of the bundle, your fixture classes *are* services. Here's how it works:

1) Thanks to our code in services.yml, our fixture classes are registered as services just like everything else
2) In the latest version of the bundle, as long as you extend the Fixture class from the bundle (https://github.com/doctrine... - or more specifically, as long as your fixture class implements the ORMFixtureInterface, the "autoconfigure" feature will automatically apply a special "tag" to your fixture services
3) When you run doctrine:fixtures:load, instead of locating your fixture classes based on some directory, it loads all services that have the above "tag". And instead of instantiating your fixtures, they are created by the container.
4) Because your fixtures are now services, you can use normal dependency injection if you need any dependencies.

Let me know if you have any issue putting this all together in practice. Fixtures are MUCH cooler to work with now :).

Cheers!

Reply
Default user avatar

Hey guys,

can we also autowire $this->get('security.authentication.guard_handler') in the Controller?

Reply

Hey Chris,

Yes, we can! You just need to find a proper class name to typehint. To know the proper class name run:
bin/console debug:autowiring

And search for "security.authentication.guard_handler". I see you can autowire it with Symfony\Component\Security\Guard\GuardAuthenticatorHandler typehint.

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.5.9",
        "symfony/symfony": "3.3.0-RC1", // v3.3.0-RC1
        "doctrine/orm": "^2.5", // 2.7.5
        "doctrine/doctrine-bundle": "^1.6", // 1.10.3
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.5
        "symfony/swiftmailer-bundle": "^2.3", // v2.6.7
        "symfony/monolog-bundle": "^3.1", // v3.2.0
        "symfony/polyfill-apcu": "^1.0", // v1.23.0
        "sensio/distribution-bundle": "^5.0", // v5.0.25
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.29
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.4
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.4
        "knplabs/knp-markdown-bundle": "^1.4", // 1.7.1
        "doctrine/doctrine-migrations-bundle": "^1.1", // v1.3.2
        "stof/doctrine-extensions-bundle": "^1.2" // v1.3.0
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.1.7
        "symfony/phpunit-bridge": "^3.0", // v3.4.47
        "nelmio/alice": "^2.1", // v2.3.6
        "doctrine/doctrine-fixtures-bundle": "^2.3" // v2.4.1
    }
}
userVoice