Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

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

Now when we refresh the browse page, the mixes are coming from a repository on GitHub! We make an HTTP request to the GitHub API, that fetches this file right here, we call $response->toArray() to decode that JSON into a $mixes array... and then we render that in the template. Yup, this file on GitHub is our temporary fake database!

One practical problem is that every single page load is now making an HTTP request... and HTTP requests are slow. If we deployed this to production, our site would be so popular, of course, that we'd pretty quickly hit our GitHub API limit. And then this page would explode.

So... I'm thinking: what if we cache the result? We could make this HTTP request, then cache the data for 10 minutes, or even an hour. That just might work! How do we cache things in Symfony? You guessed it: with a service! Which service? I dunno! So let's go find out.

Finding the Cache Service

Run:

php bin/console debug:autowiring cache

to search for services with "cache" in their name. And... yes! There are, in fact, several! There's one called CacheItemPoolInterface, and another called StoreInterface. Some of these aren't exactly what we're looking for, but CacheItemPoolInterface, CacheInterface, and TagAwareCacheInterface are all different services that you can use for caching. They all effectively do the same thing... but the easiest to use is CacheInterface.

So let's grab that.... by doing our fancy autowiring trick! Add another argument to our method typed with CacheInterface (make sure you get the one from Symfony\Contracts\Cache) and call it, how about, $cache:

... lines 1 - 7
use Symfony\Contracts\Cache\CacheInterface;
... lines 9 - 11
class VinylController extends AbstractController
{
... lines 14 - 32
public function browse(HttpClientInterface $httpClient, CacheInterface $cache, string $slug = null): Response
{
... lines 35 - 46
}
}

To use the $cache service, copy these two lines from before, delete them, and replace them with $mixes = $cache->get(), as if you're going to fetch some key out of the cache. We can invent whatever cache key we want: how about mixes_data.

Symfony's cache object works in a unique way. We call $cache->get() and pass it this key. If that result already exists in the cache, it will be returned immediately. If it does not exist in the cache yet, then it will call our second argument, which is a function. In here, our job is to return the data that should be cached. Paste in the two lines of code that we copied earlier. This $httpClient is undefined, so we need to add use ($httpClient) to bring it into scope.

There we go! And instead of setting the $mixes variable, just return this $response->toArray() line:

... lines 1 - 11
class VinylController extends AbstractController
{
... lines 14 - 32
public function browse(HttpClientInterface $httpClient, CacheInterface $cache, string $slug = null): Response
{
... lines 35 - 36
$mixes = $cache->get('mixes_data', function() use ($httpClient) {
$response = $httpClient->request('GET', 'https://raw.githubusercontent.com/SymfonyCasts/vinyl-mixes/main/mixes.json');
return $response->toArray();
});
... lines 42 - 46
}
}

If you haven't used Symfony's caching service before, this might look strange! But I love it! The first time we refresh the page, there won't be any mixes_data in the cache yet. So it will call our function, return the result, and then the cache system will store that in the cache. The next time we refresh the page, the key will be in the cache, and it will return the result immediately. So we don't need any "if" statements to see if something is already in the cache... just this!

Debugging with the Cache Profiler

But... will it blend? Let's go find out. Refresh and... beautiful! The first refresh still made the HTTP request like normal. Down on the web debug toolbar, we can see that there were three cache calls and one cache write. Open this in a new tab to jump into the cache section of the profiler.

So cool: this shows us that there was one call to the cache for mixes_data, one cache write, and one cache miss. A cache "miss" means that it called our function and wrote that to the cache.

On the next refresh, watch this icon here. It disappears! That's because there was no HTTP request. If you open the Cache profiler again, this time there was one read and one hit. That hit means that the result was loaded from the cache and it did not make an HTTP request. That's exactly what we wanted!

Setting the Cache Lifetime

Now, you might be wondering: how long will this info stay in the cache? Right now... forever. Ooooh. That's the default.

To make it expire sooner than forever, give the function a CacheItemInterface argument - make sure to hit "tab" to add that use statement - and call it $cacheItem. Now we can say $cacheItem->expiresAfter() and, to make it easy, say 5:

... lines 1 - 4
use Psr\Cache\CacheItemInterface;
... lines 6 - 12
class VinylController extends AbstractController
{
... lines 15 - 33
public function browse(HttpClientInterface $httpClient, CacheInterface $cache, string $slug = null): Response
{
... lines 36 - 37
$mixes = $cache->get('mixes_data', function(CacheItemInterface $cacheItem) use ($httpClient) {
$cacheItem->expiresAfter(5);
... lines 40 - 42
});
... lines 44 - 48
}
}

The item will expire after 5 seconds.

Clearing the Cache

Unfortunately, if we try this, the item that's already in the cache is set to never expire. So... this won't actually work until we clear the cache. But... where is the cache being stored? Another great question! We'll talk more about that in a second... but, by default, it's stored in var/cache/dev/... along with a bunch of other cache files that help Symfony do its job.

We could delete this directory manually to clear the cache... but Symfony has a better way! It is, of course, another bin/console command.

Symfony has a bunch of different "categories" of cache called "cache pools". If you run:

php bin/console cache:pool:list

you'll see all of them. Most of these are meant for Symfony to use internally. The cache pool that we're using is called cache.app. To clear that, run:

php bin/console cache:pool:clear cache.app

Thats it! This isn't something you'll need to do very often, but it's good to know, just in case.

Okay, check this out. When we refresh... we get a cache miss and you can see that it did make an HTTP call. But if we refresh again really quickly... it's gone! Refresh again and... it's back! That's because the five seconds just expired.

Ok team: we're now leveraging an HTTP client service and cache service... both of which were prepared for us by one of our bundles so that we can just... use them!

But, I do have a question. What if we need to control these services? For example, how could we tell the cache service that, instead of saving things onto the filesystem in this directory, we want to store things in Redis... or memcache? Let's explore the idea of controlling our services through configuration next.

Leave a comment!

9
Login or Register to join the conversation
Fedale Avatar

In Symfony 6.1 new class' name is Symfony\Contracts\Cache\ItemInterface so you have to change callback like this:
function(ItemInterface $cacheItem) {}

Cheers
Danilo

1 Reply

Hey @Fedale!

Thanks for the note! In truth, both work just fine in any Symfony version, though I probably should have used ItemInterface. ItemInterface extends the CacheItemInterface that we use in this video, so both work. However, if you use ItemInterface , then you have addition ->tag() and ->getMetadata() methods to give you more flexibility.

Cheers!

Reply
Vlame Avatar

Does the php bin/console cache:pool:clear cache.app command the same as the php bin/console cache:clear command?

Reply

Hey @Vlame!

Excellent question! These are actually 2 different things for the 2 different types of cache in Symfony

A) cache:pool:clear is used to clear the "cache" that we were talking about in this tutorial. This is the cache where you can store items, including things for your application but also the core stores some things in cache pools. For example, Doctrine caches the DQL -> SQL generation in a cache pool. In short, when you think about a "caching system", this command is used to clear those.

B) cache:clear has a different purpose. When Symfony loads, internally, it caches a bunch of stuff - most importantly it caches your routes after parsing them and it caches all of your services. This is all stored in the var/cache/dev directory (or var/cache/prod in the prod environment) and it's not something you typically need to think about. If a routing file changes, Symfony automatically rebuilds this cache. These are fundamental cache file that Symfony needs to even function.

You'll run cache:clear each time you deploy, because you DO want to rebuild these cache files (these cache files are 100% built from your code - so if your code changes, you should rebuild the cache). But you'll rarely run cache:pool:clear as these are typically things that you want to keep in cache until they expire.

Let me know if that helps!

Cheers!

Reply
Vlame Avatar

Hello @weaverryan,

Thanks for your answer! It is clear for me what the differences are. Thanks a lot!

Reply
Default user avatar
Default user avatar Влада Петковић | posted 5 months ago

Would there be a possibility to process some new examples of creating services and bundles in future lessons? For God's sake, since version Symfony 3 you have been demonstrating examples with the Cache and MarkDown helper - service.Are there any other examples besides those?

Reply

Hey Vlada,

We have a separate course about creating a Symfony bundle, I think you might be interested in it, you can check it out here: https://symfonycasts.com/sc...

Hm, the idea of this course is not about learning how write an implementation of a specific service, but how to write service in general. We cover everything you need to be able to create *any* service and inject any dependencies into them. But I see your point, we will try to find a different service example in future tutorials just to diversify out tutorials for those who are watching past courses as well. Thank you for you feedback! Also, if you have any ideas what specific services would be great to cover in your opinion - please, share it!

Cheers!

1 Reply
Default user avatar
Default user avatar Влада Петковић | victor | posted 5 months ago

I respect your work and the effort you put into creating new lessons, but I believe that there must be something new because sticking to old lessons and changing only the version of the Symphony does not contribute to the quality of learning and mastering the material. If the Symphony team claims that the Symphony is so powerful (I honestly believe that this is true) then demonstrate that strength and power through various examples and not through just one example. I'm not a lecturer, nor do I consider myself an excessively experienced programmer, but if you're already asking me to suggest specific examples for bundles and services, then you could, for example, in with your lessons, you show how to make a specific visual web control that could be used as a package in various web applications. For example, a DataGrid control with the implementation of data pagination, filtering and sorting data or DataView control as they exist in Yii2 framework. The KnpPaginatorBundle package almost always has some bugs and does not work in new versions of Symfony. The EasyAdmin bundle is too crowded with functions for simple display of data in a DataGrid with elementary possibilities that these controls should offer (pagination, sorting and filtering and nothing more ... ). An even better example would be the implementation of a separate layer of the database with business rules (n-tier web application) as a separate service.

Greeting.

Reply

Hey @Влада Петковић

Yea, sorry for the repetition. These "basic" courses aren't really meant to be different across Symfony versions... we just give them an "update" do keep the content current. If you're watching them across Symfony versions, you're definitely going to hear the same things over and over again :). What I need to do (and it's my goal for the summer) is to quickly finish these basic course updates so we can get into bigger and better things.

> For example, a DataGrid control with the implementation of data pagination, filtering and sorting data

That's a cool idea... and I've been liking the idea more lately of having some shorter tutorials that just solve some interesting problem vs teach a concept in Symfony (we will still have those, but just "coding up a feature" I think is an interesting idea).

Anyways, thank for the idea and patience - I realize that these "basic" Symfony tutorials are not much fun for seasoned Symfony devs :).

Cheers!

2 Reply
Cat in space

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

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "knplabs/knp-time-bundle": "^1.18", // v1.19.0
        "symfony/asset": "6.1.*", // v6.1.0-RC1
        "symfony/console": "6.1.*", // v6.1.0-RC1
        "symfony/dotenv": "6.1.*", // v6.1.0-RC1
        "symfony/flex": "^2", // v2.1.8
        "symfony/framework-bundle": "6.1.*", // v6.1.0-RC1
        "symfony/http-client": "6.1.*", // v6.1.0-RC1
        "symfony/monolog-bundle": "^3.0", // v3.8.0
        "symfony/runtime": "6.1.*", // v6.1.0-RC1
        "symfony/twig-bundle": "6.1.*", // v6.1.0-RC1
        "symfony/ux-turbo": "^2.0", // v2.1.1
        "symfony/webpack-encore-bundle": "^1.13", // v1.14.1
        "symfony/yaml": "6.1.*", // v6.1.0-RC1
        "twig/extra-bundle": "^2.12|^3.0", // v3.4.0
        "twig/twig": "^2.12|^3.0" // v3.4.0
    },
    "require-dev": {
        "symfony/debug-bundle": "6.1.*", // v6.1.0-RC1
        "symfony/maker-bundle": "^1.41", // v1.42.0
        "symfony/stopwatch": "6.1.*", // v6.1.0-RC1
        "symfony/web-profiler-bundle": "6.1.*" // v6.1.0-RC1
    }
}