Skip to content

Commit

Permalink
go further on that fix
Browse files Browse the repository at this point in the history
  • Loading branch information
soyuka committed Aug 14, 2023
1 parent c0e7522 commit 72a2c71
Show file tree
Hide file tree
Showing 10 changed files with 98 additions and 96 deletions.
32 changes: 26 additions & 6 deletions features/hydra/item_uri_template.feature
Original file line number Diff line number Diff line change
Expand Up @@ -192,11 +192,31 @@ Feature: Exposing a collection of objects should use the specified operation to

Scenario: Get a collection referencing an invalid itemUriTemplate
When I send a "GET" request to "/issue5662/admin/reviews"
Then the response status code should be 400
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 node "@context" should be equal to "/contexts/Error"
And the JSON node "@type" should be equal to "hydra:Error"
And the JSON node "hydra:title" should be equal to "An error occurred"
And the JSON node "hydra:description" should be equal to 'Unable to find operation "/issue5662/reviews/{id}{._format}"'
And the JSON node "trace" should exist
And the JSON should be equal to:
"""
{
"@context": "/contexts/Review",
"@id": "/issue5662/admin/reviews",
"@type": "hydra:Collection",
"hydra:totalItems": 2,
"hydra:member": [
{
"@id": "/issue5662/admin/reviews/1",
"@type": "Review",
"book": "/issue5662/books/a",
"id": 1,
"body": "Best book ever!"
},
{
"@id": "/issue5662/admin/reviews/2",
"@type": "Review",
"book": "/issue5662/books/b",
"id": 2,
"body": "Worst book ever!"
}
]
}
"""
20 changes: 1 addition & 19 deletions src/Hydra/Serializer/CollectionNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ final class CollectionNormalizer extends AbstractCollectionNormalizer
self::IRI_ONLY => false,
];

public function __construct(private readonly ContextBuilderInterface $contextBuilder, ResourceClassResolverInterface $resourceClassResolver, private readonly IriConverterInterface $iriConverter, private readonly ?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, array $defaultContext = [])
public function __construct(private readonly ContextBuilderInterface $contextBuilder, ResourceClassResolverInterface $resourceClassResolver, private readonly IriConverterInterface $iriConverter, readonly ?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, array $defaultContext = [])
{
$this->defaultContext = array_merge($this->defaultContext, $defaultContext);

Expand Down Expand Up @@ -80,26 +80,8 @@ protected function getItemsData(iterable $object, string $format = null, array $
{
$data = [];
$data['hydra:member'] = [];

$iriOnly = $context[self::IRI_ONLY] ?? $this->defaultContext[self::IRI_ONLY];

if (($operation = $context['operation'] ?? null) && method_exists($operation, 'getItemUriTemplate')) {
$context['item_uri_template'] = $operation->getItemUriTemplate();
}

// We need to keep this operation for serialization groups for later
if (isset($context['operation'])) {
$context['root_operation'] = $context['operation'];
}

if (isset($context['operation_name'])) {
$context['root_operation_name'] = $context['operation_name'];
}

// We need to unset the operation to ensure a proper IRI generation inside items
unset($context['operation']);
unset($context['operation_name'], $context['uri_variables']);

foreach ($object as $obj) {
if ($iriOnly) {
$data['hydra:member'][] = $this->iriConverter->getIriFromResource($obj);
Expand Down
7 changes: 0 additions & 7 deletions src/JsonLd/Serializer/ItemNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -99,20 +99,13 @@ public function normalize(mixed $object, string $format = null, array $context =
unset($context['operation'], $context['operation_name']);
}

if (($operation = $context['operation'] ?? null) && method_exists($operation, 'getItemUriTemplate') && ($itemUriTemplate = $operation->getItemUriTemplate())) {
$context['item_uri_template'] = $itemUriTemplate;
}

if ($iri = $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_PATH, $context['operation'] ?? null, $context)) {
$context['iri'] = $iri;
$metadata['@id'] = $iri;
}

$context['api_normalize'] = true;

/* @see https://github.com/api-platform/core/pull/5663 */
unset($context['item_uri_template']);

$data = parent::normalize($object, $format, $context);
if (!\is_array($data)) {
return $data;
Expand Down
25 changes: 6 additions & 19 deletions src/Serializer/AbstractCollectionNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ abstract class AbstractCollectionNormalizer implements NormalizerInterface, Norm
initContext as protected;
}
use NormalizerAwareTrait;
use OperationContextTrait;

/**
* This constant must be overridden in the child class.
Expand Down Expand Up @@ -96,27 +97,13 @@ public function normalize(mixed $object, string $format = null, array $context =
}

$resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class']);
$context = $this->initContext($resourceClass, $context);
$collectionContext = $this->initContext($resourceClass, $context);
unset($collectionContext['item_uri_template']);
$data = [];
$paginationData = $this->getPaginationData($object, $context);
$paginationData = $this->getPaginationData($object, $collectionContext);

if (($operation = $context['operation'] ?? null) && method_exists($operation, 'getItemUriTemplate')) {
$context['item_uri_template'] = $operation->getItemUriTemplate();
}

// We need to keep this operation for serialization groups for later
if (isset($context['operation'])) {
$context['root_operation'] = $context['operation'];
}

if (isset($context['operation_name'])) {
$context['root_operation_name'] = $context['operation_name'];
}

unset($context['operation']);
unset($context['operation_type'], $context['operation_name']);

$itemsData = $this->getItemsData($object, $format, $context);
$childContext = $this->createOperationContext($context, $resourceClass);
$itemsData = $this->getItemsData($object, $format, $childContext);

return array_merge_recursive($data, $paginationData, $itemsData);
}
Expand Down
31 changes: 5 additions & 26 deletions src/Serializer/AbstractItemNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
use ApiPlatform\Exception\ItemNotFoundException;
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\CollectionOperationInterface;
use ApiPlatform\Metadata\Exception\OperationNotFoundException;
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
Expand Down Expand Up @@ -56,6 +55,7 @@ abstract class AbstractItemNormalizer extends AbstractObjectNormalizer
use CloneTrait;
use ContextTrait;
use InputOutputMetadataTrait;
use OperationContextTrait;

protected PropertyAccessorInterface $propertyAccessor;
protected array $localCache = [];
Expand Down Expand Up @@ -140,6 +140,10 @@ public function normalize(mixed $object, string $format = null, array $context =
unset($context['iri']);
}

if ($context['api_sub_level'] ?? false) {
unset($context['item_uri_template']);
}

if ($this->resourceClassResolver->isResourceClass($resourceClass)) {
$context = $this->initContext($resourceClass, $context);
}
Expand Down Expand Up @@ -883,29 +887,4 @@ private function setValue(object $object, string $attributeName, mixed $value):
// Properties not found are ignored
}
}

private function createOperationContext(array $context, string $resourceClass = null): array
{
if (isset($context['operation']) && !isset($context['root_operation'])) {
$context['root_operation'] = $context['operation'];
$context['root_operation_name'] = $context['operation_name'];
}

unset($context['iri'], $context['uri_variables']);
if (!$resourceClass) {
return $context;
}

unset($context['operation'], $context['operation_name']);
$context['resource_class'] = $resourceClass;
if ($this->resourceMetadataCollectionFactory) {
try {
$context['operation'] = $this->resourceMetadataCollectionFactory->create($resourceClass)->getOperation();
$context['operation_name'] = $context['operation']->getName();
} catch (OperationNotFoundException) {
}
}

return $context;
}
}
40 changes: 40 additions & 0 deletions src/Serializer/OperationContextTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?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\Serializer;

trait OperationContextTrait
{
protected function createOperationContext(array $context, string $resourceClass = null): array
{
if (isset($context['operation']) && !isset($context['root_operation'])) {
$context['root_operation'] = $context['operation'];
$context['root_operation_name'] = $context['operation_name'] ?? $context['graphql_operation_name'] ?? null;
}

unset($context['iri'], $context['uri_variables'], $context['item_uri_template']);

if (!$resourceClass) {
return $context;
}

if (($operation = $context['operation'] ?? null) && method_exists($operation, 'getItemUriTemplate')) {
$context['item_uri_template'] = $operation->getItemUriTemplate();
}

unset($context['operation'], $context['operation_name']);
$context['resource_class'] = $resourceClass;

return $context;
}
}
3 changes: 3 additions & 0 deletions src/Serializer/SerializerContextBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ public function createFromRequest(Request $request, bool $normalization, array $
$context['uri'] = $request->getUri();
$context['input'] = $operation->getInput();
$context['output'] = $operation->getOutput();
if (method_exists($operation, 'getItemUriTemplate') && $operation->getItemUriTemplate()) {
$context['item_uri_template'] = $operation->getItemUriTemplate();
}

if ($operation->getTypes()) {
$context['types'] = $operation->getTypes();
Expand Down
30 changes: 13 additions & 17 deletions src/Symfony/Routing/IriConverter.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ final class IriConverter implements IriConverterInterface
use UriVariablesResolverTrait;

private $localOperationCache = [];
private $localIdentifiersExtractorOperationCache = [];

public function __construct(private readonly ProviderInterface $provider, private readonly RouterInterface $router, private readonly IdentifiersExtractorInterface $identifiersExtractor, ResourceClassResolverInterface $resourceClassResolver, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, UriVariablesConverterInterface $uriVariablesConverter = null, private readonly ?IriConverterInterface $decorated = null, private readonly ?OperationMetadataFactoryInterface $operationMetadataFactory = null)
{
Expand Down Expand Up @@ -105,9 +106,13 @@ public function getIriFromResource(object|string $resource, int $referenceType =
{
$resourceClass = $context['force_resource_class'] ?? (\is_string($resource) ? $resource : $this->getObjectClass($resource));

if ($this->operationMetadataFactory && isset($context['item_uri_template'])) {
$operation = $this->operationMetadataFactory->create($context['item_uri_template']);
}

$localOperationCacheKey = ($operation?->getName() ?? '').$resourceClass.(\is_string($resource) ? '_c' : '_i');
if ($operation && isset($this->localOperationCache[$localOperationCacheKey])) {
return $this->generateSymfonyRoute($resource, $referenceType, $this->localOperationCache[$localOperationCacheKey], $context);
return $this->generateSymfonyRoute($resource, $referenceType, $this->localOperationCache[$localOperationCacheKey], $context, $this->localIdentifiersExtractorOperationCache[$localOperationCacheKey] ?? null);
}

if (!$this->resourceClassResolver->isResourceClass($resourceClass)) {
Expand All @@ -128,13 +133,7 @@ public function getIriFromResource(object|string $resource, int $referenceType =
unset($context['uri_variables']);
}

if ($this->operationMetadataFactory && isset($context['item_uri_template'])) {
$operation = $this->operationMetadataFactory->create($context['item_uri_template']);
if (!$operation) {
throw new InvalidArgumentException(sprintf('Unable to find operation "%s"', $context['item_uri_template']));
}
}

$identifiersExtractorOperation = $operation;
// In symfony the operation name is the route name, try to find one if none provided
if (
!$operation->getName()
Expand All @@ -143,6 +142,7 @@ public function getIriFromResource(object|string $resource, int $referenceType =
$forceCollection = $operation instanceof CollectionOperationInterface;
try {
$operation = $this->resourceMetadataCollectionFactory->create($resourceClass)->getOperation(null, $forceCollection, true);
$identifiersExtractorOperation = $operation;
} catch (OperationNotFoundException) {
}
}
Expand All @@ -152,8 +152,9 @@ public function getIriFromResource(object|string $resource, int $referenceType =
}

$this->localOperationCache[$localOperationCacheKey] = $operation;
$this->localIdentifiersExtractorOperationCache[$localOperationCacheKey] = $identifiersExtractorOperation;

return $this->generateSymfonyRoute($resource, $referenceType, $operation, $context);
return $this->generateSymfonyRoute($resource, $referenceType, $operation, $context, $identifiersExtractorOperation);
}

private function generateSkolemIri(object|string $resource, int $referenceType = UrlGeneratorInterface::ABS_PATH, Operation $operation = null, array $context = [], string $resourceClass = null): string
Expand All @@ -166,22 +167,17 @@ private function generateSkolemIri(object|string $resource, int $referenceType =
return $this->decorated->getIriFromResource($resource, $referenceType, $operation, $context);
}

private function generateSymfonyRoute(object|string $resource, int $referenceType = UrlGeneratorInterface::ABS_PATH, Operation $operation = null, array $context = []): string
private function generateSymfonyRoute(object|string $resource, int $referenceType = UrlGeneratorInterface::ABS_PATH, Operation $operation = null, array $context = [], Operation $identifiersExtractorOperation = null): string
{
$identifiers = $context['uri_variables'] ?? [];

if (\is_object($resource)) {
try {
$identifiers = $this->identifiersExtractor->getIdentifiersFromItem($resource, $operation, $context);
$identifiers = $this->identifiersExtractor->getIdentifiersFromItem($resource, $identifiersExtractorOperation, $context);
} catch (InvalidArgumentException|RuntimeException $e) {
// We can try using context uri variables if any
if (!$identifiers) {
// Try with the first operation found
try {
$identifiers = $this->identifiersExtractor->getIdentifiersFromItem($resource, null, $context);
} catch (InvalidArgumentException|RuntimeException $e) {
throw new InvalidArgumentException(sprintf('Unable to generate an IRI for the item of type "%s"', $operation->getClass()), $e->getCode(), $e);
}
throw new InvalidArgumentException(sprintf('Unable to generate an IRI for the item of type "%s"', $operation->getClass()), $e->getCode(), $e);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Link;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy;

#[ApiResource(
operations: [
new GetCollection(uriTemplate: '/dummy_resource_with_custom_filter', itemUriTemplate: '/dummy_resource_with_custom_filter/{id}'),
new Get(uriTemplate: '/dummy_resource_with_custom_filter/{id}', uriVariables: ['id']),
new Get(uriTemplate: '/dummy_resource_with_custom_filter/{id}', uriVariables: ['id' => new Link(fromClass: Dummy::class)]),
],
stateOptions: new Options(entityClass: Dummy::class)
)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@
namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity;

use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\Link;

#[Get('/item_referenced_in_collection/{id}{._format}')]
#[Get('/item_referenced_in_collection/{id}{._format}', uriVariables: ['id' => new Link(fromClass: CollectionReferencingItem::class)])]
class ItemReferencedInCollection
{
public $id;
Expand Down

0 comments on commit 72a2c71

Please sign in to comment.