Skip to content

Commit

Permalink
Allow POST/DELETE on an ApiSubresource
Browse files Browse the repository at this point in the history
  • Loading branch information
vincentchalamon committed Apr 6, 2019
1 parent cf11380 commit 69c5e4b
Show file tree
Hide file tree
Showing 13 changed files with 371 additions and 15 deletions.
268 changes: 267 additions & 1 deletion features/main/subresource.feature
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ Feature: Subresource support

Scenario: Get the subresource relation collection
Given there is a dummy object with a fourth level relation
When I send a "GET" request to "/dummies/1/related_dummies"
When I send a "GET" request to "/dummies/1/related_dummies/2"
And the response status code should be 200
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
Expand Down Expand Up @@ -269,6 +269,272 @@ Feature: Subresource support
}
"""

@createSchema
Scenario: Create an entry on a subresource collection
Given there is a dummy object with a fourth level relation
When I add "Content-Type" header equal to "application/ld+json"
And I send a "POST" request to "/dummies/1/related_dummies" with body:
"""
{
"name": "New related dummy"
}
"""
Then the response status code should be 201
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
Then I send a "GET" request to "/dummies/1/related_dummies"
And the response status code should be 200
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
And the JSON should be equal to:
"""
{
"@context": "/contexts/RelatedDummy",
"@id": "/dummies/1/related_dummies",
"@type": "hydra:Collection",
"hydra:member": [
{
"@id": "/related_dummies/1",
"@type": "https://schema.org/Product",
"id": 1,
"name": "Hello",
"symfony": "symfony",
"dummyDate": null,
"thirdLevel": {
"@id": "/third_levels/1",
"@type": "ThirdLevel",
"fourthLevel": "/fourth_levels/1"
},
"relatedToDummyFriend": [],
"dummyBoolean": null,
"embeddedDummy": [],
"age": null
},
{
"@id": "/related_dummies/2",
"@type": "https://schema.org/Product",
"id": 2,
"name": null,
"symfony": "symfony",
"dummyDate": null,
"thirdLevel": {
"@id": "/third_levels/1",
"@type": "ThirdLevel",
"fourthLevel": "/fourth_levels/1"
},
"relatedToDummyFriend": [],
"dummyBoolean": null,
"embeddedDummy": [],
"age": null
},
{
"@id": "/related_dummies/3",
"@type": "https://schema.org/Product",
"id": 3,
"name": "New related dummy",
"symfony": "symfony",
"dummyDate": null,
"thirdLevel": null,
"relatedToDummyFriend": [],
"dummyBoolean": null,
"embeddedDummy": [],
"age": null
}
],
"hydra:totalItems": 3,
"hydra:search": {
"@type": "hydra:IriTemplate",
"hydra:template": "/dummies/1/related_dummies{?relatedToDummyFriend.dummyFriend,relatedToDummyFriend.dummyFriend[],name}",
"hydra:variableRepresentation": "BasicRepresentation",
"hydra:mapping": [
{
"@type": "IriTemplateMapping",
"variable": "relatedToDummyFriend.dummyFriend",
"property": "relatedToDummyFriend.dummyFriend",
"required": false
},
{
"@type": "IriTemplateMapping",
"variable": "relatedToDummyFriend.dummyFriend[]",
"property": "relatedToDummyFriend.dummyFriend",
"required": false
},
{
"@type": "IriTemplateMapping",
"variable": "name",
"property": "name",
"required": false
}
]
}
}
"""

@createSchema
Scenario: Add an existing entry on a subresource collection
Given there is a dummy object with a fourth level relation
And there is a RelatedDummy with 0 friends
When I add "Content-Type" header equal to "application/ld+json"
And I send a "POST" request to "/dummies/1/related_dummies" with body:
"""
{
"@id": "/related_dummies/3"
}
"""
Then the response status code should be 201
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
Then I send a "GET" request to "/dummies/1/related_dummies"
And the response status code should be 200
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
And the JSON should be equal to:
"""
{
"@context": "/contexts/RelatedDummy",
"@id": "/dummies/1/related_dummies",
"@type": "hydra:Collection",
"hydra:member": [
{
"@id": "/related_dummies/1",
"@type": "https://schema.org/Product",
"id": 1,
"name": "Hello",
"symfony": "symfony",
"dummyDate": null,
"thirdLevel": {
"@id": "/third_levels/1",
"@type": "ThirdLevel",
"fourthLevel": "/fourth_levels/1"
},
"relatedToDummyFriend": [],
"dummyBoolean": null,
"embeddedDummy": [],
"age": null
},
{
"@id": "/related_dummies/2",
"@type": "https://schema.org/Product",
"id": 2,
"name": null,
"symfony": "symfony",
"dummyDate": null,
"thirdLevel": {
"@id": "/third_levels/1",
"@type": "ThirdLevel",
"fourthLevel": "/fourth_levels/1"
},
"relatedToDummyFriend": [],
"dummyBoolean": null,
"embeddedDummy": [],
"age": null
},
{
"@id": "/related_dummies/3",
"@type": "https://schema.org/Product",
"id": 3,
"name": "RelatedDummy with friends",
"symfony": "symfony",
"dummyDate": null,
"thirdLevel": null,
"relatedToDummyFriend": [],
"dummyBoolean": null,
"embeddedDummy": [],
"age": null
}
],
"hydra:totalItems": 3,
"hydra:search": {
"@type": "hydra:IriTemplate",
"hydra:template": "/dummies/1/related_dummies{?relatedToDummyFriend.dummyFriend,relatedToDummyFriend.dummyFriend[],name}",
"hydra:variableRepresentation": "BasicRepresentation",
"hydra:mapping": [
{
"@type": "IriTemplateMapping",
"variable": "relatedToDummyFriend.dummyFriend",
"property": "relatedToDummyFriend.dummyFriend",
"required": false
},
{
"@type": "IriTemplateMapping",
"variable": "relatedToDummyFriend.dummyFriend[]",
"property": "relatedToDummyFriend.dummyFriend",
"required": false
},
{
"@type": "IriTemplateMapping",
"variable": "name",
"property": "name",
"required": false
}
]
}
}
"""

@createSchema
Scenario: Delete an entry on a subresource collection
Given there is a dummy object with a fourth level relation
When I send a "DELETE" request to "/dummies/1/related_dummies/2"
Then the response status code should be 204
And I send a "GET" request to "/dummies/1/related_dummies"
And the response status code should be 200
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
And the JSON should be equal to:
"""
{
"@context": "/contexts/RelatedDummy",
"@id": "/dummies/1/related_dummies",
"@type": "hydra:Collection",
"hydra:member": [
{
"@id": "/related_dummies/1",
"@type": "https://schema.org/Product",
"id": 1,
"name": "Hello",
"symfony": "symfony",
"dummyDate": null,
"thirdLevel": {
"@id": "/third_levels/1",
"@type": "ThirdLevel",
"fourthLevel": "/fourth_levels/1"
},
"relatedToDummyFriend": [],
"dummyBoolean": null,
"embeddedDummy": [],
"age": null
},
],
"hydra:totalItems": 1,
"hydra:search": {
"@type": "hydra:IriTemplate",
"hydra:template": "/dummies/1/related_dummies{?relatedToDummyFriend.dummyFriend,relatedToDummyFriend.dummyFriend[],name}",
"hydra:variableRepresentation": "BasicRepresentation",
"hydra:mapping": [
{
"@type": "IriTemplateMapping",
"variable": "relatedToDummyFriend.dummyFriend",
"property": "relatedToDummyFriend.dummyFriend",
"required": false
},
{
"@type": "IriTemplateMapping",
"variable": "relatedToDummyFriend.dummyFriend[]",
"property": "relatedToDummyFriend.dummyFriend",
"required": false
},
{
"@type": "IriTemplateMapping",
"variable": "name",
"property": "name",
"required": false
}
]
}
}
"""

Scenario: Get offers subresource from aggregate offers subresource
Given I have a product with offers
When I send a "GET" request to "/dummy_products/2/offers/1/offers"
Expand Down
15 changes: 15 additions & 0 deletions src/Annotation/ApiSubresource.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,26 @@
*
* @Annotation
* @Target({"METHOD", "PROPERTY"})
* @Attributes(
* @Attribute("maxDepth", type="int"),
* @Attribute("postEnabled", type="bool"),
* @Attribute("deleteEnabled", type="bool")
* )
*/
final class ApiSubresource
{
/**
* @var int
*/
public $maxDepth;

/**
* @var bool
*/
public $postEnabled = true;

/**
* @var bool
*/
public $deleteEnabled = true;
}
1 change: 1 addition & 0 deletions src/Bridge/Doctrine/Common/DataPersister.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ public function remove($data, array $context = [])

$manager->remove($data);
$manager->flush();
$manager->refresh($data);
}

/**
Expand Down
2 changes: 2 additions & 0 deletions src/Bridge/Symfony/Bundle/Resources/config/api.xml
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,8 @@
<service id="api_platform.action.put_item" alias="api_platform.action.placeholder" public="true" />
<service id="api_platform.action.delete_item" alias="api_platform.action.placeholder" public="true" />
<service id="api_platform.action.get_subresource" alias="api_platform.action.placeholder" public="true" />
<service id="api_platform.action.post_subresource" alias="api_platform.action.placeholder" public="true" />
<service id="api_platform.action.delete_subresource" alias="api_platform.action.placeholder" public="true" />

<service id="api_platform.action.entrypoint" class="ApiPlatform\Core\Action\EntrypointAction" public="true">
<argument type="service" id="api_platform.metadata.resource.name_collection_factory" />
Expand Down
2 changes: 1 addition & 1 deletion src/Bridge/Symfony/Routing/ApiLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ public function load($data, $type = null): RouteCollection
$operation['options'] ?? [],
$operation['host'] ?? '',
$operation['schemes'] ?? [],
['GET'],
[$operation['method'] ?? 'GET'],
$operation['condition'] ?? ''
));
}
Expand Down
2 changes: 2 additions & 0 deletions src/EventListener/DeserializeListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,14 @@ public function onKernelRequest(GetResponseEvent $event): void
$context[AbstractNormalizer::OBJECT_TO_POPULATE] = $data;
}

$context['api_allow_update'] = \array_key_exists('subresource_context', $attributes);
$request->attributes->set(
'data',
$this->serializer->deserialize(
$requestContent, $context['resource_class'], $format, $context
)
);
$a = 'b';
}

/**
Expand Down
7 changes: 7 additions & 0 deletions src/EventListener/ReadListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,13 @@ public function onKernelRequest(GetResponseEvent $event): void
throw new RuntimeException('No subresource data provider.');
}

// todo Move to OperationDataProviderTrait::getSubresourceRootData
// $rootData = $this->itemDataProvider->getItem($attributes['subresource_context']['identifiers'][0][1], $identifiers, null, $context);
// if (null === $rootData) {
// throw new NotFoundHttpException('Not Found');
// }
// $request->attributes->set('rootData', $rootData);

$data = $this->getSubresourceData($identifiers, $attributes, $context);
}
} catch (InvalidIdentifierException $e) {
Expand Down
16 changes: 14 additions & 2 deletions src/EventListener/WriteListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,21 @@ public function onKernelView(GetResponseForControllerResultEvent $event): void
if ($hasOutput) {
$request->attributes->set('_api_write_item_iri', $this->iriConverter->getIriFromItem($controllerResult));
}
break;

// In case of POST request on a subresource, add data to root object collection association
if ($request->attributes->has('rootData')) {
$request->attributes->get('rootData')->addRelatedDummy($request->attributes->get('data'));
$this->dataPersister->persist($request->attributes->get('rootData'));
}
break;
case 'DELETE':
$this->dataPersister->remove($controllerResult);
// In case of POST request on a subresource, add data to root object collection association
if ($request->attributes->has('rootData')) {
$request->attributes->get('rootData')->removeRelatedDummy($controllerResult);
$this->dataPersister->persist($request->attributes->get('rootData'));
} else {
$this->dataPersister->remove($controllerResult);
}
$event->setControllerResult(null);
break;
}
Expand Down
Loading

0 comments on commit 69c5e4b

Please sign in to comment.