Buy

Phew! Dependency injection, check! Registering new services, check! Delicious snack, check! Well, I hope you just had a delicious snack.

This tutorial is the start to our victory lap. We need to add caching to MarkdownTransformer: it should be pretty easy. Copy part of the old caching code and paste that into the parse() function. Remove the else part of the if and just return $cache->fetch():

32 lines src/AppBundle/Service/MarkdownTransformer.php
... lines 1 - 6
class MarkdownTransformer
{
... lines 9 - 15
public function parse($str)
{
$cache = $this->get('doctrine_cache.providers.my_markdown_cache');
$key = md5($str);
if ($cache->contains($key)) {
return $cache->fetch($key);
}
... lines 23 - 29
}
}

Below, assign the method call to the $str variable and go copy the old $cache->save() line. Return $str and re-add the sleep() call so that things are really slow - that keeps it interesting:

32 lines src/AppBundle/Service/MarkdownTransformer.php
... lines 1 - 6
class MarkdownTransformer
{
... lines 9 - 15
public function parse($str)
{
... lines 18 - 23
sleep(1);
$str = $this->markdownParser
->transformMarkdown($str);
$cache->save($key, $str);
return $str;
}
}

On top, change the $funFact variables to $str. Perfect!

We know this won't work: there is no get() function in this class. And more importantly, we don't have access to the doctrine_cache.provider.my_markdown_cache service. How can we get access? Dependency injection.

Dependency Inject!

This time, add a second argument to the constructor called $cache. And hmm, we should give this a type-hint. Copy the service name and run:

./bin/console debug:container doctrine_cache.providers.my_markdown_cache

This service is an instance of ArrayCache. But wait! Do not type-hint that. In our earlier course on environments, we setup a cool system that uses ArrayCache in the dev environment and FilesystemCache in prod:

72 lines app/config/config.yml
... lines 1 - 7
parameters:
locale: en
cache_type: file_system
... lines 11 - 65
doctrine_cache:
providers:
my_markdown_cache:
type: %cache_type%
file_system:
directory: %kernel.cache_dir%/markdown_cache

If we type-hint with ArrayCache, this will explode in prod because this service will be a different class.

Let's do some digging: open up ArrayCache:

95 lines vendor/doctrine/cache/lib/Doctrine/Common/Cache/ArrayCache.php
... lines 1 - 32
class ArrayCache extends CacheProvider
{
... lines 35 - 93
}

This extends CacheProvider:

278 lines vendor/doctrine/cache/lib/Doctrine/Common/Cache/CacheProvider.php
... lines 1 - 31
abstract class CacheProvider implements Cache, FlushableCache, ClearableCache, MultiGetCache
{
... lines 34 - 276
}

That might work. But it implements several interface - one of them is just called Cache. Let's try that. If this isn't the right interface - meaning it doesn't contain the methods we're using - PhpStorm will keep highlighting those after we add the type-hint:

35 lines src/AppBundle/Service/MarkdownTransformer.php
... lines 1 - 7
class MarkdownTransformer
{
... lines 10 - 12
public function __construct(MarkdownParserInterface $markdownParser, Cache $cache)
{
... lines 15 - 16
}
... lines 18 - 33
}

I'll use a keyboard shortcut - option+enter on a Mac - and select initialize fields:

35 lines src/AppBundle/Service/MarkdownTransformer.php
... lines 1 - 7
class MarkdownTransformer
{
... line 10
private $cache;
public function __construct(MarkdownParserInterface $markdownParser, Cache $cache)
{
... line 15
$this->cache = $cache;
}
... lines 18 - 33
}

All this did was add the private $cache property and set it in __construct(). You can also do that by hand.

Cool! Update parse() with $cache = $this->cache:

35 lines src/AppBundle/Service/MarkdownTransformer.php
... lines 1 - 7
class MarkdownTransformer
{
... lines 10 - 18
public function parse($str)
{
$cache = $this->cache;
... lines 22 - 32
}
}

And look! All of the warnings went away. That was the right interface to use. Yay!

Because we added a new constructor argument, we need to update any code that instantiates the MarkdownTransformer. But now, that's not done by us: it's done by Symfony, and we help it in services.yml. Under arguments, add a comma and quotes. Copy the service name - @doctrine_cache.providers.my_markdown_cache and paste it here:

10 lines app/config/services.yml
... lines 1 - 5
services:
app.markdown_transformer:
class: AppBundle\Service\MarkdownTransformer
arguments: ['@markdown.parser', '@doctrine_cache.providers.my_markdown_cache']

That's it! That's the dependency injection pattern.

Go back to refresh. The sleep() should make it really slow. And it is slow. Refresh again: still slow because we setup caching to really only work in the prod environment.

Clear the prod cache:

./bin/console cache:clear --env=prod

And now add /app.php/ in front of the URI to use this environment. This should be slow the first time... but then fast after. Super fast! Caching is working. And dependency injection is behind us.

Leave a comment!

  • 2016-11-04 Victor Bocharsky

    Hi Terry,

    Yes, you're right. This method returns a raw HTML, but Controller::render() method returns a Response object, check it in source code to understand how it works behind the scene: https://github.com/symfony/sym... . To get a raw HTML is useful for rendering a mail body which you will send to user, etc.

    Cheers

  • 2016-11-03 Terry Caliendo

    Figured it out... needed to use:
    $this->renderResponse
    instead of
    $this->render
    in my service.

    I remembered one of the videos where Ryan dove into the controller and "render" was just a pass through method.

  • 2016-11-03 Terry Caliendo

    I'm having issues... As you have above the method someMethod in MyService returns the $this->templating->render(...) to my controller which is called like this:


    /**
    * @Route("/myapp")
    */
    public function myappAction()
    {
    $MyService= $this->get('app.MyService');
    return $MyService->someMethod();
    }


    I was expecting the returns to bubble through back to the controller with the response object. But when I run the app, I'm getting the error message "The controller must return a response (<html><body>...".

    It looks like its returning the raw html?!

    Here is a screen shot:
    https://s16.postimg.org/4g34v3...

    ps
    FYI: I'd like to attache a screenshot of the error, but it appears as though the ability to add images is turned off.

  • 2016-11-03 Terry Caliendo

    That's exactly what I needed. I was struggling to find the EngineInterface connection. Thanks much!

  • 2016-11-03 Victor Bocharsky

    Hey Terry,

    Check my example how to inject templating service into your service:


    #in services.yml

    services:
    my_service:
    class: AppBundle\Service\MyService
    arguments: ['@templating']

    # and then in MyService.php

    namespace AppBundle\Service;

    use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface;

    class MyService
    {
    private $templating;

    public function __construct(EngineInterface $templating)
    {
    $this->templating = $templating;
    }

    public function someMethod()
    {
    $someVar = 'some-val';

    return $this->templating->render('path/to/template.html.twig', [
    'someVar' => $someVar,
    ]);
    }
    }

    So as you see injecting a service is very simple. Also if you use PhpStorm with configured Symfony plugin or actually any other good IDE - you will have an autocompletion for methods, so will see all methods which the EngineInterface interface has.

    But if you mean outputting something in services with echo/print PHP statements - it's a bad practice. You probably need a custom Twig function or filter, check How to Write a custom Twig Extension. We also have a few screencasts about custom Twig extensions, check it out:
    https://knpuniversity.com/scre...
    https://knpuniversity.com/scre...

    Cheers!

  • 2016-11-02 Terry Caliendo

    If I wanted to output a twig template from my service, how would I do so? Or is that bad practice?

    I can't figure out how to inject and use what I inject. Do I inject "@templating" in my services.yml? Also how do I configure the constructor and then call the template in my method?



    private <variable name="">;
    public function __construct(<type hint=""> <variable name="">)
    {

    $this-><variable name=""> = <variable name="">

    }

    public function myServiceMethod()
    {
    // how do I render the template?

    // if I was in the controller I'd use:
    return $this->render('myServiceOutput.html.twig');

    }

  • 2016-04-25 Roy Hochstenbach

    Yeah I thought it had to be something similar to that. Got it working now, thanks :-)

  • 2016-04-24 weaverryan

    Hi Roy!

    Ah, this is SUCH a great error. It's really hard to spot the problem the first time (the error from PHP is not that clear). You DID remember to add the use statement for Cache in MarkdownTransformer (forgetting this is the MOST common mistake). But, you have the wrong class. There are multiple classes from different libraries called cache. You want:


    use Doctrine\Common\Cache\Cache;

    If you look closely, the error says: "Hey, I'm you this ArrayCache object when MarkdownTransformer is instantiated. But, the type-hint on the argument says that you're expecting this other Sensio\Bundle\FrameworkBundle\Configuration\Cache object. That's a problem!".

    Cheers!

  • 2016-04-21 Roy Hochstenbach

    I get the following error at the end: Catchable Fatal Error: Argument 2 passed to AppBundle\Service\MarkdownTransformer::__construct() must be an instance of Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache, instance of Doctrine\Common\Cache\ArrayCache given.

    MarkdownTransformer.php:

    namespace AppBundle\Service;

    use Knp\Bundle\MarkdownBundle\MarkdownParserInterface;
    use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;

    class MarkdownTransformer
    {

    private $markdownParser;

    private $cache;

    public function __construct(MarkdownParserInterface $markdownParser, Cache $cache)
    {
    $this->markdownParser = $markdownParser;
    $this->cache = $cache;
    }

    public function parse($str)
    {
    $cache = $this->cache;
    $key = md5($str);
    if ($cache->contains($key)) {
    return $cache->fetch($key);
    }

    sleep(1);
    $str = $this->markdownParser
    ->transformMarkdown($str);
    $cache->save($key, $str);

    return $str;

    }
    }

    services.yml:

    # Learn more about services, parameters and containers at
    # http://symfony.com/doc/current...
    parameters:
    # parameter_name: value

    services:
    app.markdown_transformer:
    class: AppBundle\Service\MarkdownTransformer
    arguments: ['@markdown.parser','@doctrine_cache.providers.my_markdown_cache']

    When I run /bin/console debug:container doctrine_cache.providers.my_markdown_cache It shows the following line:

    Class Doctrine\Common\Cache\ArrayCache