Skip to content

Commit 3a3b059

Browse files
committed
Fix ResourceClassResolver handling of inheritance
1 parent cb4f3cd commit 3a3b059

File tree

48 files changed

+1288
-1058
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+1288
-1058
lines changed

features/main/operation.feature

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ Feature: Operation support
44
I need to be able to add custom operations and remove built-in ones
55

66
@createSchema
7-
@dropSchema
87
Scenario: Can not write readonly property
98
When I add "Content-Type" header equal to "application/ld+json"
109
And I send a "POST" request to "/readable_only_properties" with body:

features/main/relation.feature

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -491,7 +491,7 @@ Feature: Relations support
491491
Given there are people having pets
492492
When I add "Content-Type" header equal to "application/ld+json"
493493
And I send a "GET" request to "/people"
494-
And the response status code should be 200
494+
Then the response status code should be 200
495495
And the response should be in JSON
496496
And the JSON should be equal to:
497497
"""
@@ -621,8 +621,6 @@ Feature: Relations support
621621
}
622622
"""
623623

624-
625-
@dropSchema
626624
Scenario: Passing an invalid IRI to a relation
627625
When I add "Content-Type" header equal to "application/ld+json"
628626
And I send a "POST" request to "/relation_embedders" with body:
@@ -634,7 +632,7 @@ Feature: Relations support
634632
Then the response status code should be 400
635633
And the response should be in JSON
636634
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
637-
And the JSON node "hydra:description" should contain "Invalid value provided (invalid IRI?)."
635+
And the JSON node "hydra:description" should contain 'Invalid IRI "certainly not an iri and not a plain identifier".'
638636

639637
Scenario: Passing an invalid type to a relation
640638
When I add "Content-Type" header equal to "application/ld+json"
@@ -647,4 +645,32 @@ Feature: Relations support
647645
Then the response status code should be 400
648646
And the response should be in JSON
649647
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
650-
And the JSON node "hydra:description" should contain "Invalid value provided (invalid IRI?)."
648+
And the JSON should be valid according to this schema:
649+
"""
650+
{
651+
"type": "object",
652+
"properties": {
653+
"@context": {
654+
"type": "string",
655+
"pattern": "^/contexts/Error$"
656+
},
657+
"@type": {
658+
"type": "string",
659+
"pattern": "^hydra:Error$"
660+
},
661+
"hydra:title": {
662+
"type": "string",
663+
"pattern": "^An error occurred$"
664+
},
665+
"hydra:description": {
666+
"pattern": "^Expected IRI or document for resource \"ApiPlatform\\\\Core\\\\Tests\\\\Fixtures\\\\TestBundle\\\\(Document|Entity)\\\\RelatedDummy\", \"integer\" given.$"
667+
}
668+
},
669+
"required": [
670+
"@context",
671+
"@type",
672+
"hydra:title",
673+
"hydra:description"
674+
]
675+
}
676+
"""

features/security/strong_typing.feature

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ Feature: Handle properly invalid data submitted to the API
7373
And the JSON node "@context" should be equal to "/contexts/Error"
7474
And the JSON node "@type" should be equal to "hydra:Error"
7575
And the JSON node "hydra:title" should be equal to "An error occurred"
76-
And the JSON node "hydra:description" should be equal to 'Expected IRI or nested document for attribute "relatedDummy", "string" given.'
76+
And the JSON node "hydra:description" should be equal to 'Invalid IRI "1".'
7777
And the JSON node "trace" should exist
7878

7979
Scenario: Ignore invalid dates

features/serializer/vo_relations.feature

Lines changed: 38 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -26,28 +26,28 @@ Feature: Value object as ApiResource
2626
Then the response status code should be 201
2727
And the JSON should be equal to:
2828
"""
29-
{
30-
"@context": "/contexts/VoDummyCar",
31-
"@id": "/vo_dummy_cars/1",
32-
"@type": "VoDummyCar",
33-
"mileage": 1500,
34-
"bodyType": "suv",
35-
"inspections": [],
36-
"make": "CustomCar",
37-
"insuranceCompany": {
38-
"@id": "/vo_dummy_insurance_companies/1",
39-
"@type": "VoDummyInsuranceCompany",
40-
"name": "Safe Drive Company"
41-
},
42-
"drivers": [
43-
{
44-
"@id": "/vo_dummy_drivers/1",
45-
"@type": "VoDummyDriver",
46-
"firstName": "John",
47-
"lastName": "Doe"
48-
}
49-
]
50-
}
29+
{
30+
"@context": "/contexts/VoDummyCar",
31+
"@id": "/vo_dummy_cars/1",
32+
"@type": "VoDummyCar",
33+
"mileage": 1500,
34+
"bodyType": "suv",
35+
"inspections": [],
36+
"make": "CustomCar",
37+
"insuranceCompany": {
38+
"@id": "/vo_dummy_insurance_companies/1",
39+
"@type": "VoDummyInsuranceCompany",
40+
"name": "Safe Drive Company"
41+
},
42+
"drivers": [
43+
{
44+
"@id": "/vo_dummy_drivers/1",
45+
"@type": "VoDummyDriver",
46+
"firstName": "John",
47+
"lastName": "Doe"
48+
}
49+
]
50+
}
5151
"""
5252
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
5353

@@ -98,8 +98,7 @@ Feature: Value object as ApiResource
9898
"@type": "VoDummyInspection",
9999
"accepted": true,
100100
"car": "/vo_dummy_cars/1",
101-
"performed": "2018-08-24T00:00:00+00:00",
102-
"id": 1
101+
"performed": "2018-08-24T00:00:00+00:00"
103102
}
104103
"""
105104

@@ -117,27 +116,36 @@ Feature: Value object as ApiResource
117116
}
118117
"""
119118
Then the response status code should be 400
119+
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
120120
And the JSON should be valid according to this schema:
121121
"""
122122
{
123123
"type": "object",
124124
"properties": {
125125
"@context": {
126-
"enum": ["/contexts/Error"]
126+
"type": "string",
127+
"pattern": "^/contexts/Error$"
127128
},
128-
"type": {
129-
"enum": ["hydra:Error"]
129+
"@type": {
130+
"type": "string",
131+
"pattern": "^hydra:Error$"
130132
},
131133
"hydra:title": {
132-
"enum": ["An error occurred"]
134+
"type": "string",
135+
"pattern": "^An error occurred$"
133136
},
134137
"hydra:description": {
135138
"pattern": "^Cannot create an instance of ApiPlatform\\\\Core\\\\Tests\\\\Fixtures\\\\TestBundle\\\\(Document|Entity)\\\\VoDummyCar from serialized data because its constructor requires parameter \"drivers\" to be present.$"
136139
}
137-
}
140+
},
141+
"required": [
142+
"@context",
143+
"@type",
144+
"hydra:title",
145+
"hydra:description"
146+
]
138147
}
139148
"""
140-
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
141149

142150
@createSchema
143151
Scenario: Create Value object without default param

src/Api/CachedIdentifiersExtractor.php

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
namespace ApiPlatform\Core\Api;
1515

16-
use ApiPlatform\Core\Util\ClassInfoTrait;
16+
use ApiPlatform\Core\Util\ResourceClassInfoTrait;
1717
use Psr\Cache\CacheException;
1818
use Psr\Cache\CacheItemPoolInterface;
1919
use Symfony\Component\PropertyAccess\PropertyAccess;
@@ -26,14 +26,13 @@
2626
*/
2727
final class CachedIdentifiersExtractor implements IdentifiersExtractorInterface
2828
{
29-
use ClassInfoTrait;
29+
use ResourceClassInfoTrait;
3030

3131
public const CACHE_KEY_PREFIX = 'iri_identifiers';
3232

3333
private $cacheItemPool;
3434
private $propertyAccessor;
3535
private $decorated;
36-
private $resourceClassResolver;
3736
private $localCache = [];
3837
private $localResourceCache = [];
3938

@@ -82,9 +81,7 @@ public function getIdentifiersFromItem($item): array
8281
continue;
8382
}
8483

85-
$relatedResourceClass = $this->getObjectClass($identifiers[$propertyName]);
86-
87-
if (null !== $this->resourceClassResolver && !$this->resourceClassResolver->isResourceClass($relatedResourceClass)) {
84+
if (null === $relatedResourceClass = $this->getResourceClass($identifiers[$propertyName])) {
8885
continue;
8986
}
9087

src/Api/IdentifiersExtractor.php

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
use ApiPlatform\Core\Exception\RuntimeException;
1717
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
1818
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
19-
use ApiPlatform\Core\Util\ClassInfoTrait;
19+
use ApiPlatform\Core\Util\ResourceClassInfoTrait;
2020
use Symfony\Component\PropertyAccess\PropertyAccess;
2121
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
2222

@@ -27,12 +27,11 @@
2727
*/
2828
final class IdentifiersExtractor implements IdentifiersExtractorInterface
2929
{
30-
use ClassInfoTrait;
30+
use ResourceClassInfoTrait;
3131

3232
private $propertyNameCollectionFactory;
3333
private $propertyMetadataFactory;
3434
private $propertyAccessor;
35-
private $resourceClassResolver;
3635

3736
public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, PropertyAccessorInterface $propertyAccessor = null, ResourceClassResolverInterface $resourceClassResolver = null)
3837
{
@@ -67,7 +66,8 @@ public function getIdentifiersFromResourceClass(string $resourceClass): array
6766
public function getIdentifiersFromItem($item): array
6867
{
6968
$identifiers = [];
70-
$resourceClass = $this->getObjectClass($item);
69+
$resourceClass = $this->getResourceClass($item, true);
70+
7171
foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $propertyName) {
7272
$propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName);
7373
$identifier = $propertyMetadata->isIdentifier();
@@ -81,9 +81,7 @@ public function getIdentifiersFromItem($item): array
8181
continue;
8282
}
8383

84-
$relatedResourceClass = $this->getObjectClass($identifier);
85-
86-
if (null !== $this->resourceClassResolver && !$this->resourceClassResolver->isResourceClass($relatedResourceClass)) {
84+
if (null === $relatedResourceClass = $this->getResourceClass($identifier)) {
8785
continue;
8886
}
8987

src/Api/ResourceClassResolver.php

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -40,33 +40,45 @@ public function __construct(ResourceNameCollectionFactoryInterface $resourceName
4040
*/
4141
public function getResourceClass($value, string $resourceClass = null, bool $strict = false): string
4242
{
43-
$type = \is_object($value) && !$value instanceof \Traversable ? $this->getObjectClass($value) : $resourceClass;
44-
$resourceClass = $resourceClass ?? $type;
43+
if ($strict && null === $resourceClass) {
44+
throw new InvalidArgumentException('Strict checking is only possible when resource class is specified.');
45+
}
46+
47+
$actualClass = \is_object($value) && !$value instanceof \Traversable ? $this->getObjectClass($value) : null;
4548

46-
if (null === $resourceClass) {
47-
throw new InvalidArgumentException(sprintf('No resource class found.'));
49+
if (null === $actualClass && null === $resourceClass) {
50+
throw new InvalidArgumentException('Resource type could not be determined. Resource class must be specified.');
4851
}
4952

50-
if (
51-
null === $type
52-
|| ((!$strict || $resourceClass === $type) && $isResourceClass = $this->isResourceClass($type))
53-
) {
53+
if (null !== $resourceClass && !$this->isResourceClass($resourceClass)) {
54+
throw new InvalidArgumentException(sprintf('Specified class "%s" is not a resource class.', $resourceClass));
55+
}
56+
57+
if (null === $actualClass) {
5458
return $resourceClass;
5559
}
5660

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));
61+
if ($strict && !is_a($actualClass, $resourceClass, true)) {
62+
throw new InvalidArgumentException(sprintf('Object of type "%s" does not match "%s" resource class.', $actualClass, $resourceClass));
63+
}
64+
65+
$mostSpecificResourceClass = null;
66+
67+
foreach ($this->resourceNameCollectionFactory->create() as $resourceClassName) {
68+
if (!is_a($actualClass, $resourceClassName, true)) {
69+
continue;
70+
}
71+
72+
if (null === $mostSpecificResourceClass || is_subclass_of($resourceClassName, $mostSpecificResourceClass, true)) {
73+
$mostSpecificResourceClass = $resourceClassName;
74+
}
6075
}
6176

62-
if (
63-
($isResourceClass ?? $this->isResourceClass($type))
64-
|| (is_subclass_of($type, $resourceClass) && $this->isResourceClass($resourceClass))
65-
) {
66-
return $type;
77+
if (null === $mostSpecificResourceClass) {
78+
throw new InvalidArgumentException(sprintf('No resource class found for object of type "%s".', $actualClass));
6779
}
6880

69-
throw new InvalidArgumentException(sprintf('No resource class found for object of type "%s".', $type));
81+
return $mostSpecificResourceClass;
7082
}
7183

7284
/**
@@ -79,7 +91,7 @@ public function isResourceClass(string $type): bool
7991
}
8092

8193
foreach ($this->resourceNameCollectionFactory->create() as $resourceClass) {
82-
if ($type === $resourceClass) {
94+
if (is_a($type, $resourceClass, true)) {
8395
return $this->localIsResourceClassCache[$type] = true;
8496
}
8597
}

src/Api/ResourceClassResolverInterface.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ interface ResourceClassResolverInterface
2525
/**
2626
* Guesses the associated resource.
2727
*
28+
* @param string $resourceClass The expected resource class
29+
* @param bool $strict If true, value must match the expected resource class
30+
*
2831
* @throws InvalidArgumentException
2932
*/
3033
public function getResourceClass($value, string $resourceClass = null, bool $strict = false): string;

src/Bridge/Doctrine/EventListener/PublishMercureUpdatesListener.php

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
use ApiPlatform\Core\Exception\InvalidArgumentException;
2020
use ApiPlatform\Core\Exception\RuntimeException;
2121
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
22-
use ApiPlatform\Core\Util\ClassInfoTrait;
22+
use ApiPlatform\Core\Util\ResourceClassInfoTrait;
2323
use Doctrine\ORM\Event\OnFlushEventArgs;
2424
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
2525
use Symfony\Component\Mercure\Update;
@@ -35,9 +35,8 @@
3535
*/
3636
final class PublishMercureUpdatesListener
3737
{
38-
use ClassInfoTrait;
38+
use ResourceClassInfoTrait;
3939

40-
private $resourceClassResolver;
4140
private $iriConverter;
4241
private $resourceMetadataFactory;
4342
private $serializer;
@@ -120,8 +119,7 @@ private function reset(): void
120119
*/
121120
private function storeEntityToPublish($entity, string $property): void
122121
{
123-
$resourceClass = $this->getObjectClass($entity);
124-
if (!$this->resourceClassResolver->isResourceClass($resourceClass)) {
122+
if (null === $resourceClass = $this->getResourceClass($entity)) {
125123
return;
126124
}
127125

0 commit comments

Comments
 (0)