Skip to content

Commit

Permalink
feat(Resource): ability to read/write to a resource alias
Browse files Browse the repository at this point in the history
  • Loading branch information
soyuka committed May 21, 2021
1 parent 19a4337 commit 579d288
Show file tree
Hide file tree
Showing 21 changed files with 383 additions and 67 deletions.
45 changes: 45 additions & 0 deletions features/main/attribute_resource.feature
Expand Up @@ -54,3 +54,48 @@ Feature: Resource attributes
"name": "Foo"
}
"""

@php8
@!mysql
@!mongodb
Scenario: Retrieve the aliased resource
When I add "Content-Type" header equal to "application/ld+json"
And I send a "GET" request to "/dummy/1/attribute_resources/2"
Then 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/AttributeResource",
"@id": "/dummy/1/attribute_resources/2",
"@type": "AttributeResource",
"identifier": 2,
"dummy": "/dummies/1",
"name": "Foo"
}
"""

@php8
@!mysql
@!mongodb
Scenario: Patch the aliased resource
When I add "Content-Type" header equal to "application/merge-patch+json"
And I send a "PATCH" request to "/dummy/1/attribute_resources/2" with body:
"""
{"name": "Patched"}
"""
Then 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/AttributeResource",
"@id": "/dummy/1/attribute_resources/2",
"@type": "AttributeResource",
"identifier": 2,
"dummy": "/dummies/1",
"name": "Patched"
}
"""
9 changes: 7 additions & 2 deletions src/Api/CachedIdentifiersExtractor.php
Expand Up @@ -24,7 +24,7 @@
*
* @author Antoine Bluchet <soyuka@gmail.com>
*/
final class CachedIdentifiersExtractor implements IdentifiersExtractorInterface
final class CachedIdentifiersExtractor implements ContextAwareIdentifiersExtractorInterface
{
use ResourceClassInfoTrait;

Expand Down Expand Up @@ -63,8 +63,13 @@ public function getIdentifiersFromResourceClass(string $resourceClass): array
/**
* {@inheritdoc}
*/
public function getIdentifiersFromItem($item): array
public function getIdentifiersFromItem($item, array $context = []): array
{
// TODO: Remove this class in 3.0 as the cache is not needed anymore
if (isset($context['identifiers']) && $this->decorated instanceof ContextAwareIdentifiersExtractorInterface) {
return $this->decorated->getIdentifiersFromItem($item, $context);
}

$keys = $this->getKeys($item, function ($item) use (&$identifiers) {
return $identifiers = $this->decorated->getIdentifiersFromItem($item);
});
Expand Down
38 changes: 38 additions & 0 deletions src/Api/ContextAwareIdentifiersExtractorInterface.php
@@ -0,0 +1,38 @@
<?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\Api;

use ApiPlatform\Core\Exception\RuntimeException;

/**
* Extracts identifiers for a given Resource according to the retrieved Metadata.
*
* @author Antoine Bluchet <soyuka@gmail.com>
*/
interface ContextAwareIdentifiersExtractorInterface extends IdentifiersExtractorInterface
{
/**
* Finds identifiers from a Resource class.
*/
public function getIdentifiersFromResourceClass(string $resourceClass): array;

/**
* Finds identifiers from an Item (object).
*
* @param object $item
*
* @throws RuntimeException
*/
public function getIdentifiersFromItem($item, array $context = []): array;
}
69 changes: 69 additions & 0 deletions src/Api/ContextAwareIriConverterInterface.php
@@ -0,0 +1,69 @@
<?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\Api;

use ApiPlatform\Core\Exception\InvalidArgumentException;
use ApiPlatform\Core\Exception\ItemNotFoundException;
use ApiPlatform\Core\Exception\RuntimeException;

/**
* Converts item and resources to IRI and vice versa.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
interface ContextAwareIriConverterInterface extends IriConverterInterface
{
/**
* Retrieves an item from its IRI.
*
* @throws InvalidArgumentException
* @throws ItemNotFoundException
*
* @return object
*/
public function getItemFromIri(string $iri, array $context = []);

/**
* Gets the IRI associated with the given item.
*
* @param object $item
*
* @throws InvalidArgumentException
* @throws RuntimeException
*/
public function getIriFromItem($item, int $referenceType = UrlGeneratorInterface::ABS_PATH, array $context = []): string;

/**
* Gets the IRI associated with the given resource collection.
*
* @throws InvalidArgumentException
*/
public function getIriFromResourceClass(string $resourceClass, int $referenceType = UrlGeneratorInterface::ABS_PATH): string;

/**
* Gets the item IRI associated with the given resource.
*
* @throws InvalidArgumentException
*/
public function getItemIriFromResourceClass(string $resourceClass, array $identifiers, int $referenceType = UrlGeneratorInterface::ABS_PATH): string;

/**
* Gets the IRI associated with the given resource subresource.
*
* @deprecated
*
* @throws InvalidArgumentException
*/
public function getSubresourceIriFromResourceClass(string $resourceClass, array $identifiers, int $referenceType = UrlGeneratorInterface::ABS_PATH): string;
}
23 changes: 21 additions & 2 deletions src/Api/IdentifiersExtractor.php
Expand Up @@ -25,7 +25,7 @@
*
* @author Antoine Bluchet <soyuka@gmail.com>
*/
final class IdentifiersExtractor implements IdentifiersExtractorInterface
final class IdentifiersExtractor implements ContextAwareIdentifiersExtractorInterface
{
use ResourceClassInfoTrait;

Expand Down Expand Up @@ -71,10 +71,29 @@ public function getIdentifiersFromResourceClass(string $resourceClass): array
/**
* {@inheritdoc}
*/
public function getIdentifiersFromItem($item): array
public function getIdentifiersFromItem($item, array $context = []): array
{
$identifiers = [];
$resourceClass = $this->getResourceClass($item, true);
if (isset($context['identifiers'])) {
foreach ($context['identifiers'] as $parameterName => [$class, $property]) {
if ($item instanceof $class) {
$identifiers[$parameterName] = $this->propertyAccessor->getValue($item, $property);
continue;
}

foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $propertyName) {
$propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName);
$type = $propertyMetadata->getType();
if ($type && $type->getClassName() === $class) {
$identifiers[$parameterName] = $this->propertyAccessor->getValue($item, "$propertyName.$property");
}
}
}

return $identifiers;
}

$identifierProperties = $this->getIdentifiersFromResourceClass($resourceClass);

foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $propertyName) {
Expand Down
4 changes: 4 additions & 0 deletions src/Bridge/Symfony/Bundle/Resources/config/api.xml
Expand Up @@ -84,6 +84,7 @@

<service id="api_platform.serializer.context_builder" class="ApiPlatform\Core\Serializer\SerializerContextBuilder" public="false">
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
<argument type="service" id="api_platform.metadata.resource_collection.metadata_factory" />
</service>
<service id="ApiPlatform\Core\Serializer\SerializerContextBuilderInterface" alias="api_platform.serializer.context_builder" />

Expand Down Expand Up @@ -187,6 +188,9 @@
<argument type="service" id="api_platform.serializer" />
<argument type="service" id="api_platform.serializer.context_builder" />
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
<argument>null</argument>

<argument type="service" id="api_platform.metadata.resource_collection.metadata_factory" />

<tag name="kernel.event_listener" event="kernel.request" method="onKernelRequest" priority="2" />
</service>
Expand Down
29 changes: 25 additions & 4 deletions src/Bridge/Symfony/Routing/IriConverter.php
Expand Up @@ -13,9 +13,10 @@

namespace ApiPlatform\Core\Bridge\Symfony\Routing;

use ApiPlatform\Core\Api\ContextAwareIdentifiersExtractorInterface;
use ApiPlatform\Core\Api\ContextAwareIriConverterInterface;
use ApiPlatform\Core\Api\IdentifiersExtractor;
use ApiPlatform\Core\Api\IdentifiersExtractorInterface;
use ApiPlatform\Core\Api\IriConverterInterface;
use ApiPlatform\Core\Api\OperationType;
use ApiPlatform\Core\Api\ResourceClassResolverInterface;
use ApiPlatform\Core\Api\UrlGeneratorInterface;
Expand Down Expand Up @@ -46,7 +47,7 @@
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
final class IriConverter implements IriConverterInterface
final class IriConverter implements ContextAwareIriConverterInterface
{
use OperationDataProviderTrait;
use ResourceClassInfoTrait;
Expand Down Expand Up @@ -118,16 +119,34 @@ public function getItemFromIri(string $iri, array $context = [])
/**
* {@inheritdoc}
*/
public function getIriFromItem($item, int $referenceType = null): string
public function getIriFromItem($item, int $referenceType = null, array $context = []): string
{
$resourceClass = $this->getResourceClass($item, true);

// Special case where the Resource has no identifiers (it's a collection) and we want the operation tight to the item
if (!($context['identifiers'] ?? true) && $this->resourceCollectionMetadataFactory) {
$collection = $this->resourceCollectionMetadataFactory->create($resourceClass);
foreach ($collection as $resource) {
foreach ($resource->operations as $key => $operation) {
if ('GET' === $operation->method && $operation->identifiers) {
$context['operation_name'] = $key;
$context['identifiers'] = $operation->identifiers;
break 2;
}
}
}
}

try {
$identifiers = $this->identifiersExtractor->getIdentifiersFromItem($item);
$identifiers = $this->identifiersExtractor instanceof ContextAwareIdentifiersExtractorInterface ? $this->identifiersExtractor->getIdentifiersFromItem($item, $context) : $this->identifiersExtractor->getIdentifiersFromItem($item);
} catch (RuntimeException $e) {
throw new InvalidArgumentException(sprintf('Unable to generate an IRI for the item of type "%s"', $resourceClass), $e->getCode(), $e);
}

if (isset($context['operation_name'])) {
return $this->router->generate($context['operation_name'], $identifiers, $this->getReferenceType($resourceClass, $referenceType));
}

return $this->getItemIriFromResourceClass($resourceClass, $identifiers, $this->getReferenceType($resourceClass, $referenceType));
}

Expand Down Expand Up @@ -169,6 +188,8 @@ public function getItemIriFromResourceClass(string $resourceClass, array $identi
*/
public function getSubresourceIriFromResourceClass(string $resourceClass, array $context, int $referenceType = null): string
{
@trigger_error('getSubresourceIriFromResourceClass is deprecated since 2.7 and will not be available anymore in 3.0', \E_USER_DEPRECATED);

try {
return $this->router->generate($this->routeNameResolver->getRouteName($resourceClass, OperationType::SUBRESOURCE, $context), $context['subresource_identifiers'], $this->getReferenceType($resourceClass, $referenceType));
} catch (RoutingExceptionInterface $e) {
Expand Down
4 changes: 2 additions & 2 deletions src/Core/Metadata/Operation.php
Expand Up @@ -69,7 +69,7 @@ class Operation
* @param int $urlGenerationStrategy
* @param bool $compositeIdentifier
* @param array $identifiers
* @param array $graphql
* @param array $graphQl
*/
public function __construct(
public string $method = 'GET',
Expand Down Expand Up @@ -125,7 +125,7 @@ public function __construct(
public ?int $urlGenerationStrategy = null,
public ?bool $compositeIdentifier = null,
public ?array $identifiers = null,
public ?array $graphql = null,
public ?array $graphQl = null,
...$extraProperties
) {
$this->extraProperties = $extraProperties;
Expand Down
1 change: 1 addition & 0 deletions src/Core/Metadata/Resource.php
Expand Up @@ -68,6 +68,7 @@ class Resource
* @param int $urlGenerationStrategy
* @param bool $compositeIdentifier
* @param array $identifiers
* @param array $graphQl
*/
public function __construct(
public ?string $uriTemplate = null,
Expand Down

0 comments on commit 579d288

Please sign in to comment.