fix(metadata): uriVariables instead of identifiers (#4408) · mega/api-platform-core@b9b14df · GitHub
Skip to content

Commit b9b14df

Browse files
authored
fix(metadata): uriVariables instead of identifiers (api-platform#4408)
1 parent 6bd96ca commit b9b14df

75 files changed

Lines changed: 1465 additions & 580 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/ci.yml‎

Lines changed: 2 additions & 0 deletions

‎src/Api/IdentifiersExtractor.php‎

Lines changed: 51 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
namespace ApiPlatform\Api;
1515

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

58-
foreach ($operation->getIdentifiers() as $parameterName => [$class, $property]) {
59-
$identifierValue = $this->resolveIdentifierValue($item, $class, $property);
60-
61-
if (null === $identifierValue) {
62-
throw new RuntimeException('No identifier value found, did you forgot to persist the entity?');
63-
}
59+
foreach ($operation->getUriVariables() ?? [] as $parameterName => $uriVariableDefinition) {
60+
if (1 < \count($uriVariableDefinition['identifiers'])) {
61+
$compositeIdentifiers = [];
62+
foreach ($uriVariableDefinition['identifiers'] as $identifier) {
63+
$compositeIdentifiers[$identifier] = $this->getIdentifierValue($item, $uriVariableDefinition['class'], $identifier, $parameterName);
64+
}
6465

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

71-
// 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
72-
// instead we're deprecating this behavior in favor of something that can be transformed to a string
73-
if ($this->isResourceClass($relatedResourceClass = $this->getObjectClass($identifierValue))) {
74-
trigger_deprecation('api-platform/core', '2.7', 'Using a resource class as identifier is deprecated, please make this identifier Stringable');
75-
$relatedOperation = $this->resourceMetadataFactory->create($relatedResourceClass)->getOperation();
76-
$relatedIdentifiers = $relatedOperation->getIdentifiers();
77-
if (1 === \count($relatedIdentifiers)) {
78-
$identifierValue = $this->resolveIdentifierValue($identifierValue, $relatedResourceClass, current($relatedIdentifiers)[1]);
79-
80-
if (is_scalar($identifierValue) || method_exists($identifierValue, '__toString') || $identifierValue instanceof \Stringable) {
81-
$identifiers[$parameterName] = (string) $identifierValue;
82-
continue;
83-
}
84-
}
85-
}
86-
87-
throw new RuntimeException(sprintf('We were not able to resolve the identifier matching parameter "%s".', $parameterName));
70+
$identifiers[$parameterName] = $this->getIdentifierValue($item, $uriVariableDefinition['class'], $uriVariableDefinition['identifiers'][0], $parameterName);
8871
}
8972

9073
return $identifiers;
9174
}
9275

93-
private function resolveIdentifierValue($item, string $class, string $property)
76+
/**
77+
* Gets the value of the given class property.
78+
*/
79+
private function getIdentifierValue($item, string $class, string $property, string $parameterName)
9480
{
9581
if ($item instanceof $class) {
96-
return $this->propertyAccessor->getValue($item, $property);
82+
return $this->resolveIdentifierValue($this->propertyAccessor->getValue($item, $property), $parameterName);
9783
}
9884

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

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

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

116102
throw new RuntimeException('Not able to retrieve identifiers.');
117103
}
104+
105+
/**
106+
* TODO: in 3.0 this method just uses $identifierValue instanceof \Stringable and we remove the weird behavior.
107+
*
108+
* @param mixed|\Stringable $identifierValue
109+
*/
110+
private function resolveIdentifierValue($identifierValue, string $parameterName)
111+
{
112+
if (null === $identifierValue) {
113+
throw new RuntimeException('No identifier value found, did you forgot to persist the entity?');
114+
}
115+
116+
// TODO: php 8 remove method_exists
117+
if (is_scalar($identifierValue) || method_exists($identifierValue, '__toString') || $identifierValue instanceof \Stringable) {
118+
return (string) $identifierValue;
119+
}
120+
121+
// TODO: remove this in 3.0
122+
// 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
123+
// instead we're deprecating this behavior in favor of something that can be transformed to a string
124+
if ($this->isResourceClass($relatedResourceClass = $this->getObjectClass($identifierValue))) {
125+
trigger_deprecation('api-platform/core', '2.7', 'Using a resource class as identifier is deprecated, please make this identifier Stringable');
126+
$relatedOperation = $this->resourceMetadataFactory->create($relatedResourceClass)->getOperation();
127+
$relatedIdentifiers = $relatedOperation->getUriVariables();
128+
if (1 === \count($relatedIdentifiers)) {
129+
$identifierValue = $this->getIdentifierValue($identifierValue, $relatedResourceClass, current($relatedIdentifiers)['identifiers'][0], $parameterName);
130+
131+
if ($identifierValue instanceof \Stringable || is_scalar($identifierValue) || method_exists($identifierValue, '__toString')) {
132+
return (string) $identifierValue;
133+
}
134+
}
135+
}
136+
137+
throw new RuntimeException(sprintf('We were not able to resolve the identifier matching parameter "%s".', $parameterName));
138+
}
118139
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Api\UriVariableTransformer;
15+
16+
use ApiPlatform\Api\UriVariableTransformerInterface;
17+
use ApiPlatform\Exception\InvalidUriVariableException;
18+
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
19+
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
20+
21+
final class DateTimeUriVariableTransformer implements UriVariableTransformerInterface
22+
{
23+
private $dateTimeNormalizer;
24+
25+
public function __construct()
26+
{
27+
$this->dateTimeNormalizer = new DateTimeNormalizer();
28+
}
29+
30+
public function transform($value, array $types, array $context = [])
31+
{
32+
try {
33+
return $this->dateTimeNormalizer->denormalize($value, $types[0], null, $context);
34+
} catch (NotNormalizableValueException $e) {
35+
throw new InvalidUriVariableException($e->getMessage(), $e->getCode(), $e);
36+
}
37+
}
38+
39+
public function supportsTransformation($value, array $types, array $context = []): bool
40+
{
41+
return $this->dateTimeNormalizer->supportsDenormalization($value, $types[0]);
42+
}
43+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Api\UriVariableTransformer;
15+
16+
use ApiPlatform\Api\UriVariableTransformerInterface;
17+
use Symfony\Component\PropertyInfo\Type;
18+
19+
final class IntegerUriVariableTransformer implements UriVariableTransformerInterface
20+
{
21+
public function transform($value, array $types, array $context = [])
22+
{
23+
return (int) $value;
24+
}
25+
26+
public function supportsTransformation($value, array $types, array $context = []): bool
27+
{
28+
return Type::BUILTIN_TYPE_INT === $types[0] && \is_string($value);
29+
}
30+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Api;
15+
16+
use ApiPlatform\Exception\InvalidUriVariableException;
17+
18+
interface UriVariableTransformerInterface
19+
{
20+
/**
21+
* Denormalizes data back into an object of the given class.
22+
*
23+
* @param mixed $value The uri variable value to transform
24+
* @param array $types The guessed type behind the uri variable
25+
* @param array $context Options available to the transformer
26+
*
27+
* @throws InvalidUriVariableException Occurs when the uriVariable could not be transformed
28+
*/
29+
public function transform($value, array $types, array $context = []);
30+
31+
/**
32+
* Checks whether the given class is supported for denormalization by this normalizer.
33+
*
34+
* @param mixed $value The uri variable value to transform
35+
* @param array $types The types to which the data should be transformed
36+
* @param array $context Options available to the transformer
37+
*/
38+
public function supportsTransformation($value, array $types, array $context = []): bool;
39+
}

‎src/Api/UriVariablesConverter.php‎

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Api;
15+
16+
use ApiPlatform\Exception\InvalidUriVariableException;
17+
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
18+
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
19+
use Symfony\Component\PropertyInfo\Type;
20+
21+
/**
22+
* UriVariables converter that chains uri variables transformers.
23+
*
24+
* @author Antoine Bluchet <soyuka@gmail.com>
25+
*/
26+
final class UriVariablesConverter implements UriVariablesConverterInterface
27+
{
28+
private $propertyMetadataFactory;
29+
private $uriVariableTransformers;
30+
private $resourceMetadataCollectionFactory;
31+
32+
/**
33+
* @param iterable<UriVariableTransformerInterface> $uriVariableTransformers
34+
*/
35+
public function __construct(PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, iterable $uriVariableTransformers)
36+
{
37+
$this->propertyMetadataFactory = $propertyMetadataFactory;
38+
$this->uriVariableTransformers = $uriVariableTransformers;
39+
$this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory;
40+
}
41+
42+
/**
43+
* {@inheritdoc}
44+
*/
45+
public function convert(array $uriVariables, string $class, array $context = []): array
46+
{
47+
$operation = $context['operation'] ?? $this->resourceMetadataCollectionFactory->create($class)->getOperation();
48+
$context = $context + ['operation' => $operation];
49+
$uriVariablesDefinition = $operation->getUriVariables() ?? [];
50+
51+
foreach ($uriVariables as $parameterName => $value) {
52+
if ([] === $types = $this->getIdentifierTypes($uriVariablesDefinition[$parameterName]['class'] ?? $class, $uriVariablesDefinition[$parameterName]['identifiers'] ?? [$parameterName])) {
53+
continue;
54+
}
55+
56+
foreach ($this->uriVariableTransformers as $uriVariableTransformer) {
57+
if (!$uriVariableTransformer->supportsTransformation($value, $types, $context)) {
58+
continue;
59+
}
60+
61+
try {
62+
$uriVariables[$parameterName] = $uriVariableTransformer->transform($value, $types, $context);
63+
break;
64+
} catch (InvalidUriVariableException $e) {
65+
throw new InvalidUriVariableException(sprintf('Identifier "%s" could not be transformed.', $parameterName), $e->getCode(), $e);
66+
}
67+
}
68+
}
69+
70+
return $uriVariables;
71+
}
72+
73+
private function getIdentifierTypes(string $resourceClass, array $properties): array
74+
{
75+
$types = [];
76+
foreach ($properties as $property) {
77+
$propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $property);
78+
foreach ($propertyMetadata->getBuiltinTypes() as $type) {
79+
$types[] = Type::BUILTIN_TYPE_OBJECT === ($builtinType = $type->getBuiltinType()) ? $type->getClassName() : $builtinType;
80+
}
81+
}
82+
83+
return $types;
84+
}
85+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Api;
15+
16+
use ApiPlatform\Exception\InvalidIdentifierException;
17+
18+
/**
19+
* Identifier converter.
20+
*
21+
* @author Antoine Bluchet <soyuka@gmail.com>
22+
*/
23+
interface UriVariablesConverterInterface
24+
{
25+
/**
26+
* Takes an array of strings representing identifiers and transform their values to the expected type.
27+
*
28+
* @param array $data Identifier to convert to php values
29+
* @param string $class The class to which the identifiers belong to
30+
*
31+
* @throws InvalidIdentifierException
32+
*
33+
* @return array indexed by identifiers properties with their values denormalized
34+
*/
35+
public function convert(array $data, string $class, array $context = []): array;
36+
}

‎src/Core/Bridge/Rector/Parser/TransformApiSubresourceVisitor.php‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,13 @@ public function enterNode(Node $node)
7474
($resource[0] === $this->subresourceMetadata['resource_class']) ? 'self' : '\\'.$resource[0]
7575
),
7676
'class'
77-
)
77+
),
78+
new Node\Scalar\String_('class')
7879
),
7980
new Node\Expr\ArrayItem(
80-
new Node\Scalar\String_($resource[1])
81+
new Node\Expr\Array_(
82+
[new Node\Expr\ArrayItem(new Node\Scalar\String_($resource[1]))], ['kind' => Node\Expr\Array_::KIND_SHORT]),
83+
new Node\Scalar\String_('identifiers')
8184
),
8285
],
8386
[
@@ -103,7 +106,7 @@ public function enterNode(Node $node)
103106
false,
104107
false,
105108
[],
106-
new Node\Identifier('identifiers')
109+
new Node\Identifier('uriVariables')
107110
),
108111
new Node\Arg(
109112
new Node\Scalar\LNumber(200),

‎src/Core/Bridge/Rector/Rules/ApiResourceAnnotationToApiResourceAttributeRector.php‎

Lines changed: 1 addition & 1 deletion

0 commit comments

Comments
 (0)