Skip to content

Commit

Permalink
Merge 90a7551 into 5e2eb4d
Browse files Browse the repository at this point in the history
  • Loading branch information
soyuka committed Sep 6, 2021
2 parents 5e2eb4d + 90a7551 commit 135bd7c
Show file tree
Hide file tree
Showing 70 changed files with 1,230 additions and 551 deletions.
81 changes: 51 additions & 30 deletions src/Api/IdentifiersExtractor.php
Expand Up @@ -14,6 +14,7 @@
namespace ApiPlatform\Api;

use ApiPlatform\Core\Api\ResourceClassResolverInterface;
use ApiPlatform\Core\Identifier\CompositeIdentifierParser;
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
use ApiPlatform\Core\Util\ResourceClassInfoTrait;
Expand Down Expand Up @@ -55,45 +56,30 @@ public function getIdentifiersFromItem($item, string $operationName = null, arra
$resourceClass = $this->getResourceClass($item, true);
$operation = $context['operation'] ?? $this->resourceMetadataFactory->create($resourceClass)->getOperation($operationName);

foreach ($operation->getIdentifiers() as $parameterName => [$class, $property]) {
$identifierValue = $this->resolveIdentifierValue($item, $class, $property);

if (null === $identifierValue) {
throw new RuntimeException('No identifier value found, did you forgot to persist the entity?');
}
foreach ($operation->getUriVariables() ?? [] as $parameterName => $uriVariableDefinition) {
if (1 < \count($uriVariableDefinition['identifiers'])) {
$compositeIdentifiers = [];
foreach ($uriVariableDefinition['identifiers'] as $identifier) {
$compositeIdentifiers[$identifier] = $this->getIdentifierValue($item, $uriVariableDefinition['class'], $identifier, $parameterName);
}

// TODO: php 8 remove method_exists
if (is_scalar($identifierValue) || method_exists($identifierValue, '__toString') || $identifierValue instanceof \Stringable) {
$identifiers[$parameterName] = (string) $identifierValue;
$identifiers[$parameterName] = CompositeIdentifierParser::stringify($compositeIdentifiers);
continue;
}

// we could recurse to find correct identifiers until there it is a scalar but this is not really supported and adds a lot of complexity
// instead we're deprecating this behavior in favor of something that can be transformed to a string
if ($this->isResourceClass($relatedResourceClass = $this->getObjectClass($identifierValue))) {
trigger_deprecation('api-platform/core', '2.7', 'Using a resource class as identifier is deprecated, please make this identifier Stringable');
$relatedOperation = $this->resourceMetadataFactory->create($relatedResourceClass)->getOperation();
$relatedIdentifiers = $relatedOperation->getIdentifiers();
if (1 === \count($relatedIdentifiers)) {
$identifierValue = $this->resolveIdentifierValue($identifierValue, $relatedResourceClass, current($relatedIdentifiers)[1]);

if (is_scalar($identifierValue) || method_exists($identifierValue, '__toString') || $identifierValue instanceof \Stringable) {
$identifiers[$parameterName] = (string) $identifierValue;
continue;
}
}
}

throw new RuntimeException(sprintf('We were not able to resolve the identifier matching parameter "%s".', $parameterName));
$identifiers[$parameterName] = $this->getIdentifierValue($item, $uriVariableDefinition['class'], $uriVariableDefinition['identifiers'][0], $parameterName);
}

return $identifiers;
}

private function resolveIdentifierValue($item, string $class, string $property)
/**
* Gets the value of the given class property.
*/
private function getIdentifierValue($item, string $class, string $property, string $parameterName)
{
if ($item instanceof $class) {
return $this->propertyAccessor->getValue($item, $property);
return $this->resolveIdentifierValue($this->propertyAccessor->getValue($item, $property), $parameterName);
}

$resourceClass = $this->getResourceClass($item, true);
Expand All @@ -105,14 +91,49 @@ private function resolveIdentifierValue($item, string $class, string $property)
}

if ($type->getClassName() === $class) {
return $this->propertyAccessor->getValue($item, "$propertyName.$property");
return $this->resolveIdentifierValue($this->propertyAccessor->getValue($item, "$propertyName.$property"), $parameterName);
}

if ($type->isCollection() && ($collectionValueType = $type->getCollectionValueType()) && $collectionValueType->getClassName() === $class) {
return $this->propertyAccessor->getValue($item, sprintf('%s[0].%s', $propertyName, $property));
return $this->resolveIdentifierValue($this->propertyAccessor->getValue($item, sprintf('%s[0].%s', $propertyName, $property)), $parameterName);
}
}

throw new RuntimeException('Not able to retrieve identifiers.');
}

/**
* TODO: in 3.0 this method just uses $identifierValue instanceof \Stringable and we remove the weird behavior.
*
* @param mixed|\Stringable $identifierValue
*/
private function resolveIdentifierValue($identifierValue, string $parameterName)
{
if (null === $identifierValue) {
throw new RuntimeException('No identifier value found, did you forgot to persist the entity?');
}

// TODO: php 8 remove method_exists
if (is_scalar($identifierValue) || method_exists($identifierValue, '__toString') || $identifierValue instanceof \Stringable) {
return (string) $identifierValue;
}

// TODO: remove this in 3.0
// we could recurse to find correct identifiers until there it is a scalar but this is not really supported and adds a lot of complexity
// instead we're deprecating this behavior in favor of something that can be transformed to a string
if ($this->isResourceClass($relatedResourceClass = $this->getObjectClass($identifierValue))) {
trigger_deprecation('api-platform/core', '2.7', 'Using a resource class as identifier is deprecated, please make this identifier Stringable');
$relatedOperation = $this->resourceMetadataFactory->create($relatedResourceClass)->getOperation();
$relatedIdentifiers = $relatedOperation->getUriVariables();
if (1 === \count($relatedIdentifiers)) {
$identifierValue = $this->getIdentifierValue($identifierValue, $relatedResourceClass, current($relatedIdentifiers)['identifiers'][0], $parameterName);

if ($identifierValue instanceof \Stringable || is_scalar($identifierValue) || method_exists($identifierValue, '__toString')) {
return (string) $identifierValue;
}
}
}

throw new RuntimeException(sprintf('We were not able to resolve the identifier matching parameter "%s".', $parameterName));
}
}
43 changes: 43 additions & 0 deletions src/Api/UriVariableTransformer/DateTimeUriVariableTransformer.php
@@ -0,0 +1,43 @@
<?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\Api\UriVariableTransformer;

use ApiPlatform\Api\UriVariableTransformerInterface;
use ApiPlatform\Exception\InvalidUriVariableException;
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;

final class DateTimeUriVariableTransformer implements UriVariableTransformerInterface
{
private $dateTimeNormalizer;

public function __construct()
{
$this->dateTimeNormalizer = new DateTimeNormalizer();
}

public function transform($value, array $types, array $context = [])
{
try {
return $this->dateTimeNormalizer->denormalize($value, $types[0], null, $context);
} catch (NotNormalizableValueException $e) {
throw new InvalidUriVariableException($e->getMessage(), $e->getCode(), $e);
}
}

public function supportsTransformation($value, array $types, array $context = []): bool
{
return $this->dateTimeNormalizer->supportsDenormalization($value, $types[0]);
}
}
30 changes: 30 additions & 0 deletions src/Api/UriVariableTransformer/IntegerUriVariableTransformer.php
@@ -0,0 +1,30 @@
<?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\Api\UriVariableTransformer;

use ApiPlatform\Api\UriVariableTransformerInterface;
use Symfony\Component\PropertyInfo\Type;

final class IntegerUriVariableTransformer implements UriVariableTransformerInterface
{
public function transform($value, array $types, array $context = [])
{
return (int) $value;
}

public function supportsTransformation($value, array $types, array $context = []): bool
{
return Type::BUILTIN_TYPE_INT === $types[0] && \is_string($value);
}
}
39 changes: 39 additions & 0 deletions src/Api/UriVariableTransformerInterface.php
@@ -0,0 +1,39 @@
<?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\Api;

use ApiPlatform\Exception\InvalidUriVariableException;

interface UriVariableTransformerInterface
{
/**
* Denormalizes data back into an object of the given class.
*
* @param mixed $value The uri variable value to transform
* @param array $types The guessed type behind the uri variable
* @param array $context Options available to the transformer
*
* @throws InvalidUriVariableException Occurs when the uriVariable could not be transformed
*/
public function transform($value, array $types, array $context = []);

/**
* Checks whether the given class is supported for denormalization by this normalizer.
*
* @param mixed $value The uri variable value to transform
* @param array $types The types to which the data should be transformed
* @param array $context Options available to the transformer
*/
public function supportsTransformation($value, array $types, array $context = []): bool;
}
85 changes: 85 additions & 0 deletions src/Api/UriVariablesConverter.php
@@ -0,0 +1,85 @@
<?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\Api;

use ApiPlatform\Exception\InvalidUriVariableException;
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use Symfony\Component\PropertyInfo\Type;

/**
* UriVariables converter that chains uri variables transformers.
*
* @author Antoine Bluchet <soyuka@gmail.com>
*/
final class UriVariablesConverter implements UriVariablesConverterInterface
{
private $propertyMetadataFactory;
private $uriVariableTransformers;
private $resourceMetadataCollectionFactory;

/**
* @param iterable<UriVariableTransformerInterface> $uriVariableTransformers
*/
public function __construct(PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, iterable $uriVariableTransformers)
{
$this->propertyMetadataFactory = $propertyMetadataFactory;
$this->uriVariableTransformers = $uriVariableTransformers;
$this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory;
}

/**
* {@inheritdoc}
*/
public function convert(array $uriVariables, string $class, array $context = []): array
{
$operation = $context['operation'] ?? $this->resourceMetadataCollectionFactory->create($class)->getOperation();
$context = $context + ['operation' => $operation];
$uriVariablesDefinition = $operation->getUriVariables() ?? [];

foreach ($uriVariables as $parameterName => $value) {
if ([] === $types = $this->getIdentifierTypes($uriVariablesDefinition[$parameterName]['class'] ?? $class, $uriVariablesDefinition[$parameterName]['identifiers'] ?? [$parameterName])) {
continue;
}

foreach ($this->uriVariableTransformers as $uriVariableTransformer) {
if (!$uriVariableTransformer->supportsTransformation($value, $types, $context)) {
continue;
}

try {
$uriVariables[$parameterName] = $uriVariableTransformer->transform($value, $types, $context);
break;
} catch (InvalidUriVariableException $e) {
throw new InvalidUriVariableException(sprintf('Identifier "%s" could not be transformed.', $parameterName), $e->getCode(), $e);
}
}
}

return $uriVariables;
}

private function getIdentifierTypes(string $resourceClass, array $properties): array
{
$types = [];
foreach ($properties as $property) {
$propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $property);
foreach ($propertyMetadata->getBuiltinTypes() as $type) {
$types[] = Type::BUILTIN_TYPE_OBJECT === ($builtinType = $type->getBuiltinType()) ? $type->getClassName() : $builtinType;
}
}

return $types;
}
}
36 changes: 36 additions & 0 deletions src/Api/UriVariablesConverterInterface.php
@@ -0,0 +1,36 @@
<?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\Api;

use ApiPlatform\Exception\InvalidIdentifierException;

/**
* Identifier converter.
*
* @author Antoine Bluchet <soyuka@gmail.com>
*/
interface UriVariablesConverterInterface
{
/**
* Takes an array of strings representing identifiers and transform their values to the expected type.
*
* @param array $data Identifier to convert to php values
* @param string $class The class to which the identifiers belong to
*
* @throws InvalidIdentifierException
*
* @return array indexed by identifiers properties with their values denormalized
*/
public function convert(array $data, string $class, array $context = []): array;
}
Expand Up @@ -74,10 +74,13 @@ public function enterNode(Node $node)
($resource[0] === $this->subresourceMetadata['resource_class']) ? 'self' : '\\'.$resource[0]
),
'class'
)
),
new Node\Scalar\String_('class')
),
new Node\Expr\ArrayItem(
new Node\Scalar\String_($resource[1])
new Node\Expr\Array_(
[new Node\Expr\ArrayItem(new Node\Scalar\String_($resource[1]))], ['kind' => Node\Expr\Array_::KIND_SHORT]),
new Node\Scalar\String_('identifiers')
),
],
[
Expand All @@ -103,7 +106,7 @@ public function enterNode(Node $node)
false,
false,
[],
new Node\Identifier('identifiers')
new Node\Identifier('uriVariables')
),
new Node\Arg(
new Node\Scalar\LNumber(200),
Expand Down
Expand Up @@ -82,7 +82,7 @@ class Book
#[ApiResource]
#[Get]
#[Get(name: 'get_by_isbn', uriTemplate: '/books/by_isbn/{isbn}.{_format}', requirements: ['isbn' => '.+'], identifiers: 'isbn')]
#[Get(name: 'get_by_isbn', uriTemplate: '/books/by_isbn/{isbn}.{_format}', requirements: ['isbn' => '.+'], uriVariables: 'isbn')]
class Book
CODE_SAMPLE
, [
Expand Down
Expand Up @@ -56,7 +56,7 @@ class Book
#[ApiResource]
#[Get]
#[Get(name: 'get_by_isbn', uriTemplate: '/books/by_isbn/{isbn}.{_format}', requirements: ['isbn' => '.+'], identifiers: 'isbn')]
#[Get(name: 'get_by_isbn', uriTemplate: '/books/by_isbn/{isbn}.{_format}', requirements: ['isbn' => '.+'], uriVariables: 'isbn')]
class Book
CODE_SAMPLE
, [self::REMOVE_INITIAL_ATTRIBUTE => true])]);
Expand Down

0 comments on commit 135bd7c

Please sign in to comment.