This course is still being released! Check back later for more chapters.
Cache Warmup Command
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeAlright, so by default, our object translations are cached indefinitely. We can configure an expiry, but what if we wanted to force an early refresh? Perhaps we've updated the database with new translations, and we want the site to reflect that. Sure, we could clear the whole cache or invalidate the cache tag. But this could be 1000's of translations, and on a busy site, could lead to slow-downs while the cache repopulates.
Let's create a cache warmup functionality. This will clear each cache item and simultaneously refresh it with the latest version. To do this, we'll create a console command.
In your IDE, within our object-translation-bundle's src directory, create a new PHP class. Call this ObjectTranslationWarmupCommand. This will be in the Command namespace. With PhpStorm, we can update the namespace here, and it will create it in the proper directory.
Setting Up the Command
Here we go. First, mark the class as final and @internal. The command itself will be public but the code for the command will be internal.
Next, have it extend Command from the Symfony console component. At the top, Add the AsCommand attribute. Name: object-translation:warmup. Description: Warms up the object translation cache..
Within the class, override two methods: the constructor and execute().
In the constructor, we don't want this $name argument, but we still need to call the parent constructor - just with no arguments.
| // ... lines 1 - 13 | |
| /** | |
| * @internal | |
| */ | |
| ( | |
| name: 'object-translation:warmup', | |
| description: 'Warms up the object translation cache.', | |
| ) | |
| final class ObjectTranslationWarmupCommand extends Command | |
| { | |
| public function __construct( | |
| // ... lines 24 - 27 | |
| ) { | |
| parent::__construct(); | |
| } | |
| // ... line 31 | |
| protected function execute(InputInterface $input, OutputInterface $output): int | |
| { | |
| // ... lines 34 - 40 | |
| } | |
| } |
Injecting Services
Make some room, and inject the following services. private ObjectTranslator $translator and private TranslatableMappingManager $mappingManager.
Next, private LocaleSwitcher $localeSwitcher, this is a special service that allows us to temporarily switch the locale for the entire application while we run some code. This will be useful for warming up translations in all available locales.
Finally, we need those locales, so inject private array $locales:
| // ... lines 1 - 20 | |
| final class ObjectTranslationWarmupCommand extends Command | |
| { | |
| public function __construct( | |
| private ObjectTranslator $translator, | |
| private TranslatableMappingManager $mappingManager, | |
| private LocaleSwitcher $localeAware, | |
| private array $locales, | |
| ) { | |
| // ... line 29 | |
| } | |
| // ... lines 31 - 41 | |
| } |
execute() Method
Down in execute(), remove the parent call as the parent method is empty. This method must return an integer as the status code. So, before we forget, return self::SUCCESS.
Above, write, $io = new SymfonyStyle($input, $output). This gives us a nice way to interact with the console using Symfony's recommended styling.
Add a title for this command: $io->title('Warming up Object Translation Cache').
Initiate a count variable: $count = 0. This will help us keep track of how many objects we warmed up.
| // ... lines 1 - 20 | |
| final class ObjectTranslationWarmupCommand extends Command | |
| { | |
| // ... lines 23 - 31 | |
| protected function execute(InputInterface $input, OutputInterface $output): int | |
| { | |
| $io = new SymfonyStyle($input, $output); | |
| $io->title('Warming up Object Translation Cache'); | |
| $count = 0; | |
| return self::SUCCESS; | |
| } | |
| } |
Iterating Over Translatable Objects
We need a way to get all the translatable objects. So, over in our TranslatableMappingManager, create a new method: public function allTranslatableObjects(). Return type: iterable:
| // ... lines 1 - 11 | |
| final class TranslatableMappingManager | |
| { | |
| // ... lines 14 - 70 | |
| public function allTranslatableObjects(): iterable | |
| { | |
| // ... lines 73 - 83 | |
| } | |
| } |
First, loop over the object managers: foreach ($this->doctrine->getManagers() as $om):
| // ... lines 1 - 11 | |
| final class TranslatableMappingManager | |
| { | |
| // ... lines 14 - 70 | |
| public function allTranslatableObjects(): iterable | |
| { | |
| foreach ($this->doctrine->getManagers() as $om) { | |
| // ... lines 74 - 82 | |
| } | |
| } | |
| } |
Complex apps can have multiple object managers, so this will ensure we cover them all.
Inside this, we need to get all the entity classes that this manager supports. So foreach ($om->getMetadataFactory()->getAllMetadata() as $metadata):
| // ... lines 1 - 11 | |
| final class TranslatableMappingManager | |
| { | |
| // ... lines 14 - 70 | |
| public function allTranslatableObjects(): iterable | |
| { | |
| foreach ($this->doctrine->getManagers() as $om) { | |
| foreach ($om->getMetadataFactory()->getAllMetadata() as $metadata) { | |
| // ... lines 75 - 81 | |
| } | |
| } | |
| } | |
| } |
Here, get the entity class name with $class = $metadata->getName(). Now we need to skip classes that aren't translatable. Do this with if (!(new \ReflectionClass($class))->getAttributes(Translatable::class)). This will return an empty array if the class doesn't have the Translatable attribute. In this case, continue to move onto the next class:
| // ... lines 1 - 11 | |
| final class TranslatableMappingManager | |
| { | |
| // ... lines 14 - 70 | |
| public function allTranslatableObjects(): iterable | |
| { | |
| foreach ($this->doctrine->getManagers() as $om) { | |
| foreach ($om->getMetadataFactory()->getAllMetadata() as $metadata) { | |
| $class = $metadata->getName(); | |
| // ... line 76 | |
| if (!(new \ReflectionClass($class))->getAttributes(Translatable::class)) { | |
| continue; | |
| } | |
| // ... lines 80 - 81 | |
| } | |
| } | |
| } | |
| } |
We now know we have a translatable class, so write: yield from $this->doctrine->getRepository($class)->findAll():
| // ... lines 1 - 11 | |
| final class TranslatableMappingManager | |
| { | |
| // ... lines 14 - 70 | |
| public function allTranslatableObjects(): iterable | |
| { | |
| foreach ($this->doctrine->getManagers() as $om) { | |
| foreach ($om->getMetadataFactory()->getAllMetadata() as $metadata) { | |
| // ... lines 75 - 80 | |
| yield from $om->getRepository($class)->findAll(); | |
| } | |
| } | |
| } | |
| } |
We're returning a generator that iterates over all translatable objects in all object managers.
Ok, this isn't the most efficient way to do this, as we're loading all entities into memory - potentially 10's of thousands... Let's just make note of this as a future improvement.
Back in our command, SymfonyStyle has a super handy method for iterating things with a progress bar. After initiating the $count variable, write: foreach ($io->progressIterate($this->mappingManager->allTranslatableObjects()) as $object):
| // ... lines 1 - 20 | |
| final class ObjectTranslationWarmupCommand extends Command | |
| { | |
| // ... lines 23 - 31 | |
| protected function execute(InputInterface $input, OutputInterface $output): int | |
| { | |
| // ... lines 34 - 39 | |
| foreach ($io->progressIterate($this->mappingManager->allTranslatableObjects()) as $object) { | |
| // ... lines 41 - 47 | |
| } | |
| // ... lines 49 - 52 | |
| } | |
| } |
progressIterate essentially wraps the iterable we give it, and handles the progress bar output in the terminal for us.
Inside, create a nested loop for the locales: foreach ($this->locales as $locale):
| // ... lines 1 - 20 | |
| final class ObjectTranslationWarmupCommand extends Command | |
| { | |
| // ... lines 23 - 31 | |
| protected function execute(InputInterface $input, OutputInterface $output): int | |
| { | |
| // ... lines 34 - 39 | |
| foreach ($io->progressIterate($this->mappingManager->allTranslatableObjects()) as $object) { | |
| foreach ($this->locales as $locale) { | |
| // ... lines 42 - 44 | |
| } | |
| // ... lines 46 - 47 | |
| } | |
| // ... lines 49 - 52 | |
| } | |
| } |
Now, write $this->localeSwitcher->runWithLocale(). This method takes two arguments: the $locale we want to switch to, and a callable. For this use ($object, $locale):
| // ... lines 1 - 20 | |
| final class ObjectTranslationWarmupCommand extends Command | |
| { | |
| // ... lines 23 - 31 | |
| protected function execute(InputInterface $input, OutputInterface $output): int | |
| { | |
| // ... lines 34 - 39 | |
| foreach ($io->progressIterate($this->mappingManager->allTranslatableObjects()) as $object) { | |
| foreach ($this->locales as $locale) { | |
| $this->localeAware->runWithLocale($locale, function () use ($object, $locale) { | |
| // ... line 43 | |
| }); | |
| } | |
| // ... lines 46 - 47 | |
| } | |
| // ... lines 49 - 52 | |
| } | |
| } |
This will switch the locale app-wide for the duration of the callable. After, it'll switch it back to whatever it was before.
Inside the callable, $this->translator->... Hey! Where's my autocompletion?! Oops, I forgot to import the ObjectTranslator class. There we go.
Back down, write translate($object, $locale):
| // ... lines 1 - 20 | |
| final class ObjectTranslationWarmupCommand extends Command | |
| { | |
| // ... lines 23 - 31 | |
| protected function execute(InputInterface $input, OutputInterface $output): int | |
| { | |
| // ... lines 34 - 39 | |
| foreach ($io->progressIterate($this->mappingManager->allTranslatableObjects()) as $object) { | |
| foreach ($this->locales as $locale) { | |
| $this->localeAware->runWithLocale($locale, function () use ($object, $locale) { | |
| $this->translator->translate($object, $locale); | |
| }); | |
| } | |
| // ... lines 46 - 47 | |
| } | |
| // ... lines 49 - 52 | |
| } | |
| } |
At the end of the outer loop, increase the count with $count++:
| // ... lines 1 - 20 | |
| final class ObjectTranslationWarmupCommand extends Command | |
| { | |
| // ... lines 23 - 31 | |
| protected function execute(InputInterface $input, OutputInterface $output): int | |
| { | |
| // ... lines 34 - 39 | |
| foreach ($io->progressIterate($this->mappingManager->allTranslatableObjects()) as $object) { | |
| // ... lines 41 - 46 | |
| ++$count; | |
| } | |
| // ... lines 49 - 52 | |
| } | |
| } |
Finally, before returning, add a success summary message: $io->success("Warmed up the cache for {$count} translatable objects."):
| // ... lines 1 - 20 | |
| final class ObjectTranslationWarmupCommand extends Command | |
| { | |
| // ... lines 23 - 31 | |
| protected function execute(InputInterface $input, OutputInterface $output): int | |
| { | |
| // ... lines 34 - 49 | |
| $io->success("Warmed up cache for {$count} translatable objects."); | |
| // ... lines 51 - 52 | |
| } | |
| } |
Overriding the Locale in translate()
Up where we're calling translate(), PhpStorm isn't happy with this $locale argument. That's because it doesn't exist in the method signature.
Jump into ObjectTranslator::translate() and add it: ?string $locale = null:
| // ... lines 1 - 10 | |
| final class ObjectTranslator | |
| { | |
| // ... lines 13 - 33 | |
| public function translate(object $object, ?string $locale = null): object | |
| { | |
| // ... lines 36 - 42 | |
| } | |
| // ... lines 44 - 64 | |
| } |
Where we're fetching the current locale, attempt to use the passed locale first: $locale ??:
| // ... lines 1 - 10 | |
| final class ObjectTranslator | |
| { | |
| // ... lines 13 - 33 | |
| public function translate(object $object, ?string $locale = null): object | |
| { | |
| $locale = $locale ?? $this->localeAware->getLocale(); | |
| // ... lines 37 - 42 | |
| } | |
| // ... lines 44 - 64 | |
| } |
When passed, it will be used, otherwise, it'll be pulled from the request.
Back over in our command, PhpStorm is happy!
Next, we'll wire up this command and give it a test drive!