Skip to content

Commit cef2464

Browse files
authored
Merge pull request #1478 from soyuka/fix/identifier-normalizer-1309
Improve ":id" param normalization
2 parents dd03d9d + d4aa84f commit cef2464

31 files changed

+863
-63
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ before_install:
4141
- phpenv config-rm xdebug.ini || echo "xdebug not available"
4242
- echo "memory_limit=-1" >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini
4343
- npm install -g swagger-cli
44-
- if [[ $lint = 1 ]]; then wget https://github.com/FriendsOfPHP/PHP-CS-Fixer/releases/download/v2.8.2/php-cs-fixer.phar; fi
44+
- if [[ $lint = 1 ]]; then wget https://github.com/FriendsOfPHP/PHP-CS-Fixer/releases/download/v2.8.4/php-cs-fixer.phar; fi
4545
- if [[ $lint = 1 ]]; then composer global require --dev 'phpstan/phpstan:^0.8'; fi
4646
- export PATH="$PATH:$HOME/.composer/vendor/bin"
4747

composer.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@
4444
"phpdocumentor/type-resolver": "^0.2.1 || ^0.3 || 0.4",
4545
"phpunit/phpunit": "^6.1",
4646
"psr/log": "^1.0",
47+
"ramsey/uuid": "^3.7",
48+
"ramsey/uuid-doctrine": "^1.4",
4749
"sensio/framework-extra-bundle": "^3.0.11 || ^4.0",
4850
"symfony/asset": "^3.3 || ^4.0",
4951
"symfony/cache": "^3.3 || ^4.0",
@@ -75,6 +77,7 @@
7577
"guzzlehttp/guzzle": "To use the HTTP cache invalidation system.",
7678
"phpdocumentor/reflection-docblock": "To support extracting metadata from PHPDoc.",
7779
"psr/cache-implementation": "To use metadata caching.",
80+
"ramsey/uuid": "To support Ramsey's UUID identifiers.",
7881
"symfony/cache": "To have metadata caching when using Symfony integration.",
7982
"symfony/config": "To load XML configuration files.",
8083
"symfony/expression-language": "To use authorization features.",

features/bootstrap/FeatureContext.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\PersonToPet;
3939
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Pet;
4040
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Question;
41+
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\RamseyUuidDummy;
4142
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\RelatedDummy;
4243
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\RelatedToDummyFriend;
4344
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\RelationEmbedder;
@@ -870,4 +871,16 @@ public function thereAreDummyImmutableDateObjectsWithDummyDate(int $nb)
870871

871872
$this->manager->flush();
872873
}
874+
875+
/**
876+
* @Given there is a ramsey identified resource with uuid :uuid
877+
*/
878+
public function thereIsARamseyIdentifiedResource(string $uuid)
879+
{
880+
$dummy = new RamseyUuidDummy();
881+
$dummy->setId($uuid);
882+
883+
$this->manager->persist($dummy);
884+
$this->manager->flush();
885+
}
873886
}

features/main/uuid.feature

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,24 @@ Feature: Using uuid identifier on resource
101101
When I send a "DELETE" request to "/uuid_identifier_dummies/41b29566-144b-11e6-a148-3e1d05defe78"
102102
Then the response status code should be 204
103103
And the response should be empty
104+
105+
@createSchema
106+
Scenario: Retrieve a resource identified by Ramsey\Uuid\Uuid
107+
Given there is a ramsey identified resource with uuid "41B29566-144B-11E6-A148-3E1D05DEFE78"
108+
When I add "Content-Type" header equal to "application/ld+json"
109+
And I send a "GET" request to "/ramsey_uuid_dummies/41B29566-144B-11E6-A148-3E1D05DEFE78"
110+
Then the response status code should be 200
111+
And the response should be in JSON
112+
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
113+
114+
Scenario: Delete a resource identified by a Ramsey\Uuid\Uuid
115+
When I send a "DELETE" request to "/ramsey_uuid_dummies/41B29566-144B-11E6-A148-3E1D05DEFE78"
116+
Then the response status code should be 204
117+
And the response should be empty
118+
119+
Scenario: Retrieve a resource identified by a bad Ramsey\Uuid\Uuid
120+
When I add "Content-Type" header equal to "application/ld+json"
121+
And I send a "GET" request to "/ramsey_uuid_dummies/41B29566-144B-E1D05DEFE78"
122+
Then the response status code should be 404
123+
And the response should be in JSON
124+
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"

src/Bridge/Doctrine/Orm/ItemDataProvider.php

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use ApiPlatform\Core\DataProvider\ItemDataProviderInterface;
2121
use ApiPlatform\Core\DataProvider\RestrictedDataProviderInterface;
2222
use ApiPlatform\Core\Exception\RuntimeException;
23+
use ApiPlatform\Core\Identifier\Normalizer\ChainIdentifierNormalizer;
2324
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
2425
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
2526
use Doctrine\Common\Persistence\ManagerRegistry;
@@ -71,11 +72,13 @@ public function getItem(string $resourceClass, $id, string $operationName = null
7172
{
7273
$manager = $this->managerRegistry->getManagerForClass($resourceClass);
7374

74-
$identifiers = $this->normalizeIdentifiers($id, $manager, $resourceClass);
75+
if (!($context[ChainIdentifierNormalizer::HAS_IDENTIFIER_NORMALIZER] ?? false)) {
76+
$id = $this->normalizeIdentifiers($id, $manager, $resourceClass);
77+
}
7578

7679
$fetchData = $context['fetch_data'] ?? true;
7780
if (!$fetchData && $manager instanceof EntityManagerInterface) {
78-
return $manager->getReference($resourceClass, $identifiers);
81+
return $manager->getReference($resourceClass, $id);
7982
}
8083

8184
$repository = $manager->getRepository($resourceClass);
@@ -87,10 +90,10 @@ public function getItem(string $resourceClass, $id, string $operationName = null
8790
$queryNameGenerator = new QueryNameGenerator();
8891
$doctrineClassMetadata = $manager->getClassMetadata($resourceClass);
8992

90-
$this->addWhereForIdentifiers($identifiers, $queryBuilder, $doctrineClassMetadata);
93+
$this->addWhereForIdentifiers($id, $queryBuilder, $doctrineClassMetadata);
9194

9295
foreach ($this->itemExtensions as $extension) {
93-
$extension->applyToItem($queryBuilder, $queryNameGenerator, $resourceClass, $identifiers, $operationName, $context);
96+
$extension->applyToItem($queryBuilder, $queryNameGenerator, $resourceClass, $id, $operationName, $context);
9497

9598
if ($extension instanceof QueryResultItemExtensionInterface && $extension->supportsResult($resourceClass, $operationName, $context)) {
9699
return $extension->getResult($queryBuilder, $resourceClass, $operationName, $context);

src/Bridge/Doctrine/Orm/SubresourceDataProvider.php

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use ApiPlatform\Core\DataProvider\SubresourceDataProviderInterface;
2424
use ApiPlatform\Core\Exception\ResourceClassNotSupportedException;
2525
use ApiPlatform\Core\Exception\RuntimeException;
26+
use ApiPlatform\Core\Identifier\Normalizer\ChainIdentifierNormalizer;
2627
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
2728
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
2829
use Doctrine\Common\Persistence\ManagerRegistry;
@@ -152,11 +153,16 @@ private function buildQuery(array $identifiers, array $context, QueryNameGenerat
152153
$qb = $manager->createQueryBuilder();
153154
$alias = $queryNameGenerator->generateJoinAlias($identifier);
154155
$relationType = $classMetadata->getAssociationMapping($previousAssociationProperty)['type'];
155-
$normalizedIdentifiers = isset($identifiers[$identifier]) ? $this->normalizeIdentifiers(
156-
$identifiers[$identifier],
157-
$manager,
158-
$identifierResourceClass
159-
) : [];
156+
$normalizedIdentifiers = [];
157+
158+
if (isset($identifiers[$identifier])) {
159+
// if it's an array it's already normalized, the IdentifierManagerTrait is deprecated
160+
if ($context[ChainIdentifierNormalizer::HAS_IDENTIFIER_NORMALIZER] ?? false) {
161+
$normalizedIdentifiers = $identifiers[$identifier];
162+
} else {
163+
$normalizedIdentifiers = $this->normalizeIdentifiers($identifiers[$identifier], $manager, $identifierResourceClass);
164+
}
165+
}
160166

161167
switch ($relationType) {
162168
// MANY_TO_MANY relations need an explicit join so that the identifier part can be retrieved

src/Bridge/Doctrine/Orm/Util/IdentifierManagerTrait.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ private function normalizeIdentifiers($id, ObjectManager $manager, string $resou
7272

7373
$identifier = null === $identifiersMap ? $identifierValues[$i] ?? null : $identifiersMap[$propertyName] ?? null;
7474
if (null === $identifier) {
75-
throw new PropertyNotFoundException(sprintf('Invalid identifier "%s", "%s" has not been found.', $id, $propertyName));
75+
throw new PropertyNotFoundException(sprintf('Invalid identifier "%s", "%s" was not found.', $id, $propertyName));
7676
}
7777

7878
$doctrineTypeName = $doctrineClassMetadata->getTypeOfField($propertyName);
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
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\Core\Bridge\RamseyUuid\Identifier\Normalizer;
15+
16+
use ApiPlatform\Core\Exception\InvalidIdentifierException;
17+
use Ramsey\Uuid\Exception\InvalidUuidStringException;
18+
use Ramsey\Uuid\Uuid;
19+
use Ramsey\Uuid\UuidInterface;
20+
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
21+
22+
/**
23+
* Denormalizes an UUID string to an instance of Ramsey\Uuid.
24+
*
25+
* @author Antoine Bluchet <[email protected]>
26+
*/
27+
final class UuidNormalizer implements DenormalizerInterface
28+
{
29+
/**
30+
* {@inheritdoc}
31+
*/
32+
public function denormalize($data, $class, $format = null, array $context = [])
33+
{
34+
try {
35+
return Uuid::fromString($data);
36+
} catch (InvalidUuidStringException $e) {
37+
throw new InvalidIdentifierException($e->getMessage());
38+
}
39+
}
40+
41+
/**
42+
* {@inheritdoc}
43+
*/
44+
public function supportsDenormalization($data, $type, $format = null)
45+
{
46+
return is_a($type, UuidInterface::class, true);
47+
}
48+
}

src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use Doctrine\Common\Annotations\Annotation;
2727
use Doctrine\ORM\Version;
2828
use phpDocumentor\Reflection\DocBlockFactoryInterface;
29+
use Ramsey\Uuid\Uuid;
2930
use Symfony\Component\Cache\Adapter\ArrayAdapter;
3031
use Symfony\Component\Config\FileLocator;
3132
use Symfony\Component\Config\Resource\DirectoryResource;
@@ -122,6 +123,10 @@ public function load(array $configs, ContainerBuilder $container)
122123
$loader->load('security.xml');
123124
}
124125

126+
if (!class_exists(Uuid::class)) {
127+
$container->removeDefinition('api_platform.identifier.uuid_normalizer');
128+
}
129+
125130
$useDoctrine = isset($bundles['DoctrineBundle']) && class_exists(Version::class);
126131

127132
$this->registerMetadataConfiguration($container, $config, $loader);

src/Bridge/Symfony/Bundle/Resources/config/api.xml

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
<argument type="service" id="api_platform.router" />
5959
<argument type="service" id="api_platform.property_accessor" />
6060
<argument type="service" id="api_platform.identifiers_extractor.cached" />
61+
<argument type="service" id="api_platform.identifier.normalizer" />
6162
</service>
6263
<service id="ApiPlatform\Core\Api\IriConverterInterface" alias="api_platform.iri_converter" />
6364

@@ -138,6 +139,7 @@
138139
<argument type="service" id="api_platform.item_data_provider" />
139140
<argument type="service" id="api_platform.subresource_data_provider" />
140141
<argument type="service" id="api_platform.serializer.context_builder" />
142+
<argument type="service" id="api_platform.identifier.normalizer" />
141143

142144
<tag name="kernel.event_listener" event="kernel.request" method="onKernelRequest" priority="4" />
143145
</service>
@@ -211,7 +213,7 @@
211213
<argument>%api_platform.exception_to_status%</argument>
212214
</service>
213215

214-
<!-- Identifiers extractor -->
216+
<!-- Identifiers -->
215217

216218
<service id="api_platform.identifiers_extractor" class="ApiPlatform\Core\Api\IdentifiersExtractor" public="false">
217219
<argument type="service" id="api_platform.metadata.property.name_collection_factory" />
@@ -227,6 +229,22 @@
227229
<argument type="service" id="api_platform.resource_class_resolver" />
228230
</service>
229231

232+
<service id="api_platform.identifier.normalizer" class="ApiPlatform\Core\Identifier\Normalizer\ChainIdentifierNormalizer" public="false">
233+
<argument type="service" id="api_platform.identifiers_extractor.cached" />
234+
<argument type="service" id="api_platform.metadata.property.metadata_factory" />
235+
<argument type="tagged" tag="api_platform.identifier.normalizer" />
236+
</service>
237+
238+
<service id="api_platform.identifier.composite_identifier.normalizer" class="ApiPlatform\Core\Identifier\Normalizer\CompositeIdentifierNormalizer" public="false" />
239+
240+
<service id="api_platform.identifier.date_normalizer" class="ApiPlatform\Core\Identifier\Normalizer\DateTimeIdentifierNormalizer" public="false">
241+
<tag name="api_platform.identifier.normalizer" />
242+
</service>
243+
244+
<service id="api_platform.identifier.uuid_normalizer" class="ApiPlatform\Core\Bridge\RamseyUuid\Identifier\Normalizer\UuidNormalizer" public="false">
245+
<tag name="api_platform.identifier.normalizer" />
246+
</service>
247+
230248
<!-- Subresources -->
231249

232250
<service id="api_platform.subresource_operation_factory" class="ApiPlatform\Core\Operation\Factory\SubresourceOperationFactory" public="false">

0 commit comments

Comments
 (0)