Skip to content

Commit

Permalink
Test that Translatables can be used for values beyond strings, e. g…
Browse files Browse the repository at this point in the history
…. objects (#30)

To avoid side effects during Doctrine ORM PHP-value-to-database-value
conversion, this PR in part reverts design changes from #28:

On the `preFlush` event, remove all `PersistentTranslatable` instances
from managed entities and replace them with their plain (primary)
values. On `postFlush`, but the `PersistentTranslatable`s back in place.

That way, the default (plain) values are in place when the ORM does its
change detection.

Previously, in fact `PersistentTranslatables` were casted to string when
the ORM gathered the values to insert into the database. That does not
work when the column type is `object`, since it would try to serialize
the `PersistentTranslatable` itself.
  • Loading branch information
mpdude authored Jan 9, 2024
1 parent fe96e07 commit f7b186f
Show file tree
Hide file tree
Showing 4 changed files with 285 additions and 53 deletions.
54 changes: 14 additions & 40 deletions src/Doctrine/PersistentTranslatable.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,6 @@ final class PersistentTranslatable implements TranslatableInterface
*/
private mixed $primaryValue;

/**
* Whether original entity data was loaded by the ORM.
*/
private bool $hasOriginalEntityData;

/**
* The original field value loaded by the ORM.
*/
private mixed $originalEntityData;

private LoggerInterface $logger;

/**
Expand Down Expand Up @@ -91,50 +81,34 @@ public function __construct(
$this->oid = spl_object_id($entity);
$this->logger = $logger ?? new NullLogger();

$data = $this->unitOfWork->getOriginalEntityData($entity);

if ($data) {
$fieldName = $this->translatedProperty->getName();
$this->hasOriginalEntityData = true;
$this->originalEntityData = $data[$fieldName];

// Set $this as the "original entity data", so Doctrine ORM
// change detection will not treat this new value as a relevant change
$this->unitOfWork->setOriginalEntityProperty($this->oid, $fieldName, $this);
} else {
$this->hasOriginalEntityData = false;
}

$currentValue = $this->translatedProperty->getValue($this->entity);

if ($currentValue instanceof Translatable) {
$currentValue->copy($this);
} else {
$this->primaryValue = $currentValue;
}

$this->translatedProperty->setValue($this->entity, $this);
}

public function setPrimaryValue(mixed $value): void
{
$this->primaryValue = $value;
}

if (!$this->hasOriginalEntityData) {
return;
}

$fieldName = $this->translatedProperty->getName();
/**
* @psalm-internal Webfactory\Bundle\PolyglotBundle
*/
public function eject(): void
{
$this->translatedProperty->setValue($this->entity, $this->primaryValue);
}

if ($value !== $this->originalEntityData) {
// Reset original entity data for the property where this PersistentTranslatable instance
// is being used. This way, on changeset computation in the ORM, the original data will mismatch
// the current value (which is $this object!). This will make $this->entity show up in the list
// of entity updates in the UoW.
$this->unitOfWork->setOriginalEntityProperty($this->oid, $fieldName, $this->originalEntityData);
} else {
$this->unitOfWork->setOriginalEntityProperty($this->oid, $fieldName, $this);
}
/**
* @psalm-internal Webfactory\Bundle\PolyglotBundle
*/
public function inject(): void
{
$this->translatedProperty->setValue($this->entity, $this);
}

private function getTranslationEntity(string $locale): ?object
Expand Down
64 changes: 54 additions & 10 deletions src/Doctrine/PolyglotListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@
use Doctrine\Common\Annotations\Reader;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Event\PostFlushEventArgs;
use Doctrine\ORM\Event\PreFlushEventArgs;
use Doctrine\Persistence\Event\LifecycleEventArgs;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use WeakReference;
use Webfactory\Bundle\PolyglotBundle\Locale\DefaultLocaleProvider;

final class PolyglotListener implements EventSubscriber
Expand All @@ -36,6 +39,16 @@ final class PolyglotListener implements EventSubscriber
*/
private array $translatedClasses = [];

/**
* @var array<WeakReference>
*/
private array $entitiesWithTranslatables = [];

/**
* @var list<PersistentTranslatable>
*/
private array $ejectedTranslatables = [];

public function __construct(
private readonly Reader $annotationReader,
private readonly DefaultLocaleProvider $defaultLocaleProvider,
Expand All @@ -48,29 +61,60 @@ public function getSubscribedEvents(): array
return [
'prePersist',
'postLoad',
'preFlush',
'postFlush',
];
}

public function postLoad(LifecycleEventArgs $event): void
{
// Called when the entity has been hydrated
$objectManager = $event->getObjectManager();
$object = $event->getObject();
$this->injectPersistentTranslatables($event->getObjectManager(), $event->getObject());
}

public function prePersist(LifecycleEventArgs $event): void
{
// Called when a new entity is passed to persist() for the first time
$this->injectPersistentTranslatables($event->getObjectManager(), $event->getObject());
}

foreach ($this->getTranslationMetadatas($object, $objectManager) as $tm) {
$tm->injectPersistentTranslatables($object, $objectManager, $this->defaultLocaleProvider);
private function injectPersistentTranslatables(EntityManager $entityManager, object $object): void
{
$hasTranslatables = false;

foreach ($this->getTranslationMetadatas($object, $entityManager) as $tm) {
$tm->injectNewPersistentTranslatables($object, $entityManager, $this->defaultLocaleProvider);
$hasTranslatables = true;
}

if ($hasTranslatables) {
$this->entitiesWithTranslatables[] = WeakReference::create($object);
}
}

public function prePersist(LifecycleEventArgs $event): void
public function preFlush(PreFlushEventArgs $event): void
{
// Called before a new entity is persisted for the first time
$objectManager = $event->getObjectManager();
$object = $event->getObject();
$em = $event->getEntityManager();

foreach ($this->entitiesWithTranslatables as $key => $weakRef) {
$object = $weakRef->get();
if (null === $object) {
unset($this->entitiesWithTranslatables[$key]);
continue;
}

foreach ($this->getTranslationMetadatas($object, $objectManager) as $tm) {
$tm->injectPersistentTranslatables($object, $objectManager, $this->defaultLocaleProvider);
foreach ($this->getTranslationMetadatas($object, $em) as $tm) {
$this->ejectedTranslatables = array_merge($this->ejectedTranslatables, $tm->ejectPersistentTranslatables($object));
}
}
}

public function postFlush(PostFlushEventArgs $event): void
{
foreach ($this->ejectedTranslatables as $persistentTranslatable) {
$persistentTranslatable->inject();
}
$this->ejectedTranslatables = [];
}

/**
Expand Down
23 changes: 20 additions & 3 deletions src/Doctrine/TranslatableClassMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
namespace Webfactory\Bundle\PolyglotBundle\Doctrine;

use Doctrine\Common\Annotations\Reader;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Mapping\ClassMetadataFactory;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
Expand Down Expand Up @@ -271,10 +270,10 @@ private function parseTranslationsEntity(Reader $reader, ClassMetadataInfo $cm):
* For a given entity, find all @Translatable fields that contain new (not yet persisted)
* Translatable objects and replace those with PersistentTranslatable.
*/
public function injectPersistentTranslatables(object $entity, EntityManager $entityManager, DefaultLocaleProvider $defaultLocaleProvider): void
public function injectNewPersistentTranslatables(object $entity, EntityManager $entityManager, DefaultLocaleProvider $defaultLocaleProvider): void
{
foreach ($this->translatedProperties as $fieldName => $property) {
new PersistentTranslatable(
$persistentTranslatable = new PersistentTranslatable(
$entityManager->getUnitOfWork(),
$this->class,
$entity,
Expand All @@ -288,6 +287,24 @@ public function injectPersistentTranslatables(object $entity, EntityManager $ent
$property,
$this->logger
);
$persistentTranslatable->inject();
}
}

/**
* @return list<PersistentTranslatable>
*/
public function ejectPersistentTranslatables(object $entity): array
{
$ejectedTranslatables = [];

foreach ($this->translatedProperties as $property) {
$persistentTranslatable = $property->getValue($entity);
\assert($persistentTranslatable instanceof PersistentTranslatable);
$persistentTranslatable->eject();
$ejectedTranslatables[] = $persistentTranslatable;
}

return $ejectedTranslatables;
}
}
Loading

0 comments on commit f7b186f

Please sign in to comment.