Fetching Non-Autowireable Services

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 $10.00

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

Login Subscribe

There are many services in the container and only a small number of them can be autowired. That's by design: most services are pretty low-level and you will rarely need to use them.

But what if you do need to use one? How can we do that?

To see how, we're going to use our markdown channel logger as an example. It actually is autowireable if you use the LoggerInterface type-hint and name your argument $markdownLogger.

But back in MarkdownHelper, to go deeper, let's be complicated and change the argument's name to something else - like $mdLogger:

... lines 1 - 8
class MarkdownHelper
{
... lines 11 - 15
public function __construct(MarkdownParserInterface $markdownParser, CacheInterface $cache, bool $isDebug, LoggerInterface $mdLogger)
{
... lines 18 - 20
$this->logger = $mdLogger;
}
... lines 23 - 37
}

Excellent! If you refresh the page now, it doesn't break, but if you open the profiler and go to the Logs section, you'll notice that this is using the app channel. That's the "main" logger channel. Because our argument name doesn't match any of the "special" names, it passes us the main logger.

So here's the big picture: I want to tell Symfony that the $mdLogger argument to MarkdownHelper should be passed the monolog.logger.markdown service. I don't want any fancy autowiring: I want to tell Symfony exactly which service to pass to this argument.

And, there are two ways to do this.

Passing Services via Bind

You might guess the first: it's with bind, which works just as well to pass services as it does to pass scalar config like parameters:

... lines 1 - 8
services:
# default configuration for services in *this* file
_defaults:
... lines 12 - 13
bind:
bool $isDebug: '%kernel.debug%'
... lines 16 - 31

First, go copy the full class name for LoggerInterface, paste that under bind and add $mdLogger to match our name. But, what value do we set this to?

If you look back at debug:autowiring, the id of the service we want to use is monolog.logger.markdown. Copy that and paste it onto our bind.

But... wait. If we stopped now, Symfony would literally pass us the string monolog.logger.markdown. That's... not helpful: we want it to pass us the service that has this id. To communicate that, prefix the service id with @:

... lines 1 - 8
services:
# default configuration for services in *this* file
_defaults:
... lines 12 - 13
bind:
... line 15
Psr\Log\LoggerInterface $mdLogger: '@monolog.logger.markdown'
... lines 17 - 32

That's a super-special syntax to tell Symfony that we're referring to a service.

Let's try this thing! Refresh, then open the Logs section of the profiler. Yes! We're back to logging through the markdown channel!

The bind key is your Swiss Army knife for configuring any argument that can't be autowired.

Adding Autowiring Aliases

But there's one other way - besides bind - that we can accomplish this. I'm mentioning it... almost more because it will help you understand how the system works: it's no better or worse than bind.

Copy the LoggerInterface bind line, delete it, move to the bottom of the file, go in four spaces so that we're directly under services and paste:

... lines 1 - 8
services:
... lines 10 - 31
Psr\Log\LoggerInterface $mdLogger: '@monolog.logger.markdown'

That will work too. But... this probably deserves some explanation.

This syntax creates a service "alias": it adds a service to the container whose id is Psr\Log\LoggerInterface $mdLogger. I know, that's a strange id, but it's totally legal. If anyone ever asks for this service, they will actually receive the monolog.logger.markdown service.

Why does that help us? I told you earlier that when autowiring sees an argument type-hinted with Psr\Log\LoggerInterface, it looks in the container for a service with that exact id. And, well... that's not entirely true. It does do that, but only after it first looks for a service whose id is the type-hint + the argument name. So yes, it looks for a service whose id is Psr\Log\LoggerInterface $mdLogger. And guess what? We just created a service with that id.

To prove I'm not shouting random information, move over, refresh, and open up the profiler. Yes! It's still using the markdown channel. The super cool thing is that, back at your terminal, run debug:autowiring log again:

php bin/console debug:autowiring log

Check it out! Our $mdLogger shows up in the list! By creating that alias, we are doing the exact same thing that MonologBundle does internally to set up the other named autowiring entries. These are all service aliases: there is a service with the id of Psr\Log\LoggerInterface $markdownLogger and it's an alias to the monolog.logger.markdown service.

Phew! I promise team, that's as deep & dark as you'll probably ever need to get with all this service autowiring business. But as a bonus, the autowiring alias stuff will be great small talk for your next Zoom party. Your virtual friends are going to love it. I know I would.

Now that we are service experts, let's look back at our controller. Because, it's a service too!

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.2.5",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "knplabs/knp-markdown-bundle": "^1.8", // 1.8.1
        "sensio/framework-extra-bundle": "^5.5", // v5.5.4
        "sentry/sentry-symfony": "^3.4", // 3.4.4
        "symfony/asset": "5.0.*", // v5.0.8
        "symfony/console": "5.0.*", // v5.0.8
        "symfony/debug-bundle": "5.0.*", // v5.0.8
        "symfony/dotenv": "5.0.*", // v5.0.8
        "symfony/flex": "^1.3.1", // v1.6.2
        "symfony/framework-bundle": "5.0.*", // v5.0.8
        "symfony/monolog-bundle": "^3.0", // v3.5.0
        "symfony/profiler-pack": "*", // v1.0.4
        "symfony/twig-pack": "^1.0", // v1.0.0
        "symfony/var-dumper": "5.0.*", // v5.0.8
        "symfony/webpack-encore-bundle": "^1.7", // v1.7.3
        "symfony/yaml": "5.0.*" // v5.0.8
    },
    "require-dev": {
        "symfony/maker-bundle": "^1.15", // v1.15.0
        "symfony/profiler-pack": "^1.0" // v1.0.4
    }
}