This course is still being released! Check back later for more chapters.

Get Notified About this Course!

We will send you messages regarding this course only
and nothing else, we promise.
You can unsubscribe anytime by emailing us at:
privacy@symfonycasts.com
Login to bookmark this video
Buy Access to Course
23.

Refactoring `ObjectTranslator`

|

Share this awesome video!

|

Keep on Learning!

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Our ObjectTranslator is growing a bit large, and our translationsFor method has ballooned. To tackle this, we'll create a new service that will take care of all the Doctrine-related tasks. This involves dealing with the Translation class and all the other Doctrine operations we're using.

First, create the new class in our bundle's src directory, call it TranslatableMappingManager. Admittedly, the term "manager" is a bit of a fallback when I'm not sure what to name something, but it'll do for now. We'll mark this class as final and @internal.

// ... lines 1 - 8
/**
* @internal
*/
final class TranslatableMappingManager
{
// ... lines 14 - 69
}

This way, if we come up with a better name later, we can easily rename it.

translatableTypeFor Method

First up, create a method called public function translatableTypeFor. This method will take an object as a parameter, object $object and return a string.

In ObjectTranslator, find translationsFor and copy the type-related logic. Paste it in our new translatableTypeFor method, and import the necessary class. At the bottom, return $type:

// ... lines 1 - 11
final class TranslatableMappingManager
{
// ... lines 14 - 19
public function translatableTypeFor(object $object): string
{
$class = new \ReflectionClass($object);
$type = $class->getAttributes(Translatable::class)[0]?->newInstance()->name ?? null;
if (!$type) {
throw new \LogicException(sprintf('Class "%s" is not translatable.', $object::class));
}
return $type;
}
// ... lines 31 - 69
}

The Constructor

Next, create a constructor for this class... and open it up to give us some space. Now copy the Doctrine-related properties from ObjectTranslator's constructor ($translationClass and $doctrine) and paste them into our new constructor:

// ... lines 1 - 11
final class TranslatableMappingManager
{
public function __construct(
private string $translationClass,
private ManagerRegistry $doctrine,
) {
}
// ... lines 19 - 69
}

idFor Method

This paves the way for our next method, public function idFor(), which will once again take an object and return a string. For this, we'll return to translationsFor in ObjectTranslator, copy the logic to fetch the ID, and paste it in our new method. At the end... return $id. Oh, PhpStorm is telling me we can inline the return. So, return reset($id) and remove the return below:

// ... lines 1 - 11
final class TranslatableMappingManager
{
// ... lines 14 - 31
public function idFor(object $object): string
{
$om = $this->doctrine->getManagerForClass($object::class);
if (!$om) {
throw new \LogicException(sprintf('No object manager found for class "%s".', $object::class));
}
$id = $om->getClassMetadata($object::class)
->getIdentifierValues($object)
;
if (1 !== count($id)) {
throw new \LogicException(sprintf('Class "%s" must have a single identifier to be translatable.', $object::class));
}
$id = reset($id);
return $id;
}
// ... lines 52 - 69
}

translationsFor Method

Finally, create another method: public function translationsFor(). This will accept three parameters: string $locale, string $type, string $id and return an array. Inside this method, grab the logic that fetches and normalizes the translations in ObjectTranslator::translationsFor(), and paste it here:

// ... lines 1 - 11
final class TranslatableMappingManager
{
// ... lines 14 - 52
public function translationsFor(string $locale, string $type, string $id): array
{
/** @var Translation[] $translations */
$translations = $this->doctrine->getRepository($this->translationClass)->findBy([
'locale' => $locale,
'objectType' => $type,
'objectId' => $id,
]);
$translationValues = [];
foreach ($translations as $translation) {
$translationValues[$translation->field] = $translation->value;
}
return $translationValues;
}
}

Using TranslatableMappingManager in ObjectTranslator

Now that we've got our new class, inject it into ObjectTranslator. In the constructor, replace the two Doctrine properties with private TranslatableMappingManager $mappingManager:

// ... lines 1 - 10
final class ObjectTranslator
{
// ... lines 13 - 15
public function __construct(
private LocaleAwareInterface $localeAware,
private string $defaultLocale,
private TranslatableMappingManager $mappingManager,
?CacheInterface $cache = null,
private ?int $cacheTtl = null,
) {
// ... lines 23 - 24
}
// ... lines 26 - 64
}

First, replace the type fetching logic with $type = $this->mappingManager->translatableTypeFor($object):

// ... lines 1 - 10
final class ObjectTranslator
{
// ... lines 13 - 44
private function translationsFor(object $object, string $locale): array
{
$type = $this->mappingManager->translatableTypeFor($object);
// ... lines 48 - 63
}
}

Next replace the Doctrine code for fetching the id with $id = $this->mappingManager->idFor($object):

// ... lines 1 - 10
final class ObjectTranslator
{
// ... lines 13 - 44
private function translationsFor(object $object, string $locale): array
{
// ... line 47
$id = $this->mappingManager->idFor($object);
// ... lines 49 - 63
}
}

Finally, in the cache-get callable, replace the Doctrine logic for fetching and normalizing translations with return $this->mappingManager->translationsFor($locale, $type, $id):

// ... lines 1 - 10
final class ObjectTranslator
{
// ... lines 13 - 44
private function translationsFor(object $object, string $locale): array
{
// ... lines 47 - 49
return $this->cache->get(
// ... line 51
function(ItemInterface $item) use ($locale, $type, $id) {
// ... lines 53 - 60
return $this->mappingManager->translationsFor($locale, $type, $id);
}
);
}
}

Nice!

Updating Service Definitions

We've refactored the code but need to update our service definitions.

In services.php, add our new service with ->set('., to make it a hidden service, symfonycasts.object_translator.mapping_manager. Class: TranslatableMappingManager. For the args, use ->args([]) and expand. In the ObjectTranslator definition above, cut the Doctrine-related arguments, and paste as our new service's arguments:

// ... lines 1 - 8
return static function (ContainerConfigurator $container): void {
$container->services()
// ... lines 11 - 20
->set('.symfonycasts.object_translator.mapping_manager', TranslatableMappingManager::class)
->args([
abstract_arg('translation class'),
service('doctrine'),
])
// ... lines 26 - 28
;
};

Back up in the ObjectTranslator definition, add a new argument: service('.symfonycasts.object_translator.mapping_manager'):

// ... lines 1 - 8
return static function (ContainerConfigurator $container): void {
$container->services()
->set('symfonycasts.object_translator', ObjectTranslator::class)
->args([
// ... lines 13 - 14
service('.symfonycasts.object_translator.mapping_manager'),
])
// ... lines 17 - 28
;
};

Love that autocompletion!

Adjusting the Configuration Processing

Finally, since we adjusted arguments, we need to update our configuration processing. This is the last step, I swear!

Open ObjectTranslationBundle and find our loadExtension() method.

In our cache configuration, these argument indexes have shifted. Take a peek at the ObjectTranslator constructor to figure our the new indexes. 0, 1, 2, 3, 4. Back in loadExtension() update these two setArgument() calls to use 3, 4 instead of 5, 6:

// ... lines 1 - 12
final class ObjectTranslationBundle extends AbstractBundle
{
// ... lines 15 - 54
public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void
{
// ... lines 57 - 60
if ($config['cache']['enabled']) {
$objectTranslatorDef->setArgument(3, new Reference($config['cache']['pool']));
$objectTranslatorDef->setArgument(4, $config['cache']['ttl']);
}
// ... lines 65 - 67
}
}

The translation_class needs to be moved to our new service. So, write $builder->getDefinition('.symfonycasts.object_translator.mapping_manager'). Copy the setArgument call above and paste it here. For the index, check TranslatableMappingManager's constructor. It's 0, so back in loadExtension(), change the index to 0 and delete this rogue variable above:

// ... lines 1 - 12
final class ObjectTranslationBundle extends AbstractBundle
{
// ... lines 15 - 54
public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void
{
// ... lines 57 - 65
$builder->getDefinition('.symfonycasts.object_translator.mapping_manager')
->setArgument(0, $config['translation_class']);
}
}

Phew! That's it for the refactor. Let's test this baby out.

In the browser, refresh the French homepage... and... Sweet! The translations are still working!

Next, we'll dive into bundle console commands, starting with a translation cache warmer command.