gstreamer0.10-ffmpeg
gstreamer0.10-plugins-good
packages.
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.
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.
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!
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.
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!
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!
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!
At 2:40 he added 'use' after the function. I dont understand that. Is it maybe explained in the PHP beginner course?
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/10304027/2088447
I hope it helps. Cheers!
Hi, after "If we refresh now and check the toolbar... yea! The key changed, it was a cache "miss" and the new markdown was rendered i went back removing the asterisks around "thoughts" and refresh: i expected to hint the previous cached item and see in the profiler:
Cache Calls 5
Total time 3.02 ms
Cache hits 5 / 5 (100%)
Cache writes 0
but any time I switch back and forth (with or without asterisks) i get:
Cache Calls 105
Total time 200.83 ms
Cache hits 99 / 99 (100%)
Cache writes 6
Is that correct? Thank you for your attention
Note: using Symfony 6.3
Hey @Claudio-B,
If it's already in the cache - you will need to clear the cache, otherwise it will constantly hit it.
Cheers!
Hi Victor, the problem is the opposite: it seems not hitting the previous cached element.
Changing the text, put / remove asterisks, both are cached already and i would expect
Cache Calls 5
Total time xxx ms
Cache hits 5 / 5 (100%)
Cache writes 0
as i get if i do not change asterizks.
Hey @Claudio-B ,
It still might be due to some cache, I think clearing cache and clearing cache pools may fix the problem.
Cheers!
Thank you Victor, was my problem reading profiler info: changing the text back and forth make the cache numbers into the profiler to change, but the one from app get always hitted (sorry for my "maccheroni" english")
Ciao!
Hey @Claudio-B,
Ah, no problem! Glad you figured that out and everything now works as expected ;)
Cheers!
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.
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/current/components/cache.html
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!
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!
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
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!
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!
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": []
},
`
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);`
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!
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!
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!
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!
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...
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.
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..
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!
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;
Hi, you've hinted (autowiring) the markdown interface, but didn't it need to be a class to actually have methods? Thanks
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!
// 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
}
}
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).