Skip to content

Commit

Permalink
Add support for (de-)serializing doctrine collections (#29)
Browse files Browse the repository at this point in the history
* Add support for (de-)serializing doctrine collections

* Check in generated code if property accessor is a collection

Doing the check on demand in the generated code gives the benefit, that a property that can include both, an `array` and a doctrine `Collection`, will always be serialized properly.

* Add `ReturnTypeWillChange` attribute to `getIterator` methods

As this library still supports PHP versions where the `getIterator` does not declare a return type, we have to add the `ReturnTypeWillChange` to suppress the deprecation notice.
  • Loading branch information
Spea authored Dec 13, 2022
1 parent 2e0314d commit f3138ad
Show file tree
Hide file tree
Showing 11 changed files with 122 additions and 16 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

* Add support for generating recursive code up to a specified maximum depth
that can be defined via the `@MaxDepth` annotation/attribute from JMS
* Add support for (de-)serializing doctrine collections

# 2.0.6

Expand Down
7 changes: 4 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@
"twig/twig": "^2.7 || ^3.0"
},
"require-dev": {
"doctrine/collections": "^1.6",
"friendsofphp/php-cs-fixer": "^2.14",
"jms/serializer": "^1.13 || ^2 || ^3",
"phpstan/phpstan": "^0.12.0",
"phpstan/phpstan-phpunit": "^0.12",
"phpunit/phpunit": "^8.0",
"friendsofphp/php-cs-fixer": "^2.14",
"jms/serializer": "^1.13 || ^2 || ^3"
"phpunit/phpunit": "^8.0"
},
"autoload": {
"psr-4": {
Expand Down
1 change: 1 addition & 0 deletions src/Configuration/ClassToGenerate.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ public function addGroupCombination(GroupCombination $groupCombination): void
$this->groupCombinations[] = $groupCombination;
}

#[\ReturnTypeWillChange]
public function getIterator()
{
if ($this->groupCombinations) {
Expand Down
1 change: 1 addition & 0 deletions src/Configuration/GeneratorConfiguration.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ public function getDefaultGroupCombinations(ClassToGenerate $classToGenerate): a
}, $this->defaultGroupCombinations);
}

#[\ReturnTypeWillChange]
public function getIterator()
{
return new \ArrayIterator($this->classesToGenerate);
Expand Down
40 changes: 30 additions & 10 deletions src/DeserializerGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -213,18 +213,14 @@ private function generateInnerCodeForFieldType(
): string {
$type = $propertyMetadata->getType();

if ($type instanceof PropertyTypeArray) {
if ($type->getSubType() instanceof PropertyTypePrimitive) {
// for arrays of scalars, copy the field even when its an empty array
return $this->templating->renderAssignJsonDataToField((string) $modelPropertyPath, (string) $arrayPath);
}
switch ($type) {
case $type instanceof PropertyTypeArray:
if ($type->isCollection()) {
return $this->generateCodeForArrayCollection($propertyMetadata, $type, $arrayPath, $modelPropertyPath, $stack);
}

// either array or hashmap with second param the type of values
// the index works the same whether its numeric or hashmap
return $this->generateCodeForArray($type, $arrayPath, $modelPropertyPath, $stack);
}
return $this->generateCodeForArray($type, $arrayPath, $modelPropertyPath, $stack);

switch ($type) {
case $type instanceof PropertyTypeDateTime:
if (null !== $type->getZone()) {
throw new \RuntimeException('Timezone support is not implemented');
Expand Down Expand Up @@ -257,6 +253,11 @@ private function generateCodeForArray(
ModelPath $modelPath,
array $stack
): string {
if ($type->getSubType() instanceof PropertyTypePrimitive) {
// for arrays of scalars, copy the field even when its an empty array
return $this->templating->renderAssignJsonDataToField((string) $modelPath, (string) $arrayPath);
}

$index = ModelPath::indexVariable((string) $arrayPath);
$arrayPropertyPath = $arrayPath->withVariable((string) $index);
$modelPropertyPath = $modelPath->withArray((string) $index);
Expand Down Expand Up @@ -284,4 +285,23 @@ private function generateCodeForArray(

return $code;
}

private function generateCodeForArrayCollection(
PropertyMetadata $propertyMetadata,
PropertyTypeArray $type,
ArrayPath $arrayPath,
ModelPath $modelPath,
array $stack
): string {
$tmpVariable = ModelPath::tempVariable([(string) $modelPath, $propertyMetadata->getName()]);
$innerCode = $this->generateCodeForArray($type, $arrayPath, $tmpVariable, $stack);

if ('' === $innerCode) {
return '';
}

$code = $innerCode . $this->templating->renderArrayCollection((string) $modelPath, (string) $tmpVariable);

return $code;
}
}
2 changes: 1 addition & 1 deletion src/SerializerGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ private function generateCodeForFieldType(
if ($type instanceof PropertyTypeArray) {
if ($type->getSubType() instanceof PropertyTypePrimitive) {
// for arrays of scalars, copy the field even when its an empty array
return $this->templating->renderAssign($fieldPath, $modelPropertyPath);
return $this->templating->renderArrayAssign($fieldPath, $modelPropertyPath);
}

// either array or hashmap with second param the type of values
Expand Down
13 changes: 13 additions & 0 deletions src/Template/Deserialization.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ function {{functionName}}(array {{jsonPath}}): {{className}}
{{code}}
}

EOT;

private const TMPL_ARRAY_COLLECTION = <<<'EOT'
{{modelPath}} = new \Doctrine\Common\Collections\ArrayCollection({{tmpVariable}});

EOT;

private const TMPL_UNSET = <<<'EOT'
Expand Down Expand Up @@ -237,6 +242,14 @@ public function renderLoop(string $jsonPath, string $indexVariable, string $code
]);
}

public function renderArrayCollection(string $modelPath, string $tmpVariable): string
{
return $this->render(self::TMPL_ARRAY_COLLECTION, [
'modelPath' => $modelPath,
'tmpVariable' => $tmpVariable,
]);
}

/**
* @param string[] $variableNames
*/
Expand Down
30 changes: 28 additions & 2 deletions src/Template/Serialization.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,24 @@ function {{functionName}}({{className}} $model, bool $useStdClass = true)

private const TMPL_ASSIGN = <<<'EOT'
$jsonData{{jsonPath}} = {{propertyAccessor}};
EOT;

private const TMPL_ARRAY_ASSIGN = <<<'EOT'
if ({{propertyAccessor}} instanceof \Doctrine\Common\Collections\Collection) {
$jsonData{{jsonPath}} = {{propertyAccessor}}->toArray();
} else {
$jsonData{{jsonPath}} = {{propertyAccessor}};
}
EOT;

private const TMPL_LOOP_ARRAY = <<<'EOT'
{{indexVariable}}Array = {{propertyAccessor}};
if ({{propertyAccessor}} instanceof \Doctrine\Common\Collections\Collection) {
{{indexVariable}}Array = {{propertyAccessor}}->toArray();
}
$jsonData{{jsonPath}} = [];
foreach (array_keys({{propertyAccessor}}) as {{indexVariable}}) {
foreach (array_keys({{indexVariable}}Array) as {{indexVariable}}) {
{{code}}
}

Expand All @@ -61,7 +74,12 @@ function {{functionName}}({{className}} $model, bool $useStdClass = true)
if (0 === \count({{propertyAccessor}})) {
$jsonData{{jsonPath}} = $emptyHashmap;
} else {
foreach (array_keys({{propertyAccessor}}) as {{indexVariable}}) {
{{indexVariable}}Array = {{propertyAccessor}};
if ({{propertyAccessor}} instanceof \Doctrine\Common\Collections\Collection) {
{{indexVariable}}Array = {{propertyAccessor}}->toArray();
}
foreach (array_keys({{indexVariable}}Array) as {{indexVariable}}) {
{{code}}
}
}
Expand Down Expand Up @@ -121,6 +139,14 @@ public function renderAssign(string $jsonPath, string $propertyAccessor): string
]);
}

public function renderArrayAssign(string $jsonPath, string $propertyAccessor): string
{
return $this->render(self::TMPL_ARRAY_ASSIGN, [
'jsonPath' => $jsonPath,
'propertyAccessor' => $propertyAccessor,
]);
}

public function renderLoopArray(string $jsonPath, string $propertyAccessor, string $indexVariable, string $code): string
{
return $this->render(self::TMPL_LOOP_ARRAY, [
Expand Down
15 changes: 15 additions & 0 deletions tests/Fixtures/ListModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Tests\Liip\Serializer\Fixtures;

use Doctrine\Common\Collections\Collection;
use JMS\Serializer\Annotation as Serializer;

class ListModel
Expand Down Expand Up @@ -33,6 +34,20 @@ class ListModel
*/
public $optionalList;

/**
* @var string[]|Collection|null
*
* @Serializer\Type("ArrayCollection<string>")
*/
public $collection;

/**
* @var Nested[string]|Collection|null
*
* @Serializer\Type("ArrayCollection<string, Tests\Liip\Serializer\Fixtures\Nested>")
*/
public $collectionNested;

public function getOptionalList()
{
return $this->optionalList ?: null;
Expand Down
20 changes: 20 additions & 0 deletions tests/Unit/DeserializerGeneratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Tests\Liip\Serializer\Unit;

use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\Common\Collections\ArrayCollection;
use Liip\MetadataParser\Builder;
use Liip\MetadataParser\ModelParser\JMSParser;
use Liip\MetadataParser\ModelParser\PhpDocParser;
Expand Down Expand Up @@ -83,6 +84,11 @@ public function testLists(): void
['nested_string' => 'nested1'],
['nested_string' => 'nested2'],
],
'collection' => ['entry', 'second entry'],
'collection_nested' => [
'first' => ['nested_string' => 'nested3'],
'second' => ['nested_string' => 'nested4'],
],
];

/** @var ListModel $model */
Expand All @@ -91,10 +97,24 @@ public function testLists(): void
static::assertSame(['a', 'b'], $model->array);
static::assertIsArray($model->listNested);
static::assertCount(2, $model->listNested);

foreach ($model->listNested as $index => $nested) {
static::assertInstanceOf(Nested::class, $nested);
static::assertSame('nested'.($index + 1), $nested->nestedString);
}

static::assertInstanceOf(ArrayCollection::class, $model->collection);
static::assertCount(2, $model->collection);
static::assertSame(['entry', 'second entry'], $model->collection->toArray());

static::assertInstanceOf(ArrayCollection::class, $model->collectionNested);
static::assertCount(2, $model->collectionNested);
static::assertArrayHasKey('first', $model->collectionNested);
static::assertSame('nested3', $model->collectionNested['first']->nestedString);

static::assertArrayHasKey('second', $model->collectionNested);
static::assertSame('nested4', $model->collectionNested['second']->nestedString);

}

public function testRecursion(): void
Expand Down
8 changes: 8 additions & 0 deletions tests/Unit/SerializerGeneratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Tests\Liip\Serializer\Unit;

use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\Common\Collections\ArrayCollection;
use Liip\MetadataParser\Builder;
use Liip\MetadataParser\ModelParser\JMSParser;
use Liip\MetadataParser\ModelParser\PhpDocParser;
Expand Down Expand Up @@ -102,6 +103,8 @@ public function testArrays(): void
new Nested('opt1'),
new Nested('opt2'),
];
$list->collection = new ArrayCollection(['a', 'b']);
$list->collectionNested = new ArrayCollection(['a' => new Nested('nested1'), 'b' => new Nested('nested2')]);

$expected = [
'array' => ['a', 'b'],
Expand All @@ -113,6 +116,11 @@ public function testArrays(): void
['nested_string' => 'opt1'],
['nested_string' => 'opt2'],
],
'collection' => ['a', 'b'],
'collection_nested' => [
'a' => ['nested_string' => 'nested1'],
'b' => ['nested_string' => 'nested2'],
],
];

$data = $functionName($list);
Expand Down

0 comments on commit f3138ad

Please sign in to comment.