Buy
Buy

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

Login Subscribe

The way we coded in Symfony 3 was a bit different than Symfony 4. And... well... we need to learn just a little bit about the Symfony 3 way. Why? Because, when you find bundles with outdated docs, or old StackOverflow answers, I want you to be able to translate that into Symfony 4.

Public Versus Private Services

In Symfony 3, services were defined as public. This means that you could use a $this->get() shortcut method in your controller to fetch a service by its id. Or, if you had the container object itself - yep, that's totally possible - you could say $container->get() to do the same thing.

But in Symfony 4, most services are private. What does that mean? Very simply, when a service is private, you cannot use the $this->get() shortcut to fetch it.

At first, it might seem like we're just making life more difficult! But actually, Symfony 4 simply has a new philosophy.

Open services.yaml and, below _defaults, check out the public: false config:

... lines 1 - 5
services:
# default configuration for services in *this* file
_defaults:
... lines 9 - 10
public: false # Allows optimizing the container by removing unused services; this also means
# fetching services directly from the container via $container->get() won't work.
# The best practice is to be explicit about your dependencies anyway.
... lines 14 - 34

Thanks to this, any service that we create is private. And so, we cannot fetch our services with $this->get(). Increasingly, more and more third-party bundles are also making their services private.

And because so many services are now private, instead of using $this->get(), we need to fetch services via "dependency injection" - a fancy, scary-sounding term that describes what we've been doing... this entire tutorial: passing services and config as arguments. This is considered a better coding practice than $this->get(), which means that we get to write nice code. Woo!

And actually... it also makes your app faster! It's not huge, but private services are faster than public services.

If you DO Want to use $this->get()

Side note, if you do want to use the $this->get() shortcut to fetch a public service - which you should not - you'll need to change your base controller class to Controller instead of AbstractController:

... lines 1 - 7
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
... lines 9 - 12
class ArticleController extends AbstractController
{
... lines 15 - 83
}

It's not important why... and you shouldn't do it anyways :p.

Fetching a Service by id

Okay okay, so if we can't use the $this->get() shortcut, how the heck can we fetch this service? Let's experiment! Copy the service id, find your terminal, and run:

php bin/console debug:container nexy_slack.client

Apparently the class for this object is Nexy\Slack\Client. We didn't see any autowiring type-hints that would work in debug:autowiring... but... maybe it will work if we type-hint this class?

Let's try it! In ArticleController::show(), add another argument: Client - make sure you get the one from Nexy\Slack - then $slack:

... lines 1 - 5
use Nexy\Slack\Client;
... lines 7 - 13
class ArticleController extends AbstractController
{
... lines 16 - 36
public function show($slug, MarkdownHelper $markdownHelper, Client $slack)
{
... lines 39 - 79
}
... lines 81 - 92
}

Add an if statement: if $slug === 'khaaaaaan':

... lines 1 - 5
use Nexy\Slack\Client;
... lines 7 - 13
class ArticleController extends AbstractController
{
... lines 16 - 36
public function show($slug, MarkdownHelper $markdownHelper, Client $slack)
{
if ($slug === 'khaaaaaan') {
... lines 40 - 44
}
... lines 46 - 79
}
... lines 81 - 92
}

Then we need to know about this! Go copy some code from the docs. Then, simplify a bit - we don't need the attachment stuff, this is coming from Khan and the text should be "Ah, Kirk, my old friend.":

... lines 1 - 5
use Nexy\Slack\Client;
... lines 7 - 13
class ArticleController extends AbstractController
{
... lines 16 - 36
public function show($slug, MarkdownHelper $markdownHelper, Client $slack)
{
if ($slug === 'khaaaaaan') {
$message = $slack->createMessage()
->from('Khan')
->withIcon(':ghost:')
->setText('Ah, Kirk, my old friend...');
$slack->sendMessage($message);
}
... lines 46 - 79
}
... lines 81 - 92
}

Excellent! Copy the slug. Then go to that page in your browser. And... it totally did not work: the $slack argument is missing.

Well... I guess debug:autowiring doesn't lie. Copy the $slack argument to the constructor:

... lines 1 - 5
use Nexy\Slack\Client;
... lines 7 - 13
class ArticleController extends AbstractController
{
... lines 16 - 20
public function __construct(bool $isDebug, Client $slack)
{
... line 23
}
... lines 25 - 92
}

No, it won't work here either... but we will get a better error message. Actually, this is due to another shortcoming with controller autowiring: when it fails, the error isn't great. That will hopefully also be improved in the future. Again, a few of these features are still brand new!

Refresh! Ah, much better:

Cannot autowire service ArticleController: argument $slack of method __construct() references class Nexy\Slack\Client, but no such service exists.

This basically means that we're missing configuration to tell the container which services to pass for this type-hint. So... are we dead? No way! We are in total control of autowiring.

Open services.yaml, then go copy the full class name for the client: Nexy\Slack\Client. Back under bind, instead of using the argument name, like $slack, put the class name: Nexy\Slack\Client. Set this to the target service id: @nexy_slack.client:

... lines 1 - 5
services:
# default configuration for services in *this* file
_defaults:
... lines 9 - 14
# setup special, global autowiring rules
bind:
... lines 17 - 18
Nexy\Slack\Client: '@nexy_slack.client'
... lines 20 - 35

That's it! Bind has two super-powers: you can bind by the argument name or you can bind by a class or interface. We're defining our own rules for autowiring!

Let's make sure I'm not lying: refresh! Yes! There's our Slack notification.

Autowiring Aliases

But I want to make just one small tweak. In services.yaml, instead of putting this beneath _defaults and bind, let's un-indent it so that it's at the root of services:

... lines 1 - 5
services:
... lines 7 - 19
# custom aliases for autowiring
Nexy\Slack\Client: '@nexy_slack.client'
... lines 22 - 37

Refresh again. It works exactly like before!

The difference is subtle. Config beneath _defaults only affects services that are added in this file. But when you put this same config directly under services, it will affect all services in the system. In practice... that makes no difference: only our code uses autowiring: third-party libraries do not using autowiring, to keep things predicatable.

The biggest reason I did this is that, when you run debug:autowiring:

php bin/console debug:autowiring

and search for "Slack", there it is! When it's under bind, it won't show up here.

About Autowiring Logic

But... this trick also shows us a bit more about how autowiring works. By putting this config directly under services, we're creating a new service in the container with the id Nexy\Slack\Client. But this is not a real service, it's just an "alias" - a "shortcut" - to fetch the existing nexy_slack.client service.

Here's the important question: when an argument to a service hasn't been configured under bind or arguments, how does the autowiring figure out which service to pass? The answer is super simple: it just looks for a service whose id exactly matches the type-hint. Yep, now that there is a service whose id is Nexy\Slack\Client, we can use that class as a type-hint. That's also why our classes - like MarkdownHelper can be autowired: each class in src/ is auto-registered as a service and given an id that matches the class name.

Ok, it's time to turn to something different, but very important in Symfony 4: environment variables. This will help us to not hardcode our secret Slack URL inside our code.

Leave a comment!

  • 2019-03-05 Marius

    Hey,

    Sorry, but no. I've did it multiple time if it helps. I did it because I ran the "composer require nexylan/slack-bundle php-http/guzzle6-adapter:1.1.1" without the version part (:1.1.1), and I thought that might be the problem.

    Good luck!

  • 2019-03-05 Hugo Dessomme

    Hello,
    I have the same problem... I deleted the "vendor" directory, and then did the "composer update" command, but I still have this error. Did you do something else to solve it?

  • 2019-03-04 Diego Aguiar

    Ah, I get it. This is the main Symfony repository: https://github.com/symfony/...
    In there you can find all components and bundles (but not third party bundles)

  • 2019-03-04 Victor Bocharsky

    Hey Marius,

    Thank you for sharing how you solved this issue with others!

    Cheers!

  • 2019-03-03 Marius

    Nevermind, it seems to be working after I deleted the "vendor" directory and used the "composer update" command.

  • 2019-03-02 Marius

    Hello, after adding the class in the service.yaml file, instead of a nice, loaded page, I get this error:

    Argument 1 passed to Http\HttplugBundle\Collector\ProfileClient::collectExceptionInformations() must be an instance of Exception, instance of Error given, called in C:\wamp64\www\the_spacebar\vendor\php-http\httplug-bundle\Collector\ProfileClient.php on line 145

    Could you help me, please?

  • 2019-03-02 Hermen

    Hi Diego,

    I was thinking about the Git-repo where the file lives and I could find some mentioning in git log. But, I'm just a hobbyist programmer, so just winging it...

  • 2019-02-25 Diego Aguiar

    Hey Hermen

    Can you tell me specifically what documentation are you tring to find?

  • 2019-02-24 Hermen

    I'm guessing the real programmers know where to find this information (somewhere on Github), but not me. Where can I find that? It took me a while before I admitted defeat and found this post...

  • 2019-02-11 Diego Aguiar

    That's because Symfony4 uses Symfony Flex which leverage the "recipes" system. Ff a bundle has a recipe you will get a pretty default configuration automatically, plus, if the bundle is well designed, it will give you public services ready to use.

  • 2019-02-11 Cryptoblob

    So why didn't we have to do any configuration for the KnpMarkdownBundle?

  • 2019-02-04 weaverryan

    Hey payskin!

    Yes, you're 100% correct that starting in Symfony 4.0 (actually), all services default to public: false. For a while, we still shipped that public: false part there, even though it wasn't needed, but starting in Symfony 4.2, we removed it. Unfortunately, sometimes when a little tweak like this is made to the recipe, the docs aren't always updated perfectly - it's something we're working on actually :).

    Thanks for pinging us on this - I think we may add a note so others aren't confused!

    Cheers!

  • 2019-02-02 payskin

    So, regarding the public: false config in services.yaml, it's not there anymore in Symfony 4.2.2.

    I guess it was removed probably because false is the default value for _defaults now, and not being there does not encourage the bad idea of changing it to true. I tried to look up something about this, and found the Service Container documentation for Symfony 4.2 to be outdated as it clearly shows public:false in the services.yaml example (in Automatic Service Loading in services.yaml section). ;)

  • 2018-08-15 weaverryan

    Haha, I actually thought about that too when I wrote it ;)

  • 2018-08-15 Jaroslav Nenarik

    5:14, first thought was about Khan academy.