This course is still being released! Check back later for more chapters.
Warmup Command Configuration
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 SubscribeWe have our warmup command class created, now we need to configure it as a service. In your apps, this would be done automatically because of the AsCommand attribute, but in bundles, like any other service, we need to configure it manually.
Open our bundle's services.php file. Add it as a new service with ->set('.symfonycasts.object_translator.warmup_command'). Class: ObjectTranslationWarmupCommand:
| // ... lines 1 - 9 | |
| return static function (ContainerConfigurator $container): void { | |
| $container->services() | |
| // ... lines 12 - 27 | |
| ->set('.symfonycasts.object_translator.warmup_command', ObjectTranslationWarmupCommand::class) | |
| // ... lines 29 - 38 | |
| ; | |
| }; |
Add some args(): First is the object translator service, so write service() and copy-paste that service ID. Next is the mapping manager service(). Copy-paste that service ID as well. The next service() is the locale switcher, it's the same service we used above, so copy-paste that.
The last argument is the enabled locales, write param('kernel.enabled_locales'):
| // ... lines 1 - 9 | |
| return static function (ContainerConfigurator $container): void { | |
| $container->services() | |
| // ... lines 12 - 27 | |
| ->set('.symfonycasts.object_translator.warmup_command', ObjectTranslationWarmupCommand::class) | |
| ->args([ | |
| service('symfonycasts.object_translator'), | |
| service('.symfonycasts.object_translator.mapping_manager'), | |
| service('translation.locale_switcher'), | |
| param('kernel.enabled_locales'), | |
| ]) | |
| // ... lines 35 - 38 | |
| ; | |
| }; |
Finally, mark this service as a console command with ->tag('console.command'):
| // ... lines 1 - 9 | |
| return static function (ContainerConfigurator $container): void { | |
| $container->services() | |
| // ... lines 12 - 27 | |
| ->set('.symfonycasts.object_translator.warmup_command', ObjectTranslationWarmupCommand::class) | |
| // ... lines 29 - 34 | |
| ->tag('console.command') | |
| // ... lines 36 - 38 | |
| ; | |
| }; |
Let's test this command out in the terminal. Run:
symfony console object-translation:warmup -v
The -v will give us a stack trace of any errors we encounter.
And... we do have an error. Well a warning actually: "Undefined array key 0". Look at the first line of the stack trace, it's pointing us to line 23 of TranslatableMappingManager.
Undefined Array Key Fix
Check out that class and find the line.
It's subtle but, we're trying to use the null safe operator on an array. It doesn't protect us from undefined keys. To fix this, wrap this in brackets, and add ?? null:
| // ... lines 1 - 12 | |
| final class TranslatableMappingManager | |
| { | |
| // ... lines 15 - 20 | |
| public function translatableTypeFor(object $object): string | |
| { | |
| // ... lines 23 - 28 | |
| $type = ($class->getAttributes(Translatable::class)[0] ?? null)?->newInstance()->name ?? null; | |
| // ... lines 30 - 35 | |
| } | |
| // ... lines 37 - 90 | |
| } |
Try the command again:
symfony console object-translation:warmup -v
Cool! That worked!
But there's actually an even more subtle bug that can pop up here...
Clear the cache with no warm-up:
symfony console cache:clear --no-warmup
Now run the warmup command again:
symfony console object-translation:warmup -v
Hmm... "Class Proxies__CG__\App\Entity\Category is not translatable." What's the deal with this class name?
Accounting for Doctrine Proxies
What's happening is that Doctrine generates a proxy class that extends your entity class behind the scenes. In this case, App\Entity\Category. This proxy class is what enables Doctrine to do it's magic.
In TranslatableMappingManager, the passed object is sometimes one of these proxies. And that proxy class doesn't have the Translatable attribute, that's the problem.
Remember, the proxy class extends our entity class. We can use reflection to get the parent class if we detect that it's a proxy.
After creating the ReflectionClass, write if ($class->implementsInterface(Proxy::class)). Import the class from Doctrine\Persistence:
| // ... lines 1 - 12 | |
| final class TranslatableMappingManager | |
| { | |
| // ... lines 15 - 20 | |
| public function translatableTypeFor(object $object): string | |
| { | |
| // ... lines 23 - 24 | |
| if ($class->implementsInterface(Proxy::class)) { | |
| // ... line 26 | |
| } | |
| // ... lines 28 - 35 | |
| } | |
| // ... lines 37 - 90 | |
| } |
All generated proxies have this interface.
Inside, write $class = $class->getParentClass() to get the true entity class:
| // ... lines 1 - 12 | |
| final class TranslatableMappingManager | |
| { | |
| // ... lines 15 - 20 | |
| public function translatableTypeFor(object $object): string | |
| { | |
| // ... lines 23 - 24 | |
| if ($class->implementsInterface(Proxy::class)) { | |
| $class = $class->getParentClass(); | |
| } | |
| // ... lines 28 - 35 | |
| } | |
| // ... lines 37 - 90 | |
| } |
This should fix the issue but confirm by running the cache clear with no warm-up again:
symfony console cache:clear --no-warmup
Then run the warm-up command again:
symfony console object-translation:warmup -v
Nice! No errors this time, and our progress bar shows 12 entities being processed. This is some stylish output!
Forcing Cache Recalculation
Now, there's just one last thing we need to do. Currently, when we're warming up the cache, if a translation is already cached, it won't be recalculated. Currently, this command is only really useful to warm an empty cache.
We need to force the recalculation of translations, even if they're already cached.
Symfony Cache Contracts has our back!
In ObjectTranslator::translationsFor(), dig into the cache get() method. The one on CacheInterface. See this float $beta parameter? Check out it's docblock. "A float, that as it grows, controls the likeliness of triggering early expiration. 0 disables it, INF forces immediate expiration."
This feature helps with cache stampedes, if a bunch of items expire at the same time, there's a probability that some will expire early to help spread out the load.
For our purpose, we can pass infinity to force immediate expiration.
Back in ObjectTranslator::translationsFor(), add a new parameter: bool $forceRefresh = false:
| // ... lines 1 - 10 | |
| final class ObjectTranslator | |
| { | |
| // ... lines 13 - 48 | |
| private function translationsFor(object $object, string $locale, bool $forceRefresh): array | |
| { | |
| // ... lines 51 - 68 | |
| } | |
| } |
Now, for the third argument of cache->get(), pass $forceRefresh ? \INF : null:
| // ... lines 1 - 10 | |
| final class ObjectTranslator | |
| { | |
| // ... lines 13 - 48 | |
| private function translationsFor(object $object, string $locale, bool $forceRefresh): array | |
| { | |
| // ... lines 51 - 53 | |
| return $this->cache->get( | |
| // ... line 55 | |
| function(ItemInterface $item) use ($locale, $type, $id) { | |
| // ... lines 57 - 66 | |
| $forceRefresh ? \INF : null, | |
| ); | |
| } | |
| } |
If true, use infinity as the $beta to force expiration, otherwise, pass null to use the default behavior.
Up in translate(), PhpStorm is complaining that we need to pass this new parameter. Add a third argument to the method: array $options = []:
| // ... lines 1 - 10 | |
| final class ObjectTranslator | |
| { | |
| // ... lines 13 - 33 | |
| public function translate(object $object, ?string $locale = null, array $options = []): object | |
| { | |
| // ... lines 36 - 46 | |
| } | |
| // ... lines 48 - 69 | |
| } |
We could have used a dedicated parameter for this, but using an options array will make it easier to add more options in the future.
Expand the translationsFor() call and for the third argument: $options['force_refresh'] ?? false:
| // ... lines 1 - 10 | |
| final class ObjectTranslator | |
| { | |
| // ... lines 13 - 33 | |
| public function translate(object $object, ?string $locale = null, array $options = []): object | |
| { | |
| // ... lines 36 - 41 | |
| return $this->translatedObjects[$object] ??= new TranslatedObject($object, $this->translationsFor( | |
| // ... lines 43 - 44 | |
| $options['force_refresh'] ?? false, | |
| )); | |
| } | |
| // ... lines 48 - 69 | |
| } |
Finally, back in our warm-up command, in the innermost loop where we're calling translate(), add a third argument: ['force_refresh' => true]:
| // ... 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, ['force_refresh' => true]); | |
| }); | |
| } | |
| // ... lines 46 - 47 | |
| } | |
| // ... lines 49 - 52 | |
| } | |
| } |
Over in the terminal, run the warm-up command again:
symfony console object-translation:warmup -v
No errors, that's a good sign!
If you want to test that the refresh is actually happening, you can change some translations in the database, then run the command. You should see these changes reflected when refreshing the page - and no new database queries.
Up next, we'll add two more commands we need for a 1.0 release!