docs: Doctrine section with Doctrine Behaviors and Doctrine Helper (uk + en)
en/docs/doctrine/doctrine-behaviors/translatable.md
new file +150
−0
Translatable
Translations of entity fields in a separate translation entity — one row per locale.
Two entities
For an entity <X> the behavior expects an <X>Translation entity in the same namespace: App\Entity\Category and App\Entity\CategoryTranslation.
The translation entity holds the translated fields and its own identifier:
use Doctrine\ORM\Mapping as ORM;
use Ufo\DoctrineBehaviors\Contract\Entity\TranslationInterface;
use Ufo\DoctrineBehaviors\Model\Translatable\TranslationTrait;
#[ORM\Entity]
class CategoryTranslation implements TranslationInterface
{
use TranslationTrait;
#[ORM\Id, ORM\GeneratedValue, ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $name = null;
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): void
{
$this->name = $name;
}
}
The translatable entity holds only the fields that are not translated:
use Doctrine\ORM\Mapping as ORM;
use Ufo\DoctrineBehaviors\Contract\Entity\TranslatableInterface;
use Ufo\DoctrineBehaviors\Model\Translatable\TranslatableTrait;
#[ORM\Entity]
class Category implements TranslatableInterface
{
use TranslatableTrait;
#[ORM\Id, ORM\GeneratedValue, ORM\Column]
private ?int $id = null;
}
Mapping
| Entity | What the bundle adds |
|---|---|
Category |
OneToMany translations → CategoryTranslation, indexed by locale, cascade: persist, remove, orphanRemoval |
CategoryTranslation |
ManyToOne translatable → Category (column translatable_id, ON DELETE CASCADE), a locale string of length 5, a unique index (translatable_id, locale) |
The unique index is named <table>_unique_translation; if the name is longer than 63 characters, since 2.0.0 t_<hash>_unique_translation is used instead. #[ORM\MappedSuperclass] is skipped; in an inheritance hierarchy only the root translation entity gets the index.
Usage
$category = new Category();
$category->translate('fr')->setName('Chaussures');
$category->translate('en')->setName('Shoes');
$entityManager->persist($category);
$category->mergeNewTranslations(); // before flush()
$entityManager->flush();
$category->translate('en')->getName(); // Shoes
$category->translate()->getName(); // current locale
translate($locale = null, $fallbackToDefault = true):
- looks for a non-empty translation for
$locale(without an argument — the current locale); - if there is none and
$fallbackToDefaultis set — tries the locale without the region (uk_UA→uk), then the default locale; - if nothing is found — creates a new translation and puts it into
getNewTranslations().
New translations get into translations (and into the database) only after mergeNewTranslations(). The same method removes empty translations — isEmpty() treats a translation as empty when all its properties except id, translatable and locale are empty.
Other methods: getTranslations(), setTranslations(iterable), addTranslation(), removeTranslation(), getCurrentLocale() / setCurrentLocale(), getDefaultLocale() / setDefaultLocale().
Locales
On postLoad and prePersist the bundle sets the entity's current and default locale from LocaleProviderInterface (see Configuration). Outside an HTTP request the current locale is not set and equals the default locale; the trait's initial default locale is en.
The locale column has length 5 — locales like sr_Latn or zh_Hant do not fit. See Known issues.
Proxy to the translation
proxyCurrentLocaleTranslation() calls a method of the current locale's translation; handy in __call() so that $category->getName() (or category.name in Twig) returns the translated value:
public function __call(string $method, array $arguments): mixed
{
return $this->proxyCurrentLocaleTranslation($method, $arguments);
}
Other class names
To name the translation entity differently or move it to another namespace, override both static methods — each must return the class of the other side:
class Category implements TranslatableInterface
{
use TranslatableTrait;
public static function getTranslationEntityClass(): string
{
return \App\Entity\Translation\CategoryTranslation::class;
}
}
class CategoryTranslation implements TranslationInterface
{
use TranslationTrait;
public static function getTranslatableEntityClass(): string
{
return \App\Entity\Category::class;
}
}
Fetch strategy
# config/services.yaml
parameters:
doctrine_behaviors_translatable_fetch_mode: EXTRA_LAZY # translations collection
doctrine_behaviors_translation_fetch_mode: LAZY # translatable relation
| Value | Doctrine behavior |
|---|---|
LAZY |
Loads translations on first access (default). |
EAGER |
Loads them together with the entity. |
EXTRA_LAZY |
count(), contains() and similar calls do not load the whole collection. |
The bundle adds no JOINs — only the fetch strategy changes.