Translatable

← Translatable

Ashterix

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

uk/docs/doctrine/doctrine-behaviors/translatable.md новий файл +150 −0

Translatable

Переклади полів сутності в окремій сутності перекладу — по одному рядку на локаль.

Дві сутності

Для сутності <X> поведінка очікує сутність <X>Translation у тому самому просторі імен: App\Entity\Category і App\Entity\CategoryTranslation.

Сутність перекладу містить поля, що перекладаються, і власний ідентифікатор:

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;
    }
}

Сутність, що перекладається, містить лише поля без перекладу:

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;
}

Мапінг

Сутність Що додає бандл
Category OneToMany translations → CategoryTranslation, індекс за locale, cascade: persist, remove, orphanRemoval
CategoryTranslation ManyToOne translatable → Category (колонка translatable_id, ON DELETE CASCADE), рядок locale довжиною 5, унікальний індекс (translatable_id, locale)

Унікальний індекс називається <таблиця>_unique_translation; якщо назва довша за 63 символи, з 2.0.0 замість неї використовується t_<хеш>_unique_translation. #[ORM\MappedSuperclass] пропускається; в ієрархії наслідування індекс отримує лише коренева сутність перекладу.

Використання

$category = new Category();
$category->translate('uk')->setName('Взуття');
$category->translate('en')->setName('Shoes');

$entityManager->persist($category);
$category->mergeNewTranslations(); // перед flush()
$entityManager->flush();

$category->translate('en')->getName(); // Shoes
$category->translate()->getName();     // поточна локаль

translate($locale = null, $fallbackToDefault = true):

  1. шукає непорожній переклад для $locale (без аргументу — поточна локаль);
  2. якщо його немає і $fallbackToDefault — пробує локаль без регіону (uk_UA → uk), потім локаль за замовчуванням;
  3. якщо нічого не знайдено — створює новий переклад і кладе його в getNewTranslations().

Нові переклади потрапляють у translations (і в базу) лише після mergeNewTranslations(). Той самий метод прибирає порожні переклади — isEmpty() вважає порожнім переклад, у якого всі властивості, крім id, translatable і locale, порожні.

Інші методи: getTranslations(), setTranslations(iterable), addTranslation(), removeTranslation(), getCurrentLocale() / setCurrentLocale(), getDefaultLocale() / setDefaultLocale().

Локалі

Під час postLoad і prePersist бандл ставить сутності поточну локаль і локаль за замовчуванням з LocaleProviderInterface (див. Налаштування). Поза HTTP-запитом поточна локаль не задається і дорівнює локалі за замовчуванням; початкове значення локалі за замовчуванням у трейті — en.

Колонка locale має довжину 5 — локалі на кшталт sr_Latn чи zh_Hant у неї не вміщаються. Див. Відомі помилки.

Проксі до перекладу

proxyCurrentLocaleTranslation() викликає метод перекладу поточної локалі; зручно в __call(), щоб $category->getName() (або category.name у Twig) повертав перекладене значення:

public function __call(string $method, array $arguments): mixed
{
    return $this->proxyCurrentLocaleTranslation($method, $arguments);
}

Інші назви класів

Щоб назвати сутність перекладу інакше або винести в інший простір імен, перевизначте обидва статичні методи — кожен має повертати клас іншої сторони:

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;
    }
}

Стратегія завантаження

# config/services.yaml
parameters:
    doctrine_behaviors_translatable_fetch_mode: EXTRA_LAZY # колекція translations
    doctrine_behaviors_translation_fetch_mode: LAZY        # зв'язок translatable
Значення Поведінка Doctrine
LAZY Завантажує переклади при першому зверненні (за замовчуванням).
EAGER Завантажує разом із сутністю.
EXTRA_LAZY count(), contains() і подібні виклики не завантажують усю колекцію.

JOIN бандл не додає — змінюється лише стратегія завантаження.