There is a Latin proverb:
Quot linguas calles, tot homines vales, meaning — As Many languages you know, as many times you are a human being…
Well, we code using languages, so I reckon it’s fair enough to apply the proverb in the IT world as well. I dare to say there is still no 100% reliable translation service out there, so allowing users to dynamically define languages and corresponding data sounds pretty catchy, doesn’t it?
So without any further ado, let’s get onto the topic. For implementation of this multilingual feature, we will use the Knp Doctrine2 Behaviors bundle based on a collection of traits and interfaces that add behaviors to Doctrine 2 entities and repositories. It oers a lot for a bundle, but what we need this time is just translatable behavior.
Oicial documentation in the given link is enough to get you started, but there are still some additional key requirements and tricks you need to make this whole thing work. In this article, we will go through the steps of implementation and show how this feature works using the basic CRUD functionalities realized as REST API.
Things that are not covered in this article but you should be familiar with are:
- Basic OOP principles
- PHP 7.1
- Symfony 4.2.4
- Controller, Traits
- Serializer
- Docker
First, let’s install the Knp Doctrine 2 Behaviors bundle and go through the setup steps. Enter the project directory and enter the following command:
composer require knplabs/doctrine-behaviors:~1.1
Since we use only translatable feature, we can disable other features/subscribers by adding the following code in our config.yml file:
knp_doctrine_behaviors:
blameable: false
geocodable: ~ # Here null is converted to false
loggable: ~
sluggable: false
soft_deletable: false
translatable: true
# All others behaviors are disabled```
If this is omitted, all subscribers will be enabled. After defining which subscribers will be enabled and which will not, we need to register them. Depending on a version of Symfony, it can be done in several ways.
If using Symfony 2 or 3, you can edit registerBundles() method in AppKernel class and add the following to the $bundles array:
class AppKernel
{
function registerBundles()
{ $bundles = array(
//...
new
Knp\DoctrineBehaviors\Bundle\DoctrineBehaviorsBundle(),
//...
);
//...
return $bundles;
}
}
or you can do it the deprecated way by adding the following to your config.yml file:
# app/config/config.yml
imports:
- { resource: ../../vendor/knplabs/doctrine-behaviors/config/orm-services.yml }
The third and the last way to do the mentioned is to add subscriber(s) using the EventManager:
$em->getEventManager()->addEventSubscriber(new \Knp\DoctrineBehaviors\ORM\Translatable\TranslatableSubscriber);
In Symfony 4+ you will just need to update your bundles.php file and add something like this in the returning array:
return = [
//...
Knp\DoctrineBehaviors\Bundle\DoctrineBehaviorsBundle::class => ['all' => true],
//...
]
Once we are set up, we can start the implementation. Let’s create two classes Movie and MovieTranslation.
By default, translatable class Movie and translation class MovieTranslation should be within the same namespace. When working on bigger projects, that could result in a messy project structure. Luckily, there is a way to use a separate namespace and to define dierent class names for translation classes. To do that, we need to override Translation and Translatable traits and their methods getTranslationEntityClass() and getTranslatableEntityClass().
To keep the project structure even more organized, let’s store our custom traits TranslationTrait and TranslatableTrait under the newly created App\TranslationBundle\Translation namespace where we will keep everything we need for a proper implementation of Doctrine2 translatable behavior.
The last step is overriding trait parameters of DoctrineBehaviors inside your parameters.yml or doctrine.yml file:
parameters:
knp.doctrine_behaviors.translatable_subscriber.translatable_trait: App\TranslationBundle\Translation\TranslatableTrait
knp.doctrine_behaviors.translatable_subscriber.translation_trait: App\TranslationBundle\Translation\TranslationTrait
Now, we can create sub-namespace App\Entity\Translation where we will store all corresponding translation classes, in this case — class MovieTranslation.
After we’ve done this and made sure that class Movie uses TranslatableTrait and MovieTranslation class uses TranslationTrait, we can upgrade our doctrine schema using the command:
bin/console doctrine:schema:update --force
Bam! We are ready to do the fun stu. There are a few of the key methods you will find useful during the implementation of translatable behavior.
These are:
- translate($locale = null, $fallbackToDefault = true)
- getTranslations()
- mergeNewTranslations()
If you have only one entity with translatable properties, you can do it the simple way as it is stated in the documentation:
To get translations, you can either use getTranslations method to get all the translations (all languages) or translate(‘en’)→getTitle() to get translation in a given language, in this example, English.
Since our goal here is to allow users to define languages dynamically, we need to optimize this a little bit further. Speaking of it, we can create service TranslatonService under App\Service namespace and TranslationInterface under App\TranslationBundle\Translation namespace. Each translatable class (e.g. Movie) should implement 2 key methods of TranslationInterface.
The method setTranslatableProperties($lang, $value) is used to set translatable fields’ values for given language. Depending on a number of translatable properties, the method can be less or more complex. However, in this example, we have only 2 translatable properties title and description.
The second method getTranslation is a custom method used to get translations in all languages. In this example, we implemented it as virtual property to include translations in REST API response when fetching data from a database. Notice that method calls method getTranslations predefined in TranslatableMethods trait of Knp Doctrine2 behaviors bundle. So, basically, if your use-case scenario doesn’t require automatic binding of translations, you might call getTranslations method directly where and when you need it.
Now when we implemented two of the mentioned methods, we can create a service that will handle the actual translation based on a given JSON object. JSON request body we will use is given in the following code snippet:
You can modify this JSON regarding to your needs (mind that you will have to update for each loop logic in the translateEntity method of TranslationService).
The final thing we need to do is to create movieController that will handle requests for CRUD functionalities. In this example, I used dependency injection (auto wiring) to inject a few of the services we will need in our controller methods.
The READ functionality doesn’t require any additional work for translations since everything is done automatically in our getTranslation() method.
It’s pretty much the same when fetching a single movie:
When creating a new entity, we have to call the translateEntity method before persisting and flushing the entity so it can map corresponding translations during the insertion.
Update action is not much dierent than creating action but there is one thing we need to pay attention to. As you may know, the usual flow of update action is:
1. fetch the object from the database
2. update fields 3. store the object with new values
There is one tricky part that is not covered in oicial documentation and it may look redundant but gave me a headache when I implemented translatable behavior for the first time.
Before calling mergeNewTranslations method (from within the translateEntity method), we need to do the flush. This is a workaround to avoid integrity constraint violation for duplicate entry probably caused by conflict between background work of Knp Doctrine2 translatable behavior and lazy loading of translations when fetching the object. However, that was the only way to make the update action work (I might be missing something here, feel free to let me know if that’s the case).
The remaining delete functionality is pretty straight forward:
There is one additional cool feature you might need depending on your use-case scenario. Imagine you have a twig template where you need to use something like {{ movie.getTitle() }} to show the movie’s title. To be able to use property getter which is defined in MovieTranslator class, you need to add the following to your translatable class (e.g. Movie):
or do it with PropertyAccessor that ships with Symfony SE:
Mind that it will return translation in a language that is defined as your default locale. In case you want to change your current locale, use setDefaultLocale($locale) method of TranslatableTrait.
Hopefully, you will find this article helpful, and feel free to share it around!