Skip to content

Commit e40e3de

Browse files
authored
Fix translations collection detection for hierarchies with mapped superclasses (#90)
This fixes that the "translations collection" was not detected when it is defined in a mapped superclass and inherited by an entity. Due to [how MappedSuperclasses work for one-to-many associations](https://www.doctrine-project.org/projects/doctrine-orm/en/3.6/reference/inheritance-mapping.html#:~:text=One%2DTo%2DMany%20associations%20are%20not%20generally%20possible%20on%20a%20mapped%20superclass), this requires to set up the `ResolveTargetEntityListener` with an interface to point back from the translation class to the concrete entity class. See https://github.com/doctrine/orm/blob/580a95ce3f5f016547d15ecc6cc94dd85453bed5/src/Mapping/AssociationMapping.php#L34-L57 for the meaning of the two `inherited` and `declared` mapping entries. `inherited` means that the field in question exists also in a parent *entity* class. This parent entity class is named in `ClassMetadata::$parentClasses`, and will be inspected by `PolyglotListener::getTranslationMetadatas()`. The previous check, which was looking at `declared`, is wrong for fields declared in mapped superclasses: Mapped superclasses are not part of the `parentClasses` list, and so we missed translation collections defined in those.
1 parent 1273914 commit e40e3de

6 files changed

+190
-2
lines changed

src/Doctrine/TranslatableClassMetadata.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,8 +232,10 @@ private function findTranslatedProperties(ClassMetadata $cm, ClassMetadataFactor
232232
private function findTranslationsCollection(ClassMetadata $cm, ClassMetadataFactory $classMetadataFactory): void
233233
{
234234
foreach ($cm->associationMappings as $fieldName => $mapping) {
235-
if (isset($mapping['declared'])) {
236-
// The association is inherited from a parent class
235+
if (isset($mapping['inherited'])) {
236+
// "inherited" means that there is another (inheritance parent) entity class containing this
237+
// field (https://github.com/doctrine/orm/blob/580a95ce3f5f016547d15ecc6cc94dd85453bed5/src/Mapping/AssociationMapping.php#L34-L46).
238+
// Since PolyglotListener::getTranslationMetadatas() loops over these parent classes as well, we can skip the field here.
237239
continue;
238240
}
239241

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
namespace Webfactory\Bundle\PolyglotBundle\Tests\Fixtures\Entity\EntityInheritance;
4+
5+
use Doctrine\ORM\Mapping as ORM;
6+
use Webfactory\Bundle\PolyglotBundle\Attribute as Polyglot;
7+
8+
#[ORM\Entity]
9+
#[Polyglot\Locale(primary: 'en_GB')]
10+
class EntityInheritance_MappedSuperclassTranslation
11+
{
12+
#[ORM\Id]
13+
#[ORM\GeneratedValue]
14+
#[ORM\Column(type: 'integer')]
15+
private ?int $id = null;
16+
17+
#[Polyglot\Locale]
18+
#[ORM\Column]
19+
private string $locale;
20+
21+
#[ORM\ManyToOne(targetEntity: EntityInheritance_MappedSuperclassWithTranslationsInterface::class, inversedBy: 'translations')]
22+
private EntityInheritance_MappedSuperclassWithTranslationsInterface $entity;
23+
24+
#[ORM\Column]
25+
private string $text;
26+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
namespace Webfactory\Bundle\PolyglotBundle\Tests\Fixtures\Entity\EntityInheritance;
4+
5+
use Doctrine\Common\Collections\ArrayCollection;
6+
use Doctrine\Common\Collections\Collection;
7+
use Doctrine\ORM\Mapping as ORM;
8+
use Webfactory\Bundle\PolyglotBundle\Attribute as Polyglot;
9+
use Webfactory\Bundle\PolyglotBundle\TranslatableInterface;
10+
11+
/**
12+
* A mapped superclass that carries a translatable property, including translations.
13+
* Since translations are a one-to-many relationship, the ResolveTargetEntityListener
14+
* must be used to make the translation class ::$entity field reference back to this
15+
* class here, see https://www.doctrine-project.org/projects/doctrine-orm/en/3.6/reference/inheritance-mapping.html#:~:text=ResolveTargetEntityListener.
16+
*/
17+
#[ORM\MappedSuperclass]
18+
abstract class EntityInheritance_MappedSuperclassWithTranslations implements EntityInheritance_MappedSuperclassWithTranslationsInterface
19+
{
20+
#[ORM\Id]
21+
#[ORM\GeneratedValue]
22+
#[ORM\Column(type: 'integer')]
23+
private ?int $id = null;
24+
25+
#[Polyglot\TranslationCollection]
26+
#[ORM\OneToMany(targetEntity: EntityInheritance_MappedSuperclassTranslation::class, mappedBy: 'entity')]
27+
private Collection $translations;
28+
29+
#[ORM\Column(type: 'string', nullable: true)]
30+
#[Polyglot\Translatable]
31+
protected TranslatableInterface|string|null $text = null;
32+
33+
public function __construct()
34+
{
35+
$this->translations = new ArrayCollection();
36+
}
37+
38+
public function getId(): ?int
39+
{
40+
return $this->id;
41+
}
42+
43+
public function setText(TranslatableInterface $text): void
44+
{
45+
$this->text = $text;
46+
}
47+
48+
public function getText(): TranslatableInterface|string|null
49+
{
50+
return $this->text;
51+
}
52+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
namespace Webfactory\Bundle\PolyglotBundle\Tests\Fixtures\Entity\EntityInheritance;
4+
5+
interface EntityInheritance_MappedSuperclassWithTranslationsInterface
6+
{
7+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
namespace Webfactory\Bundle\PolyglotBundle\Tests\Fixtures\Entity\EntityInheritance;
4+
5+
use Doctrine\ORM\Mapping as ORM;
6+
use Webfactory\Bundle\PolyglotBundle\Attribute as Polyglot;
7+
8+
#[Polyglot\Locale(primary: 'en_GB')]
9+
#[ORM\Entity]
10+
class EntityInheritance_MappedSuperclassWithTranslations_Entity extends EntityInheritance_MappedSuperclassWithTranslations
11+
{
12+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<?php
2+
3+
namespace Webfactory\Bundle\PolyglotBundle\Tests\Functional;
4+
5+
use Doctrine\ORM\Tools\ResolveTargetEntityListener;
6+
use Webfactory\Bundle\PolyglotBundle\Tests\Fixtures\Entity\EntityInheritance\EntityInheritance_MappedSuperclassTranslation;
7+
use Webfactory\Bundle\PolyglotBundle\Tests\Fixtures\Entity\EntityInheritance\EntityInheritance_MappedSuperclassWithTranslations;
8+
use Webfactory\Bundle\PolyglotBundle\Tests\Fixtures\Entity\EntityInheritance\EntityInheritance_MappedSuperclassWithTranslations_Entity;
9+
use Webfactory\Bundle\PolyglotBundle\Tests\Fixtures\Entity\EntityInheritance\EntityInheritance_MappedSuperclassWithTranslationsInterface;
10+
use Webfactory\Bundle\PolyglotBundle\Translatable;
11+
12+
class MappedSuperclassWithTranslationsTest extends DatabaseFunctionalTestCase
13+
{
14+
protected function setUp(): void
15+
{
16+
parent::setUp();
17+
18+
$resolveTargetEntity = new ResolveTargetEntityListener();
19+
$resolveTargetEntity->addResolveTargetEntity(
20+
EntityInheritance_MappedSuperclassWithTranslationsInterface::class,
21+
EntityInheritance_MappedSuperclassWithTranslations_Entity::class,
22+
[]
23+
);
24+
25+
$this->entityManager->getEventManager()->addEventSubscriber($resolveTargetEntity);
26+
27+
self::setupSchema([
28+
EntityInheritance_MappedSuperclassWithTranslations::class,
29+
EntityInheritance_MappedSuperclassWithTranslations_Entity::class,
30+
EntityInheritance_MappedSuperclassTranslation::class,
31+
]);
32+
}
33+
34+
public function testPersistAndReloadEntity(): void
35+
{
36+
$entity = new EntityInheritance_MappedSuperclassWithTranslations_Entity();
37+
$t = new Translatable('base text');
38+
$t->setTranslation('Basistext', 'de_DE');
39+
$entity->setText($t);
40+
41+
self::import([$entity]);
42+
43+
$loaded = $this->entityManager->find(EntityInheritance_MappedSuperclassWithTranslations_Entity::class, $entity->getId());
44+
45+
self::assertSame('base text', $loaded->getText()->translate('en_GB'));
46+
self::assertSame('Basistext', $loaded->getText()->translate('de_DE'));
47+
}
48+
49+
public function testAddTranslation(): void
50+
{
51+
$entityManager = $this->entityManager;
52+
53+
$entity = new EntityInheritance_MappedSuperclassWithTranslations_Entity();
54+
$entity->setText(new Translatable('base text'));
55+
self::import([$entity]);
56+
57+
$loaded = $entityManager->find(EntityInheritance_MappedSuperclassWithTranslations_Entity::class, $entity->getId());
58+
$loaded->getText()->setTranslation('Basistext', 'de_DE');
59+
$entityManager->flush();
60+
61+
$entityManager->clear();
62+
$reloaded = $entityManager->find(EntityInheritance_MappedSuperclassWithTranslations_Entity::class, $entity->getId());
63+
64+
self::assertSame('base text', $reloaded->getText()->translate('en_GB'));
65+
self::assertSame('Basistext', $reloaded->getText()->translate('de_DE'));
66+
}
67+
68+
public function testUpdateTranslations(): void
69+
{
70+
$entityManager = $this->entityManager;
71+
72+
$entity = new EntityInheritance_MappedSuperclassWithTranslations_Entity();
73+
$t = new Translatable('old text');
74+
$t->setTranslation('alter Text', 'de_DE');
75+
$entity->setText($t);
76+
self::import([$entity]);
77+
78+
$loaded = $entityManager->find(EntityInheritance_MappedSuperclassWithTranslations_Entity::class, $entity->getId());
79+
$loaded->getText()->setTranslation('new text');
80+
$loaded->getText()->setTranslation('neuer Text', 'de_DE');
81+
$entityManager->flush();
82+
83+
$entityManager->clear();
84+
$reloaded = $entityManager->find(EntityInheritance_MappedSuperclassWithTranslations_Entity::class, $entity->getId());
85+
86+
self::assertSame('new text', $reloaded->getText()->translate('en_GB'));
87+
self::assertSame('neuer Text', $reloaded->getText()->translate('de_DE'));
88+
}
89+
}

0 commit comments

Comments
 (0)