Skip to content

Commit

Permalink
fix: support empty array as object
Browse files Browse the repository at this point in the history
Fixes an issue with the use of `EMPTY_ARRAY_AS_OBJECT` and
`PRESERVE_EMPTY_OBJECTS` in the serialization context.
Since the CollectionNormalizer was taking over the serialization
of all iterables, the Symfony serializer was not
called when normalizing a "raw" collection.
Adding a better supports method fixes the issue.
The supports method is not cacheable anymore since it relies on the context.
  • Loading branch information
alanpoulain committed Sep 22, 2022
1 parent 14c7cba commit 469d907
Show file tree
Hide file tree
Showing 9 changed files with 136 additions and 205 deletions.
33 changes: 33 additions & 0 deletions features/serializer/empty_array_as_object.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
Feature: Serialize empty array as object
In order to have a coherent JSON representation
As a developer
I should be able to serialize some empty array properties as objects

@createSchema
Scenario: Get a resource with empty array properties as objects
When I add "Content-Type" header equal to "application/ld+json"
And I send a "GET" request to "/empty_array_as_objects/5"
Then the response status code should be 200
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
And the JSON should be equal to:
"""
{
"@context": "/contexts/EmptyArrayAsObject",
"@id": "/empty_array_as_objects/6",
"@type": "EmptyArrayAsObject",
"id": 6,
"emptyArray": [],
"emptyArrayAsObject": {},
"arrayObjectAsArray": [],
"arrayObject": {},
"stringArray": [
"foo",
"bar"
],
"objectArray": {
"foo": 67,
"bar": "baz"
}
}
"""
21 changes: 2 additions & 19 deletions src/Hydra/Serializer/CollectionNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public function __construct(private readonly ContextBuilderInterface $contextBui
*/
public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool
{
return self::FORMAT === $format && is_iterable($data);
return self::FORMAT === $format && is_iterable($data) && isset($context['resource_class']) && !isset($context['api_sub_level']);
}

/**
Expand All @@ -66,10 +66,6 @@ public function supportsNormalization(mixed $data, string $format = null, array
*/
public function normalize(mixed $object, string $format = null, array $context = []): array
{
if (!isset($context['resource_class']) || isset($context['api_sub_level'])) {
return $this->normalizeRawCollection($object, $format, $context);
}

$resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class']);
$context = $this->initContext($resourceClass, $context);
$data = $this->addJsonLdContext($this->contextBuilder, $resourceClass, $context);
Expand Down Expand Up @@ -106,19 +102,6 @@ public function normalize(mixed $object, string $format = null, array $context =

public function hasCacheableSupportsMethod(): bool
{
return true;
}

/**
* Normalizes a raw collection (not API resources).
*/
private function normalizeRawCollection(iterable $object, ?string $format, array $context): array
{
$data = [];
foreach ($object as $index => $obj) {
$data[$index] = $this->normalizer->normalize($obj, $format, $context);
}

return $data;
return false;
}
}
21 changes: 2 additions & 19 deletions src/Serializer/AbstractCollectionNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,12 @@ public function __construct(protected ResourceClassResolverInterface $resourceCl
*/
public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool
{
return static::FORMAT === $format && is_iterable($data);
return static::FORMAT === $format && is_iterable($data) && isset($context['resource_class']) && !isset($context['api_sub_level']);
}

public function hasCacheableSupportsMethod(): bool
{
return true;
return false;
}

/**
Expand All @@ -65,10 +65,6 @@ public function hasCacheableSupportsMethod(): bool
*/
public function normalize(mixed $object, string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null
{
if (!isset($context['resource_class']) || isset($context['api_sub_level'])) {
return $this->normalizeRawCollection($object, $format, $context);
}

$resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class']);
$context = $this->initContext($resourceClass, $context);
$data = [];
Expand All @@ -86,19 +82,6 @@ public function normalize(mixed $object, string $format = null, array $context =
return array_merge_recursive($data, $paginationData, $itemsData);
}

/**
* Normalizes a raw collection (not API resources).
*/
protected function normalizeRawCollection(iterable $object, string $format = null, array $context = []): array
{
$data = [];
foreach ($object as $index => $obj) {
$data[$index] = $this->normalizer->normalize($obj, $format, $context);
}

return $data;
}

/**
* Gets the pagination configuration.
*/
Expand Down
47 changes: 47 additions & 0 deletions tests/Fixtures/TestBundle/Model/EmptyArrayAsObject.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Tests\Fixtures\TestBundle\Model;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Tests\Fixtures\TestBundle\State\EmptyArrayAsObjectProvider;
use Symfony\Component\Serializer\Annotation\Context;
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
use Symfony\Component\Serializer\Serializer;

#[ApiResource(operations: [new Get()], provider: EmptyArrayAsObjectProvider::class)]
class EmptyArrayAsObject
{
public int $id = 6;

public array $emptyArray = [];

#[Context([Serializer::EMPTY_ARRAY_AS_OBJECT => true, AbstractObjectNormalizer::PRESERVE_EMPTY_OBJECTS => true])]
public array $emptyArrayAsObject = [];

public \ArrayObject $arrayObjectAsArray;

#[Context([AbstractObjectNormalizer::PRESERVE_EMPTY_OBJECTS => true])]
public \ArrayObject $arrayObject;

public array $stringArray = ['foo', 'bar'];

public array $objectArray = ['foo' => 67, 'bar' => 'baz'];

public function __construct()
{
$this->arrayObjectAsArray = new \ArrayObject();
$this->arrayObject = new \ArrayObject();
}
}
26 changes: 26 additions & 0 deletions tests/Fixtures/TestBundle/State/EmptyArrayAsObjectProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Tests\Fixtures\TestBundle\State;

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use ApiPlatform\Tests\Fixtures\TestBundle\Model\EmptyArrayAsObject;

final class EmptyArrayAsObjectProvider implements ProviderInterface
{
public function provide(Operation $operation, array $uriVariables = [], array $context = []): EmptyArrayAsObject
{
return new EmptyArrayAsObject();
}
}
5 changes: 5 additions & 0 deletions tests/Fixtures/app/config/config_common.yml
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,11 @@ services:
tags:
- { name: 'api_platform.state_provider' }

ApiPlatform\Tests\Fixtures\TestBundle\State\EmptyArrayAsObjectProvider:
class: 'ApiPlatform\Tests\Fixtures\TestBundle\State\EmptyArrayAsObjectProvider'
tags:
- { name: 'api_platform.state_provider' }

ApiPlatform\Tests\Fixtures\TestBundle\State\CarProcessor:
class: 'ApiPlatform\Tests\Fixtures\TestBundle\State\CarProcessor'
tags:
Expand Down
27 changes: 7 additions & 20 deletions tests/Hal/Serializer/CollectionNormalizerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,26 +39,13 @@ public function testSupportsNormalize(): void
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class);
$normalizer = new CollectionNormalizer($resourceClassResolverProphecy->reveal(), 'page', $resourceMetadataFactoryProphecy->reveal());

$this->assertTrue($normalizer->supportsNormalization([], CollectionNormalizer::FORMAT));
$this->assertTrue($normalizer->supportsNormalization(new \ArrayObject(), CollectionNormalizer::FORMAT));
$this->assertFalse($normalizer->supportsNormalization([], 'xml'));
$this->assertFalse($normalizer->supportsNormalization(new \ArrayObject(), 'xml'));
$this->assertTrue($normalizer->hasCacheableSupportsMethod());
}

public function testNormalizeApiSubLevel(): void
{
$resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
$resourceClassResolverProphecy->getResourceClass()->shouldNotBeCalled();
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class);

$itemNormalizer = $this->prophesize(NormalizerInterface::class);
$itemNormalizer->normalize('bar', null, ['api_sub_level' => true])->willReturn(22);

$normalizer = new CollectionNormalizer($resourceClassResolverProphecy->reveal(), 'page', $resourceMetadataFactoryProphecy->reveal());
$normalizer->setNormalizer($itemNormalizer->reveal());

$this->assertEquals(['foo' => 22], $normalizer->normalize(['foo' => 'bar'], null, ['api_sub_level' => true]));
$this->assertTrue($normalizer->supportsNormalization([], CollectionNormalizer::FORMAT, ['resource_class' => 'Foo']));
$this->assertFalse($normalizer->supportsNormalization([], CollectionNormalizer::FORMAT, ['resource_class' => 'Foo', 'api_sub_level' => true]));
$this->assertFalse($normalizer->supportsNormalization([], CollectionNormalizer::FORMAT, []));
$this->assertTrue($normalizer->supportsNormalization(new \ArrayObject(), CollectionNormalizer::FORMAT, ['resource_class' => 'Foo']));
$this->assertFalse($normalizer->supportsNormalization([], 'xml', ['resource_class' => 'Foo']));
$this->assertFalse($normalizer->supportsNormalization(new \ArrayObject(), 'xml', ['resource_class' => 'Foo']));
$this->assertFalse($normalizer->hasCacheableSupportsMethod());
}

public function testNormalizePaginator(): void
Expand Down

0 comments on commit 469d907

Please sign in to comment.