Skip to content

Commit c99a148

Browse files
authored
Merge pull request #1927 from soyuka/allow-subresource-iri-bis
Allow subresource items in the iri converter
2 parents 7d311da + 6d40a8c commit c99a148

File tree

11 files changed

+458
-375
lines changed

11 files changed

+458
-375
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;
@@ -877,4 +878,38 @@ public function thereAreDummyImmutableDateObjectsWithDummyDate(int $nb)
877878

878879
$this->manager->flush();
879880
}
881+
882+
/**
883+
* @Given there is a dummy object with a fourth level relation
884+
*/
885+
public function thereIsADummyObjectWithAFourthLevelRelation()
886+
{
887+
$fourthLevel = new FourthLevel();
888+
$fourthLevel->setLevel(4);
889+
$this->manager->persist($fourthLevel);
890+
891+
$thirdLevel = new ThirdLevel();
892+
$thirdLevel->setLevel(3);
893+
$thirdLevel->setFourthLevel($fourthLevel);
894+
$this->manager->persist($thirdLevel);
895+
896+
$namedRelatedDummy = new RelatedDummy();
897+
$namedRelatedDummy->setName('Hello');
898+
$namedRelatedDummy->setThirdLevel($thirdLevel);
899+
$this->manager->persist($namedRelatedDummy);
900+
901+
$relatedDummy = new RelatedDummy();
902+
$relatedDummy = new RelatedDummy();
903+
$relatedDummy->setThirdLevel($thirdLevel);
904+
$this->manager->persist($relatedDummy);
905+
906+
$dummy = new Dummy();
907+
$dummy->setName('Dummy with relations');
908+
$dummy->setRelatedDummy($namedRelatedDummy);
909+
$dummy->addRelatedDummy($namedRelatedDummy);
910+
$dummy->addRelatedDummy($relatedDummy);
911+
$this->manager->persist($dummy);
912+
913+
$this->manager->flush();
914+
}
880915
}

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
@@ -58,6 +58,7 @@
5858
<argument type="service" id="api_platform.router" />
5959
<argument type="service" id="api_platform.property_accessor" />
6060
<argument type="service" id="api_platform.identifiers_extractor.cached" />
61+
<argument type="service" id="api_platform.subresource_data_provider" on-invalid="ignore" />
6162
</service>
6263
<service id="ApiPlatform\Core\Api\IriConverterInterface" alias="api_platform.iri_converter" />
6364

src/Bridge/Symfony/Routing/IriConverter.php

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,14 @@
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;
2325
use ApiPlatform\Core\Exception\ItemNotFoundException;
2426
use ApiPlatform\Core\Exception\RuntimeException;
2527
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
2628
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
29+
use ApiPlatform\Core\Util\AttributesExtractor;
2730
use ApiPlatform\Core\Util\ClassInfoTrait;
2831
use Symfony\Component\PropertyAccess\PropertyAccess;
2932
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
@@ -38,20 +41,21 @@
3841
final class IriConverter implements IriConverterInterface
3942
{
4043
use ClassInfoTrait;
44+
use OperationDataProviderTrait;
4145

42-
private $itemDataProvider;
4346
private $routeNameResolver;
4447
private $router;
4548
private $identifiersExtractor;
4649

47-
public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ItemDataProviderInterface $itemDataProvider, RouteNameResolverInterface $routeNameResolver, RouterInterface $router, PropertyAccessorInterface $propertyAccessor = null, IdentifiersExtractorInterface $identifiersExtractor = null)
50+
public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ItemDataProviderInterface $itemDataProvider, RouteNameResolverInterface $routeNameResolver, RouterInterface $router, PropertyAccessorInterface $propertyAccessor = null, IdentifiersExtractorInterface $identifiersExtractor = null, SubresourceDataProviderInterface $subresourceDataProvider = null)
4851
{
4952
$this->itemDataProvider = $itemDataProvider;
5053
$this->routeNameResolver = $routeNameResolver;
5154
$this->router = $router;
55+
$this->subresourceDataProvider = $subresourceDataProvider;
5256

5357
if (null === $identifiersExtractor) {
54-
@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);
58+
@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);
5559
$this->identifiersExtractor = new IdentifiersExtractor($propertyNameCollectionFactory, $propertyMetadataFactory, $propertyAccessor ?? PropertyAccess::createPropertyAccessor());
5660
} else {
5761
$this->identifiersExtractor = $identifiersExtractor;
@@ -69,11 +73,22 @@ public function getItemFromIri(string $iri, array $context = [])
6973
throw new InvalidArgumentException(sprintf('No route matches "%s".', $iri), $e->getCode(), $e);
7074
}
7175

72-
if (!isset($parameters['_api_resource_class'], $parameters['id'])) {
76+
if (!isset($parameters['_api_resource_class'])) {
7377
throw new InvalidArgumentException(sprintf('No resource associated to "%s".', $iri));
7478
}
7579

76-
if ($item = $this->itemDataProvider->getItem($parameters['_api_resource_class'], $parameters['id'], $parameters['_api_item_operation_name'] ?? null, $context)) {
80+
$attributes = AttributesExtractor::extractAttributes($parameters);
81+
$identifiers = $this->extractIdentifiers($parameters, $attributes);
82+
83+
if (isset($attributes['subresource_operation_name'])) {
84+
if ($item = $this->getSubresourceData($identifiers, $attributes, $context)) {
85+
return $item;
86+
}
87+
88+
throw new ItemNotFoundException(sprintf('Item not found for "%s".', $iri));
89+
}
90+
91+
if ($item = $this->getItemData($identifiers, $attributes, $context)) {
7792
return $item;
7893
}
7994

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

0 commit comments

Comments
 (0)