Skip to content

Allow subresource items in the iri converter #1877

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 26, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions features/bootstrap/FeatureContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\FileConfigDummy;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Foo;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\FooDummy;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\FourthLevel;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Node;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Person;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\PersonToPet;
Expand Down Expand Up @@ -883,4 +884,38 @@ public function thereIsARamseyIdentifiedResource(string $uuid)
$this->manager->persist($dummy);
$this->manager->flush();
}

/**
* @Given there is a dummy object with a fourth level relation
*/
public function thereIsADummyObjectWithAFourthLevelRelation()
{
$fourthLevel = new FourthLevel();
$fourthLevel->setLevel(4);
$this->manager->persist($fourthLevel);

$thirdLevel = new ThirdLevel();
$thirdLevel->setLevel(3);
$thirdLevel->setFourthLevel($fourthLevel);
$this->manager->persist($thirdLevel);

$namedRelatedDummy = new RelatedDummy();
$namedRelatedDummy->setName('Hello');
$namedRelatedDummy->setThirdLevel($thirdLevel);
$this->manager->persist($namedRelatedDummy);

$relatedDummy = new RelatedDummy();
$relatedDummy = new RelatedDummy();
$relatedDummy->setThirdLevel($thirdLevel);
$this->manager->persist($relatedDummy);

$dummy = new Dummy();
$dummy->setName('Dummy with relations');
$dummy->setRelatedDummy($namedRelatedDummy);
$dummy->addRelatedDummy($namedRelatedDummy);
$dummy->addRelatedDummy($relatedDummy);
$this->manager->persist($dummy);

$this->manager->flush();
}
}
94 changes: 14 additions & 80 deletions features/main/subresource.feature
Original file line number Diff line number Diff line change
Expand Up @@ -53,87 +53,8 @@ Feature: Subresource support
}
"""

Scenario: Create a fourth level
When I add "Content-Type" header equal to "application/ld+json"
And I send a "POST" request to "/fourth_levels" with body:
"""
{"level": 4}
"""
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"
And the JSON should be equal to:
"""
{
"@context": "/contexts/FourthLevel",
"@id": "/fourth_levels/1",
"@type": "FourthLevel",
"id": 1,
"level": 4
}
"""

Scenario: Create a third level
When I add "Content-Type" header equal to "application/ld+json"
And I send a "POST" request to "/third_levels" with body:
"""
{"level": 3, "fourthLevel": "/fourth_levels/1"}
"""
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"
And the JSON should be equal to:
"""
{
"@context": "/contexts/ThirdLevel",
"@id": "/third_levels/1",
"@type": "ThirdLevel",
"fourthLevel": "/fourth_levels/1",
"id": 1,
"level": 3,
"test": true
}
"""

Scenario: Create a named related dummy
When I add "Content-Type" header equal to "application/ld+json"
And I send a "POST" request to "/related_dummies" with body:
"""
{"name": "Hello", "thirdLevel": "/third_levels/1"}
"""
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"

Scenario: Create an unnamed related dummy
When I add "Content-Type" header equal to "application/ld+json"
And I send a "POST" request to "/related_dummies" with body:
"""
{"thirdLevel": "/third_levels/1"}
"""
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"

Scenario: Create a dummy with relations
When I add "Content-Type" header equal to "application/ld+json"
And I send a "POST" request to "/dummies" with body:
"""
{
"name": "Dummy with relations",
"relatedDummy": "http://example.com/related_dummies/1",
"relatedDummies": [
"/related_dummies/1",
"/related_dummies/2"
],
"name_converted": null
}
"""
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"

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"
And the response status code should be 200
And the response should be in JSON
Expand Down Expand Up @@ -299,6 +220,19 @@ Feature: Subresource support
}
"""

Scenario: Create a dummy with a relation that is a subresource
When I add "Content-Type" header equal to "application/ld+json"
And I send a "POST" request to "/dummies" with body:
"""
{
"name": "Dummy with relations",
"relatedDummy": "/dummies/1/related_dummies/2"
}
"""
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"

Scenario: Get the embedded relation subresource item at the third level
When I send a "GET" request to "/dummies/1/related_dummies/1/third_level"
And the response status code should be 200
Expand Down
1 change: 1 addition & 0 deletions src/Bridge/Symfony/Bundle/Resources/config/api.xml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
<argument type="service" id="api_platform.property_accessor" />
<argument type="service" id="api_platform.identifiers_extractor.cached" />
<argument type="service" id="api_platform.identifier.denormalizer" />
<argument type="service" id="api_platform.subresource_data_provider" />
</service>
<service id="ApiPlatform\Core\Api\IriConverterInterface" alias="api_platform.iri_converter" />

Expand Down
40 changes: 26 additions & 14 deletions src/Bridge/Symfony/Routing/IriConverter.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,16 @@
use ApiPlatform\Core\Api\OperationType;
use ApiPlatform\Core\Api\UrlGeneratorInterface;
use ApiPlatform\Core\DataProvider\ItemDataProviderInterface;
use ApiPlatform\Core\DataProvider\OperationDataProviderTrait;
use ApiPlatform\Core\DataProvider\SubresourceDataProviderInterface;
use ApiPlatform\Core\Exception\InvalidArgumentException;
use ApiPlatform\Core\Exception\InvalidIdentifierException;
use ApiPlatform\Core\Exception\ItemNotFoundException;
use ApiPlatform\Core\Exception\RuntimeException;
use ApiPlatform\Core\Identifier\Normalizer\ChainIdentifierDenormalizer;
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
use ApiPlatform\Core\Util\AttributesExtractor;
use ApiPlatform\Core\Util\ClassInfoTrait;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
Expand All @@ -39,29 +43,24 @@
final class IriConverter implements IriConverterInterface
{
use ClassInfoTrait;
use OperationDataProviderTrait;

private $itemDataProvider;
private $routeNameResolver;
private $router;
private $identifiersExtractor;
private $identifierDenormalizer;

public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ItemDataProviderInterface $itemDataProvider, RouteNameResolverInterface $routeNameResolver, RouterInterface $router, PropertyAccessorInterface $propertyAccessor = null, IdentifiersExtractorInterface $identifiersExtractor = null, ChainIdentifierDenormalizer $identifierDenormalizer = null)
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)
{
$this->itemDataProvider = $itemDataProvider;
$this->routeNameResolver = $routeNameResolver;
$this->router = $router;
$this->identifiersExtractor = $identifiersExtractor;
$this->identifierDenormalizer = $identifierDenormalizer;
$this->subresourceDataProvider = $subresourceDataProvider;

if (null === $identifiersExtractor) {
@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);
@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);
$this->identifiersExtractor = new IdentifiersExtractor($propertyNameCollectionFactory, $propertyMetadataFactory, $propertyAccessor ?? PropertyAccess::createPropertyAccessor());
} else {
$this->identifiersExtractor = $identifiersExtractor;
}

if (null === $identifierDenormalizer) {
@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);
}
}

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

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

$identifiers = $parameters['id'];
$attributes = AttributesExtractor::extractAttributes($parameters);

try {
$identifiers = $this->extractIdentifiers($parameters, $attributes);
} catch (InvalidIdentifierException $e) {
throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
}

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

if ($item = $this->itemDataProvider->getItem($parameters['_api_resource_class'], $identifiers, $parameters['_api_item_operation_name'] ?? null, $context)) {
if (isset($attributes['subresource_operation_name'])) {
if ($item = $this->getSubresourceData($identifiers, $attributes, $context)) {
return $item;
}

throw new ItemNotFoundException(sprintf('Item not found for "%s".', $iri));
}

if ($item = $this->getItemData($identifiers, $attributes, $context)) {
return $item;
}

Expand Down
121 changes: 121 additions & 0 deletions src/DataProvider/OperationDataProviderTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Core\DataProvider;

use ApiPlatform\Core\Exception\InvalidIdentifierException;
use ApiPlatform\Core\Exception\RuntimeException;
use ApiPlatform\Core\Identifier\Normalizer\ChainIdentifierDenormalizer;

/**
* @internal
*/
trait OperationDataProviderTrait
{
/**
* @var CollectionDataProviderInterface
*/
private $collectionDataProvider;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a blank line between props (btw, it's often better to have traits without props).


/**
* @var ItemDataProviderInterface
*/
private $itemDataProvider;

/**
* @var SubresourceDataProviderInterface
*/
private $subresourceDataProvider;

/**
* @var ChainIdentifierDenormalizer
*/
private $identifierDenormalizer;

/**
* Retrieves data for a collection operation.
*
* @return iterable|null
*/
private function getCollectionData(array $attributes, array $context)
{
return $this->collectionDataProvider->getCollection($attributes['resource_class'], $attributes['collection_operation_name'], $context);
}

/**
* Gets data for an item operation.
*
* @throws NotFoundHttpException
*
* @return object|null
*/
private function getItemData($identifiers, array $attributes, array $context)
{
return $this->itemDataProvider->getItem($attributes['resource_class'], $identifiers, $attributes['item_operation_name'], $context);
}

/**
* Gets data for a nested operation.
*
* @throws NotFoundHttpException
* @throws RuntimeException
*
* @return object|null
*/
private function getSubresourceData($identifiers, array $attributes, array $context)
{
if (!$this->subresourceDataProvider) {
throw new RuntimeException('Subresources not supported');
}

return $this->subresourceDataProvider->getSubresource($attributes['resource_class'], $identifiers, $attributes['subresource_context'] + $context, $attributes['subresource_operation_name']);
}

/**
* @param array $parameters - usually comes from $request->attributes->all()
*
* @throws InvalidIdentifierException
*/
private function extractIdentifiers(array $parameters, array $attributes)
{
if (isset($attributes['item_operation_name'])) {
if (!isset($parameters['id'])) {
throw new InvalidIdentifierException('Parameter "id" not found');
}

$id = $parameters['id'];

if ($this->identifierDenormalizer) {
return $this->identifierDenormalizer->denormalize((string) $id, $attributes['resource_class']);
}

return $id;
}

$identifiers = [];

foreach ($attributes['subresource_context']['identifiers'] as $key => list($id, $resourceClass, $hasIdentifier)) {
if (false === $hasIdentifier) {
continue;
}

$identifiers[$id] = $parameters[$id];

if ($this->identifierDenormalizer) {
$identifiers[$id] = $this->identifierDenormalizer->denormalize((string) $identifiers[$id], $resourceClass);
}
}

return $identifiers;
}
}
Loading