Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 19 additions & 9 deletions src/Doctrine/TranslatableClassMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ private function findTranslatedProperties(ClassMetadata $cm, ClassMetadataFactor
}

/* Class-level #[TranslatedProperty] attributes */
foreach ($reflectionClass->getAttributes(Attribute\TranslatedProperty::class) as $classAttribute) {
foreach (self::getAttributesIncludingParents($reflectionClass, Attribute\TranslatedProperty::class) as $classAttribute) {
$attribute = $classAttribute->newInstance();
$propertyName = $attribute->getPropertyName();

Expand All @@ -219,7 +219,7 @@ private function findTranslatedProperties(ClassMetadata $cm, ClassMetadataFactor
continue;
}

$candidates[$propertyName] = $attribute->getTranslationFieldname();
$candidates[$propertyName] ??= $attribute->getTranslationFieldname();
}

/* Register all collected candidates */
Expand All @@ -229,6 +229,20 @@ private function findTranslatedProperties(ClassMetadata $cm, ClassMetadataFactor
}
}

private static function getAttributesIncludingParents(ReflectionClass $rc, ?string $attributeName = null, int $flags = 0): array
{
$attributes = [];

do {
$attributes = array_merge(
$attributes,
$rc->getAttributes($attributeName, $flags)
);
} while ($rc = $rc->getParentClass());

return $attributes;
}

private function findTranslationsCollection(ClassMetadata $cm, ClassMetadataFactory $classMetadataFactory): void
{
foreach ($cm->associationMappings as $fieldName => $mapping) {
Expand Down Expand Up @@ -256,14 +270,10 @@ private function findTranslationsCollection(ClassMetadata $cm, ClassMetadataFact

private function findPrimaryLocale(ClassMetadata $cm): void
{
foreach (array_merge([$cm->name], $cm->parentClasses) as $class) {
$reflectionClass = new ReflectionClass($class);

foreach ($reflectionClass->getAttributes(Attribute\Locale::class) as $attribute) {
$this->primaryLocale = $attribute->newInstance()->getPrimary();
$attributes = self::getAttributesIncludingParents($cm->getReflectionClass(), Attribute\Locale::class);

return;
}
if ($attributes) {
$this->primaryLocale = $attributes[0]->newInstance()->getPrimary();
}
}

Expand Down
16 changes: 16 additions & 0 deletions tests/Doctrine/TranslatableClassMetadataTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Doctrine\Persistence\Mapping\RuntimeReflectionService;
use Webfactory\Bundle\PolyglotBundle\Doctrine\TranslatableClassMetadata;
use Webfactory\Bundle\PolyglotBundle\Tests\Fixtures\Entity\EntityInheritance\EntityInheritance_MappedSuperclassChainEntityLocaleOverride;
use Webfactory\Bundle\PolyglotBundle\Tests\Fixtures\Entity\TestEntity;
use Webfactory\Bundle\PolyglotBundle\Tests\Functional\DatabaseFunctionalTestCase;

Expand All @@ -22,6 +23,21 @@ public function can_be_serialized_and_retrieved(): void
self::assertEquals($metadata, $unserialized);
}

/**
* @test
*/
public function subclass_locale_takes_priority_over_parent_locale(): void
{
$metadata = TranslatableClassMetadata::parseFromClass(
EntityInheritance_MappedSuperclassChainEntityLocaleOverride::class,
$this->entityManager->getMetadataFactory()
);

// EntityInheritance_MappedSuperclassChain (parent) declares en_GB,
// EntityInheritance_MappedSuperclassChainEntityLocaleOverride (child) declares de_DE.
self::assertSame('de_DE', $metadata->sleep()->primaryLocale);
}

private function createMetadata(): TranslatableClassMetadata
{
$entityManager = $this->entityManager;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

namespace Webfactory\Bundle\PolyglotBundle\Tests\Fixtures\Entity\EntityInheritance;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Webfactory\Bundle\PolyglotBundle\Attribute as Polyglot;
use Webfactory\Bundle\PolyglotBundle\TranslatableInterface;

/**
* An intermediate mapped superclass that extends the base mapped superclass and carries
* the full Polyglot configuration: the #[TranslatedProperty] declaration for the inherited
* property, and the translations collection. The concrete entity class further down the
* chain needs no Polyglot configuration at all (beyond #[Locale] on the entity itself).
*/
#[ORM\MappedSuperclass]
#[Polyglot\Locale(primary: 'en_GB')]
#[Polyglot\TranslatedProperty('text')]
abstract class EntityInheritance_MappedSuperclassChain extends EntityInheritance_MappedSuperclass
{
#[Polyglot\TranslationCollection]
#[ORM\OneToMany(targetEntity: EntityInheritance_MappedSuperclassChainEntityTranslation::class, mappedBy: 'entity')]
private Collection $translations;

public function __construct()
{
$this->translations = new ArrayCollection();
}

public function setText(TranslatableInterface $text): void
{
$this->text = $text;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace Webfactory\Bundle\PolyglotBundle\Tests\Fixtures\Entity\EntityInheritance;

use Doctrine\ORM\Mapping as ORM;

/**
* A concrete entity two levels below the property declaration:
* EntityInheritance_MappedSuperclass (property defined here, no Polyglot config)
* └─ EntityInheritance_MappedSuperclassChain (#[Locale], #[TranslatedProperty], translations collection here)
* └─ EntityInheritance_MappedSuperclassChainEntity (bare entity, no Polyglot config needed)
*/
#[ORM\Entity]
class EntityInheritance_MappedSuperclassChainEntity extends EntityInheritance_MappedSuperclassChain
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace Webfactory\Bundle\PolyglotBundle\Tests\Fixtures\Entity\EntityInheritance;

use Doctrine\ORM\Mapping as ORM;
use Webfactory\Bundle\PolyglotBundle\Attribute as Polyglot;

/**
* A second concrete entity extending the chain, overriding the locale from the
* intermediate mapped superclass with a different primary locale.
* Used to verify that a subclass's #[Locale] takes priority over a parent class's.
*/
#[Polyglot\Locale(primary: 'de_DE')]
#[ORM\Entity]
class EntityInheritance_MappedSuperclassChainEntityLocaleOverride extends EntityInheritance_MappedSuperclassChain
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace Webfactory\Bundle\PolyglotBundle\Tests\Fixtures\Entity\EntityInheritance;

use Doctrine\ORM\Mapping as ORM;
use Webfactory\Bundle\PolyglotBundle\Attribute as Polyglot;

#[ORM\Entity]
class EntityInheritance_MappedSuperclassChainEntityTranslation
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private ?int $id = null;

#[Polyglot\Locale]
#[ORM\Column]
private string $locale;

#[ORM\ManyToOne(inversedBy: 'translations')]
private EntityInheritance_MappedSuperclassChainEntity $entity;

#[ORM\Column]
private string $text;
}
89 changes: 89 additions & 0 deletions tests/Functional/MappedSuperclassChainInheritanceTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php

namespace Webfactory\Bundle\PolyglotBundle\Tests\Functional;

use Webfactory\Bundle\PolyglotBundle\Tests\Fixtures\Entity\EntityInheritance\EntityInheritance_MappedSuperclass;
use Webfactory\Bundle\PolyglotBundle\Tests\Fixtures\Entity\EntityInheritance\EntityInheritance_MappedSuperclassChain;
use Webfactory\Bundle\PolyglotBundle\Tests\Fixtures\Entity\EntityInheritance\EntityInheritance_MappedSuperclassChainEntity;
use Webfactory\Bundle\PolyglotBundle\Tests\Fixtures\Entity\EntityInheritance\EntityInheritance_MappedSuperclassChainEntityTranslation;
use Webfactory\Bundle\PolyglotBundle\Translatable;

/**
* Tests the two-level mapped superclass chain:
* EntityInheritance_MappedSuperclass — property defined here, no Polyglot config
* └─ EntityInheritance_MappedSuperclassChain — #[Locale], #[TranslatedProperty], translations collection here
* └─ EntityInheritance_MappedSuperclassChainEntity — bare entity, no Polyglot config needed
*
* The concrete entity must pick up the #[TranslatedProperty] and #[Locale] declarations
* from the intermediate mapped superclass two levels up.
*/
class MappedSuperclassChainInheritanceTest extends DatabaseFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();

self::setupSchema([
EntityInheritance_MappedSuperclass::class,
EntityInheritance_MappedSuperclassChain::class,
EntityInheritance_MappedSuperclassChainEntity::class,
EntityInheritance_MappedSuperclassChainEntityTranslation::class,
]);
}

public function testPersistAndReloadEntity(): void
{
$entity = new EntityInheritance_MappedSuperclassChainEntity();
$t = new Translatable('base text');
$t->setTranslation('Basistext', 'de_DE');
$entity->setText($t);

self::import([$entity]);

$loaded = $this->entityManager->find(EntityInheritance_MappedSuperclassChainEntity::class, $entity->getId());

self::assertSame('base text', $loaded->getText()->translate('en_GB'));
self::assertSame('Basistext', $loaded->getText()->translate('de_DE'));
}

public function testAddTranslation(): void
{
$entityManager = $this->entityManager;

$entity = new EntityInheritance_MappedSuperclassChainEntity();
$entity->setText(new Translatable('base text'));
self::import([$entity]);

$loaded = $entityManager->find(EntityInheritance_MappedSuperclassChainEntity::class, $entity->getId());
$loaded->getText()->setTranslation('Basistext', 'de_DE');
$entityManager->flush();

$entityManager->clear();
$reloaded = $entityManager->find(EntityInheritance_MappedSuperclassChainEntity::class, $entity->getId());

self::assertSame('base text', $reloaded->getText()->translate('en_GB'));
self::assertSame('Basistext', $reloaded->getText()->translate('de_DE'));
}

public function testUpdateTranslations(): void
{
$entityManager = $this->entityManager;

$entity = new EntityInheritance_MappedSuperclassChainEntity();
$t = new Translatable('old text');
$t->setTranslation('alter Text', 'de_DE');
$entity->setText($t);
self::import([$entity]);

$loaded = $entityManager->find(EntityInheritance_MappedSuperclassChainEntity::class, $entity->getId());
$loaded->getText()->setTranslation('new text');
$loaded->getText()->setTranslation('neuer Text', 'de_DE');
$entityManager->flush();

$entityManager->clear();
$reloaded = $entityManager->find(EntityInheritance_MappedSuperclassChainEntity::class, $entity->getId());

self::assertSame('new text', $reloaded->getText()->translate('en_GB'));
self::assertSame('neuer Text', $reloaded->getText()->translate('de_DE'));
}
}
Loading