Buy
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:

... 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:

... 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:

... 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!

... 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:

... 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:

... 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):

... 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:

... 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:

... 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():

... 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:

... 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:

... 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:

... 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:

... 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:

... 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:

... 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:

... 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-10-22 weaverryan

    Hey Saar Verberght!

    Ah! This is a tricky one! Ok, look at this part of the error message closely:

    > $cache" of method "__construct()" has type "App\Service\AdapterInterface" but this class was not found.

    The clue is that, for some reason, PHP thinks that you have type-hinted your argument with App\Service\AdapterInterface. But of course, you intended this to be Symfony\Component\Cache\Adapter\AdapterInterface.

    The problem is that you're missing the use statement for the AdapterInterface class at the top of your MarkdownHelper service. Because of this, PHP assumes that the AdapterInterface must live in the *current* namespace (App\Service) not the one you want.

    Add the use statement and this will all work instantly! Missing use statements are one of the most annoying, but common issues. You'll quickly learn to spot them and fix them :).

    Cheers!

  • 2018-10-21 Saar Verberght

    I get following error:

    Cannot resolve argument $markdownHelper of "App\Controller\ArticleController::show()": Cannot autowire service "App\Service\MarkdownHelper": argument "$cache" of method "__construct()" has type "App\Service\AdapterInterface" but this class was not found.

    In the logging:

    CRITICAL
    17:31:11
    request Uncaught PHP Exception Symfony\Component\DependencyInjection\Exception\RuntimeException: "Cannot resolve argument $markdownHelper of "App\Controller\ArticleController::show()": Cannot autowire service "App\Service\MarkdownHelper": argument "$cache" of method "__construct()" has type "App\Service\AdapterInterface" but this class was not found." at /Users/sarah/Sites/the_spacebar/var/cache/dev/ContainerKfYMbmx/getMarkdownHelperService.php line 9

    Any idea?

    Does it have to do with this when I do ./bin/console debug:autowiring
    Symfony\Component\Cache\Adapter\AdapterInterface
    alias to cache.app

  • 2018-10-02 Victor Bocharsky

    Hey Abelardo,

    Great, let me know if you still have this issue and we'll continue solving it together ;)

    Cheers!

  • 2018-10-02 Abelardo León González

    Thanks, @Victor. I didn't see your next-to-last reply. ¿?

    I will read it in order to solve my issue.

    Cheers...and thanks again!

    Brs.

  • 2018-10-01 Victor Bocharsky

    Hey Abelardo!

    Yeah, we're here! Sorry for the delay. I answered your previous comment, hope this helps you, if not - let us know.

    Cheers!

  • 2018-10-01 Victor Bocharsky

    Hey Abelardo,

    Did you download the code for this course? I just tried to download the code and run "bin/console debug:autowiring" and I do not see this error. Yes, Diego means deleting vendor/ dir and re-installing dependencies, i.e:
    $ rm -rf vendor/
    $ composer install

    I don't see we're talking about "session.storage.native" service in this course, or tell me if I'm wrong. So probably this error does not relates to some changes we're doing in this screencast.

    So try to clear the cache before running "bin/console debug:autowiring". Does it helps? Or you still see this error?

    And yeah, we understand that you're on Symfony 4, but could you tell us more precise version, like v4.0.14 that we're using in this course. You can see it in the Symfony web debug toolbar in the right bottom cornet.

    Cheers!

  • 2018-10-01 Abelardo León González

    Anybody here?

  • 2018-09-28 Abelardo León González

    Hi @Diego Aguiar,

    "services.yaml" file? Where is it explained during the video?

    Symfony 4.

    You meant to delete the vendor dir and execute composer install ...or what another command should I use to reinstall my vendors?

    Best regards.

  • 2018-09-28 Diego Aguiar

    Hey Abelardo León González

    That's odd, are you requesting that service in your "services.yaml" file?
    Can you tell me which Symfony version are you using?

    If you haven't done anything interesting, probably is just an unexpected error, try clearing the cache and/or reinstalling your vendors

    Cheers!

  • 2018-09-28 Abelardo León González

    Hi everyone,

    Why does this message appear after to write "php bin/console debug:autowiring"?

    In ContainerBuilder.php line 1011:

    You have requested a non-existent Service "session.storage.native".

    What's wrong?

    Best regards.

  • 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