Buy

Open ArticleController and find the show() action:

81 lines src/Controller/ArticleController.php
... lines 1 - 13
class ArticleController extends AbstractController
{
... lines 16 - 26
public function show($slug, MarkdownInterface $markdown, AdapterInterface $cache)
{
... lines 29 - 53
$item = $cache->getItem('markdown_'.md5($articleContent));
if (!$item->isHit()) {
$item->set($markdown->transform($articleContent));
$cache->save($item);
}
$articleContent = $item->get();
... lines 60 - 66
}
... lines 68 - 79
}

I think it's time to move our markdown & caching logic to a different file. Why? Two reasons. First, this method is getting a bit long and hard to read. And second, we can't re-use any of this code when it's stuck in our controller. And... bonus reason! If you're into unit testing, this code cannot be tested.

On the surface, this is the oldest trick in the programming book: if you want to re-use some code, move it into its own function. But, what we're about to do will form the cornerstone of almost everything else in Symfony.

Create the Service Class

Instead of moving this code to a function, we're going to create a new class and move into a new method. Inside src/, create a new directory called Service. And then a new PHP class called MarkdownHelper:

18 lines src/Service/MarkdownHelper.php
... lines 1 - 2
namespace App\Service;
class MarkdownHelper
{
... lines 7 - 16
}

The name of the directory - Service - and the name of the class are not important at all: you can put your code wherever you want. The power!

Inside, let's add a public function called, how about, parse(): with a string $source argument that will return a string:

18 lines src/Service/MarkdownHelper.php
... lines 1 - 4
class MarkdownHelper
{
public function parse(string $source): string
{
... lines 9 - 15
}
}

And... yea! Let's just copy our markdown code from the controller and paste it here!

18 lines src/Service/MarkdownHelper.php
... lines 1 - 4
class MarkdownHelper
{
public function parse(string $source): string
{
$item = $cache->getItem('markdown_'.md5($source));
if (!$item->isHit()) {
$item->set($markdown->transform($source));
$cache->save($item);
}
return $item->get();
}
}

I know, it's not going to work yet - we've got undefined variables. But, worry about that later. Return the string at the bottom:

18 lines src/Service/MarkdownHelper.php
... lines 1 - 4
class MarkdownHelper
{
public function parse(string $source): string
{
... lines 9 - 14
return $item->get();
}
}

And... congrats! We just created our first service! What? Remember, a service is just a class that does work! And yea, this class does work! The really cool part is that we can automatically autowire our new service.

Find your terminal and run:

./bin/console debug:autowiring

Scroll up. Boom! There is MarkdownHelper. It already lives in the container, just like all the core services. That means, in ArticleController, instead of needing to say new MarkdownHelper(), we can autowire it: add another argument: MarkdownHelper $markdownHelper:

77 lines src/Controller/ArticleController.php
... lines 1 - 4
use App\Service\MarkdownHelper;
... lines 6 - 14
class ArticleController extends AbstractController
{
... lines 17 - 24
/**
* @Route("/news/{slug}", name="article_show")
*/
public function show($slug, MarkdownInterface $markdown, AdapterInterface $cache, MarkdownHelper $markdownHelper)
{
... lines 30 - 62
}
... lines 64 - 75
}

Below, simplify: $articleContent = $markdownHelper->parse($articleContent):

77 lines src/Controller/ArticleController.php
... lines 1 - 4
use App\Service\MarkdownHelper;
... lines 6 - 14
class ArticleController extends AbstractController
{
... lines 17 - 24
/**
* @Route("/news/{slug}", name="article_show")
*/
public function show($slug, MarkdownInterface $markdown, AdapterInterface $cache, MarkdownHelper $markdownHelper)
{
... lines 30 - 35
$articleContent = <<<EOF
... lines 37 - 52
EOF;
$articleContent = $markdownHelper->parse($articleContent);
... lines 56 - 62
}
... lines 64 - 75
}

Ok, let's try it! Refresh! We expected this:

Undefined variable $cache

Inside MarkdownHelper. But hold on! This proves that Symfony's container is instantiating the MarkdownHelper and then passing it to us. So cool!

Dependency Injection: The Wrong Way First

In MarkdownHelper, oh, update the code to use the $source variable:

18 lines src/Service/MarkdownHelper.php
... lines 1 - 4
class MarkdownHelper
{
public function parse(string $source): string
{
$item = $cache->getItem('markdown_'.md5($source));
if (!$item->isHit()) {
$item->set($markdown->transform($source));
$cache->save($item);
}
return $item->get();
}
}

Here's the problem: MarkdownHelper needs the cache and markdown services. To say it differently, they're dependencies. So how can we get them from here?

Symfony follows object-orientated best practices... which means that there's no way to magically fetch them out of thin air. But that's no problem! If you ever need a service or some config, just pass them in.

The easiest way to do this is to add them as arguments to parse(). I'll show you a different solution in a minute - but let's get it working. Add AdapterInterface $cache and MarkdownInterface $markdown:

21 lines src/Service/MarkdownHelper.php
... lines 1 - 4
use Michelf\MarkdownInterface;
use Symfony\Component\Cache\Adapter\AdapterInterface;
class MarkdownHelper
{
public function parse(string $source, AdapterInterface $cache, MarkdownInterface $markdown): string
{
... lines 12 - 18
}
}

If you try it now... it fails:

Too few arguments passed to parse(): 1 passed, 3 expected.

This makes sense! In ArticleController, we are calling parse():

77 lines src/Controller/ArticleController.php
... lines 1 - 14
class ArticleController extends AbstractController
{
... lines 17 - 27
public function show($slug, MarkdownInterface $markdown, AdapterInterface $cache, MarkdownHelper $markdownHelper)
{
... lines 30 - 54
$articleContent = $markdownHelper->parse($articleContent);
... lines 56 - 62
}
... lines 64 - 75
}

This is important: that whole autowiring thing works for controller actions, because that is a unique time when Symfony is calling our method. But everywhere else, it's good old-fashioned object-oriented coding: if we call a method, we need to pass all the arguments.

No problem! Add $cache and $markdown:

81 lines src/Controller/ArticleController.php
... lines 1 - 14
class ArticleController extends AbstractController
{
... lines 17 - 27
public function show($slug, MarkdownInterface $markdown, AdapterInterface $cache, MarkdownHelper $markdownHelper)
{
... lines 30 - 54
$articleContent = $markdownHelper->parse(
$articleContent,
$cache,
$markdown
);
... lines 60 - 66
}
... lines 68 - 79
}

And... refresh! It works! We just isolated our code into a re-usable service. We rule. Go high-five some strangers!

Proper Dependency Injection

Then come back! Because there's a much better way to do all of this. Whenever you have a service that depends on other services, like $cache or $markdown, instead of passing those in as arguments to the individual method, you should pass them via a constructor.

Let me show you: create a public function __construct(). Next, move the two arguments into the constructor, and create properties for each: private $cache; and private $markdown:

30 lines src/Service/MarkdownHelper.php
... lines 1 - 7
class MarkdownHelper
{
private $cache;
private $markdown;
public function __construct(AdapterInterface $cache, MarkdownInterface $markdown)
{
... lines 15 - 16
}
public function parse(string $source): string
{
... lines 21 - 27
}
}

Inside the constructor, set these: $this->cache = $cache and $this->markdown = $markdown:

30 lines src/Service/MarkdownHelper.php
... lines 1 - 7
class MarkdownHelper
{
private $cache;
private $markdown;
public function __construct(AdapterInterface $cache, MarkdownInterface $markdown)
{
$this->cache = $cache;
$this->markdown = $markdown;
}
... lines 18 - 28
}

By putting this in the constructor, we're basically saying that whoever uses the MarkdownHelper is required to pass us a cache object and a markdown object. From the perspective of this class, we don't care who uses us, but we know that they will be forced to pass us our dependencies.

Thanks to that, in parse() we can safely use $this->cache and $this->markdown:

30 lines src/Service/MarkdownHelper.php
... lines 1 - 7
class MarkdownHelper
{
private $cache;
private $markdown;
public function __construct(AdapterInterface $cache, MarkdownInterface $markdown)
{
$this->cache = $cache;
$this->markdown = $markdown;
}
public function parse(string $source): string
{
$item = $this->cache->getItem('markdown_'.md5($source));
if (!$item->isHit()) {
$item->set($this->markdown->transform($source));
$this->cache->save($item);
}
... lines 26 - 27
}
}

One of the advantages of passing dependencies through the constructor is that it's easier to call our methods: we only need to pass arguments that are specific to that method - like the article content:

77 lines src/Controller/ArticleController.php
... lines 1 - 14
class ArticleController extends AbstractController
{
... lines 17 - 27
public function show($slug, MarkdownInterface $markdown, AdapterInterface $cache, MarkdownHelper $markdownHelper)
{
... lines 30 - 54
$articleContent = $markdownHelper->parse($articleContent);
... lines 56 - 62
}
... lines 64 - 75
}

And, hey! We can also remove the extra controller arguments. And, on top, we don't need to, but let's remove the old use statements:

75 lines src/Controller/ArticleController.php
... lines 1 - 4
use App\Service\MarkdownHelper;
use Psr\Log\LoggerInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Twig\Environment;
class ArticleController extends AbstractController
{
... lines 15 - 25
public function show($slug, MarkdownHelper $markdownHelper)
{
... lines 28 - 60
}
... lines 62 - 73
}

Configuring the Constructor Args?

But there's still one big question! How did nobody notice that there was a thermal exhaust pipe that would cause the whole Deathstar to explode? And also, because the container is responsible for instantiating MarkdownHelper, how will it know what values to pass? Don't we need to somehow tell it that it needs to pass the cache and markdown services as arguments?

Actually, no! Move over to your browser and refresh. It just works.

Black magic! Well, not really. When you create a service class, the arguments to its constructor are autowired. That means that we can use any of the classes or interfaces from debug:autowiring as type-hints. When Symfony creates our MarkdownHelper:

75 lines src/Controller/ArticleController.php
... lines 1 - 4
use App\Service\MarkdownHelper;
... lines 6 - 12
class ArticleController extends AbstractController
{
... lines 15 - 25
public function show($slug, MarkdownHelper $markdownHelper)
{
... lines 28 - 60
}
... lines 62 - 73
}

It knows what to do!

Yep, we just organized our code into a brand new service and touched zero config files. This is huge!

Next, let's get smarter, and find out how we can access core services that cannot be autowired.

Leave a comment!