Skip to content

Commit 80ff056

Browse files
authored
Merge pull request #2442 from teohhanhui/fix/collection-normalizer-non-resource
Fix normalization of raw collections (not API resources)
2 parents 6a30bd5 + fa8d31b commit 80ff056

File tree

7 files changed

+414
-41
lines changed

7 files changed

+414
-41
lines changed

src/Hydra/Serializer/CollectionFiltersNormalizer.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use ApiPlatform\Core\Api\FilterInterface;
1818
use ApiPlatform\Core\Api\FilterLocatorTrait;
1919
use ApiPlatform\Core\Api\ResourceClassResolverInterface;
20+
use ApiPlatform\Core\Exception\InvalidArgumentException;
2021
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
2122
use Psr\Container\ContainerInterface;
2223
use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;
@@ -74,7 +75,15 @@ public function normalize($object, $format = null, array $context = [])
7475
return $data;
7576
}
7677

77-
$resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class'] ?? null, true);
78+
try {
79+
$resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class'] ?? null, true);
80+
} catch (InvalidArgumentException $e) {
81+
if (!isset($context['resource_class'])) {
82+
return $data;
83+
}
84+
85+
throw $e;
86+
}
7887
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
7988

8089
$operationName = $context['collection_operation_name'] ?? null;

src/Hydra/Serializer/CollectionNormalizer.php

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use ApiPlatform\Core\Api\ResourceClassResolverInterface;
1919
use ApiPlatform\Core\DataProvider\PaginatorInterface;
2020
use ApiPlatform\Core\DataProvider\PartialPaginatorInterface;
21+
use ApiPlatform\Core\Exception\InvalidArgumentException;
2122
use ApiPlatform\Core\JsonLd\ContextBuilderInterface;
2223
use ApiPlatform\Core\JsonLd\Serializer\JsonLdContextTrait;
2324
use ApiPlatform\Core\Serializer\ContextTrait;
@@ -65,15 +66,18 @@ public function supportsNormalization($data, $format = null)
6566
public function normalize($object, $format = null, array $context = [])
6667
{
6768
if (isset($context['api_sub_level'])) {
68-
$data = [];
69-
foreach ($object as $index => $obj) {
70-
$data[$index] = $this->normalizer->normalize($obj, $format, $context);
69+
return $this->normalizeRawCollection($object, $format, $context);
70+
}
71+
72+
try {
73+
$resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class'] ?? null, true);
74+
} catch (InvalidArgumentException $e) {
75+
if (!isset($context['resource_class'])) {
76+
return $this->normalizeRawCollection($object, $format, $context);
7177
}
7278

73-
return $data;
79+
throw $e;
7480
}
75-
76-
$resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class'] ?? null, true);
7781
$data = $this->addJsonLdContext($this->contextBuilder, $resourceClass, $context);
7882
$context = $this->initContext($resourceClass, $context);
7983

@@ -109,4 +113,17 @@ public function hasCacheableSupportsMethod(): bool
109113
{
110114
return true;
111115
}
116+
117+
/**
118+
* Normalizes a raw collection (not API resources).
119+
*/
120+
private function normalizeRawCollection($object, $format = null, array $context = []): array
121+
{
122+
$data = [];
123+
foreach ($object as $index => $obj) {
124+
$data[$index] = $this->normalizer->normalize($obj, $format, $context);
125+
}
126+
127+
return $data;
128+
}
112129
}

src/Serializer/AbstractCollectionNormalizer.php

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use ApiPlatform\Core\Api\ResourceClassResolverInterface;
1717
use ApiPlatform\Core\DataProvider\PaginatorInterface;
1818
use ApiPlatform\Core\DataProvider\PartialPaginatorInterface;
19+
use ApiPlatform\Core\Exception\InvalidArgumentException;
1920
use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;
2021
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
2122
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
@@ -28,7 +29,9 @@
2829
*/
2930
abstract class AbstractCollectionNormalizer implements NormalizerInterface, NormalizerAwareInterface, CacheableSupportsMethodInterface
3031
{
31-
use ContextTrait { initContext as protected; }
32+
use ContextTrait {
33+
initContext as protected;
34+
}
3235
use NormalizerAwareTrait;
3336

3437
/**
@@ -68,19 +71,21 @@ public function hasCacheableSupportsMethod(): bool
6871
*/
6972
public function normalize($object, $format = null, array $context = [])
7073
{
71-
$data = [];
7274
if (isset($context['api_sub_level'])) {
73-
foreach ($object as $index => $obj) {
74-
$data[$index] = $this->normalizer->normalize($obj, $format, $context);
75+
return $this->normalizeRawCollection($object, $format, $context);
76+
}
77+
78+
try {
79+
$resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class'] ?? null, true);
80+
} catch (InvalidArgumentException $e) {
81+
if (!isset($context['resource_class'])) {
82+
return $this->normalizeRawCollection($object, $format, $context);
7583
}
7684

77-
return $data;
85+
throw $e;
7886
}
79-
80-
$context = $this->initContext(
81-
$this->resourceClassResolver->getResourceClass($object, $context['resource_class'] ?? null, true),
82-
$context
83-
);
87+
$data = [];
88+
$context = $this->initContext($resourceClass, $context);
8489

8590
return array_merge_recursive(
8691
$data,
@@ -89,6 +94,19 @@ public function normalize($object, $format = null, array $context = [])
8994
);
9095
}
9196

97+
/**
98+
* Normalizes a raw collection (not API resources).
99+
*/
100+
protected function normalizeRawCollection($object, $format = null, array $context = []): array
101+
{
102+
$data = [];
103+
foreach ($object as $index => $obj) {
104+
$data[$index] = $this->normalizer->normalize($obj, $format, $context);
105+
}
106+
107+
return $data;
108+
}
109+
92110
/**
93111
* Gets the pagination configuration.
94112
*

tests/Fixtures/Foo.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Core\Tests\Fixtures;
15+
16+
class Foo
17+
{
18+
public $id;
19+
public $bar;
20+
}

tests/Fixtures/NotAResource.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
namespace ApiPlatform\Core\Tests\Fixtures;
1515

1616
/**
17-
* This class is mapped as an API resource.
17+
* This class is not mapped as an API resource.
1818
*
1919
* @author Kévin Dunglas <[email protected]>
2020
*/

tests/Hydra/Serializer/CollectionFiltersNormalizerTest.php

Lines changed: 133 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,19 @@
1414
namespace ApiPlatform\Core\Tests\Hydra\Serializer;
1515

1616
use ApiPlatform\Core\Api\FilterCollection;
17+
use ApiPlatform\Core\Api\OperationType;
1718
use ApiPlatform\Core\Api\ResourceClassResolverInterface;
1819
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\FilterInterface;
1920
use ApiPlatform\Core\Exception\InvalidArgumentException;
2021
use ApiPlatform\Core\Hydra\Serializer\CollectionFiltersNormalizer;
22+
use ApiPlatform\Core\Hydra\Serializer\CollectionNormalizer;
2123
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
2224
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
25+
use ApiPlatform\Core\Tests\Fixtures\Foo;
26+
use ApiPlatform\Core\Tests\Fixtures\NotAResource;
2327
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy;
2428
use PHPUnit\Framework\TestCase;
29+
use Prophecy\Argument;
2530
use Psr\Container\ContainerInterface;
2631
use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;
2732
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
@@ -49,24 +54,141 @@ public function testSupportsNormalization()
4954
$this->assertTrue($normalizer->hasCacheableSupportsMethod());
5055
}
5156

52-
public function testDoNothingIfSubLevel()
57+
public function testNormalizeNonResourceCollection()
5358
{
54-
$dummy = new Dummy();
59+
$notAResourceA = new NotAResource('A', 'buzz');
60+
$notAResourceB = new NotAResource('B', 'bzzt');
61+
62+
$data = [$notAResourceA, $notAResourceB];
63+
64+
$normalizedNotAResourceA = [
65+
'foo' => 'A',
66+
'bar' => 'buzz',
67+
];
68+
69+
$normalizedNotAResourceB = [
70+
'foo' => 'B',
71+
'bar' => 'bzzt',
72+
];
5573

5674
$decoratedProphecy = $this->prophesize(NormalizerInterface::class);
57-
$decoratedProphecy->normalize($dummy, null, ['api_sub_level' => true])->willReturn(['name' => 'foo'])->shouldBeCalled();
75+
$decoratedProphecy->normalize($data, CollectionNormalizer::FORMAT, Argument::any())->willReturn([
76+
$normalizedNotAResourceA,
77+
$normalizedNotAResourceB,
78+
]);
79+
80+
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
5881

5982
$resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
60-
$resourceClassResolverProphecy->getResourceClass()->shouldNotBeCalled();
83+
$resourceClassResolverProphecy->getResourceClass($data, null, true)->willThrow(InvalidArgumentException::class);
6184

62-
$normalizer = new CollectionFiltersNormalizer(
63-
$decoratedProphecy->reveal(),
64-
$this->prophesize(ResourceMetadataFactoryInterface::class)->reveal(),
65-
$resourceClassResolverProphecy->reveal(),
66-
$this->prophesize(ContainerInterface::class)->reveal()
67-
);
85+
$filterLocatorProphecy = $this->prophesize(ContainerInterface::class);
86+
87+
$normalizer = new CollectionFiltersNormalizer($decoratedProphecy->reveal(), $resourceMetadataFactoryProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $filterLocatorProphecy->reveal());
6888

69-
$this->assertEquals(['name' => 'foo'], $normalizer->normalize($dummy, null, ['api_sub_level' => true]));
89+
$actual = $normalizer->normalize($data, CollectionNormalizer::FORMAT, [
90+
]);
91+
92+
$this->assertEquals([
93+
$normalizedNotAResourceA,
94+
$normalizedNotAResourceB,
95+
], $actual);
96+
}
97+
98+
public function testNormalizeSubLevelResourceCollection()
99+
{
100+
$fooOne = new Foo();
101+
$fooOne->id = 1;
102+
$fooOne->bar = 'baz';
103+
104+
$fooThree = new Foo();
105+
$fooThree->id = 3;
106+
$fooThree->bar = 'bzz';
107+
108+
$data = [$fooOne, $fooThree];
109+
110+
$normalizedFooOne = [
111+
'@id' => '/foos/1',
112+
'@type' => 'Foo',
113+
'bar' => 'baz',
114+
];
115+
116+
$normalizedFooThree = [
117+
'@id' => '/foos/3',
118+
'@type' => 'Foo',
119+
'bar' => 'bzz',
120+
];
121+
122+
$decoratedProphecy = $this->prophesize(NormalizerInterface::class);
123+
$decoratedProphecy->normalize($data, CollectionNormalizer::FORMAT, Argument::allOf(
124+
Argument::withEntry('resource_class', Foo::class),
125+
Argument::withEntry('api_sub_level', true)
126+
))->willReturn([
127+
$normalizedFooOne,
128+
$normalizedFooThree,
129+
]);
130+
131+
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
132+
133+
$resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
134+
135+
$filterLocatorProphecy = $this->prophesize(ContainerInterface::class);
136+
137+
$normalizer = new CollectionFiltersNormalizer($decoratedProphecy->reveal(), $resourceMetadataFactoryProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $filterLocatorProphecy->reveal());
138+
139+
$actual = $normalizer->normalize($data, CollectionNormalizer::FORMAT, [
140+
'collection_operation_name' => 'get',
141+
'operation_type' => OperationType::COLLECTION,
142+
'resource_class' => Foo::class,
143+
'api_sub_level' => true,
144+
]);
145+
146+
$this->assertEquals([
147+
$normalizedFooOne,
148+
$normalizedFooThree,
149+
], $actual);
150+
}
151+
152+
public function testNormalizeSubLevelNonResourceCollection()
153+
{
154+
$notAResourceA = new NotAResource('A', 'buzz');
155+
$notAResourceB = new NotAResource('B', 'bzzt');
156+
157+
$data = [$notAResourceA, $notAResourceB];
158+
159+
$normalizedNotAResourceA = [
160+
'foo' => 'A',
161+
'bar' => 'buzz',
162+
];
163+
164+
$normalizedNotAResourceB = [
165+
'foo' => 'B',
166+
'bar' => 'bzzt',
167+
];
168+
169+
$decoratedProphecy = $this->prophesize(NormalizerInterface::class);
170+
$decoratedProphecy->normalize($data, CollectionNormalizer::FORMAT, Argument::any())->willReturn([
171+
$normalizedNotAResourceA,
172+
$normalizedNotAResourceB,
173+
]);
174+
175+
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
176+
177+
$resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
178+
$resourceClassResolverProphecy->getResourceClass($data, null, true)->willThrow(InvalidArgumentException::class);
179+
180+
$filterLocatorProphecy = $this->prophesize(ContainerInterface::class);
181+
182+
$normalizer = new CollectionFiltersNormalizer($decoratedProphecy->reveal(), $resourceMetadataFactoryProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $filterLocatorProphecy->reveal());
183+
184+
$actual = $normalizer->normalize($data, CollectionNormalizer::FORMAT, [
185+
'api_sub_level' => true,
186+
]);
187+
188+
$this->assertEquals([
189+
$normalizedNotAResourceA,
190+
$normalizedNotAResourceB,
191+
], $actual);
70192
}
71193

72194
public function testDoNothingIfNoFilter()

0 commit comments

Comments
 (0)