Service Config & Non-Autowireable 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 $10.00

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

Login Subscribe

At your terminal, run

php bin/console debug:container --parameters

Most parameters are low-level values that probably aren't useful to us. But there are several that start with kernel. that are useful. These are added by Symfony itself. Need to know the current environment? You can use kernel.environment. Oh, and I use kernel.project_dir pretty frequently: if you ever need to point to a file path from a config file, this is super handy.

But what the one I want to use right now is kernel.debug, which is currently set to true. Basically when you're in the dev or test environments, this is true. When you're in prod, it's false.

Here's the challenge: let's pretend that we're trying to customize the markdown-parsing logic itself: we're making changes to the HTML it outputs somehow. But because we're caching the Markdown, each time we make a change to the parser, we need to clear the cache before we can see it. To make life nicer, let's use this flag to disable markdown caching when kernel.debug is set to true.

Dependency Injection with Scalar Values

Open up MarkdownHelper. In the same way that this class needs the MarkdownParserInterface and CacheInterface services to do its job, it now also needs to know whether or not we're in debug mode. What do we do when we're inside a service and need access to a service or some config that we don't have?

The answer is always the same: create a __construct() method if you don't have one already, add an argument, set that argument on a new property, then use it. Usually the "thing" we need is another service. But occasionally you'll need some configuration - like a debug boolean or maybe an API key. Even in those cases, we use this dependency injection flow.

Add a new argument called, how about, bool $isDebug. Create a property for this - private $isDebug - and set that in the constructor: $this->isDebug = $isDebug:

... lines 1 - 7
class MarkdownHelper
{
... lines 10 - 11
private $isDebug;
public function __construct(MarkdownParserInterface $markdownParser, CacheInterface $cache, bool $isDebug)
{
... lines 16 - 17
$this->isDebug = $isDebug;
}
... lines 20 - 30
}

Down in parse, use it: if $this->isDebug, then copy the return statement from below and paste it here:

... lines 1 - 7
class MarkdownHelper
{
... lines 10 - 20
public function parse(string $source): string
{
if ($this->isDebug) {
return $this->markdownParser->transformMarkdown($source);
}
... lines 26 - 29
}
}

Go team!

Non-Autowireable Arguments

So far, each time we've added an argument to a constructor, Symfony has known what to pass to it thanks to autowiring. But what about now? Do you think that, when Symfony tries to instantiate our service, it will know what value to pass to this $isDebug argument?

Let's find out! Move over and refresh. Doh! Syntax error! Come on Ryan! I'll add my missing semicolon and... drum roll... refresh!

The answer is no: Symfony does not know what to pass to $isDebug. But we get an awesome error: "cannot resolve argument $markdownHelper of QuestionController::show()" - that's telling us which controller this all starts with - and then:

Cannot autowire service MarkdownHelper: argument $isDebug of method __construct is type-hinted bool. You should configure its value explicitly.

Yep, autowiring only works with class or interface type-hints. And that makes sense: how could Symfony possibly guess what we want for this argument? It's not, fortunately, that magic.

Adding Extra Service Config to services.yaml

This is our first example of a constructor argument that can't be autowired. When this happens, it's no problem: we just need to give Symfony a little "hint" about what we want.

How? Open up config/services.yaml. At a high level, this is where we configure our own services and parameters. For now, skip passed all the stuff on top - we're going to explore what that does soon. At the bottom of the file, indent four spaces so that you're under the services key, then type the full class name to our service: App\Service\MarkdownHelper:. Below this, we can pass configuration to help Symfony instantiate the object. Do that by saying arguments: and, beneath that, $isDebug set to, for now, just true:

... lines 1 - 8
services:
... lines 10 - 29
App\Service\MarkdownHelper:
arguments:
$isDebug: true

Yep, we're literally saying:

Hey Symfony! If you see an argument named $isDebug in the constructor, pass true. But please keep autowiring the other arguments, because that rocks.

So... that should be enough to get it working! Try it! When we refresh... it's back! Let's really make sure it's doing what we want: inside MarkdownHelper, add dump($isDebug):

... lines 1 - 7
class MarkdownHelper
{
... lines 10 - 13
public function __construct(MarkdownParserInterface $markdownParser, CacheInterface $cache, bool $isDebug)
{
... lines 16 - 18
dump($isDebug);
}
... lines 21 - 31
}

This time when we reload... there it is: true.

Referencing %kernel.debug%

Of course, we don't really want to hardcode true: we want to reference the kernel.debug parameter. No problem: in services.yaml, add quotes then %kernel.debug%:

... lines 1 - 8
services:
... lines 10 - 29
App\Service\MarkdownHelper:
arguments:
$isDebug: '%kernel.debug%'

When we try the page, it should still be true... and it is! Let's double-check the prod environment. Find the .env file, change APP_ENV to prod:

22 lines .env
... lines 1 - 15
###> symfony/framework-bundle ###
APP_ENV=prod
... lines 18 - 22

Then go clear the cache:

php bin/console cache:clear

When that's done, find your browser and take it for a spin. Yep! Up on top, it prints false. The power! Change the environment back to dev:

22 lines .env
... lines 1 - 16
APP_ENV=dev
... lines 18 - 22

The Amazing (but not yet impressive) bind

Before we keep going, head back to services.yaml. There are other config keys we can use below a service to control how it's instantiated, but most of them aren't too important or common. However, there is one I want to show you. Rename arguments to bind:

... lines 1 - 8
services:
... lines 10 - 29
App\Service\MarkdownHelper:
bind:
$isDebug: '%kernel.debug%'

If you move over and refresh... that makes no difference at all. In fact, arguments and bind are almost identical. Really, they're so similar, that I'm not even going to explain the subtle difference. Just know that bind is slightly more powerful and it's what I typically use.

Next: I want to demystify what this file is doing on top so that we can really understand how services are being added to the container and how we can control them.

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.2.5",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "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.9.10
        "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
    }
}