Buy

Thanks to the 7 bundles installed in our app, we already have a bunch of useful services. In fact, Symfony ships with a killer cache system out of the box! Run:

./bin/console debug:autowiring

Scroll to the top. Ah! Check out CacheItemPoolInterface. Notice it's an alias to cache.app. And, further below, there's another called AdapterInterface that's an alias to that same key.

Understanding Autowiring Types & Aliases

Honestly, this can be confusing at first. Internally, each service has a unique name, or "id", just like routes. The internal id for Symfony's cache service is cache.app. That's not very important yet... except that, if you see two entries that are both aliases to the same service, it means that you can use either type hint to get the exact same object. Yep, both CacheItemPoolInterface and AdapterInterface will cause the exact same object to be passed to you.

So... which one should we use? The docs will recommend one, but it technically does not matter. The only difference is that PhpStorm may auto-complete different methods for you based on the interface or class you choose. So if it doesn't auto-complete the method you're looking for, try the other interface.

Using Symfony's Cache

Let's use the AdapterInterface. Go back to our controller. Here's our next mission: to cache the markdown transformation: there's no reason to do that on every request! At the top of the method, add AdapterInterface $cache:

81 lines src/Controller/ArticleController.php
... lines 1 - 8
use Symfony\Component\Cache\Adapter\AdapterInterface;
... lines 10 - 13
class ArticleController extends AbstractController
{
... lines 16 - 26
public function show($slug, MarkdownInterface $markdown, AdapterInterface $cache)
{
... lines 29 - 66
}
... lines 68 - 79
}

Cool! Let's go use it! Symfony's cache service implements the PHP-standard cache interface, called PSR-6... in case you want Google it and geek-out over the details. But, you probably shouldn't care about this... it just means better interoperability between libraries. So... I guess... yay!

But... there's a downside.... a dark side. The standard is very powerful... but kinda weird to use at first. So, watch closely.

Start with $item = $cache->getItem(). We need to pass this a cache key. Use markdown_ and then md5($articleContent):

81 lines src/Controller/ArticleController.php
... lines 1 - 8
use Symfony\Component\Cache\Adapter\AdapterInterface;
... lines 10 - 13
class ArticleController extends AbstractController
{
... lines 16 - 26
public function show($slug, MarkdownInterface $markdown, AdapterInterface $cache)
{
... lines 29 - 34
$articleContent = <<<EOF
... lines 36 - 51
EOF;
$item = $cache->getItem('markdown_'.md5($articleContent));
... lines 55 - 66
}
... lines 68 - 79
}

Excellent! Different markdown content will have a different key. Now, when we call getItem() this does not actually go and fetch that from the cache. Nope, it just creates a CacheItem object in memory that can help us fetch and save to the cache.

For example, to check if this key is not already cached, use if (!$item->isHit()):

81 lines src/Controller/ArticleController.php
... lines 1 - 8
use Symfony\Component\Cache\Adapter\AdapterInterface;
... lines 10 - 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()) {
... lines 56 - 57
}
... lines 59 - 66
}
... lines 68 - 79
}

Inside we need to put the item into cache. That's a two-step process. Step 1: $item->set() and then the value, which is $markdown->transform($articleContent). Step 2: $cache->save($item):

81 lines src/Controller/ArticleController.php
... lines 1 - 8
use Symfony\Component\Cache\Adapter\AdapterInterface;
... lines 10 - 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);
}
... lines 59 - 66
}
... lines 68 - 79
}

I know, I know - it smells a bit over-engineered... but it's crazy powerful and insanely quick.

Tip

In Symfony 4.1, you will be able to use the Psr\SimpleCache\CacheInterface type-hint to get a "simpler" (but less powerful) cache object.

After all of this, add $articleContent = $item->get() to fetch the value from cache:

81 lines src/Controller/ArticleController.php
... lines 1 - 8
use Symfony\Component\Cache\Adapter\AdapterInterface;
... lines 10 - 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
}

Debugging the Cache

Ok, let's do this! Find your browser and refresh! Check this out: remember that we have a web debug toolbar icon for the cache! I'll click and open that in a new tab.

Hmm. There are a number of things called "pools". Pools are different cache systems and most are used internally by Symfony. The one we're using is called cache.app. And, cool! We had a cache "miss" and two calls: we wrote to the cache and then read from it.

Refresh the page again... and re-open the cache profiler. This time we hit the cache. Yes!

And just to make sure we did our job correctly, go back to the markdown content. Let's emphasize "turkey" with two asterisks:

81 lines src/Controller/ArticleController.php
... lines 1 - 8
use Symfony\Component\Cache\Adapter\AdapterInterface;
... lines 10 - 13
class ArticleController extends AbstractController
{
... lines 16 - 26
public function show($slug, MarkdownInterface $markdown, AdapterInterface $cache)
{
... lines 29 - 34
$articleContent = <<<EOF
... lines 36 - 38
**turkey** shank eu pork belly meatball non cupim.
... lines 40 - 51
EOF;
$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
}

Refresh again! Yes! The change does show up thanks to the new cache key. And this time, in the profiler, we had another miss and write on cache.app.

Check you out! You just learned Symfony's cache service! Add that to your toolkit!

But this leaves some questions: it's great that Symfony gives us a cache service... but where is it saving the cache files? And more importantly, what if I need to change the cache service to save the cache somewhere else, like Redis? That's next!

Leave a comment!

  • 2018-07-16 Victor Bocharsky

    Hey Marc,

    Not at all, apcupsd is APC UPS Daemon, see their website: http://www.apcupsd.org/ - that sounds like something different . Try to google how to install APCu for your PHP version on your OS. I see you use Mac... Unfortunately, brew deprecates https://github.com/Homebrew... repo, that's why now you can install only php with some enabled-out-of-the-box extensions with brew, but to install more extensions that does not come with php by default you need to use pecl. I suppose now you can do something like "pecl install apcu" to install it on your Mac. If it does not work for you, try to google alternative ways.

    Cheers!

  • 2018-07-13 Marc Cavada

    When I uncomment app: cache.adapter.apcu
    - it gives an error
    "(1/1) CacheException
    APCu is not enabled"

    - I install brew apcupsd successfully. Is this what he mean install Apcu Php Extension?
    Please help!!!!

  • 2018-06-27 Victor Bocharsky

    Hey Ahmed,

    Hm, what exactly namespace do you have? Or is it just MarkdownInterface? Then, you need to use its namespace in the beginning of your file. The fully qualified class name should be: \Michelf\MarkdownInterface , so if you use this namespace above you probably won't see this error:


    use Michelf\MarkdownInterface;

    PhpStorm can do it for you if you try to re-type this class, choose the correct one from the list of autocompletion and press enter.

    Cheers!

  • 2018-06-26 Ahmed Chouihi

    Hello!
    I got problem undefined MarkdownInterface , how to fix it?

  • 2018-05-15 Diego Aguiar

    Hey Carlo Mario Chierotti
    I'm glad to hear that you could fix your problem. Cheers!

  • 2018-05-15 Carlo Mario Chierotti

    Sorry, my fault.

    There was an error in the Vagrantfile and the owner of the folder was wrong. now everything works perfectly.

    have a nice day,

    carlo

  • 2018-05-15 Carlo Mario Chierotti

    Hello Ryan,

    I have a problem: I get the error "Failed to save key "markdown_xyz" and the Exception message is "touch(): Utime failed: Operation not permitted"

    I am in a Vagrant Box Ubuntu 16.04 with apache and I tried to chmod 777 the /var/cache folder but with no success.

    thank you for your help

    carlo

  • 2018-04-30 Chmlls

    Yeah, that was it.
    Thanks a lot for your advise.

  • 2018-04-30 weaverryan

    Hey Chmlls!

    Great question :). If you have a JSON string (if you have JSON in PHP, it's always JSON - never an object. You can convert JSON to an object, but then it's an object, not JSON anymore, which is important for your question!).... then you can just store that - strings are exactly what you want to store.

    If you want to cache an array, then yes, you need to convert it to a string, and using serialize() is the right way to do this (just note, if your array also contains objects, you may need to do more work).

    In your case, the error is talking about the cache *key*. It makes it sound like the problem is not how you are trying to cache the *value*, but what you're using as the *key*. So, I would check what you're setting as the cache key - it must be a string, and, as you can see, it has a few rules :).

    Cheers!

  • 2018-04-29 Chmlls

    Thanks for the great tutorials as always.
    How can we cache arrays or json objects?
    I tried to cache an array but I need to convert it to a string, I tried with serialize but I get this error Cache key "" contains reserved characters {}()/\@:

  • 2018-04-17 Victor Bocharsky

    Hey Tony,

    Thanks for reaching us back and sharing this information. Glad you figured it out! And improving docs always is an excellent idea, thanks for caring about it!

    Cheers!

  • 2018-04-17 Tony

    Hi Victor, thanks for your suggestions. I reported it as a bug, and Nicolas Grekas helped me clarify how it needs to be configured. I'll be opening a PR to add some more documentation to the ChainAdapter with Nicolas' explanation and a few examples. Here's the bug report thread for everyone's reference: https://github.com/symfony/...

    Thanks again!

  • 2018-04-17 Victor Bocharsky

    Hey Tony,

    I was wrong, you need to debug not in the constructor of ChainAdapter but in the place before instantiating that class, i.e. like before line 405 of srcDevDebugProjectContainer.php file. Because due to that error you're not get into constructor at all. Well, I'm not sure if it's 100% bug, it needed in more debugging anyway, especially if we're talking about the error you sent in the previous message.

    Cheers!

  • 2018-04-16 Tony

    Hello Victor,

    I've been looking for this for quite a while and I believe it is a bug in the Cache ChainAdapter. I did try adding the @ in the services and I get the same error with or without. I asked for help in the Symfony-dev slack channel and a couple people in there believe is a bug also. If I dump the adapters in the ChainCache class, I get a ApcuAdapter object and a FilesystemAdapter (instead of RedisAdapter), not sure why. If you think this is a config or setup issue, please let me know, otherwise I'll report it as a bug.

    Thank you for your time looking into this.

  • 2018-04-16 Victor Bocharsky

    Hey Tony,

    Woops, sorry, I missed it before but you have to add "@" char before service names, so your config should be like:


    # services.yaml
    services:
    my.cache.pool.chain:
    class: 'Symfony\Component\Cache\Adapter\ChainAdapter'
    arguments:
    - ['@cache.adapter.apcu', '@cache.adapter.redis']

    But probably the error you show is not related to it. Could you make a bit more debugging? Try to dump the 1st argument passed to the ChainAdapter class, for example, open ChainAdapter.php file and add the next line to the constructor:


    public function __construct(array $adapters, int $maxLifetime = 0)
    {
    var_dump($adapters); die;
    // ...
    }

    And try again. What do you see in the output?

    Cheers!

  • 2018-04-16 Tony

    This is what I'm getting with the setup above.

    Type error: Argument 1 passed to Symfony\Component\Cache\Adapter\ChainAdapter::__construct() must be of the type array, string given, called in /var/www/www.banging.io/var/cache/dev/ContainerZmpvOhh/srcDevDebugProjectContainer.php on line 405


    There's not a lot of info in the docs or online on how to properly configure a CacheChain. I've been looking into this for a couple days, so if we can figure this one out I'll definitely be making a PR to the symfony docs.
    Thanks!

  • 2018-04-16 Victor Bocharsky

    Hey Tony,

    Your setup looks valid according to the docs: https://symfony.com/doc/cur... . You said you keep getting errors, what errors exactly? It would be easy to help you seeing those errors ;)

    Cheers!

  • 2018-04-13 Tony

    Hello knpu. Thanks for the great tutorials. I can't find any docs on how to properly set up a cache chain adapter with config in SF4. I want to have apcu then redis. I'm attempting to do it like so but keep getting errors:

    # services.yaml
    my.cache.pool.chain:
    class: 'Symfony\Component\Cache\Adapter\ChainAdapter'
    arguments:
    - ['cache.adapter.apcu', 'cache.adapter.redis']
    # framework.yaml
    cache:
    app: my.cache.pool.chain


    Any help would be greatly appreciated.

  • 2018-02-19 Victor Bocharsky

    Hey AdFlorin,

    I agree, Doctrine is very popular in Symfony applications if we're talking about database, but sometimes database is not a requirement for an application. Anyway, we do not cover database and Doctrine in particular in this course on purpose - we're going to talk about this complex stuff in the next course, thanks for your patience!

    Cheers!

  • 2018-02-19 Victor Bocharsky

    Hey AdFlorin,

    We're going to talk about Doctrine in the next course on purpose and that course will be all about Doctrine. But for now I can give you a quick example:


    public function show($slug, MarkdownInterface $markdown, AdapterInterface $cache)
    {
    $em = $this->getDoctrine()->getManager();
    $article = $em->getRepository(Article::class)->findOneBySlug($slug);
    $articleContent = $article->getContent();
    }

    So as you can see it's easy, you need to fetch a proper Article entity from the repository, in our case we need to use $slug to do so. Of course, you need to have Article entity and it should have "slug" and "content" fields at least. If you used to work with Doctrine - I think you'll nail it. If no, please, wait for the Doctrine course for the Symfony 4 track or check out our https://knpuniversity.com/s... from Symfony 3 track to get more information how to work with database using Doctrine.

    Cheers!

  • 2018-02-19 AdFlorin

    you should have examples on how to do that with doctrine ... because no one will use symfony without doctrine ...

  • 2018-02-19 AdFlorin

    how do we transform fields from entities ( markdown && cache )
    i'm using doctrine to fetch the "$articleContent" from mysql
    how do i tranform only the "content" field ?

  • 2018-01-31 Jérôme 

    Wow, I wasn't expecting this additional information. Thanks! I'll take a look at those two links, they may be useful. I'm not sure I'll use this for the project I'm currently working on, but maybe I'll use it in a future update. Thanks again! :)

  • 2018-01-31 Victor Bocharsky

    Hey Jerome,

    Yeah, don't hesitate to ask questions later when you have questions. Btw, you can totally use this cache component on Symfony 3, I mean you don't need to upgrade to Symfony 4 first to start using it ;) Unfortunately, we do not have a separate screencast about caching, but these docs may be useful for you:
    https://symfony.com/doc/cur...

    P.S. There's another type of caching which called HTTP cache: https://symfony.com/doc/cur... - it allows you to cache the whole page instead of a separate things, but this one is not so great for pages where you have user-related dynamic data, like pages where you show users avatar and name in the header when they are logged in, because due to it you can't cache the whole page (well, you can, but you probably don't want to show all users the same cached page with someones avatar and name on it :) ). Though Edge Side Includes (ESI) could help to solve this problem.

    Cheers!

  • 2018-01-31 Jérôme 

    Hi Victor,

    Thanks for answering my question. It's very kind of you. Now that I read your comment, I think I'll need to practice more, but only on side projects first. SF4 is great, and I'll probably need some time to adapt, because I'm still using SF3 for my current projects.

    I'll contact you again if I have more questions about the course in the future. Have a good day!

  • 2018-01-31 Victor Bocharsky

    Hi Jérôme,

    Well, cache always brings more complexity in your code, so if your website is simple and your server resources are enough on highest traffic and you're happy with page's response time - I'd say you can ignore caching :) But in case you really need it, or, maybe you just want to play with Symfony cache system, let's find out what is cache.

    Yes, you're right, in short cache allows your app return response faster to your users, but it depends on implementation. Rationally to use cache when you want to avoid some time-consuming processes, for example, like in this screencast, when we convert markdown into HTML - no reasons to do this operation again and again for each user who want to see this page, we can convert it only once, then cache the result and use already converted HTML for all further users, i.e. we avoid resource-consuming operation from N time to only 1 and save some response time. Or, for example, you have some queries to the DB that are really slow so you can cache the result for some time to reduce server load. But in some cases you should think about cache invalidation and cache expiration time, you probably won't cache result of DB queries forever because results may cache with time, so sometimes make sense to invalidate the cache manually on some event, like adding new post, etc. or for example set expiration time, so after some time like 1 hour, 1 day, etc. you will do same operations again to get a new cache result for another 1 hour, 1 day, etc. And of course, sometimes it means that some users will see a bit outdated *cached* information. If it's not a bit deal for you - go for it! But if it is, then you should avoid caching those things that are changed with time or manually invalidate cache on some events to recalculate data again.

    So, it depends on your website implementation, but I think you know it best and can find some bottle necks where you use many resources, or repeat the same operations/calculations for each user which give you the same result.

    Hope this helps you ;)

    Cheers!

  • 2018-01-30 Jérôme 

    Hello,

    Thanks for the great tutorial. Unfortunately, I'm not very familiar with the cache. I'm currently working on a website for a sport association which allows the admin to post news and create "events" (posts with title, starting & ending date, starting & ending time, and description). I'm wondering if I should use this cache service anywhere. I think I understand what it does: it allows the app to load faster (I guess...), but could you give me a better explanation please?

    Thanks!