Skip to content

Commit

Permalink
Merge pull request #1357 from re2bit/master
Browse files Browse the repository at this point in the history
add support for persistent collections so orphan removal works
  • Loading branch information
goetas authored Nov 8, 2021
2 parents 832c8f2 + 1aa5bfa commit e2724bd
Show file tree
Hide file tree
Showing 6 changed files with 364 additions and 36 deletions.
6 changes: 6 additions & 0 deletions .run/phpunit.run.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="phpunit" type="PHPUnitRunConfigurationType" factoryName="PHPUnit">
<TestRunner configuration_file="$PROJECT_DIR$/phpunit.xml.dist" scope="XML" />
<method v="2" />
</configuration>
</component>
4 changes: 3 additions & 1 deletion src/Construction/DoctrineObjectConstructor.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
use JMS\Serializer\Metadata\PropertyMetadata;
use JMS\Serializer\Visitor\DeserializationVisitorInterface;

use function is_array;

/**
* Doctrine object constructor for new (or existing) objects during deserialization.
*/
Expand Down Expand Up @@ -79,7 +81,7 @@ public function construct(DeserializationVisitorInterface $visitor, ClassMetadat
}

// Managed entity, check for proxy load
if (!\is_array($data) && !(is_object($data) && 'SimpleXMLElement' === get_class($data))) {
if (!is_array($data) && !(is_object($data) && 'SimpleXMLElement' === get_class($data))) {
// Single identifier, load proxy
return $objectManager->getReference($metadata->name, $data);
}
Expand Down
87 changes: 73 additions & 14 deletions src/Handler/ArrayCollectionHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,40 @@
use Doctrine\ODM\MongoDB\PersistentCollection as MongoPersistentCollection;
use Doctrine\ODM\PHPCR\PersistentCollection as PhpcrPersistentCollection;
use Doctrine\ORM\PersistentCollection as OrmPersistentCollection;
use Doctrine\Persistence\ManagerRegistry;
use JMS\Serializer\DeserializationContext;
use JMS\Serializer\GraphNavigatorInterface;
use JMS\Serializer\Metadata\PropertyMetadata;
use JMS\Serializer\SerializationContext;
use JMS\Serializer\Visitor\DeserializationVisitorInterface;
use JMS\Serializer\Visitor\SerializationVisitorInterface;

final class ArrayCollectionHandler implements SubscribingHandlerInterface
{
public const COLLECTION_TYPES = [
'ArrayCollection',
ArrayCollection::class,
OrmPersistentCollection::class,
MongoPersistentCollection::class,
PhpcrPersistentCollection::class,
];

/**
* @var bool
*/
private $initializeExcluded;

public function __construct(bool $initializeExcluded = true)
{
/**
* @var ManagerRegistry|null
*/
private $managerRegistry;

public function __construct(
bool $initializeExcluded = true,
?ManagerRegistry $managerRegistry = null
) {
$this->initializeExcluded = $initializeExcluded;
$this->managerRegistry = $managerRegistry;
}

/**
Expand All @@ -34,15 +52,8 @@ public static function getSubscribingMethods()
{
$methods = [];
$formats = ['json', 'xml', 'yml'];
$collectionTypes = [
'ArrayCollection',
ArrayCollection::class,
OrmPersistentCollection::class,
MongoPersistentCollection::class,
PhpcrPersistentCollection::class,
];

foreach ($collectionTypes as $type) {

foreach (self::COLLECTION_TYPES as $type) {
foreach ($formats as $format) {
$methods[] = [
'direction' => GraphNavigatorInterface::DIRECTION_SERIALIZATION,
Expand Down Expand Up @@ -92,11 +103,59 @@ public function serializeCollection(SerializationVisitorInterface $visitor, Coll
/**
* @param mixed $data
*/
public function deserializeCollection(DeserializationVisitorInterface $visitor, $data, array $type, DeserializationContext $context): ArrayCollection
{
public function deserializeCollection(
DeserializationVisitorInterface $visitor,
$data,
array $type,
DeserializationContext $context
): Collection {
// See above.
$type['name'] = 'array';

return new ArrayCollection($visitor->visitArray($data, $type));
$elements = new ArrayCollection($visitor->visitArray($data, $type));

if (null === $this->managerRegistry) {
return $elements;
}

$propertyMetadata = $context->getMetadataStack()->top();
if (!$propertyMetadata instanceof PropertyMetadata) {
return $elements;
}

$objectManager = $this->managerRegistry->getManagerForClass($propertyMetadata->class);
if (null === $objectManager) {
return $elements;
}

$classMetadata = $objectManager->getClassMetadata($propertyMetadata->class);
$currentObject = $visitor->getCurrentObject();

if (
array_key_exists('name', $propertyMetadata->type)
&& in_array($propertyMetadata->type['name'], self::COLLECTION_TYPES)
&& $classMetadata->isCollectionValuedAssociation($propertyMetadata->name)
) {
$existingCollection = $classMetadata->getFieldValue($currentObject, $propertyMetadata->name);
if (!$existingCollection instanceof OrmPersistentCollection) {
return $elements;
}

foreach ($elements as $element) {
if (!$existingCollection->contains($element)) {
$existingCollection->add($element);
}
}

foreach ($existingCollection as $collectionElement) {
if (!$elements->contains($collectionElement)) {
$existingCollection->removeElement($collectionElement);
}
}

return $existingCollection;
}

return $elements;
}
}
70 changes: 70 additions & 0 deletions tests/Fixtures/Doctrine/PersistendCollection/App.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

declare(strict_types=1);

namespace JMS\Serializer\Tests\Fixtures\Doctrine\PersistendCollection;

use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer;

/** @ORM\Entity */
class App
{
/**
* @Serializer\SerializedName("id")
* @Serializer\Type("string")
* @ORM\Id
* @ORM\Column(type="string", name="id")
*
* @var string
*/
#[Serializer\Type(name: 'string')]
protected $id;

/**
* @Serializer\Type("string")
* @ORM\Column(type="string")
*
* @var string
*/
#[Serializer\Type(name: 'string')]
private $name;

/**
* @ORM\ManyToOne(targetEntity="SmartPhone")
*
* @var SmartPhone
*/
#[Serializer\Type(name: SmartPhone::class)]
private $smartPhone;

/**
* @param string $id
* @param string $name
* @param string $smartPhone
*/
public function __construct(
$id,
$name,
$smartPhone
) {
$this->id = $id;
$this->name = $name;
$this->smartPhone = $smartPhone;
}

public function getId(): string
{
return $this->id;
}

public function getName(): string
{
return $this->name;
}

public function getSmartPhone(): SmartPhone
{
return $this->smartPhone;
}
}
90 changes: 90 additions & 0 deletions tests/Fixtures/Doctrine/PersistendCollection/SmartPhone.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php

declare(strict_types=1);

namespace JMS\Serializer\Tests\Fixtures\Doctrine\PersistendCollection;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer;

/** @ORM\Entity */
class SmartPhone
{
/**
* @Serializer\SerializedName("id")
* @Serializer\Type("string")
* @ORM\Id
* @ORM\Column(type="string", name="id")
*
* @var string
*/
#[Serializer\Type(name: 'string')]
protected $id;

/**
* @Serializer\Type("string")
* @ORM\Column(type="string")
*
* @var string
*/
#[Serializer\Type(name: 'string')]
private $name;

/**
* @Serializer\Type("ArrayCollection<JMS\Serializer\Tests\Fixtures\Doctrine\PersistendCollection\App>")
* @Serializer\SerializedName("applications")
* @ORM\OneToMany (targetEntity="App", mappedBy="smartPhone", cascade={"persist"}, orphanRemoval=true)
*
* @var ArrayCollection<int, App>
*/
#[Serializer\Type(name: 'ArrayCollection<JMS\Serializer\Tests\Fixtures\Doctrine\PersistendCollection\App>')]
private $apps;

/**
* @param string $name
* @param string $phoneId
*/
public function __construct($name, $phoneId)
{
$this->name = $name;
$this->id = $phoneId;
$this->apps = new ArrayCollection();
}

public function getName(): string
{
return $this->name;
}

public function getId(): string
{
return $this->id;
}

public function addApp(App $app): void
{
$this->apps[] = $app;
}

/**
* @param Criteria|null $criteria
*
* @return Collection<int, App>
*/
public function getApps(?Criteria $criteria = null): Collection
{
if (null === $criteria) {
$criteria = Criteria::create();
}

return $this->apps->matching($criteria);
}

public function getAppsRaw(): Collection
{
return $this->apps;
}
}
Loading

0 comments on commit e2724bd

Please sign in to comment.