Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

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!

19
Login or Register to join the conversation
Takfarines M. Avatar
Takfarines M. Avatar Takfarines M. | posted 1 year ago

hey, thank you for this cours
plaise i want know all list of low level services in symfony

Reply

Hey Takfarines M.!

The php bin/console debug:container command will show you ALL of the services in Symfony, including the low-level ones :). Well, you can actually run php bin/console debug:container --show-hidden to show even MORE low-level services... but those are SUPER low-level - I don't personally use that flag.

Cheers!

Reply
davidmintz Avatar
davidmintz Avatar davidmintz | posted 1 year ago

totally irrelevant noobish question... sort of. I notice that your debug/logging thing's Deprecations tab says you have 0 [zero] deprecation warnings. I had 131. Then I upgraded Symfony from 5.0.1 to 5.4. Now I have a mere 6 deprecation warnings -- and wondering what if anything I should do about them. Things like:

'Since symfony/framework-bundle 5.1: Using type "Symfony\Component\Routing\RouteCollectionBuilder" for argument 1 of method "App\Kernel:configureRoutes()" is deprecated, use "Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator" instead.'

Reply

Hey davidmintz

Updating Symfony is a complex work. Have you upgraded recipes which are installed? Tip composer recipes. In most cases if you are not going to update Symfony to version 6 having deprecations is ok, because that is not an error. But if you have continues update cycle, then it will be better to get rid of them. And for some of them you all you need just update some library or recipe

PS we do not have depreactions because we do not update course. Versions in course are locked to version to have better experience =)

Cheers!

Reply
davidmintz Avatar
davidmintz Avatar davidmintz | sadikoff | posted 1 year ago | edited

OK thanks. I may have missed a step or something at setup time. Maybe I should have likewise locked myself to the same versions --- unfortunately I still don't completely understand composer. I tried updating a recipe with composer recipes:install symfony/framework-bundle --force -v and when I ran the app I got a fatal Error: Failed opening required '[/path/to/my/]vendor/autoload_runtime.php'. Seeing I was in over my head, I reverted the changes.

After I bit more research it looks like I might need to composer require symfony/runtime Yes?

Reply

Hey davidmintz!

Ah, welcome to updating! So let's back up a bit. You were originally asking about the deprecations you saw and what to do about them. Excellent question! You don't really need to do anything until you want to upgrade to Symfony 6, but it's still a good thing to look at. And we usually (and will do again) have a screencast for upgrading - e.g. "Upgrading to Symfony 6" where we talk more about these.

Anyways, deprecations mean that you need to change something in your code to stop using the "old stuff" and start using something new. Sometimes these deprecation warnings are super clear and you'll be able to easily find and tweak that part in your code. Other times... it's not so clear, like this in this case! The deprecation you originally mentioned relates to some code in your src/Kernel.php file that you didn't originally write, don't really need to worry about... but suddenly you DO need to tweak in order to remove the deprecation warning.

Here's how this all relates to updating recipes. When you start a new Symfony app, recipes help give you files you need... and it gives you several that are critically important, but that you don't care about, like src/Kernel.php, bin/console and public/index.php. If the recipe that gave you those files (symfony/framework-bundle is responsible for the 1st and 3rd in this case) is updated to generate them differently, then ideally you would want to "update that recipe". And in fact, if you did, it would fix your deprecation warning. Woo!

But, things can get a bit complicated. At the moment, the only way to update a recipe is the command you ran: composer recipes:install symfony/framework-bundle --force -v. This doesn't really update your recipe: it completely "reinstalls" the latest version. For files that you never modify like src/Kernel.php, that's no problem! It just overwrote your file with the latest version. But for other files that you DO modify - e.g. config/services.yaml - this command will entirely replace your file with the newest version. This means it will remove your custom changes. And you need to use git diff carefully to keep the new changes, but put your old changes back.

Anyways, it sounds like you are getting an error about needing to run composer require symfony/runtime. This is because (yay!) you updated the symfony/framework-bundle recipe, which updated public/index.php, which now requires that component. So yes, go ahead and install it. This is a case where that file gives you an excellent error if you're missing that! You're experiencing Symfony "guiding you" towards upgrading your app to newer versions.

Two more things:

A) About Error: Failed opening required '[/path/to/my/]vendor/autoload_runtime.php', I would ignore this. It sounds like your Composer dependencies weren't installed... or something weird. Probably a composer install would fix this: it's not related to the recipe update stuff.

B) VERY soon (probably this week), there will be a MUCH cooler way to update recipes called composer recipes:update: https://github.com/symfony/flex/pull/845 - Once this is merged, you will be able to get the latest version of flex (composer update symfony/flex) and use this new command to update your recipes. Instead of "replacing your files with the newest versions", it does an intelligent "merge" where it just applies actual recipe "updates" to your app. The result is that it will update files like src/Kernel.php and public/index.php perfectly (just like now), but also will carefully "update" your config/services.yaml file with the latest changes to the recipe without removing your custom changes.

Cheers!

1 Reply
Antoine R. Avatar
Antoine R. Avatar Antoine R. | posted 2 years ago | edited

Hello,

There is something weird about aliasing loggers...
For example, the code below shows 2 ways to create an alias for PhpMailer with the id app.mailer

<blockquote>services:<br />    app.mailer:<br />        alias: '@App\Mail\PhpMailer'<br />services:<br />    app.mailer: '@App\Mail\PhpMailer' # shortcut way to create the same alias<br /></blockquote>

In our case, the shortcut way works to create an alias of monolog.logger.markdown, but the long way does not work to create the alias. It gives the error "You have requested a non-existent service "@monolog.logger.markdown".

<blockquote>services:<br />    Psr\Log\LoggerInterface $mdLogger: '@monolog.logger.markdown' # shortcut works<br />services:<br />    # throws an error<br />    Psr\Log\LoggerInterface $mdLogger:<br />        alias: '@monolog.logger.markdown'<br /></blockquote>

Do you know what is the cause of this error ?

PS : How do I identent the code without using non-breaking spaces ?

Reply

Hey Antoine R.!

PS : How do I identent the code without using non-breaking spaces ?

Yea, sorry about that - we fixed your example. You have to use a crazy &lt;pre&gt;&lt;code&gt; around ALL of your code blocks... Disqus has terrible code formatting :/.

Do you know what is the cause of this error ?

There is just WANT tiny problem with your "longer" version of the code:


services:
    # this is perfect
    Psr\Log\LoggerInterface $mdLogger: '@monolog.logger.markdown'

services:
    # throws an error because you should NOT have the '@'
    Psr\Log\LoggerInterface $mdLogger:
        alias: '@monolog.logger.markdown'

    # this works
    Psr\Log\LoggerInterface $mdLogger:
        alias: 'monolog.logger.markdown'

That's it! This whole "@" symbol is kind of confusing. It's primarily used in service arguments, where "logger" would mean "the string logger" and @logger would mean "the service logger". So, next to "alias", you are "of course" passing a service id, so the "@" is not required (it's even illegal - Symfony thinks it's part of the service's name). But it IS required when you use the shorter syntax... even though... in that situation, we also know that you are passing us a service id. The reason is that... well... we just wanted to be a little bit more explicit in the "shortcut" version to make it obvious that you were creating an alias to a service. I actually argued against requiring the "@" in the short version... but I can see it both ways :).

I hope that helps!

Cheers!

1 Reply
Antoine R. Avatar

Oh ok ! Thanks. So in fact my first example with the mailer did not work ! I must have done things wrong when testing.

Reply
triemli Avatar
triemli Avatar triemli | posted 2 years ago | edited

Guys can you advise me?
I often need to receive "current Host" in my services so I get it in a long way of dependencies


class MyService
{
    protected Frontend $frontend;
    protected Host $host;

    public function __construct(Frontend $frontend)
    {
        $this->frontend = $frontend;
        $this->host = $this->frontend->getHost();
    }

}

From <b>getHost</b> method I get a <b>Host</b> <i>instance</i>


//------ getHost in Frontend service just returns current host. -----------
    public function getHost(): Client
    {
        return $this->host;
    }

Can I resolve it with services.yaml?
For example at the end I want to:


class MyService
{
    protected Host $host;

    public function __construct(Host $host)
    {
        $this->host = $host;
    }

Is it possible or needs manually factory create? Thanks xD

Reply

Hey Sergei,

If you're talking about host as a string - I think the most easiest solution would be to put the $host on bind under _defaults key in service.yaml, see related screencast about it: https://symfonycasts.com/screencast/symfony4-fundamentals/services-config-bind

So, basically, you can do something like this:


services:
    _defaults:
        bind:
            $host: '%your host here%'

Then, thanks to this, you will be able to inject the host to any service you want thanks to autowiring feature, e.g.:


class MyService
{
    public function __construct($host)
    {
        dd($host); // contains the '%your host here%' here
    }
}

But if the host isn't a string, it depends on how you create it then, difficult to say something without more details.

I hope this helps!

Cheers!

Reply
triemli Avatar
triemli Avatar triemli | Victor | posted 2 years ago | edited

Hmm not rly. The Host is an <b>instance </b>of object Host. That's problem xD
Basically Frontend service can calculate where am I (domain) and create for me a Host instance.
So depends on host domain in constructor I will receive different instances.
Method <b>getHost()</b> already knows it and return the right one. But right now i'm doing this:

`public function __construct(Frontend $frontend)

{
    $this->frontend = $frontend;
    $this->host = $this->frontend->getHost();
}

`

But I want the same logic but I want:

` public function __construct(Host $host)

{
    $this->host = $host;
}

`

I tried already with that approach https://symfony.com/doc/current/service_container/configurators.html and https://symfony.com/doc/current/service_container/factories.html but not sure if it right way.

Reply

Hey WebAdequate,

OK, I think I see now. Hm, but if the only Frontend can determine where you're and create a Host instance - it sounds like you should inject Frontend everywhere from where you will get Host. Otherwise, you would probably need to move that logic completely into Host::__construct() instead and then you would be able to inject that Host instance that automatically would determine where you are.

About factories - I'm not sure it's the best approach here, as I would suppose you need only once instance of that Host in your application, I don't see a value in creating new instances every time you call that factory. For me, the host is something you need to determine once per request.

P.S. If you create a new instance of Host every time you call the Frontend::getHost() - here's the tip for you how to avoid it:


class Frontend
{
    private $host;

    public function getHost()
    {
        if ($this->host) {
            return $this->host;
        }

        // Otherwise, create the Host instance here and put it on the property
       $host = new Host();

       // ... Your main business logic to determine where you are

       $this->host = $host;
    }
}

I hope this helps!

Cheers!

Reply
triemli Avatar
triemli Avatar triemli | Victor | posted 2 years ago | edited

I made some unsubstantiated example. Of course in this case single object would be better ;]
But the question is: can I inject <b>$this->host;</b> directly in the constructor without static factory?
Like this:
`
class MyService
{

protected Host $host;

public function __construct(Host $host)
{
    $this->host = $host; // here I have $this->host;
}

}
`

Reply

Hey triemli!

But the question is: can I inject $this->host; directly in the constructor without static factory?

Well, yes and no :). But actually, I think the solution you want WILL contain a "non-static" factory. And I think you are already much closer than you think.

First, how could you do this without a factory? You would register the Host class as a service (like any normal service). Then, in order for the Host to have the correct data (based on the URL), you would need to create an event listener early in Symfony that ran some logic and set the correct host information onto that service. This would work, but I think your current setup is better :).

So, let's talk about how I think you should do this. You already have a "factory" (from an object-oriented point of view) for the Host object: it's the Frontend class. If I call getHost(), it returns the Host object. All we need to do is tell Symfony: "Hey! When someone needs the Host service, just get it by calling Frontend::getHost(). You would do that with Symfony's factory config:


services:
    # all the normal stuff on top
    # ...

    App\Something\Host:
        factory: ['@App\Something\Frontend', 'getHost']

That's it! Your Frontend class is already perfectly built. And now, whenever something autowires the Host object, it WILL be available, but instead of Symfony trying to instantiate it, it will fetch it by calling the getHost() method on your Frontend service.

Let me know if this helps... or if I "missed the point", which happens a lot, especially when I jump into the middle of a conversation ;).

Cheers!

1 Reply
Sean M. Avatar
Sean M. Avatar Sean M. | posted 2 years ago | edited

As soon as I put the Psr\Log\LoggerInterface line in, I get:

<blockquote>The file "/home/mcmullin/cauldron2/config/services.yaml" does not contain valid YAML: Unexpected characters near " g gg<br /># makes classes in src/ available..." on line 19</blockquote>

It's like Symfony is treating the \Lo in Psr\Log\LoggerInterface as an escape sequence. But it hasn't been behaving badly with App\Controller: or App:

Any idea what's going on? I'm not having much luck searching the larger web or Symfony's boards.

Reply

Hey Sean

That's odd, double-check your identation. I'd need to see your file to get a better idea of what's going on. If you can upload a photo or share the code it would be nice

Cheers!

Reply

Hi,

can you confirm me that with the first method (_defaults, bind), it's not really a service alias (unlike the second method) but rather a "hack" ?
Because with the bin/console debug:autowiring there is no alias listed.

Thanks and cheers!

Reply

Hey Steven!

> can you confirm me that with the first method (_defaults, bind), it's not really a service alias (unlike the second method) but rather a "hack" ?

Yes! You understand correctly. I wouldn't call it a hack, but you are 100% correct that it is *not* an alias. The logic internally looks like this:

A) Symfony checks to see if an argument has been specifically configured for an argument (e.g. either via "bind" or "arguments"). And, with bind, you can be very specific that you only want to configure an argument if it has a certain name and type.

B) If the argument has still not been configured, then it tries to autowire it, which uses the alias system.

So, if you specified both a "bind" and an autowireable alias, the bind would win (situation A) because, to Symfony, it looks like you are specifically configuring the argument.

I hope that helps - excellent question!

Cheers!

2 Reply
Cat in space

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

This tutorial also works great for Symfony 6!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.3.0 || ^8.0.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.8", // 1.9.0
        "sensio/framework-extra-bundle": "^6.0", // v6.2.1
        "sentry/sentry-symfony": "^4.0", // 4.0.3
        "symfony/asset": "5.0.*", // v5.0.11
        "symfony/console": "5.0.*", // v5.0.11
        "symfony/debug-bundle": "5.0.*", // v5.0.11
        "symfony/dotenv": "5.0.*", // v5.0.11
        "symfony/flex": "^1.3.1", // v1.17.5
        "symfony/framework-bundle": "5.0.*", // v5.0.11
        "symfony/monolog-bundle": "^3.0", // v3.6.0
        "symfony/profiler-pack": "*", // v1.0.5
        "symfony/routing": "5.1.*", // v5.1.11
        "symfony/twig-pack": "^1.0", // v1.0.1
        "symfony/var-dumper": "5.0.*", // v5.0.11
        "symfony/webpack-encore-bundle": "^1.7", // v1.8.0
        "symfony/yaml": "5.0.*" // v5.0.11
    },
    "require-dev": {
        "symfony/maker-bundle": "^1.15", // v1.23.0
        "symfony/profiler-pack": "^1.0" // v1.0.5
    }
}
userVoice