Cache Service

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

Parsing markdown on every request is going to make our app unnecessarily slow. So... let's cache that! Of course, caching something is "work"... and as I keep saying, all "work" in Symfony is done by a service.

Finding the Cache Service

So let's use our trusty debug:autowiring command to see if there are any services that include the word "cache". And yes, you can also just Google this and read the docs: we're learning how to do things the hard way to make you dangerous:

php bin/console debug:autowiring cache

And... cool! There is already a caching system in our app! Apparently, there are several services to choose from. But, as we talked about earlier, the blue text is the "id" of the service. So 3 of these type-hints are different ways to get the same service object, one of these is actually a logger, not a cache and the last one - TagAwareCacheInterface - is a different cache object: a more powerful one if you want to do something called "tag-based invalidation". If you don't know what I'm talking about, don't worry.

For us, we'll use the normal cache service... and the CacheInterface is my favorite type-hint because its methods are the easiest to work with.

Using the Cache Service

Head back to the controller and add another argument: CacheInterface - the one from Symfony\Contracts - and call it $cache:

... lines 1 - 8
use Symfony\Contracts\Cache\CacheInterface;
... lines 10 - 11
class QuestionController extends AbstractController
{
... lines 14 - 31
public function show($slug, MarkdownParserInterface $markdownParser, CacheInterface $cache)
{
... lines 34 - 49
}
}

This object makes caching fun. Here's how it works: say $parsedQuestionText = $cache->get(). The first argument is a unique cache key. Let's pass markdown_ and then an md5() of $questionText. This will give every unique markdown text its own unique key.

Now, you might be thinking:

Hey Ryan! Don't you need to first check to see if this key is in the cache already? Something like if ($cache->has())?

Yes... but no. This object works a bit different: the get() function has a second argument, a callback function. Here's the idea: if this key is already in the cache, the get() method will return the value immediately. But if it's not - that's a cache "miss" - then it will call our function, we will return the parsed HTML, and it will store that in the cache.

Copy the markdown-transforming code, paste it inside the callback and return. Hmm, we have two undefined variables because we need to get them into the function's scope. Do that by adding use ($questionText, $markdownParser):

... lines 1 - 11
class QuestionController extends AbstractController
{
... lines 14 - 31
public function show($slug, MarkdownParserInterface $markdownParser, CacheInterface $cache)
{
... lines 34 - 40
$parsedQuestionText = $cache->get('markdown_'.md5($questionText), function() use ($questionText, $markdownParser) {
return $markdownParser->transformMarkdown($questionText);
});
... lines 44 - 49
}
}

It's happy! I'm happy! Let's try it! Move over and refresh. Ok... it didn't break. Did it cache? Down on the web debug toolbar, for the first time, the cache icon - these 3 little boxes - shows a "1" next to it. It says: cache hits 0, cache writes 1. Right click that and open the profiler in a new tab.

Cool! Under cache.app - that's the "id" of the cache service - it shows one get() call to some markdown_ key. It was a cache "miss" because it didn't already exist in the cache. Close this then refresh again. This time on the web debug toolbar... yea! We have 1 cache hit! It's alive!

Where is the Cache Stored?

Oh, and if you're wondering where the cache is being stored, the answer is: on the filesystem - in a var/cache/dev/pools/ directory. We'll to talk more about that in a little while.

In the controller, make a tweak to our question - how about some asterisks around "thoughts":

... lines 1 - 11
class QuestionController extends AbstractController
{
... lines 14 - 31
public function show($slug, MarkdownParserInterface $markdownParser, CacheInterface $cache)
{
... lines 34 - 38
$questionText = 'I\'ve been turned into a cat, any *thoughts* on how to turn back? While I\'m **adorable**, I don\'t really care for cat food.';
... lines 40 - 49
}
}

If we refresh now and check the toolbar... yea! The key changed, it was a cache "miss" and the new markdown was rendered.

So the cache system is working and it's storing things inside a var/cache/dev/pools/ directory. But... that leaves me with a question. Having these "tools" - these services - automatically available is awesome. We're getting a lot of work done quickly.

But because something else is instantiating these objects, we don't really have any control over them. Like, what if, instead of caching on the filesystem, I wanted to cache in Redis or APCu? How can we do that? More generally, how can we control the behavior of services that are given to us by bundles.

That is what we're going to discover next.

Leave a comment!

  • 2020-07-13 Dirk J. Faber

    Hi Ryan,
    Please excuse my late reply, I had some other things on my mind. I really appreciate you taking the time to give such an elaborate explanation. Thank you, it really helps!
    Cheers!

  • 2020-07-09 Diego Aguiar

    Hey Joao Andrade

    When you type for an interface Symfony will give you an instance of a class that implements such interface. It works that way by default taking into account that you only have one implementation of the interface. If you have more than one, then you'll have to choose which instance you want through configuration

    Cheers!

  • 2020-07-09 Joao Andrade

    Hi, you've hinted (autowiring) the markdown interface, but didn't it need to be a class to actually have methods? Thanks

  • 2020-06-23 weaverryan

    Hey Dirk J. Faber!

    I hear yea - that lower-level cache is very powerful, but mystical ;). I can say a few things:

    A) That lower-level cache is the framework caching anything and everything it can that will NEVER need to change after deploy. This is, in reality, a lot of stuff: all of the YAML files (services, bundle config, routing, etc), Doctrine annotations, validation metadata, Twig templates: these are all "code" that you can read once, cache into the fastest-possible format, and re-use forever until the next deploy. In the dev environment, Symfony intelligently "notices" when you've changed one of these files and automatically rebuilds that cache. But you're 100% correct that in the prod environment (for speed), once that cache is set, it uses it forever (until you cache:clear, usually on your next deploy).

    B) The best answer to when to use extra caching is probably best answered by profiling once your live - e.g with https://symfonycasts.com/sc...

    But another way to answer that is this: the framework is *massively* optimized for everything that it does. What's going to slow down your app is *your* code. So, you really want to be focusing on the stuff that *you* do that's slow. Maybe you're making an external HTTP request on a popular page - that will slow down that page big time. Or maybe you're loading 500 records from the database on a page - that would also slow things down. And then, once you've identified something that you think could be cached (again, Blackfire is the best tool for this because it will tell you for certain when you have a problem, instead of trying to guess where a problem might be), the next challenge is *doing* that cache. If what you're caching is highly dynamic and would need to be invalidated frequently, then it makes it harder to cache that correctly from a "complexity" standpoint. Sometimes we won't cache something that *could* be cached (but isn't that painful) because the logic to correctly invalidate it when it needs to be invalidated would be super ugly :).

    Let me know if that helps! Each component in Symfony handles storing its own stuff into that "low level" cache, which is why I can't point you straight to one spot to see it all. The two most important caches, however, are probably the container and routing cache, but there are many more.

    Cheers!

  • 2020-06-22 Dirk J. Faber

    Hi Ryan,

    Thanks for your explanation. The way the cache service (or component) as described in B work is understandable. The 'invisible'/default cache is harder to understand. I cannot find documentation about it and I have some doubts about how it *really* works. Even though I could probably ignore it, knowing what it does makes it easier to decide in which situation you would need the other cache service (B) or just stick to the default caching. For example, how does the default caching behave in prod mode? It seems to me that the cached files are not only depending on the config/YAML files (and changes therein). When changing a template for example, in prod, you will not see the changes unless you clear the cache. Ultimately, I am trying to understand when to use the "extra" caching service/component and when to simply rely on the default caching.

    I hope my explanation was clear. It can be confusing when using the same term for two different thing ;)

    Cheers!

  • 2020-06-18 weaverryan

    Hey Dirk J. Faber

    REALLY excellent question actually - it can be a bit confusing. Here's what's going on:

    A) When you first load Symfony (let's assume that the var/cache directory is empty), Symfony reads a lot of YAML files (for services and routing, for example) and does processing on those. It then writes some cache files to var/cache so that it doesn't need to do that on every request. This type of cache is (more or less) not something you can even disable or that you need to think about (except that you need to clear it when you are deploying). In dev mode, on each page refresh, Symfony checks the last modified times of all those YAML files (or XML, the format doesn't matter) and if anything has changed, it automatically rebuilds all that stuff in the var/cache directory. This type of cache is really meant to be invisible.

    B) Completely independent of all of that, Symfony provides a cache service, which YOU can use to store anything you want. By default, this *also* stores in var/cache (in a pools sub-directory) but that cache is really a different system. If you re-configured the cache.app service to store in apcu (like we do), you will still see that var/cache is full all the stuff from the first system (A) above (but not cache_pools).

    So... that's the idea - 2 totally different systems and you can almost pretend like (A) doesn't exist ;). 2 more things I'll say:

    1) If you run cache:clear, it will NOT clear the cache from the "cache service" - you would use cache:pool:clear for that - you could (for example) clear the "cache.app" pool.

    2) There is actually another cache service in the container called cache.system. This is part of the caching system I describe in (B). But, it's actually used internally to store the results of YAML or annotation parsing. There are just a *few* things that can't be cached when you first load your page - and which must be loaded (and cached) at runtime. The cache.system service handles this (e.g validation rules).

    Let me know if this helps!

    Cheers!

  • 2020-06-17 Dirk J. Faber

    What exactly is the difference between using this caching service and the default caching that happens? (The cache you clear with php bin/console cache:clear).