diff --git a/src/Bundle/Resources/config/services.xml b/src/Bundle/Resources/config/services.xml index 5cbe5dc..ed7726e 100644 --- a/src/Bundle/Resources/config/services.xml +++ b/src/Bundle/Resources/config/services.xml @@ -12,6 +12,8 @@ + + @@ -29,6 +31,7 @@ + diff --git a/src/Normalizer/TraversableNormalizer.php b/src/Normalizer/TraversableNormalizer.php new file mode 100644 index 0000000..7332f2c --- /dev/null +++ b/src/Normalizer/TraversableNormalizer.php @@ -0,0 +1,52 @@ + true, + ]; + } + + public function normalize(mixed $object, ?string $format = null, array $context = []): array + { + $result = []; + foreach (iterator_to_array($object) as $key => $item) { + $result[$key] = $this->normalizer->normalize($item, $format, $context); + } + return $result; + } + + public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool + { + return $data instanceof Traversable; + } + + public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): object + { + $result = []; + foreach ($data as $key => $item) { + $result[$key] = $this->denormalizer->denormalize($item, $type, $format, $context); + } + return new $type($result); + } + + public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool + { + return is_array($data) && is_subclass_of($type, Traversable::class); + } +} diff --git a/src/Serializer.php b/src/Serializer.php index c820c5c..15f3477 100644 --- a/src/Serializer.php +++ b/src/Serializer.php @@ -18,6 +18,7 @@ final class Serializer extends BaseSerializer { use SerializerTrait; + private const CONTEXT_SERIALIZER = '#serializer'; private const KEY_TYPE = '#type'; private const KEY_SCALAR = '#scalar'; } @@ -27,6 +28,7 @@ final class Serializer extends BaseSerializer { use TypedSerializerTrait; + private const CONTEXT_SERIALIZER = '#serializer'; private const KEY_TYPE = '#type'; private const KEY_SCALAR = '#scalar'; } diff --git a/src/SerializerTrait.php b/src/SerializerTrait.php index 8c98237..15e8484 100644 --- a/src/SerializerTrait.php +++ b/src/SerializerTrait.php @@ -41,53 +41,77 @@ public function __construct(array $normalizers = [], array $encoders = [], ?Type * @param mixed $data * @param string|null $format * - * @return array|\ArrayObject|bool|float|int|string|null + * @return array|\ArrayObject|scalar|null */ public function normalize($data, $format = null, array $context = []) { - $normalizedData = parent::normalize($data, $format, $context); + if (\is_array($data)) { + $normData = []; + foreach ($data as $idx => $datum) { + $normData[$idx] = $this->normalize($datum, $format, $context); + } + + return $normData; + } if (\is_object($data)) { $typeName = \get_class($data); + $normData = parent::normalize($data, $format, $context + [self::CONTEXT_SERIALIZER => $this]); + if ($this->typeMapper) { $typeName = $this->typeMapper->getTypeByClass($typeName); } $typeData = [self::KEY_TYPE => $typeName]; - $valueData = is_scalar($normalizedData) ? [self::KEY_SCALAR => $normalizedData] : $normalizedData; - $normalizedData = array_merge($typeData, $valueData); + + if (\is_array($normData) && !isset($normData[self::KEY_TYPE])) { + $normData = $this->normalize($normData, $format, $context); + } + if (\is_scalar($normData)) { + $normData = [self::KEY_SCALAR => $normData]; + } + + return \array_merge($typeData, $normData); } - return $normalizedData; + return $data; } /** - * @param $data + * @param null|scalar|array $data + * @param string $type + * @param string|null $format * * @return mixed */ public function denormalize($data, $type, $format = null, array $context = []) { - if (\is_array($data) && (isset($data[self::KEY_TYPE]))) { + if (!\is_array($data)) { + return $data; + } + + if (isset($data[self::KEY_TYPE])) { $keyType = $data[self::KEY_TYPE]; + unset($data[self::KEY_TYPE]); if ($this->typeMapper) { $keyType = $this->typeMapper->getClassByType($keyType); } - unset($data[self::KEY_TYPE]); - $data = $data[self::KEY_SCALAR] ?? $data; - $data = $this->denormalize($data, $keyType, $format, $context); - return parent::denormalize($data, $keyType, $format, $context); - } + if (\is_array($data)) { + foreach ($data as $idx => $datum) { + $data[$idx] = $this->denormalize($datum, $keyType, $format, $context); + } + } - if (is_iterable($data)) { - $type = ('' === $type) ? 'stdClass' : $type; + return parent::denormalize($data, $keyType, $format, $context + [self::CONTEXT_SERIALIZER => $this]); + } - return parent::denormalize($data, $type.'[]', $format, $context); + foreach ($data as $idx => $datum) { + $data[$idx] = $this->denormalize($datum, '', $format, $context); } return $data; diff --git a/tests/Fixtures/Normalizer/VectorNormalizer.php b/tests/Fixtures/Normalizer/VectorNormalizer.php new file mode 100644 index 0000000..355d600 --- /dev/null +++ b/tests/Fixtures/Normalizer/VectorNormalizer.php @@ -0,0 +1,52 @@ + true, + ]; + } + + public function normalize(mixed $object, ?string $format = null, array $context = []): array + { + if (!$this->supportsNormalization($object)) { + throw new InvalidArgumentException(sprintf('The object must be an instance of "%s".', Vector::class)); + } + + return ['position' => $object->key(), '[]' => $object->getArray()]; + } + + public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool + { + return $data instanceof Vector; + } + + public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): object + { + if (!$this->supportsDenormalization($data, $type)) { + throw NotNormalizableValueException::createForUnexpectedDataType('Data expected to be a array of shape {"position": int, "[]": array}.', $data, ['array'], $context['deserialization_path'] ?? null); + } + + return new Vector($data['[]'], $data['position']); + } + + public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool + { + return is_array($data) && is_a($type, Vector::class, true); + } +} diff --git a/tests/Fixtures/TestBundle/DependencyInjection/InjectCustomNormalizerPass.php b/tests/Fixtures/TestBundle/DependencyInjection/InjectCustomNormalizerPass.php index 77885d5..b7e0c4e 100644 --- a/tests/Fixtures/TestBundle/DependencyInjection/InjectCustomNormalizerPass.php +++ b/tests/Fixtures/TestBundle/DependencyInjection/InjectCustomNormalizerPass.php @@ -9,6 +9,7 @@ namespace Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\DependencyInjection; +use Dunglas\DoctrineJsonOdm\Tests\Fixtures\Normalizer\VectorNormalizer; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; @@ -21,9 +22,16 @@ public function process(ContainerBuilder $container): void { $container->setDefinition('dunglas_doctrine_json_odm.normalizer.custom', new Definition(CustomNormalizer::class)); + $vectorDefinition = new Definition(VectorNormalizer::class); + $vectorDefinition->addTag('serializer.normalizer'); + $container->setDefinition(VectorNormalizer::class, $vectorDefinition); + $serializerDefinition = $container->getDefinition('dunglas_doctrine_json_odm.serializer'); $arguments = $serializerDefinition->getArguments(); - $arguments[0] = array_merge([new Reference('dunglas_doctrine_json_odm.normalizer.custom')], $arguments[0]); + $arguments[0] = array_merge([ + new Reference(VectorNormalizer::class), + new Reference('dunglas_doctrine_json_odm.normalizer.custom'), + ], $arguments[0]); $serializerDefinition->setArguments($arguments); } } diff --git a/tests/Fixtures/TestBundle/Document/TraversableValue.php b/tests/Fixtures/TestBundle/Document/TraversableValue.php new file mode 100644 index 0000000..1e2a87e --- /dev/null +++ b/tests/Fixtures/TestBundle/Document/TraversableValue.php @@ -0,0 +1,43 @@ +array = $array; + } + + public function offsetExists(mixed $offset): bool + { + return array_key_exists($offset, $this->array); + } + + public function offsetGet(mixed $offset): mixed + { + return $this->array[$offset]; + } + + public function offsetSet(mixed $offset, mixed $value): void + { + $this->array[$offset] = $value; + } + + public function offsetUnset(mixed $offset): void + { + unset($this->array[$offset]); + } + + public function getIterator(): Traversable + { + return new ArrayIterator($this->array); + } +} diff --git a/tests/Fixtures/TestBundle/Document/Vector.php b/tests/Fixtures/TestBundle/Document/Vector.php new file mode 100644 index 0000000..7e310d5 --- /dev/null +++ b/tests/Fixtures/TestBundle/Document/Vector.php @@ -0,0 +1,48 @@ +array = $array; + $this->position = $position; + } + + public function getArray(): array + { + return $this->array; + } + + public function current(): mixed + { + return $this->array[$this->key()]; + } + + public function key(): mixed + { + return $this->position; + } + + public function next(): void + { + ++$this->position; + } + + public function rewind(): void + { + $this->position = 0; + } + + public function valid(): bool + { + return isset($this->array[$this->key()]); + } +} diff --git a/tests/SerializerTest.php b/tests/SerializerTest.php index 29d9792..5e628f9 100644 --- a/tests/SerializerTest.php +++ b/tests/SerializerTest.php @@ -16,6 +16,8 @@ use Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Document\Bar; use Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Document\Baz; use Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Document\ScalarValue; +use Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Document\TraversableValue; +use Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Document\Vector; use Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Document\WithMappedType; use Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Entity\Foo; use Dunglas\DoctrineJsonOdm\Tests\Fixtures\TestBundle\Enum\InputMode; @@ -241,4 +243,39 @@ public function testSerializeUid(): void $this->assertEquals($value, $restoredValue); } + + /** Uses {@link VectorNormalizer} to normalize Vector, otherwise it will be treated as Traversable and fail */ + public function testSerializeObjectWithConfiguredNormalizer(): void + { + $serializer = self::$kernel->getContainer()->get('dunglas_doctrine_json_odm.serializer'); + + $attribute = new Attribute(); + $attribute->key = 'foo'; + $attribute->value = 'bar'; + + $vector = new Vector([$attribute, 2, 3, 4, 5]); + $vector->next(); + + $data = $serializer->serialize($vector, 'json'); + $restoredVector = $serializer->deserialize($data, '', 'json'); + + $this->assertEquals($vector, $restoredVector); + } + + /** {@see \Dunglas\DoctrineJsonOdm\Normalizer\TraversableNormalizer} */ + public function testTraversableNormalizer(): void + { + $serializer = self::$kernel->getContainer()->get('dunglas_doctrine_json_odm.serializer'); + + $attribute = new Attribute(); + $attribute->key = 'foo'; + $attribute->value = 'bar'; + + $vector = new TraversableValue([$attribute, 'x' => 2, 'y'=>[3], 'z'=>'4', 5, ['' => null]]); + + $data = $serializer->serialize($vector, 'json'); + $restoredVector = $serializer->deserialize($data, '', 'json'); + + $this->assertEquals($vector, $restoredVector); + } }