vendor/knplabs/doctrine-behaviors/src/EventSubscriber/TranslatableEventSubscriber.php line 58

  1. <?php
  2. declare(strict_types=1);
  3. namespace Knp\DoctrineBehaviors\EventSubscriber;
  4. use Doctrine\Bundle\DoctrineBundle\EventSubscriber\EventSubscriberInterface;
  5. use Doctrine\ORM\Event\LifecycleEventArgs;
  6. use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
  7. use Doctrine\ORM\Events;
  8. use Doctrine\ORM\Mapping\ClassMetadataInfo;
  9. use Doctrine\Persistence\ObjectManager;
  10. use Knp\DoctrineBehaviors\Contract\Entity\TranslatableInterface;
  11. use Knp\DoctrineBehaviors\Contract\Entity\TranslationInterface;
  12. use Knp\DoctrineBehaviors\Contract\Provider\LocaleProviderInterface;
  13. use ReflectionClass;
  14. final class TranslatableEventSubscriber implements EventSubscriberInterface
  15. {
  16.     /**
  17.      * @var string
  18.      */
  19.     public const LOCALE 'locale';
  20.     private int $translatableFetchMode;
  21.     private int $translationFetchMode;
  22.     public function __construct(
  23.         private LocaleProviderInterface $localeProvider,
  24.         string $translatableFetchMode,
  25.         string $translationFetchMode
  26.     ) {
  27.         $this->translatableFetchMode $this->convertFetchString($translatableFetchMode);
  28.         $this->translationFetchMode $this->convertFetchString($translationFetchMode);
  29.     }
  30.     /**
  31.      * Adds mapping to the translatable and translations.
  32.      */
  33.     public function loadClassMetadata(LoadClassMetadataEventArgs $loadClassMetadataEventArgs): void
  34.     {
  35.         $classMetadata $loadClassMetadataEventArgs->getClassMetadata();
  36.         if (! $classMetadata->reflClass instanceof ReflectionClass) {
  37.             // Class has not yet been fully built, ignore this event
  38.             return;
  39.         }
  40.         if ($classMetadata->isMappedSuperclass) {
  41.             return;
  42.         }
  43.         if (is_a($classMetadata->reflClass->getName(), TranslatableInterface::class, true)) {
  44.             $this->mapTranslatable($classMetadata);
  45.         }
  46.         if (is_a($classMetadata->reflClass->getName(), TranslationInterface::class, true)) {
  47.             $this->mapTranslation($classMetadata$loadClassMetadataEventArgs->getObjectManager());
  48.         }
  49.     }
  50.     public function postLoad(LifecycleEventArgs $lifecycleEventArgs): void
  51.     {
  52.         $this->setLocales($lifecycleEventArgs);
  53.     }
  54.     public function prePersist(LifecycleEventArgs $lifecycleEventArgs): void
  55.     {
  56.         $this->setLocales($lifecycleEventArgs);
  57.     }
  58.     /**
  59.      * @return string[]
  60.      */
  61.     public function getSubscribedEvents(): array
  62.     {
  63.         return [Events::loadClassMetadataEvents::postLoadEvents::prePersist];
  64.     }
  65.     /**
  66.      * Convert string FETCH mode to required string
  67.      */
  68.     private function convertFetchString(string|int $fetchMode): int
  69.     {
  70.         if (is_int($fetchMode)) {
  71.             return $fetchMode;
  72.         }
  73.         if ($fetchMode === 'EAGER') {
  74.             return ClassMetadataInfo::FETCH_EAGER;
  75.         }
  76.         if ($fetchMode === 'EXTRA_LAZY') {
  77.             return ClassMetadataInfo::FETCH_EXTRA_LAZY;
  78.         }
  79.         return ClassMetadataInfo::FETCH_LAZY;
  80.     }
  81.     private function mapTranslatable(ClassMetadataInfo $classMetadataInfo): void
  82.     {
  83.         if ($classMetadataInfo->hasAssociation('translations')) {
  84.             return;
  85.         }
  86.         $classMetadataInfo->mapOneToMany([
  87.             'fieldName' => 'translations',
  88.             'mappedBy' => 'translatable',
  89.             'indexBy' => self::LOCALE,
  90.             'cascade' => ['persist''merge''remove'],
  91.             'fetch' => $this->translatableFetchMode,
  92.             'targetEntity' => $classMetadataInfo->getReflectionClass()
  93.                 ->getMethod('getTranslationEntityClass')
  94.                 ->invoke(null),
  95.             'orphanRemoval' => true,
  96.         ]);
  97.     }
  98.     private function mapTranslation(ClassMetadataInfo $classMetadataInfoObjectManager $objectManager): void
  99.     {
  100.         if (! $classMetadataInfo->hasAssociation('translatable')) {
  101.             $targetEntity $classMetadataInfo->getReflectionClass()
  102.                 ->getMethod('getTranslatableEntityClass')
  103.                 ->invoke(null);
  104.             /** @var ClassMetadataInfo $classMetadata */
  105.             $classMetadata $objectManager->getClassMetadata($targetEntity);
  106.             $singleIdentifierFieldName $classMetadata->getSingleIdentifierFieldName();
  107.             $classMetadataInfo->mapManyToOne([
  108.                 'fieldName' => 'translatable',
  109.                 'inversedBy' => 'translations',
  110.                 'cascade' => ['persist''merge'],
  111.                 'fetch' => $this->translationFetchMode,
  112.                 'joinColumns' => [[
  113.                     'name' => 'translatable_id',
  114.                     'referencedColumnName' => $singleIdentifierFieldName,
  115.                     'onDelete' => 'CASCADE',
  116.                 ]],
  117.                 'targetEntity' => $targetEntity,
  118.             ]);
  119.         }
  120.         $name $classMetadataInfo->getTableName() . '_unique_translation';
  121.         if (! $this->hasUniqueTranslationConstraint($classMetadataInfo$name) &&
  122.             $classMetadataInfo->getName() === $classMetadataInfo->rootEntityName) {
  123.             $classMetadataInfo->table['uniqueConstraints'][$name] = [
  124.                 'columns' => ['translatable_id'self::LOCALE],
  125.             ];
  126.         }
  127.         if (! $classMetadataInfo->hasField(self::LOCALE) && ! $classMetadataInfo->hasAssociation(self::LOCALE)) {
  128.             $classMetadataInfo->mapField([
  129.                 'fieldName' => self::LOCALE,
  130.                 'type' => 'string',
  131.                 'length' => 5,
  132.             ]);
  133.         }
  134.     }
  135.     private function setLocales(LifecycleEventArgs $lifecycleEventArgs): void
  136.     {
  137.         $entity $lifecycleEventArgs->getEntity();
  138.         if (! $entity instanceof TranslatableInterface) {
  139.             return;
  140.         }
  141.         $currentLocale $this->localeProvider->provideCurrentLocale();
  142.         if ($currentLocale) {
  143.             $entity->setCurrentLocale($currentLocale);
  144.         }
  145.         $fallbackLocale $this->localeProvider->provideFallbackLocale();
  146.         if ($fallbackLocale) {
  147.             $entity->setDefaultLocale($fallbackLocale);
  148.         }
  149.     }
  150.     private function hasUniqueTranslationConstraint(ClassMetadataInfo $classMetadataInfostring $name): bool
  151.     {
  152.         return isset($classMetadataInfo->table['uniqueConstraints'][$name]);
  153.     }
  154. }