From 9c3420b86181d47b54161d03c18b0b93e4ef8e62 Mon Sep 17 00:00:00 2001 From: Matthias Pigulla Date: Thu, 25 Apr 2024 14:21:35 +0200 Subject: [PATCH] Fix that `#[Polyglot\Translatable]` may be used on unmapped fields (#68) It may be useful to use the `#[Polyglot\Translatable]` attribute (or the corresponding annotation) on properties that are not configured as mapped Doctrine ORM fields. For example, the property value might be set by entity lifecycle callbacks or Doctrine listeners. It was possible to do so until #28 changed `\Webfactory\Bundle\PolyglotBundle\Doctrine\TranslatableClassMetadata::findTranslatedProperties` to work based on Doctrine ORM `ClassMetadata` information and fields. This was done to simplify detection of whether a particular property belongs to a given class or needs to be mapped through base class translations. Obviously, `ClassMetadata` does not know about unmapped properties. This PR changes the code to work on PHP reflection data instead, restoring functionality. --- src/Doctrine/TranslatableClassMetadata.php | 23 +-- .../TranslatingUnmappedPropertiesTest.php | 141 ++++++++++++++++++ 2 files changed, 155 insertions(+), 9 deletions(-) create mode 100644 tests/Functional/TranslatingUnmappedPropertiesTest.php diff --git a/src/Doctrine/TranslatableClassMetadata.php b/src/Doctrine/TranslatableClassMetadata.php index e943ac6..4080cac 100644 --- a/src/Doctrine/TranslatableClassMetadata.php +++ b/src/Doctrine/TranslatableClassMetadata.php @@ -187,16 +187,23 @@ private function findTranslatedProperties(ClassMetadataInfo $cm, Reader $reader, return; } + $reflectionService = $classMetadataFactory->getReflectionService(); $translationClassMetadata = $classMetadataFactory->getMetadataFor($this->translationClass->getName()); - foreach ($cm->fieldMappings as $fieldName => $mapping) { - if (isset($mapping['declared'])) { - // The association is inherited from a parent class + /* Iterate all properties of the class, not only those mapped by Doctrine */ + foreach ($cm->getReflectionClass()->getProperties() as $reflectionProperty) { + $propertyName = $reflectionProperty->name; + + /* + If the property is inherited from a parent class, and our parent entity class + already contains that declaration, we need not include it. + */ + $declaringClass = $reflectionProperty->getDeclaringClass()->name; + if ($declaringClass !== $cm->name && $cm->parentClasses && is_a($cm->parentClasses[0], $declaringClass, true)) { continue; } $foundAttributeOrAnnotation = null; - $reflectionProperty = $cm->getReflectionProperty($fieldName); $attributes = $reflectionProperty->getAttributes(Attribute\Translatable::class); if ($attributes) { @@ -210,11 +217,9 @@ private function findTranslatedProperties(ClassMetadataInfo $cm, Reader $reader, } if ($foundAttributeOrAnnotation) { - $translationFieldname = $foundAttributeOrAnnotation->getTranslationFieldname() ?: $fieldName; - $translationFieldReflectionProperty = $translationClassMetadata->getReflectionProperty($translationFieldname); - - $this->translatedProperties[$fieldName] = $reflectionProperty; - $this->translationFieldMapping[$fieldName] = $translationFieldReflectionProperty; + $this->translatedProperties[$propertyName] = $reflectionService->getAccessibleProperty($cm->name, $propertyName); + $translationFieldname = $foundAttributeOrAnnotation->getTranslationFieldname() ?: $propertyName; + $this->translationFieldMapping[$propertyName] = $reflectionService->getAccessibleProperty($translationClassMetadata->name, $translationFieldname); } } } diff --git a/tests/Functional/TranslatingUnmappedPropertiesTest.php b/tests/Functional/TranslatingUnmappedPropertiesTest.php new file mode 100644 index 0000000..b681c6d --- /dev/null +++ b/tests/Functional/TranslatingUnmappedPropertiesTest.php @@ -0,0 +1,141 @@ +setupOrmInfrastructure([ + TranslatingUnmappedPropertiesTest_Entity::class, + TranslatingUnmappedPropertiesTest_Translation::class, + ]); + } + + public function testPersistAndReloadEntity(): void + { + $entity = new TranslatingUnmappedPropertiesTest_Entity(); + $entity->text = new Translatable('base text'); + $entity->text->setTranslation('Basistext', 'de_DE'); + + $this->infrastructure->import($entity); + + $loaded = $this->infrastructure->getEntityManager()->find(TranslatingUnmappedPropertiesTest_Entity::class, $entity->id); + + self::assertSame('Basistext', $loaded->text->translate('de_DE')); + self::assertSame('base text', $loaded->text->translate('en_GB')); + } +} + +/** + * @ORM\Entity + * + * @ORM\HasLifecycleCallbacks + */ +#[Polyglot\Locale(primary: 'en_GB')] +class TranslatingUnmappedPropertiesTest_Entity +{ + /** + * @ORM\Column(type="integer") + * + * @ORM\Id + * + * @ORM\GeneratedValue + */ + public ?int $id = null; + + /** + * @ORM\OneToMany(targetEntity="TranslatingUnmappedPropertiesTest_Translation", mappedBy="entity") + */ + #[Polyglot\TranslationCollection] + protected Collection $translations; + + /** + * @ORM\Column(type="string") + */ + public $mappedText; + + // (!) This field is unmapped from the ORM point of view + #[Polyglot\Translatable] + public $text; + + public function __construct() + { + $this->translations = new ArrayCollection(); + $this->text = new Translatable(); + } + + /** @ORM\PreFlush() */ + public function copyToMappedField(): void + { + $this->mappedText = $this->text; + } + + /** @ORM\PostLoad() */ + public function copyFromMappedField(): void + { + $this->text = $this->mappedText; + } +} + +/** + * @ORM\Entity + * + * @ORM\HasLifecycleCallbacks + */ +class TranslatingUnmappedPropertiesTest_Translation +{ + /** + * @ORM\Id + * + * @ORM\GeneratedValue + * + * @ORM\Column(type="integer") + */ + private ?int $id = null; + + /** + * @ORM\Column + */ + #[Polyglot\Locale] + private string $locale; + + /** + * @ORM\ManyToOne(targetEntity="TranslatingUnmappedPropertiesTest_Entity", inversedBy="translations") + */ + private TranslatingUnmappedPropertiesTest_Entity $entity; + + /** + * @ORM\Column + */ + private $mappedText; + + // (!) This field is unmapped from the ORM point of view + private $text; + + /** @ORM\PreFlush() */ + public function copyToMappedField(): void + { + $this->mappedText = $this->text; + } + + /** @ORM\PostLoad() */ + public function copyFromMappedField(): void + { + $this->text = $this->mappedText; + } +}