Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

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!

28
Login or Register to join the conversation
Dirk Avatar

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

2 Reply

Hey Dirk

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!

Reply
Dirk Avatar

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!

Reply

Hey Dirk!

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!

4 Reply
Farshad Avatar
Farshad Avatar Farshad | posted 1 year ago

At 2:40 he added 'use' after the function. I dont understand that. Is it maybe explained in the PHP beginner course?

1 Reply

Hey Farry7,

That use keyword is used for telling PHP compiler that we want to have access to any variables we declare on it on our anonymous function. I believe we do not talk about it in the PHP beginner course but you can read a bit more about it here https://stackoverflow.com/a...

I hope it helps. Cheers!

Reply
Cheshire Y. Avatar
Cheshire Y. Avatar Cheshire Y. | posted 9 months ago

Hi and thanks for the great tutorial! Minor question.. I downloaded the code and am running it on PHP 7.4. I'm seeing this in the cache folder:
```
$ ls /var/cache/
cups/ libvirt/ private/
```
A little different from what you see in the tutorial. Where can I learn more about what folders I'll see here by default and why it is different from your version in the tutorial? Thank you.

Reply

Hey Cheshire Y.

Symfony stores a few files inside the var/cache/{env} directory after the bootrstrap process. I'm not sure if there are docs about each folder but you can learn more abouth caching on Symfony here https://symfony.com/doc/cur...
Oh, also some bundles can cache stuff and may use their own directory (inside the cache folder), so, it's normal to see other folders in there.

Cheers!

1 Reply
Jakub Avatar

After this chapter i have one question... when should i really use cache?

Reply

Hey Jakub!

Very fair question :). The downside of using cache is... just added code and complexity... so you don't want to run around and add it everywhere. In most cases, you want to identify that some "code path" is truly a performance problem and *then* add caching. My favorite tool to do this is Blackfire: I can run this on production, identify what parts of my code are *actually* slow, and then consider adding caching to those.

More generally speaking, the places where we tend to use cache relate to complex (or frequent) database calls, network requests (like if we make an API request to a 3rd party serve in order to load a page) and certain filesystem operations. But again, ideally you let a tool like Blackfire guide you to find these things :).

Cheers!

Reply
Nirav R. Avatar
Nirav R. Avatar Nirav R. | posted 1 year ago | edited

there

PHP: Version 8
Symfony: Version 5.2 with API platform

I am trying to store Doctrine entity object to cache.

When next time I retrieve same object from cache, doctrine consider that object as new object and try to persist it.

Can u help me to solve this issue ?

Thanks

Reply

Hey Nirav R.

I'm afraid you cannot do that, Doctrine needs to instantiate objects for you, so it doesn't get confused about if it's a new or old object. What you can do is to store the object's id on the database, and then, query for it

Cheers!

Reply
Nirav R. Avatar
Nirav R. Avatar Nirav R. | MolloKhan | posted 1 year ago | edited

MolloKhan

I am adding object to cache bcz. I not want to execute that query again.

Reply

In that case, you can get the UnitOfWork out of the EntityManager and add the object manually to the identityMap
$entityManager->getUnitOfWork()->addToIdentityMap($object);

Cheers!

Reply
Nirav R. Avatar

I tried that but it gives error like...


{
"type": "https://tools.ietf.org/html/rfc2616#section-10",
"title": "An error occurred",
"detail": "Warning: Undefined array key \"000000005fdb2782000000003494e2a0\"",
"trace": [
{
"namespace": "",
"short_class": "",
"class": "",
"type": "",
"function": "",
"file": "/app/vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php",
"line": 1544,
"args": []
},

Reply

Could someone explain this syntax to me? I don't understand what this bit does...

'markdown_'.md5($questionText),

and the whole line seems a bit hinky, to me. Someone's already asked about the use statement, so I'm fine with that.

$parsedQuestionText = $cache->get('markdown_'.md5($questionText), function() use ($questionText, $markdownParser) {
return $markdownParser->transformMarkdown($questionText);

Reply

Hey somecallmetim27,

I'd recommend you to rewatch this video again. The md5() is a core PHP function, that generates a specific hash, i.e. an exactly 32-length string with some random chars. But every time you generate hash of the same string - it will give you the exact same hash. You may want to read the docs about this function here: https://www.php.net/manual/...

So in short, we create a cache key with that "'markdown_'.md5($questionText)" and then if the Symfony Cache component has a value for this key and that value isn't expired - it just returns the value. If there're no value or that value is expired - Cache component will generate a new value calling that anonymous function passed as the 2nd argument to that get() method. That anonymous function main job is to generate the proper value for the given key and return it, then Cache component will write it into cache and return the cached value on the next call. Please, also read the docs about PHP anonymous functions here: https://www.php.net/manual/... - it will explain how it works and how to pass some extra arguments in it - we do this via "use ()" as you can already guess :)

I hope this helps!

Cheers!

Reply
Przemysław S. Avatar
Przemysław S. Avatar Przemysław S. | posted 2 years ago

Hey Ryan!
I was wondering how to get an individual cache key if we want to fetch all data (like $repository->findAll()) from the database? Do I have to make the database query before and get all the needed data then make the md5($result)? You used hard coded content here, what about getting a new key when the content of the fetched data changed?

I hope you get this, Cheerz!

Reply

Hey Przemysław S.

That's a good question. Invalidating cache is one of the hardest thing to do in programming because is difficult to know when to refresh the content. The easiest way is just to add an expire date to the cache item
Another approach is to specifically invalidate a cache item on demand. For example, whenever you add a new record, then you refresh the cache
And the last approach I can think of is to just count the records and use it as part of the cache key. For example, you can name a key as "all_users_1536", the 1536 part is the total number of users in your table. The downside is that you'll do a count query all the time

If you get a better idea, let me know! Cheers!

Reply
Przemysław S. Avatar
Przemysław S. Avatar Przemysław S. | MolloKhan | posted 2 years ago

Thank you for responding!
I found a solution, just invalidating the tagged cache key by using TagAwareCacheInterface after adding/ editing an article. Not sure if this is the best solution, just found this working for me.
Cheers!

Reply

Hey Przemysław S.

Nice, that's a good solution too. BTW, did you see the latest news about caching? https://symfony.com/blog/ne...

Reply
Shahadat H. Avatar
Shahadat H. Avatar Shahadat H. | posted 2 years ago

Hi Ryan,
when I implements the CacheInterface It removes my questionText.

my code is:-
[
public function show($slug, MarkdownParserInterface $markdownParser, CacheInterface $cache)
{
$answers = [
'Make sure your cat is sitting `purrrfectly` still 🤣',
'Honestly, I like furry shoes better than MY cat',
'Maybe... try saying the spell backwards?',
];

$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.";
$parsedQuestionText = $cache->get('markdown_'.md5($questionText), function () use ($markdownParser, $questionText) {
return $markdownParser->transformMarkdown($questionText);
});

return $this->render('question/show.html.twig', [
'question' => ucwords(str_replace('-', ' ', $slug)),
'answers' => $answers,
'questionText' => $parsedQuestionText,
]);
}

]

I cant understand why this is happening.

Reply

My PHP version is 7.4.4 and it didn't work. For me that was resolved by upgrading to 8.0.5 but now I see 139 deprecation alerts in toolbar..

Reply

Hey Andrii,

Difficult to say what was wrong on 7.4.4 for you without seeing the actual error, but yeah, this project should work on 8.0 too. Well, I believe most deprecations are not PHP 8 related but Symfony version related. And since it's a learning project, you can completely ignore them, they are not too important for learning purposes. But if you're talking about your personal project - yeah, it would be a good idea to start fixing those deprecations before your next major Symfony upgrade.

Cheers!

Reply
Tim C. Avatar

I'm having the same issue running PHP 7.3.22. What version did you upgrade to?

Reply

Hmm, your code looks fine to me, the only thing I can think about is that you may have imported the wrong CacheInterface. Double check that you imported this one Symfony\Contracts\Cache\CacheInterface;

Reply
Joao P. Avatar
Joao P. Avatar Joao P. | posted 2 years ago

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

Reply

Hey Joao P.

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!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial also works great for Symfony 6!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.3.0 || ^8.0.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.8", // 1.9.0
        "sensio/framework-extra-bundle": "^6.0", // v6.2.1
        "sentry/sentry-symfony": "^4.0", // 4.0.3
        "symfony/asset": "5.0.*", // v5.0.11
        "symfony/console": "5.0.*", // v5.0.11
        "symfony/debug-bundle": "5.0.*", // v5.0.11
        "symfony/dotenv": "5.0.*", // v5.0.11
        "symfony/flex": "^1.3.1", // v1.17.5
        "symfony/framework-bundle": "5.0.*", // v5.0.11
        "symfony/monolog-bundle": "^3.0", // v3.6.0
        "symfony/profiler-pack": "*", // v1.0.5
        "symfony/routing": "5.1.*", // v5.1.11
        "symfony/twig-pack": "^1.0", // v1.0.1
        "symfony/var-dumper": "5.0.*", // v5.0.11
        "symfony/webpack-encore-bundle": "^1.7", // v1.8.0
        "symfony/yaml": "5.0.*" // v5.0.11
    },
    "require-dev": {
        "symfony/maker-bundle": "^1.15", // v1.23.0
        "symfony/profiler-pack": "^1.0" // v1.0.5
    }
}