Translatable

← Translatable

Ashterix

docs: Doctrine section with Doctrine Behaviors and Doctrine Helper (uk + en)

en/docs/doctrine/doctrine-behaviors/translatable.md new file +150 āˆ’0

1 + # Translatable
2 +
3 + Translations of entity fields in a separate translation entity — one row per locale.
4 +
5 + ## Two entities
6 +
7 + For an entity `<X>` the behavior expects an `<X>Translation` entity in the same namespace: `App\Entity\Category` and `App\Entity\CategoryTranslation`.
8 +
9 + The translation entity holds the translated fields and its own identifier:
10 +
11 + ```php
12 + use Doctrine\ORM\Mapping as ORM;
13 + use Ufo\DoctrineBehaviors\Contract\Entity\TranslationInterface;
14 + use Ufo\DoctrineBehaviors\Model\Translatable\TranslationTrait;
15 +
16 + #[ORM\Entity]
17 + class CategoryTranslation implements TranslationInterface
18 + {
19 + use TranslationTrait;
20 +
21 + #[ORM\Id, ORM\GeneratedValue, ORM\Column]
22 + private ?int $id = null;
23 +
24 + #[ORM\Column(length: 255, nullable: true)]
25 + private ?string $name = null;
26 +
27 + public function getName(): ?string
28 + {
29 + return $this->name;
30 + }
31 +
32 + public function setName(string $name): void
33 + {
34 + $this->name = $name;
35 + }
36 + }
37 + ```
38 +
39 + The translatable entity holds only the fields that are not translated:
40 +
41 + ```php
42 + use Doctrine\ORM\Mapping as ORM;
43 + use Ufo\DoctrineBehaviors\Contract\Entity\TranslatableInterface;
44 + use Ufo\DoctrineBehaviors\Model\Translatable\TranslatableTrait;
45 +
46 + #[ORM\Entity]
47 + class Category implements TranslatableInterface
48 + {
49 + use TranslatableTrait;
50 +
51 + #[ORM\Id, ORM\GeneratedValue, ORM\Column]
52 + private ?int $id = null;
53 + }
54 + ```
55 +
56 + ## Mapping
57 +
58 + | Entity | What the bundle adds |
59 + | --- | --- |
60 + | `Category` | `OneToMany` `translations` → `CategoryTranslation`, indexed by `locale`, `cascade: persist, remove`, `orphanRemoval` |
61 + | `CategoryTranslation` | `ManyToOne` `translatable` → `Category` (column `translatable_id`, `ON DELETE CASCADE`), a `locale` string of length 5, a unique index (`translatable_id`, `locale`) |
62 +
63 + 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.
64 +
65 + ## Usage
66 +
67 + ```php
68 + $category = new Category();
69 + $category->translate('fr')->setName('Chaussures');
70 + $category->translate('en')->setName('Shoes');
71 +
72 + $entityManager->persist($category);
73 + $category->mergeNewTranslations(); // before flush()
74 + $entityManager->flush();
75 +
76 + $category->translate('en')->getName(); // Shoes
77 + $category->translate()->getName(); // current locale
78 + ```
79 +
80 + `translate($locale = null, $fallbackToDefault = true)`:
81 +
82 + 1. looks for a non-empty translation for `$locale` (without an argument — the current locale);
83 + 2. if there is none and `$fallbackToDefault` is set — tries the locale without the region (`uk_UA` → `uk`), then the default locale;
84 + 3. if nothing is found — creates a new translation and puts it into `getNewTranslations()`.
85 +
86 + 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.
87 +
88 + Other methods: `getTranslations()`, `setTranslations(iterable)`, `addTranslation()`, `removeTranslation()`, `getCurrentLocale()` / `setCurrentLocale()`, `getDefaultLocale()` / `setDefaultLocale()`.
89 +
90 + ## Locales
91 +
92 + On `postLoad` and `prePersist` the bundle sets the entity's current and default locale from `LocaleProviderInterface` (see [Configuration](/docs/doctrine/doctrine-behaviors/config)). Outside an HTTP request the current locale is not set and equals the default locale; the trait's initial default locale is `en`.
93 +
94 + :::warning
95 + The `locale` column has length 5 — locales like `sr_Latn` or `zh_Hant` do not fit. See [Known issues](/docs/doctrine/doctrine-behaviors/known_issues/open).
96 + :::
97 +
98 + ## Proxy to the translation
99 +
100 + `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:
101 +
102 + ```php
103 + public function __call(string $method, array $arguments): mixed
104 + {
105 + return $this->proxyCurrentLocaleTranslation($method, $arguments);
106 + }
107 + ```
108 +
109 + ## Other class names
110 +
111 + 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:
112 +
113 + ```php
114 + class Category implements TranslatableInterface
115 + {
116 + use TranslatableTrait;
117 +
118 + public static function getTranslationEntityClass(): string
119 + {
120 + return \App\Entity\Translation\CategoryTranslation::class;
121 + }
122 + }
123 +
124 + class CategoryTranslation implements TranslationInterface
125 + {
126 + use TranslationTrait;
127 +
128 + public static function getTranslatableEntityClass(): string
129 + {
130 + return \App\Entity\Category::class;
131 + }
132 + }
133 + ```
134 +
135 + ## Fetch strategy
136 +
137 + ```yaml
138 + # config/services.yaml
139 + parameters:
140 + doctrine_behaviors_translatable_fetch_mode: EXTRA_LAZY # translations collection
141 + doctrine_behaviors_translation_fetch_mode: LAZY # translatable relation
142 + ```
143 +
144 + | Value | Doctrine behavior |
145 + | --- | --- |
146 + | `LAZY` | Loads translations on first access (default). |
147 + | `EAGER` | Loads them together with the entity. |
148 + | `EXTRA_LAZY` | `count()`, `contains()` and similar calls do not load the whole collection. |
149 +
150 + The bundle adds no JOINs — only the fetch strategy changes.