Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions src/Configuration/GeneratorConfiguration.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

namespace Liip\Serializer\Configuration;

use Liip\Serializer\DeserializerHandlerInterface;
use Liip\Serializer\SerializerHandlerInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
Expand Down Expand Up @@ -151,6 +153,28 @@ public function shouldAllowGenericArrays(): bool
return $this->options['allow_generic_arrays'];
}

public function findSerializerHandlerForClass(string $className): ?SerializerHandlerInterface
{
foreach ($this->options['handlers'] as $handler) {
if ($handler instanceof SerializerHandlerInterface && $this->supportsClass($handler->getSerializationClasses(), $className)) {
return $handler;
}
}

return null;
}

public function findDeserializerHandlerForClass(string $className): ?DeserializerHandlerInterface
{
foreach ($this->options['handlers'] as $handler) {
if ($handler instanceof DeserializerHandlerInterface && $this->supportsClass($handler->getDeserializationClasses(), $className)) {
return $handler;
}
}

return null;
}

public function getIterator(): \Traversable
{
return new \ArrayIterator($this->classesToGenerate);
Expand All @@ -166,10 +190,26 @@ private function resolveOptions(array $options): array
$resolver = new OptionsResolver();
$resolver->setDefaults([
'allow_generic_arrays' => false,
'handlers' => [],
]);

$resolver->setAllowedTypes('allow_generic_arrays', 'boolean');
$resolver->setAllowedTypes('handlers', 'array');

return $resolver->resolve($options);
}

/**
* @param string[] $supportedClasses
*/
private function supportsClass(array $supportedClasses, string $actualClass): bool
{
foreach ($supportedClasses as $supportedClass) {
if (is_a($actualClass, $supportedClass, true)) {
return true;
}
}

return false;
}
}
14 changes: 10 additions & 4 deletions src/DeserializerGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,18 @@ private function generateCodeForClass(
ModelPath $modelPath,
array $stack = [],
): string {
$className = $classMetadata->getClassName();
$handler = $this->configuration->findDeserializerHandlerForClass($className);
if (null !== $handler) {
return $this->templating->renderAssignJsonDataToField((string) $modelPath, $handler->generateDeserializeExpression($className, (string) $arrayPath));
}

$discriminatorMetadata = $classMetadata->getDiscriminatorMetadata();
if (null !== $discriminatorMetadata && $discriminatorMetadata->baseClass == $classMetadata->getClassName()) {
if (null !== $discriminatorMetadata && $discriminatorMetadata->baseClass == $className) {
return $this->generateCodeForDiscriminatorClass($classMetadata, $arrayPath, $modelPath, $stack);
}

$stack[$classMetadata->getClassName()] = ($stack[$classMetadata->getClassName()] ?? 0) + 1;
$stack[$className] = ($stack[$className] ?? 0) + 1;

$constructorArgumentNames = [];
$overwrittenNames = [];
Expand Down Expand Up @@ -136,7 +142,7 @@ private function generateCodeForClass(
continue;
}
if ($definition->isRequired()) {
$msg = \sprintf('Unknown constructor argument "%s". Class %s only has properties that tell how to handle %s.', $definition->getName(), $classMetadata->getClassName(), implode(', ', array_keys($constructorArgumentNames)));
$msg = \sprintf('Unknown constructor argument "%s". Class %s only has properties that tell how to handle %s.', $definition->getName(), $className, implode(', ', array_keys($constructorArgumentNames)));
if ($overwrittenNames) {
$msg .= \sprintf(' Multiple definitions for fields %s seen - the last one overwrites previous ones.', implode(', ', array_keys($overwrittenNames)));
}
Expand All @@ -148,7 +154,7 @@ private function generateCodeForClass(
$code .= $this->templating->renderUnset(array_values($constructorArgumentNames));
}

return $this->templating->renderClass((string) $modelPath, $classMetadata->getClassName(), $constructorArguments, $code, $initCode);
return $this->templating->renderClass((string) $modelPath, $className, $constructorArguments, $code, $initCode);
}

/**
Expand Down
18 changes: 18 additions & 0 deletions src/DeserializerHandlerInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace Liip\Serializer;

interface DeserializerHandlerInterface
{
/**
* Returns a PHP expression that evaluates to the deserialized object.
*/
public function generateDeserializeExpression(string $className, string $arrayPath): string;

/**
* @return string[]
*/
public function getDeserializationClasses(): array;
}
10 changes: 8 additions & 2 deletions src/SerializerGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -122,12 +122,18 @@ private function generateCodeForClass(
string $modelPath,
array $stack = [],
): string {
$className = $classMetadata->getClassName();
$handler = $this->configuration->findSerializerHandlerForClass($className);
if (null !== $handler) {
return $this->templating->renderAssign($arrayPath, $handler->generateSerializeExpression($className, $modelPath));
}

$discriminatorMetadata = $classMetadata->getDiscriminatorMetadata();
if (null !== $discriminatorMetadata && $discriminatorMetadata->baseClass == $classMetadata->getClassName()) {
if (null !== $discriminatorMetadata && $discriminatorMetadata->baseClass == $className) {
return $this->generateCodeForDiscriminatorClass($classMetadata, $apiVersion, $serializerGroups, $arrayPath, $modelPath, $stack);
}

$stack[$classMetadata->getClassName()] = ($stack[$classMetadata->getClassName()] ?? 0) + 1;
$stack[$className] = ($stack[$className] ?? 0) + 1;

$code = '';
foreach ($classMetadata->getProperties() as $propertyMetadata) {
Expand Down
18 changes: 18 additions & 0 deletions src/SerializerHandlerInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace Liip\Serializer;

interface SerializerHandlerInterface
{
/**
* Returns a PHP expression that evaluates to the serialized scalar value.
*/
public function generateSerializeExpression(string $className, string $modelPath): string;

/**
* @return string[]
*/
public function getSerializationClasses(): array;
}
24 changes: 24 additions & 0 deletions tests/Fixtures/CustomType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace Tests\Liip\Serializer\Fixtures;

class CustomType
{
public string $key = 'static value';

public function __construct(public readonly string $value)
{
}

public function getValue(): string
{
return $this->value;
}

public static function fromString(string $value): self
{
return new self($value);
}
}
31 changes: 31 additions & 0 deletions tests/Fixtures/CustomTypeHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace Tests\Liip\Serializer\Fixtures;

use Liip\Serializer\DeserializerHandlerInterface;
use Liip\Serializer\SerializerHandlerInterface;

class CustomTypeHandler implements SerializerHandlerInterface, DeserializerHandlerInterface
{
public function generateSerializeExpression(string $className, string $modelPath): string
{
return $modelPath.'->getValue()';
}

public function generateDeserializeExpression(string $className, string $arrayPath): string
{
return '\\'.CustomType::class.'::fromString('.$arrayPath.')';
}

public function getDeserializationClasses(): array
{
return [CustomType::class];
}

public function getSerializationClasses(): array
{
return [CustomType::class];
}
}
15 changes: 15 additions & 0 deletions tests/Fixtures/ModelWithCustomType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace Tests\Liip\Serializer\Fixtures;

use JMS\Serializer\Annotation as Serializer;

class ModelWithCustomType
{
public ?CustomType $value = null;

#[Serializer\Type('array<'.CustomType::class.'>')]
public ?array $values = null;
}
30 changes: 30 additions & 0 deletions tests/Unit/DeserializerGeneratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
use Tests\Liip\Serializer\Fixtures\BackedStringEnum;
use Tests\Liip\Serializer\Fixtures\ComplexUnionTyping;
use Tests\Liip\Serializer\Fixtures\ContainsNonEmptyConstructor;
use Tests\Liip\Serializer\Fixtures\CustomType;
use Tests\Liip\Serializer\Fixtures\CustomTypeHandler;
use Tests\Liip\Serializer\Fixtures\DiscriminatorAuthor;
use Tests\Liip\Serializer\Fixtures\DiscriminatorComment;
use Tests\Liip\Serializer\Fixtures\DiscriminatorDependency;
Expand All @@ -27,6 +29,7 @@
use Tests\Liip\Serializer\Fixtures\Inheritance;
use Tests\Liip\Serializer\Fixtures\ListModel;
use Tests\Liip\Serializer\Fixtures\Model;
use Tests\Liip\Serializer\Fixtures\ModelWithCustomType;
use Tests\Liip\Serializer\Fixtures\Nested;
use Tests\Liip\Serializer\Fixtures\NonEmptyConstructor;
use Tests\Liip\Serializer\Fixtures\PostDeserialize;
Expand Down Expand Up @@ -453,4 +456,31 @@ public function testArraysWithUnknownSubType(): void
self::assertInstanceOf(UnknownArraySubType::class, $model);
self::assertSame($unknownSubtype, $list->unknownSubType);
}

public function testHandler(): void
{
$functionName = 'deserialize_Tests_Liip_Serializer_Fixtures_ModelWithCustomType';
self::generateDeserializer(
self::$metadataBuilder,
ModelWithCustomType::class,
$functionName,
['handlers' => [new CustomTypeHandler()]]
);

$input = [
'value' => 'hello',
'values' => ['foo', 'bar'],
];

/** @var ModelWithCustomType $model */
$model = $functionName($input);

self::assertInstanceOf(ModelWithCustomType::class, $model);
self::assertInstanceOf(CustomType::class, $model->value);
self::assertSame('hello', $model->value->getValue());
self::assertCount(2, $model->values);
self::assertContainsOnlyInstancesOf(CustomType::class, $model->values);
self::assertSame('foo', $model->values[0]->getValue());
self::assertSame('bar', $model->values[1]->getValue());
}
}
25 changes: 25 additions & 0 deletions tests/Unit/SerializerGeneratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
use Tests\Liip\Serializer\Fixtures\BackedStringEnum;
use Tests\Liip\Serializer\Fixtures\ComplexUnionTyping;
use Tests\Liip\Serializer\Fixtures\ContainsPrivateProperty;
use Tests\Liip\Serializer\Fixtures\CustomType;
use Tests\Liip\Serializer\Fixtures\CustomTypeHandler;
use Tests\Liip\Serializer\Fixtures\DiscriminatorAuthor;
use Tests\Liip\Serializer\Fixtures\DiscriminatorComment;
use Tests\Liip\Serializer\Fixtures\DiscriminatorDependency;
Expand All @@ -26,6 +28,7 @@
use Tests\Liip\Serializer\Fixtures\Inheritance;
use Tests\Liip\Serializer\Fixtures\ListModel;
use Tests\Liip\Serializer\Fixtures\Model;
use Tests\Liip\Serializer\Fixtures\ModelWithCustomType;
use Tests\Liip\Serializer\Fixtures\MultidimensionalArrayForPrimitive;
use Tests\Liip\Serializer\Fixtures\Nested;
use Tests\Liip\Serializer\Fixtures\PostDeserialize;
Expand Down Expand Up @@ -572,6 +575,28 @@ public function testEnum(): void
self::assertSame(['H', 'D'], $data['backed_string_array']);
}

public function testHandler(): void
{
$functionName = 'serialize_Tests_Liip_Serializer_Fixtures_ModelWithCustomType';
self::generateSerializers(
self::$metadataBuilder,
ModelWithCustomType::class,
[$functionName],
[],
[],
['handlers' => [new CustomTypeHandler()]]
);

$model = new ModelWithCustomType();
$model->value = new CustomType('hello');
$model->values = [new CustomType('foo'), new CustomType('bar')];

$data = $functionName($model);

self::assertSame('hello', $data['value']);
self::assertSame(['foo', 'bar'], $data['values']);
}

public function testInaccessibleProperty(): void
{
$this->expectException(\Exception::class);
Expand Down
Loading