diff --git a/CHANGELOG.md b/CHANGELOG.md index adf9836..cd4138f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/composer.json b/composer.json index 6d55eb3..7e0f7d6 100644 --- a/composer.json +++ b/composer.json @@ -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": { diff --git a/src/Configuration/ClassToGenerate.php b/src/Configuration/ClassToGenerate.php index d01cd4c..7aaa4fc 100644 --- a/src/Configuration/ClassToGenerate.php +++ b/src/Configuration/ClassToGenerate.php @@ -62,6 +62,7 @@ public function addGroupCombination(GroupCombination $groupCombination): void $this->groupCombinations[] = $groupCombination; } + #[\ReturnTypeWillChange] public function getIterator() { if ($this->groupCombinations) { diff --git a/src/Configuration/GeneratorConfiguration.php b/src/Configuration/GeneratorConfiguration.php index 8d1c406..4dbc872 100644 --- a/src/Configuration/GeneratorConfiguration.php +++ b/src/Configuration/GeneratorConfiguration.php @@ -107,6 +107,7 @@ public function getDefaultGroupCombinations(ClassToGenerate $classToGenerate): a }, $this->defaultGroupCombinations); } + #[\ReturnTypeWillChange] public function getIterator() { return new \ArrayIterator($this->classesToGenerate); diff --git a/src/DeserializerGenerator.php b/src/DeserializerGenerator.php index 93cf97e..606de9b 100644 --- a/src/DeserializerGenerator.php +++ b/src/DeserializerGenerator.php @@ -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'); @@ -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); @@ -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; + } } diff --git a/src/SerializerGenerator.php b/src/SerializerGenerator.php index e6a0651..bc748a5 100644 --- a/src/SerializerGenerator.php +++ b/src/SerializerGenerator.php @@ -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 diff --git a/src/Template/Deserialization.php b/src/Template/Deserialization.php index 5bcd3a5..00b1d91 100644 --- a/src/Template/Deserialization.php +++ b/src/Template/Deserialization.php @@ -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' @@ -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 */ diff --git a/src/Template/Serialization.php b/src/Template/Serialization.php index d1a6af7..8e8225f 100644 --- a/src/Template/Serialization.php +++ b/src/Template/Serialization.php @@ -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}} } @@ -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}} } } @@ -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, [ diff --git a/tests/Fixtures/ListModel.php b/tests/Fixtures/ListModel.php index b70aad8..d0f86ad 100644 --- a/tests/Fixtures/ListModel.php +++ b/tests/Fixtures/ListModel.php @@ -4,6 +4,7 @@ namespace Tests\Liip\Serializer\Fixtures; +use Doctrine\Common\Collections\Collection; use JMS\Serializer\Annotation as Serializer; class ListModel @@ -33,6 +34,20 @@ class ListModel */ public $optionalList; + /** + * @var string[]|Collection|null + * + * @Serializer\Type("ArrayCollection") + */ + public $collection; + + /** + * @var Nested[string]|Collection|null + * + * @Serializer\Type("ArrayCollection") + */ + public $collectionNested; + public function getOptionalList() { return $this->optionalList ?: null; diff --git a/tests/Unit/DeserializerGeneratorTest.php b/tests/Unit/DeserializerGeneratorTest.php index abe9a62..46c1e4f 100644 --- a/tests/Unit/DeserializerGeneratorTest.php +++ b/tests/Unit/DeserializerGeneratorTest.php @@ -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; @@ -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 */ @@ -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 diff --git a/tests/Unit/SerializerGeneratorTest.php b/tests/Unit/SerializerGeneratorTest.php index 1123167..5c3429b 100644 --- a/tests/Unit/SerializerGeneratorTest.php +++ b/tests/Unit/SerializerGeneratorTest.php @@ -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; @@ -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'], @@ -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);