Skip to content

Commit 309e0ff

Browse files
authored
Merge pull request #1877 from soyuka/allow-subresource-iri
Allow subresource items in the iri converter
2 parents ca162c1 + 985f0c1 commit 309e0ff

File tree

12 files changed

+454
-516
lines changed

12 files changed

+454
-516
lines changed

features/bootstrap/FeatureContext.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\FileConfigDummy;
3434
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Foo;
3535
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\FooDummy;
36+
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\FourthLevel;
3637
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Node;
3738
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Person;
3839
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\PersonToPet;
@@ -883,4 +884,38 @@ public function thereIsARamseyIdentifiedResource(string $uuid)
883884
$this->manager->persist($dummy);
884885
$this->manager->flush();
885886
}
887+
888+
/**
889+
* @Given there is a dummy object with a fourth level relation
890+
*/
891+
public function thereIsADummyObjectWithAFourthLevelRelation()
892+
{
893+
$fourthLevel = new FourthLevel();
894+
$fourthLevel->setLevel(4);
895+
$this->manager->persist($fourthLevel);
896+
897+
$thirdLevel = new ThirdLevel();
898+
$thirdLevel->setLevel(3);
899+
$thirdLevel->setFourthLevel($fourthLevel);
900+
$this->manager->persist($thirdLevel);
901+
902+
$namedRelatedDummy = new RelatedDummy();
903+
$namedRelatedDummy->setName('Hello');
904+
$namedRelatedDummy->setThirdLevel($thirdLevel);
905+
$this->manager->persist($namedRelatedDummy);
906+
907+
$relatedDummy = new RelatedDummy();
908+
$relatedDummy = new RelatedDummy();
909+
$relatedDummy->setThirdLevel($thirdLevel);
910+
$this->manager->persist($relatedDummy);
911+
912+
$dummy = new Dummy();
913+
$dummy->setName('Dummy with relations');
914+
$dummy->setRelatedDummy($namedRelatedDummy);
915+
$dummy->addRelatedDummy($namedRelatedDummy);
916+
$dummy->addRelatedDummy($relatedDummy);
917+
$this->manager->persist($dummy);
918+
919+
$this->manager->flush();
920+
}
886921
}

features/main/subresource.feature

Lines changed: 14 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -53,87 +53,8 @@ Feature: Subresource support
5353
}
5454
"""
5555

56-
Scenario: Create a fourth level
57-
When I add "Content-Type" header equal to "application/ld+json"
58-
And I send a "POST" request to "/fourth_levels" with body:
59-
"""
60-
{"level": 4}
61-
"""
62-
Then the response status code should be 201
63-
And the response should be in JSON
64-
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
65-
And the JSON should be equal to:
66-
"""
67-
{
68-
"@context": "/contexts/FourthLevel",
69-
"@id": "/fourth_levels/1",
70-
"@type": "FourthLevel",
71-
"id": 1,
72-
"level": 4
73-
}
74-
"""
75-
76-
Scenario: Create a third level
77-
When I add "Content-Type" header equal to "application/ld+json"
78-
And I send a "POST" request to "/third_levels" with body:
79-
"""
80-
{"level": 3, "fourthLevel": "/fourth_levels/1"}
81-
"""
82-
Then the response status code should be 201
83-
And the response should be in JSON
84-
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
85-
And the JSON should be equal to:
86-
"""
87-
{
88-
"@context": "/contexts/ThirdLevel",
89-
"@id": "/third_levels/1",
90-
"@type": "ThirdLevel",
91-
"fourthLevel": "/fourth_levels/1",
92-
"id": 1,
93-
"level": 3,
94-
"test": true
95-
}
96-
"""
97-
98-
Scenario: Create a named related dummy
99-
When I add "Content-Type" header equal to "application/ld+json"
100-
And I send a "POST" request to "/related_dummies" with body:
101-
"""
102-
{"name": "Hello", "thirdLevel": "/third_levels/1"}
103-
"""
104-
Then the response status code should be 201
105-
And the response should be in JSON
106-
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
107-
108-
Scenario: Create an unnamed related dummy
109-
When I add "Content-Type" header equal to "application/ld+json"
110-
And I send a "POST" request to "/related_dummies" with body:
111-
"""
112-
{"thirdLevel": "/third_levels/1"}
113-
"""
114-
Then the response status code should be 201
115-
And the response should be in JSON
116-
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
117-
118-
Scenario: Create a dummy with relations
119-
When I add "Content-Type" header equal to "application/ld+json"
120-
And I send a "POST" request to "/dummies" with body:
121-
"""
122-
{
123-
"name": "Dummy with relations",
124-
"relatedDummy": "http://example.com/related_dummies/1",
125-
"relatedDummies": [
126-
"/related_dummies/1",
127-
"/related_dummies/2"
128-
],
129-
"name_converted": null
130-
}
131-
"""
132-
Then the response status code should be 201
133-
And the response should be in JSON
134-
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
135-
13656
Scenario: Get the subresource relation collection
57+
Given there is a dummy object with a fourth level relation
13758
When I send a "GET" request to "/dummies/1/related_dummies"
13859
And the response status code should be 200
13960
And the response should be in JSON
@@ -299,6 +220,19 @@ Feature: Subresource support
299220
}
300221
"""
301222

223+
Scenario: Create a dummy with a relation that is a subresource
224+
When I add "Content-Type" header equal to "application/ld+json"
225+
And I send a "POST" request to "/dummies" with body:
226+
"""
227+
{
228+
"name": "Dummy with relations",
229+
"relatedDummy": "/dummies/1/related_dummies/2"
230+
}
231+
"""
232+
Then the response status code should be 201
233+
And the response should be in JSON
234+
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
235+
302236
Scenario: Get the embedded relation subresource item at the third level
303237
When I send a "GET" request to "/dummies/1/related_dummies/1/third_level"
304238
And the response status code should be 200

src/Bridge/Symfony/Bundle/Resources/config/api.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
<argument type="service" id="api_platform.property_accessor" />
6060
<argument type="service" id="api_platform.identifiers_extractor.cached" />
6161
<argument type="service" id="api_platform.identifier.denormalizer" />
62+
<argument type="service" id="api_platform.subresource_data_provider" />
6263
</service>
6364
<service id="ApiPlatform\Core\Api\IriConverterInterface" alias="api_platform.iri_converter" />
6465

src/Bridge/Symfony/Routing/IriConverter.php

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,16 @@
1919
use ApiPlatform\Core\Api\OperationType;
2020
use ApiPlatform\Core\Api\UrlGeneratorInterface;
2121
use ApiPlatform\Core\DataProvider\ItemDataProviderInterface;
22+
use ApiPlatform\Core\DataProvider\OperationDataProviderTrait;
23+
use ApiPlatform\Core\DataProvider\SubresourceDataProviderInterface;
2224
use ApiPlatform\Core\Exception\InvalidArgumentException;
25+
use ApiPlatform\Core\Exception\InvalidIdentifierException;
2326
use ApiPlatform\Core\Exception\ItemNotFoundException;
2427
use ApiPlatform\Core\Exception\RuntimeException;
2528
use ApiPlatform\Core\Identifier\Normalizer\ChainIdentifierDenormalizer;
2629
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
2730
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
31+
use ApiPlatform\Core\Util\AttributesExtractor;
2832
use ApiPlatform\Core\Util\ClassInfoTrait;
2933
use Symfony\Component\PropertyAccess\PropertyAccess;
3034
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
@@ -39,29 +43,24 @@
3943
final class IriConverter implements IriConverterInterface
4044
{
4145
use ClassInfoTrait;
46+
use OperationDataProviderTrait;
4247

43-
private $itemDataProvider;
4448
private $routeNameResolver;
4549
private $router;
4650
private $identifiersExtractor;
47-
private $identifierDenormalizer;
4851

49-
public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ItemDataProviderInterface $itemDataProvider, RouteNameResolverInterface $routeNameResolver, RouterInterface $router, PropertyAccessorInterface $propertyAccessor = null, IdentifiersExtractorInterface $identifiersExtractor = null, ChainIdentifierDenormalizer $identifierDenormalizer = null)
52+
public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ItemDataProviderInterface $itemDataProvider, RouteNameResolverInterface $routeNameResolver, RouterInterface $router, PropertyAccessorInterface $propertyAccessor = null, IdentifiersExtractorInterface $identifiersExtractor = null, ChainIdentifierDenormalizer $identifierDenormalizer = null, SubresourceDataProviderInterface $subresourceDataProvider = null)
5053
{
5154
$this->itemDataProvider = $itemDataProvider;
5255
$this->routeNameResolver = $routeNameResolver;
5356
$this->router = $router;
57+
$this->identifiersExtractor = $identifiersExtractor;
5458
$this->identifierDenormalizer = $identifierDenormalizer;
59+
$this->subresourceDataProvider = $subresourceDataProvider;
5560

5661
if (null === $identifiersExtractor) {
57-
@trigger_error('Not injecting ItemIdentifiersExtractor is deprecated since API Platform 2.1 and will not be possible anymore in API Platform 3', E_USER_DEPRECATED);
62+
@trigger_error(sprintf('Not injecting "%s" is deprecated since API Platform 2.1 and will not be possible anymore in API Platform 3', IdentifiersExtractorInterface::class), E_USER_DEPRECATED);
5863
$this->identifiersExtractor = new IdentifiersExtractor($propertyNameCollectionFactory, $propertyMetadataFactory, $propertyAccessor ?? PropertyAccess::createPropertyAccessor());
59-
} else {
60-
$this->identifiersExtractor = $identifiersExtractor;
61-
}
62-
63-
if (null === $identifierDenormalizer) {
64-
@trigger_error(sprintf('Not injecting "%s" is deprecated since API Platform 2.2 and will not be possible anymore in API Platform 3.', ChainIdentifierDenormalizer::class), E_USER_DEPRECATED);
6564
}
6665
}
6766

@@ -76,18 +75,31 @@ public function getItemFromIri(string $iri, array $context = [])
7675
throw new InvalidArgumentException(sprintf('No route matches "%s".', $iri), $e->getCode(), $e);
7776
}
7877

79-
if (!isset($parameters['_api_resource_class'], $parameters['id'])) {
78+
if (!isset($parameters['_api_resource_class'])) {
8079
throw new InvalidArgumentException(sprintf('No resource associated to "%s".', $iri));
8180
}
8281

83-
$identifiers = $parameters['id'];
82+
$attributes = AttributesExtractor::extractAttributes($parameters);
83+
84+
try {
85+
$identifiers = $this->extractIdentifiers($parameters, $attributes);
86+
} catch (InvalidIdentifierException $e) {
87+
throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
88+
}
8489

8590
if ($this->identifierDenormalizer) {
86-
$identifiers = $this->identifierDenormalizer->denormalize((string) $parameters['id'], $parameters['_api_resource_class']);
8791
$context[ChainIdentifierDenormalizer::HAS_IDENTIFIER_DENORMALIZER] = true;
8892
}
8993

90-
if ($item = $this->itemDataProvider->getItem($parameters['_api_resource_class'], $identifiers, $parameters['_api_item_operation_name'] ?? null, $context)) {
94+
if (isset($attributes['subresource_operation_name'])) {
95+
if ($item = $this->getSubresourceData($identifiers, $attributes, $context)) {
96+
return $item;
97+
}
98+
99+
throw new ItemNotFoundException(sprintf('Item not found for "%s".', $iri));
100+
}
101+
102+
if ($item = $this->getItemData($identifiers, $attributes, $context)) {
91103
return $item;
92104
}
93105

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
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\DataProvider;
15+
16+
use ApiPlatform\Core\Exception\InvalidIdentifierException;
17+
use ApiPlatform\Core\Exception\RuntimeException;
18+
use ApiPlatform\Core\Identifier\Normalizer\ChainIdentifierDenormalizer;
19+
20+
/**
21+
* @internal
22+
*/
23+
trait OperationDataProviderTrait
24+
{
25+
/**
26+
* @var CollectionDataProviderInterface
27+
*/
28+
private $collectionDataProvider;
29+
30+
/**
31+
* @var ItemDataProviderInterface
32+
*/
33+
private $itemDataProvider;
34+
35+
/**
36+
* @var SubresourceDataProviderInterface
37+
*/
38+
private $subresourceDataProvider;
39+
40+
/**
41+
* @var ChainIdentifierDenormalizer
42+
*/
43+
private $identifierDenormalizer;
44+
45+
/**
46+
* Retrieves data for a collection operation.
47+
*
48+
* @return iterable|null
49+
*/
50+
private function getCollectionData(array $attributes, array $context)
51+
{
52+
return $this->collectionDataProvider->getCollection($attributes['resource_class'], $attributes['collection_operation_name'], $context);
53+
}
54+
55+
/**
56+
* Gets data for an item operation.
57+
*
58+
* @throws NotFoundHttpException
59+
*
60+
* @return object|null
61+
*/
62+
private function getItemData($identifiers, array $attributes, array $context)
63+
{
64+
return $this->itemDataProvider->getItem($attributes['resource_class'], $identifiers, $attributes['item_operation_name'], $context);
65+
}
66+
67+
/**
68+
* Gets data for a nested operation.
69+
*
70+
* @throws NotFoundHttpException
71+
* @throws RuntimeException
72+
*
73+
* @return object|null
74+
*/
75+
private function getSubresourceData($identifiers, array $attributes, array $context)
76+
{
77+
if (!$this->subresourceDataProvider) {
78+
throw new RuntimeException('Subresources not supported');
79+
}
80+
81+
return $this->subresourceDataProvider->getSubresource($attributes['resource_class'], $identifiers, $attributes['subresource_context'] + $context, $attributes['subresource_operation_name']);
82+
}
83+
84+
/**
85+
* @param array $parameters - usually comes from $request->attributes->all()
86+
*
87+
* @throws InvalidIdentifierException
88+
*/
89+
private function extractIdentifiers(array $parameters, array $attributes)
90+
{
91+
if (isset($attributes['item_operation_name'])) {
92+
if (!isset($parameters['id'])) {
93+
throw new InvalidIdentifierException('Parameter "id" not found');
94+
}
95+
96+
$id = $parameters['id'];
97+
98+
if ($this->identifierDenormalizer) {
99+
return $this->identifierDenormalizer->denormalize((string) $id, $attributes['resource_class']);
100+
}
101+
102+
return $id;
103+
}
104+
105+
$identifiers = [];
106+
107+
foreach ($attributes['subresource_context']['identifiers'] as $key => list($id, $resourceClass, $hasIdentifier)) {
108+
if (false === $hasIdentifier) {
109+
continue;
110+
}
111+
112+
$identifiers[$id] = $parameters[$id];
113+
114+
if ($this->identifierDenormalizer) {
115+
$identifiers[$id] = $this->identifierDenormalizer->denormalize((string) $identifiers[$id], $resourceClass);
116+
}
117+
}
118+
119+
return $identifiers;
120+
}
121+
}

0 commit comments

Comments
 (0)