Buy

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

Login Subscribe

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!

  • 2018-06-29 weaverryan

    Haha, no worries - I like that you were thinking, "Wait... this doesn't make sense!" :D

  • 2018-06-29 Hansi Hanson

    Ah. Video 10 2:00 explains it.
    Sry :-)

  • 2018-06-29 Hansi Hanson

    Hey guys.
    In the video it is said that it is not important what the name of a ServiceClass is or in which directory it lives.
    So how can Symfony guess that MarkdownHelper must be a ServiceClass?

  • 2018-06-07 Victor Bocharsky

    Hey cybernet2u ,

    The error make sense, Symfony unable to guess what exactly value to pass for $isDebug parameter, so just specify it obviously:


    services:
    App\Service\MarkdownHelper:
    arguments:
    $isDebug: 'your-value-here'

    Cheers!

  • 2018-06-02 cybernet2u

    ./bin/console debug:autowiring

    In DefinitionErrorExceptionPass.php line 54:

    Cannot autowire service "App\Service\MarkdownHelper": argument "$isDebug" o
    f method "__construct()" is type-hinted "bool", you should configure its value explicitly.

    any idea ?

  • 2018-04-18 Petru Lebada

    A'ight, thanks a lot. :D

  • 2018-04-18 weaverryan

    Hey Petru Lebada!

    Hmm, yea, so this is weird :). Here's how it *should* work:

    1) Yes. Before you reload in the prod environment, you need to clear the cache in the prod environment. But, I think you already knew that

    2) When you run bin/console, it reads the APP_ENV in .env file to know what environment to use. BUT, you can *override* that with the -e (or --env) flag.

    Based on your output, when you ran bin/console cache:clear, it DID use "prod" as the environment... and so this *should* have been enough to make your page work on production. I can't explain why that seemed to not work. If you can repeat the problem, let me know. Otherwise, just ignore it - it could have been a weird moment in the universe :).

    Cheers!

  • 2018-04-18 Petru Lebada

    Hey Diego Aguiar ,

    No i havent specified the enviroment,i just modified the APP_ENV inside .env to prod and ran: bin/console cache:clear and it returned
    // Clearing the cache for the prod environment with debug false

    [OK] Cache for the "prod" environment (debug=false) was successfully cleared.

    So why isnt this working? Indeed your suggestion fixed the problem but why am i getting a message like that if i have to manually specify the enviroment?

  • 2018-04-17 Diego Aguiar

    Hey Petru Lebada

    As you figured it out, to debug in production you have to read the logs. Look's like the autowiring is not working for your ArticleController. How did you clear cache?
    You have to specify the environment when you are not working on "dev"

    bin/console cache:clear -e prod

    Cheers!

  • 2018-04-17 Petru Lebada

    Hi,

    Everything works on dev enviroment, but just out of curiosity i switched to prod enviroment,cleared the cache and i get a 500 error page...aaaaand i dont know how to debug this, i tried to ini_set('display_errors) and error_reporting() in index but it doesnt output anything.A bit of help please?

    Update: I found the logs directory for the prod env and i'll just paste the error since i have no idea what it means...:

    [2018-04-17 22:04:26] request.CRITICAL: Uncaught PHP Exception RuntimeException: "Controller "App\Controller\ArticleController::show()" requires that you provide a value for the "$markdownHelper" argument. Either the argument is nullable and no null value has been provided, no default value has been provided or because there is a non optional argument after this one." at D:\apps\xampp\htdocs\myproject\vendor\symfony\http-kernel\Controller\ArgumentResolver.php line 78 {"exception":"[object] (RuntimeException(code: 0): Controller \"App\\Controller\\ArticleController::show()\" requires that you provide a value for the \"$markdownHelper\" argument. Either the argument is nullable and no null value has been provided, no default value has been provided or because there is a non optional argument after this one. at D:\\apps\\xampp\\htdocs\\myproject\\vendor\\symfony\\http-kernel\\Controller\\ArgumentResolver.php:78)"} []

  • 2018-03-23 Diego Aguiar

    Hey Tess Hsu

    If you only add the use statement into your controller, it won't do much, what you actually have to do to inject services into any controller's action is to add the service as an argument but type-hinting it (of course, do not forget to add the import)

    Cheers!

  • 2018-03-23 Tess Hsu

    Hi team,

    this is awesome,
    So this core is tell us:
    in any of controller, you could add as many customise service by for example:

    use App\Service\MarkdownHelper;
    use App\Service\OtherService;

    and this would equal to injection those dependency bundles ( which provide services) :
    use Symfony\Component\Cache\Adapter\AdapterInterface;

    right?
    and we could use as much as we could?

    As we installed the bundles, those use services will be automatic add to the top of controller? or acutally we do have to add by our own by see the alias from this command ? $ ./bin/console debug:autowiring

    thanks

  • 2018-03-15 Diego Aguiar

    Hey AlexTurtles

    Ohh, shame to Sublime!
    Or, maybe there is a plugin that you can install for automate imports? Sorry that I can't help you further because I do not use Sublime :(

    Cheers!

  • 2018-03-15 AlexTurtles

    Oh no I get it
    The thing is I'm using Sublime and it doesn't add the use statements like PhpStorm, and it's a pain.

  • 2018-03-15 AlexTurtles

    Hi
    "No problem! Add $cache and $markdown"

    But $markdown is not defined