Skip to content

Commit a5c1919

Browse files
committed
Fix resource resolution for interface as resources
Before this patch, when you configure an interface as resource the serializer was not understanding the implementation was a resource. It also fix the behavior of child of ApiResources that were not detected as it.
1 parent 6826437 commit a5c1919

File tree

9 files changed

+135
-12
lines changed

9 files changed

+135
-12
lines changed

features/main/table_inheritance.feature

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,16 @@ Feature: Table inheritance
307307
"items": {
308308
"type": "object",
309309
"properties": {
310+
"@type": {
311+
"type": "string",
312+
"pattern": "^ResourceInterface$",
313+
"required": "true"
314+
},
315+
"@id": {
316+
"type": "string",
317+
"pattern": "^/resource_interfaces/",
318+
"required": "true"
319+
},
310320
"foo": {
311321
"type": "string",
312322
"required": "true"
@@ -338,6 +348,16 @@ Feature: Table inheritance
338348
"type": "string",
339349
"pattern": "ResourceInterface$"
340350
},
351+
"@id": {
352+
"type": "string",
353+
"pattern": "^/resource_interfaces",
354+
"required": "true"
355+
},
356+
"@type": {
357+
"type": "string",
358+
"pattern": "^ResourceInterface$",
359+
"required": "true"
360+
},
341361
"foo": {
342362
"type": "string",
343363
"required": "true"

src/Api/IdentifiersExtractor.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,9 @@ public function getIdentifiersFromResourceClass(string $resourceClass): array
6767
public function getIdentifiersFromItem($item): array
6868
{
6969
$identifiers = [];
70-
$resourceClass = $this->getObjectClass($item);
70+
$type = $this->getObjectClass($item);
71+
$resourceClass = $this->resourceClassResolver->isResourceClass($type) ? $this->resourceClassResolver->getResourceClass($item) : $type;
72+
7173
foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $propertyName) {
7274
$propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName);
7375
$identifier = $propertyMetadata->isIdentifier();

src/Api/IriConverterInterface.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
* Converts item and resources to IRI and vice versa.
2222
*
2323
* @author Kévin Dunglas <[email protected]>
24+
*
25+
* @deprecated in favor of ResourceIriConverterInterface to be removed in ApiPlatform 3.0
2426
*/
2527
interface IriConverterInterface
2628
{
@@ -41,6 +43,10 @@ public function getItemFromIri(string $iri, array $context = []);
4143
*
4244
* @throws InvalidArgumentException
4345
* @throws RuntimeException
46+
*
47+
* @return string Class name of resource
48+
*
49+
* @deprecated in favor of `ResourceIriConverterInterface::getIriFromItemWithResource`
4450
*/
4551
public function getIriFromItem($item, int $referenceType = UrlGeneratorInterface::ABS_PATH): string;
4652

src/Api/ResourceClassResolver.php

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,18 +47,27 @@ public function getResourceClass($value, string $resourceClass = null, bool $str
4747
throw new InvalidArgumentException(sprintf('No resource class found.'));
4848
}
4949

50+
$resolvedResourceClass = null;
51+
foreach ($this->resourceNameCollectionFactory->create() as $currentResourceClass) {
52+
if ($resourceClass === $currentResourceClass) {
53+
$resolvedResourceClass = $currentResourceClass;
54+
}
55+
if (null === $resolvedResourceClass && is_subclass_of($type, $currentResourceClass)) {
56+
$resolvedResourceClass = $currentResourceClass;
57+
}
58+
}
59+
if (null !== $resolvedResourceClass) {
60+
$resourceClass = $resolvedResourceClass;
61+
}
62+
5063
if (
5164
null === $type
5265
|| ((!$strict || $resourceClass === $type) && $isResourceClass = $this->isResourceClass($type))
66+
|| null !== $resolvedResourceClass && interface_exists($resourceClass)
5367
) {
5468
return $resourceClass;
5569
}
5670

57-
// The Resource is an interface
58-
if ($value instanceof $resourceClass && $type !== $resourceClass && interface_exists($resourceClass)) {
59-
throw new InvalidArgumentException(sprintf('The given object\'s resource is the interface "%s", finding a class is not possible.', $resourceClass));
60-
}
61-
6271
if (
6372
($isResourceClass ?? $this->isResourceClass($type))
6473
|| (is_subclass_of($type, $resourceClass) && $this->isResourceClass($resourceClass))
@@ -79,7 +88,7 @@ public function isResourceClass(string $type): bool
7988
}
8089

8190
foreach ($this->resourceNameCollectionFactory->create() as $resourceClass) {
82-
if ($type === $resourceClass) {
91+
if ($type === $resourceClass || is_subclass_of($type, $resourceClass)) {
8392
return $this->localIsResourceClassCache[$type] = true;
8493
}
8594
}

src/Api/ResourceClassResolverInterface.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,20 @@ interface ResourceClassResolverInterface
2525
/**
2626
* Guesses the associated resource.
2727
*
28+
* @param object $value Object you're playing with
29+
* @param string $resourceClass Resource class it is supposed to be (could be parent class for instance)
30+
* @param bool $strict value must be type of resource class given or it will return type
31+
*
2832
* @throws InvalidArgumentException
33+
*
34+
* @return string Resolved resource class
2935
*/
3036
public function getResourceClass($value, string $resourceClass = null, bool $strict = false): string;
3137

3238
/**
33-
* Is the given class a resource class?
39+
* Is the given class name an api platform resource?
40+
*
41+
* @param string $type FQCN of the resource
3442
*/
3543
public function isResourceClass(string $type): bool;
3644
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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\Api;
15+
16+
use ApiPlatform\Core\Exception\InvalidArgumentException;
17+
use ApiPlatform\Core\Exception\RuntimeException;
18+
19+
/**
20+
* Converts item and resources to IRI and vice versa.
21+
*
22+
* @author Kévin Dunglas <[email protected]>
23+
*/
24+
interface ResourceIriConverterInterface extends IriConverterInterface
25+
{
26+
/**
27+
* Gets the IRI associated with the given item and resource class.
28+
*
29+
* @param object $item
30+
* @param string $resourceClass resource class to use for generation
31+
*
32+
* @throws InvalidArgumentException
33+
* @throws RuntimeException
34+
*
35+
* @return string Class name of resource
36+
*/
37+
public function getIriFromItemWithResource($item, string $resourceClass, int $referenceType = UrlGeneratorInterface::ABS_PATH): string;
38+
}

src/Bridge/Symfony/Routing/IriConverter.php

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515

1616
use ApiPlatform\Core\Api\IdentifiersExtractor;
1717
use ApiPlatform\Core\Api\IdentifiersExtractorInterface;
18-
use ApiPlatform\Core\Api\IriConverterInterface;
1918
use ApiPlatform\Core\Api\OperationType;
19+
use ApiPlatform\Core\Api\ResourceIriConverterInterface;
2020
use ApiPlatform\Core\Api\UrlGeneratorInterface;
2121
use ApiPlatform\Core\DataProvider\ItemDataProviderInterface;
2222
use ApiPlatform\Core\DataProvider\OperationDataProviderTrait;
@@ -40,7 +40,7 @@
4040
*
4141
* @author Kévin Dunglas <[email protected]>
4242
*/
43-
final class IriConverter implements IriConverterInterface
43+
final class IriConverter implements ResourceIriConverterInterface
4444
{
4545
use ClassInfoTrait;
4646
use OperationDataProviderTrait;
@@ -112,10 +112,21 @@ public function getItemFromIri(string $iri, array $context = [])
112112

113113
/**
114114
* {@inheritdoc}
115+
*
116+
* @deprecated use `getIriFromItemWithResource` instead to be removed in ApiPlatform 3.0
115117
*/
116118
public function getIriFromItem($item, int $referenceType = UrlGeneratorInterface::ABS_PATH): string
117119
{
118120
$resourceClass = $this->getObjectClass($item);
121+
122+
return $this->getIriFromItemWithResource($item, $resourceClass, $referenceType);
123+
}
124+
125+
/**
126+
* {@inheritdoc}
127+
*/
128+
public function getIriFromItemWithResource($item, string $resourceClass, int $referenceType = UrlGeneratorInterface::ABS_PATH): string
129+
{
119130
$routeName = $this->routeNameResolver->getRouteName($resourceClass, OperationType::ITEM);
120131

121132
try {

src/JsonLd/Serializer/ItemNormalizer.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public function normalize($object, $format = null, array $context = [])
7272
// Use resolved resource class instead of given resource class to support multiple inheritance child types
7373
$resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class'] ?? null, true);
7474
$context = $this->initContext($resourceClass, $context);
75-
$iri = $this->iriConverter->getIriFromItem($object);
75+
$iri = $this->iriConverter->getIriFromItemWithResource($object, $resourceClass);
7676
$context['iri'] = $iri;
7777
$context['api_normalize'] = true;
7878

tests/Api/ResourceClassResolverTest.php

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ public function testGetResourceClassWithChildResource()
170170

171171
$resourceClassResolver = new ResourceClassResolver($resourceNameCollectionFactoryProphecy->reveal());
172172

173-
$this->assertEquals(DummyTableInheritanceChild::class, $resourceClassResolver->getResourceClass($t, DummyTableInheritance::class));
173+
$this->assertEquals(DummyTableInheritance::class, $resourceClassResolver->getResourceClass($t, DummyTableInheritance::class));
174174
}
175175

176176
public function testGetResourceClassWithInterfaceResource()
@@ -183,4 +183,33 @@ public function testGetResourceClassWithInterfaceResource()
183183
$resourceClassResolver = new ResourceClassResolver($resourceNameCollectionFactoryProphecy->reveal());
184184
$resourceClassResolver->getResourceClass($dummy, DummyResourceInterface::class, true);
185185
}
186+
187+
public function testGetResourceClassWithoutSecondParameterIsAccurate()
188+
{
189+
$dummy = new DummyResourceImplementation();
190+
$resourceNameCollectionFactoryProphecy = $this->prophesize(ResourceNameCollectionFactoryInterface::class);
191+
$resourceNameCollectionFactoryProphecy->create()->willReturn(new ResourceNameCollection([DummyResourceInterface::class]))->shouldBeCalled();
192+
193+
$resourceClassResolver = new ResourceClassResolver($resourceNameCollectionFactoryProphecy->reveal());
194+
$this->assertEquals(DummyResourceInterface::class, $resourceClassResolver->getResourceClass($dummy));
195+
}
196+
197+
public function testGetResourceInterfaceWithResourceClassParameterReturnTheResouceClass()
198+
{
199+
$dummy = new DummyResourceImplementation();
200+
$resourceNameCollectionFactoryProphecy = $this->prophesize(ResourceNameCollectionFactoryInterface::class);
201+
$resourceNameCollectionFactoryProphecy->create()->willReturn(new ResourceNameCollection([DummyResourceInterface::class]))->shouldBeCalled();
202+
203+
$resourceClassResolver = new ResourceClassResolver($resourceNameCollectionFactoryProphecy->reveal());
204+
$this->assertEquals(DummyResourceInterface::class, $resourceClassResolver->getResourceClass($dummy, DummyResourceInterface::class));
205+
}
206+
207+
public function testInterfaceCanBeResourceClass()
208+
{
209+
$resourceNameCollectionFactoryProphecy = $this->prophesize(ResourceNameCollectionFactoryInterface::class);
210+
$resourceNameCollectionFactoryProphecy->create()->willReturn(new ResourceNameCollection([DummyResourceInterface::class]))->shouldBeCalled();
211+
212+
$resourceClassResolver = new ResourceClassResolver($resourceNameCollectionFactoryProphecy->reveal());
213+
$this->assertTrue($resourceClassResolver->isResourceClass(DummyResourceImplementation::class));
214+
}
186215
}

0 commit comments

Comments
 (0)