Skip to content
Merged
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
43 changes: 41 additions & 2 deletions src/Hal/Serializer/ItemNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,20 @@
use ApiPlatform\Serializer\ContextTrait;
use ApiPlatform\Serializer\TagCollectorInterface;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
use Symfony\Component\PropertyInfo\Type as LegacyType;
use Symfony\Component\Serializer\Exception\CircularReferenceException;
use Symfony\Component\Serializer\Exception\LogicException;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\TypeInfo\Type;
use Symfony\Component\TypeInfo\Type\CollectionType;
use Symfony\Component\TypeInfo\Type\CompositeTypeInterface;
use Symfony\Component\TypeInfo\Type\ObjectType;
use Symfony\Component\TypeInfo\Type\WrappingTypeInterface;

/**
* Converts between objects and array including HAL metadata.
Expand Down Expand Up @@ -175,22 +182,54 @@
foreach ($attributes as $attribute) {
$propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $options);

$types = $propertyMetadata->getBuiltinTypes() ?? [];
if (method_exists(PropertyInfoExtractor::class, 'getType')) {
$type = $propertyMetadata->getNativeType();
$types = $type instanceof CompositeTypeInterface ? $type->getTypes() : (null === $type ? [] : [$type]);
/** @var class-string|null $className */
$className = null;
} else {
$types = $propertyMetadata->getBuiltinTypes() ?? [];

Check warning on line 191 in src/Hal/Serializer/ItemNormalizer.php

View check run for this annotation

Codecov / codecov/patch

src/Hal/Serializer/ItemNormalizer.php#L191

Added line #L191 was not covered by tests
}

// prevent declaring $attribute as attribute if it's already declared as relationship
$isRelationship = false;
$typeIsResourceClass = function (Type $type) use (&$typeIsResourceClass, &$className): bool {
return match (true) {

Check warning on line 197 in src/Hal/Serializer/ItemNormalizer.php

View check run for this annotation

Codecov / codecov/patch

src/Hal/Serializer/ItemNormalizer.php#L197

Added line #L197 was not covered by tests
$type instanceof WrappingTypeInterface => $type->wrappedTypeIsSatisfiedBy($typeIsResourceClass),
$type instanceof CompositeTypeInterface => $type->composedTypesAreSatisfiedBy($typeIsResourceClass),
default => $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($className = $type->getClassName()),
};

Check warning on line 201 in src/Hal/Serializer/ItemNormalizer.php

View check run for this annotation

Codecov / codecov/patch

src/Hal/Serializer/ItemNormalizer.php#L201

Added line #L201 was not covered by tests
};

foreach ($types as $type) {
$isOne = $isMany = false;

if (null !== $type) {
/** @var Type|LegacyType|null $valueType */
$valueType = null;

if ($type instanceof LegacyType) {
if ($type->isCollection()) {
$valueType = $type->getCollectionValueTypes()[0] ?? null;
$isMany = null !== $valueType && ($className = $valueType->getClassName()) && $this->resourceClassResolver->isResourceClass($className);
} else {
$className = $type->getClassName();
$isOne = $className && $this->resourceClassResolver->isResourceClass($className);
}
} elseif ($type instanceof Type) {
$typeIsCollection = function (Type $type) use (&$typeIsCollection, &$valueType): bool {
return match (true) {

Check warning on line 220 in src/Hal/Serializer/ItemNormalizer.php

View check run for this annotation

Codecov / codecov/patch

src/Hal/Serializer/ItemNormalizer.php#L220

Added line #L220 was not covered by tests
$type instanceof CollectionType => null !== $valueType = $type->getCollectionValueType(),
$type instanceof WrappingTypeInterface => $type->wrappedTypeIsSatisfiedBy($typeIsCollection),
$type instanceof CompositeTypeInterface => $type->composedTypesAreSatisfiedBy($typeIsCollection),
default => false,
};

Check warning on line 225 in src/Hal/Serializer/ItemNormalizer.php

View check run for this annotation

Codecov / codecov/patch

src/Hal/Serializer/ItemNormalizer.php#L225

Added line #L225 was not covered by tests
};

if ($type->isSatisfiedBy($typeIsCollection)) {
$isMany = $valueType->isSatisfiedBy($typeIsResourceClass);
} else {
$isOne = $type->isSatisfiedBy($typeIsResourceClass);
}
}

if (!$isOne && !$isMany) {
Expand Down
3 changes: 2 additions & 1 deletion src/Hal/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
"php": ">=8.2",
"api-platform/state": "^4.1",
"api-platform/metadata": "^4.1",
"api-platform/serializer": "^4.1"
"api-platform/serializer": "^3.4 || ^4.0",
"symfony/type-info": "^7.2"
},
"autoload": {
"psr-4": {
Expand Down
26 changes: 10 additions & 16 deletions tests/Hal/Serializer/ItemNormalizerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\PropertyInfo\Type;
use Symfony\Component\Serializer\Exception\LogicException;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader;
Expand All @@ -41,6 +40,7 @@
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\TypeInfo\Type;

/**
* @author Kévin Dunglas <dunglas@gmail.com>
Expand Down Expand Up @@ -119,10 +119,10 @@

$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
$propertyMetadataFactoryProphecy->create(Dummy::class, 'name', [])->willReturn(
(new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(true)
(new ApiProperty())->withNativeType(Type::string())->withDescription('')->withReadable(true)

Check warning on line 122 in tests/Hal/Serializer/ItemNormalizerTest.php

View check run for this annotation

Codecov / codecov/patch

tests/Hal/Serializer/ItemNormalizerTest.php#L122

Added line #L122 was not covered by tests
);
$propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', [])->willReturn(
(new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, false, RelatedDummy::class)])->withDescription('')->withReadable(true)->withWritable(false)->withWritableLink(false)
(new ApiProperty())->withNativeType(Type::object(RelatedDummy::class))->withDescription('')->withReadable(true)->withWritable(false)->withWritableLink(false)

Check warning on line 125 in tests/Hal/Serializer/ItemNormalizerTest.php

View check run for this annotation

Codecov / codecov/patch

tests/Hal/Serializer/ItemNormalizerTest.php#L125

Added line #L125 was not covered by tests
);

$iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
Expand Down Expand Up @@ -183,16 +183,10 @@

$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
$propertyMetadataFactoryProphecy->create(Book::class, 'author', [])->willReturn(
(new ApiProperty())->withBuiltinTypes([
new Type(Type::BUILTIN_TYPE_OBJECT, false, ActivableInterface::class),
new Type(Type::BUILTIN_TYPE_OBJECT, false, TimestampableInterface::class),
])->withReadable(true)
(new ApiProperty())->withNativeType(Type::intersection(Type::object(ActivableInterface::class), Type::object(TimestampableInterface::class)))->withReadable(true)

Check warning on line 186 in tests/Hal/Serializer/ItemNormalizerTest.php

View check run for this annotation

Codecov / codecov/patch

tests/Hal/Serializer/ItemNormalizerTest.php#L186

Added line #L186 was not covered by tests
);
$propertyMetadataFactoryProphecy->create(Book::class, 'library', [])->willReturn(
(new ApiProperty())->withBuiltinTypes([
new Type(Type::BUILTIN_TYPE_OBJECT, false, ActivableInterface::class),
new Type(Type::BUILTIN_TYPE_OBJECT, false, TimestampableInterface::class),
])->withReadable(true)
(new ApiProperty())->withNativeType(Type::intersection(Type::object(ActivableInterface::class), Type::object(TimestampableInterface::class)))->withReadable(true)

Check warning on line 189 in tests/Hal/Serializer/ItemNormalizerTest.php

View check run for this annotation

Codecov / codecov/patch

tests/Hal/Serializer/ItemNormalizerTest.php#L189

Added line #L189 was not covered by tests
);

$iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
Expand Down Expand Up @@ -247,10 +241,10 @@

$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
$propertyMetadataFactoryProphecy->create(Dummy::class, 'name', [])->willReturn(
(new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(true)
(new ApiProperty())->withNativeType(Type::string())->withDescription('')->withReadable(true)

Check warning on line 244 in tests/Hal/Serializer/ItemNormalizerTest.php

View check run for this annotation

Codecov / codecov/patch

tests/Hal/Serializer/ItemNormalizerTest.php#L244

Added line #L244 was not covered by tests
);
$propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', [])->willReturn(
(new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, false, RelatedDummy::class)])->withDescription('')->withReadable(true)->withWritable(false)->withReadableLink(false)
(new ApiProperty())->withNativeType(Type::object(RelatedDummy::class))->withDescription('')->withReadable(true)->withWritable(false)->withReadableLink(false)

Check warning on line 247 in tests/Hal/Serializer/ItemNormalizerTest.php

View check run for this annotation

Codecov / codecov/patch

tests/Hal/Serializer/ItemNormalizerTest.php#L247

Added line #L247 was not covered by tests
);

$iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
Expand Down Expand Up @@ -325,13 +319,13 @@

$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
$propertyMetadataFactoryProphecy->create(MaxDepthDummy::class, 'id', [])->willReturn(
(new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_INT)])->withDescription('')->withReadable(true)
(new ApiProperty())->withNativeType(Type::int())->withDescription('')->withReadable(true)

Check warning on line 322 in tests/Hal/Serializer/ItemNormalizerTest.php

View check run for this annotation

Codecov / codecov/patch

tests/Hal/Serializer/ItemNormalizerTest.php#L322

Added line #L322 was not covered by tests
);
$propertyMetadataFactoryProphecy->create(MaxDepthDummy::class, 'name', [])->willReturn(
(new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(true)
(new ApiProperty())->withNativeType(Type::string())->withDescription('')->withReadable(true)

Check warning on line 325 in tests/Hal/Serializer/ItemNormalizerTest.php

View check run for this annotation

Codecov / codecov/patch

tests/Hal/Serializer/ItemNormalizerTest.php#L325

Added line #L325 was not covered by tests
);
$propertyMetadataFactoryProphecy->create(MaxDepthDummy::class, 'child', [])->willReturn(
(new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, false, MaxDepthDummy::class)])->withDescription('')->withReadable(true)->withWritable(false)->withReadableLink(true)
(new ApiProperty())->withNativeType(Type::object(MaxDepthDummy::class))->withDescription('')->withReadable(true)->withWritable(false)->withReadableLink(true)

Check warning on line 328 in tests/Hal/Serializer/ItemNormalizerTest.php

View check run for this annotation

Codecov / codecov/patch

tests/Hal/Serializer/ItemNormalizerTest.php#L328

Added line #L328 was not covered by tests
);

$iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
Expand Down
Loading