Buy

The Dreaded Dependency Injection

The MarkdownTransformer will do two things: parse markdown and eventually cache it. Let's start with the first.

Open up GenusController and copy the code that originally parsed the text through markdown. Now... paste this into the parse function, return that and update the variable to $str. Wow, that was easy:

13 lines src/AppBundle/Service/MarkdownTransformer.php
... lines 1 - 4
class MarkdownTransformer
{
public function parse($str)
{
return $this->get('markdown.parser')
->transform($str);
}
}

Go back and refresh. It explodes!

Attempted to call an undefined method named "get" on MarkdownTransformer

Forget about Symfony: this makes sense. The class we just created does not have a get() function and it doesn't extend anything that would give this to us.

In a controller, we do have this function. But more importantly, we have access to the container, either via that shortcut method or by saying $this->container. From there we can fetch any service by calling ->get() on it.

But that's special to the controller: as soon as you're not in a controller - like MarkdownTransformer - you don't have access to the container, its services... or anything.

The Dependency Injection Flow

So here's the quadrillion bitcoin question: how can we get access to the container inside MarkdownTransformer? But wait, we don't really need the whole container: all we really need is the markdown parser object. So a better question is: how can we get access to the markdown parser object inside MarkdownTransformer?

The answer is probably the scariest word that was ever created for such a simple idea: dependency injection. Ah, ah, ah. I think someone invented that word just to be a jerk... especially because it's so simple...

Here's how it goes: whenever you're inside of a class and you need access to an object that you don't have - like the markdown parser - add a public function __construct() and add the object you need as an argument:

20 lines src/AppBundle/Service/MarkdownTransformer.php
... lines 1 - 4
class MarkdownTransformer
{
... lines 7 - 8
public function __construct($markdownParser)
{
... line 11
}
... lines 13 - 18
}

Next create a private property and in the constructor, assign that to the object: $this->markdownParser = $markdownParser:

20 lines src/AppBundle/Service/MarkdownTransformer.php
... lines 1 - 4
class MarkdownTransformer
{
private $markdownParser;
public function __construct($markdownParser)
{
$this->markdownParser = $markdownParser;
}
... lines 13 - 18
}

Now that the markdown parser is set on the property, use it in parse(): get rid of $this->get() and just use $this->markdownParser:

20 lines src/AppBundle/Service/MarkdownTransformer.php
... lines 1 - 4
class MarkdownTransformer
{
... lines 7 - 13
public function parse($str)
{
return $this->markdownParser
->transform($str);
}
}

We're done! Well, done with this class. You see: whoever instantiates our MarkdownTransformer will now be forced to pass in a markdown parser object.

Of course now we broke the code in our controller. Yep, in GenusController PhpStorm is angry: we're missing the required $markdownParser argument in the new MarkdownTransformer() call. That's cool - because now that we're in the controller, we do have access to that object. Pass in $this->get('markdown.parser'):

127 lines src/AppBundle/Controller/GenusController.php
... lines 1 - 13
class GenusController extends Controller
{
... lines 16 - 58
public function showAction($genusName)
{
... lines 61 - 69
$markdownTransformer = new MarkdownTransformer(
$this->get('markdown.parser')
);
... lines 73 - 99
}
... lines 101 - 125
}

Try it out!

It's alive! Twig is escaping the <p> tag - but that proves that markdown parsing is happening. The process we just did is dependency injection. It basically says: if an object needs something, you should pass it to that object. It's really programming 101. But if it still feels weird, you'll see a lot more of it.

Leave a comment!

  • 2016-12-03 weaverryan

    Hey Muhammad!

    Yea, really good question! First, when a bundle adds a service to the system, it can choose to call it whatever it wants - there are no rules about what a service can be called. So, there is no technical reason for these different names. What you are seeing is am mix between some standards and some "practicality". Basically:

    1) If you're creating a re-usable/shareable bundle (this includes the core bundles), it's typical a best practice to somewhat "namespace" your service ids so they don't collide with others. In other words, if I made a KnpCoolGuyBundle, then I might prefix my service ids with knp_cool_guy_ so that they don't collide with service ids from other bundles. This is what you're seeing: service id's are basically being prefixed with a string that's similar to their namespace. But this is just a standard, there's no technical reason for this.

    2) On the other hand, nobody likes long names, especially for really commonly-used things. It would be a bummer to have a service called symfony_component_dependency_injection_service_container :). So, sometimes, a bundle will be opinionated and create a service with a very short name for convenience. This is why you have services like doctrine, templating or logger.

    I hope that explains it a bit! Cheers!

  • 2016-12-03 Muhammad Taqi Hassan Bukhari

    Hi, when i run php app/console container:debug, most of the service id has class Names infront of them, but some of them has not, i.e service_container and request . Why this?

  • 2016-08-08 weaverryan

    Hey 3amprogrammer!

    You should get auto-completion in Twig in both cases. Well, actually, the auto-completion seems to be inconsistent in Twig - I only use the simple template name (e.g. genus/show.html.twig) and I get auto-completion *most* of the time, but not always. I'm not sure why it works sometimes, but not other times - I would love to know why! :)

    Cheers!

  • 2016-08-07 3amprogrammer

    I have found another cool trick! I see that you render view like this:


    return $this->render('genus/show.html.twig', array(
    'genus' => $genus,
    'funFact' => $funFact,
    'recentNoteCount' => count($recentNotes)
    ));

    That's okay, but If you change the string indicating the view name you will get autocompletion in twig!


    return $this->render(':genus:show.html.twig', array(
    'genus' => $genus,
    'funFact' => $funFact,
    'recentNoteCount' => count($recentNotes)
    ));