Skip to content

Commit d4aec53

Browse files
committed
feat: Allow to alias with identifiedBy
1 parent 22ce639 commit d4aec53

File tree

15 files changed

+228
-3
lines changed

15 files changed

+228
-3
lines changed

features/bootstrap/DoctrineContext.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\AbsoluteUrlRelationDummy as AbsoluteUrlRelationDummyDocument;
1616
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\Address as AddressDocument;
1717
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\Answer as AnswerDocument;
18+
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\Book as BookDocument;
1819
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\CompositeItem as CompositeItemDocument;
1920
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\CompositeLabel as CompositeLabelDocument;
2021
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\CompositePrimitiveItem as CompositePrimitiveItemDocument;
@@ -77,6 +78,7 @@
7778
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\AbsoluteUrlRelationDummy;
7879
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Address;
7980
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Answer;
81+
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Book;
8082
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\CompositeItem;
8183
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\CompositeLabel;
8284
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\CompositePrimitiveItem;
@@ -1589,6 +1591,18 @@ public function thereAreNetworkPathDummies(int $nb)
15891591
$this->manager->flush();
15901592
}
15911593

1594+
/**
1595+
* @Given there is a book
1596+
*/
1597+
public function thereIsABook()
1598+
{
1599+
$book = $this->buildBook();
1600+
$book->name = '1984';
1601+
$book->isbn = '9780451524935';
1602+
$this->manager->persist($book);
1603+
$this->manager->flush();
1604+
}
1605+
15921606
private function isOrm(): bool
15931607
{
15941608
return null !== $this->schemaTool;
@@ -2006,4 +2020,12 @@ private function buildNetworkPathRelationDummy()
20062020
{
20072021
return $this->isOrm() ? new NetworkPathRelationDummy() : new NetworkPathRelationDummyDocument();
20082022
}
2023+
2024+
/**
2025+
* @return BookDocument | Book
2026+
*/
2027+
private function buildBook()
2028+
{
2029+
return $this->isOrm() ? new Book() : new BookDocument();
2030+
}
20092031
}

features/main/operation.feature

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,23 @@ Feature: Operation support
6363
Scenario: Get a 404 response for the disabled item operation
6464
When I send a "GET" request to "/disable_item_operations/1"
6565
Then the response status code should be 404
66+
67+
@createSchema
68+
Scenario: Get a book by it's ISBN
69+
Given there is a book
70+
When I send a "GET" request to "books/by_isbn/9780451524935"
71+
Then the response status code should be 200
72+
And the response should be in JSON
73+
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
74+
And the JSON should be equal to:
75+
"""
76+
{
77+
"@context": "/contexts/Book",
78+
"@id": "/books/1",
79+
"@type": "Book",
80+
"name": "1984",
81+
"isbn": "9780451524935",
82+
"id": 1
83+
}
84+
"""
85+

src/Bridge/Symfony/Routing/ApiLoader.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,12 +224,15 @@ private function addRoute(RouteCollection $routeCollection, string $resourceClas
224224
$path = trim(trim($resourceMetadata->getAttribute('route_prefix', '')), '/');
225225
$path .= $this->operationPathResolver->resolveOperationPath($resourceShortName, $operation, $operationType, $operationName);
226226

227+
$identifiers = $operation['identifiedBy'] ?? ['id'];
228+
227229
$route = new Route(
228230
$path,
229231
[
230232
'_controller' => $controller,
231233
'_format' => null,
232234
'_api_resource_class' => $resourceClass,
235+
'_api_identified_by' => \is_array($identifiers) ? $identifiers : [$identifiers],
233236
sprintf('_api_%s_operation_name', $operationType) => $operationName,
234237
] + ($operation['defaults'] ?? []),
235238
$operation['requirements'] ?? [],

src/Bridge/Symfony/Routing/IriConverter.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,6 @@ public function getIriFromResourceClass(string $resourceClass, int $referenceTyp
147147
public function getItemIriFromResourceClass(string $resourceClass, array $identifiers, int $referenceType = null): string
148148
{
149149
$routeName = $this->routeNameResolver->getRouteName($resourceClass, OperationType::ITEM);
150-
151150
try {
152151
$identifiers = $this->generateIdentifiersUrl($identifiers, $resourceClass);
153152

src/DataProvider/OperationDataProviderTrait.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,19 @@ private function getSubresourceData($identifiers, array $attributes, array $cont
8585
*/
8686
private function extractIdentifiers(array $parameters, array $attributes)
8787
{
88+
if (isset($attributes['identified_by'])) {
89+
$identifiers = [];
90+
foreach ($attributes['identified_by'] as $identifier) {
91+
if (!isset($parameters[$identifier])) {
92+
throw new InvalidIdentifierException(sprintf('Parameter "%s" not found', $identifier));
93+
}
94+
95+
$identifiers[$identifier] = $parameters[$identifier];
96+
}
97+
98+
return $this->identifierConverter->normalizeIdentifiers($identifiers, $attributes['resource_class'], array_keys($identifiers));
99+
}
100+
88101
if (isset($attributes['item_operation_name'])) {
89102
if (!isset($parameters['id'])) {
90103
throw new InvalidIdentifierException('Parameter "id" not found');

src/Identifier/IdentifierConverter.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
*
2626
* @author Antoine Bluchet <[email protected]>
2727
*/
28-
final class IdentifierConverter implements ContextAwareIdentifierConverterInterface
28+
final class IdentifierConverter implements NormalizeIdentifierConverterInterface
2929
{
3030
private $propertyMetadataFactory;
3131
private $identifiersExtractor;
@@ -63,6 +63,11 @@ public function convert(string $data, string $class, array $context = []): array
6363
$identifiers = [$keys[0] => $data];
6464
}
6565

66+
return $this->normalizeIdentifiers($identifiers, $class, $keys);
67+
}
68+
69+
public function normalizeIdentifiers(array $identifiers, string $class, array $keys, array $context = []): array
70+
{
6671
// Normalize every identifier (DateTime, UUID etc.)
6772
foreach ($keys as $key) {
6873
if (!isset($identifiers[$key])) {
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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\Identifier;
15+
16+
/**
17+
* Gives access to the context in the IdentifierConverter.
18+
*
19+
* @author Antoine Bluchet <[email protected]>
20+
*/
21+
interface NormalizeIdentifierConverterInterface extends ContextAwareIdentifierConverterInterface
22+
{
23+
/**
24+
* {@inheritdoc}
25+
*/
26+
public function normalizeIdentifiers(array $identifiers, string $class, array $keys, array $context = []): array;
27+
}

src/Util/AttributesExtractor.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ private function __construct()
3434
*/
3535
public static function extractAttributes(array $attributes): array
3636
{
37-
$result = ['resource_class' => $attributes['_api_resource_class'] ?? null];
37+
$result = ['resource_class' => $attributes['_api_resource_class'] ?? null, 'identified_by' => $attributes['_api_identified_by'] ?? null];
3838
if ($subresourceContext = $attributes['_api_subresource_context'] ?? null) {
3939
$result['subresource_context'] = $subresourceContext;
4040
}

tests/Bridge/Symfony/Bundle/DataCollector/RequestDataCollectorTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ public function testWithResource()
138138

139139
$this->assertSame([
140140
'resource_class' => DummyEntity::class,
141+
'identified_by' => null,
141142
'item_operation_name' => 'get',
142143
'receive' => true,
143144
'respond' => true,

tests/Bridge/Symfony/Routing/ApiLoaderTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,7 @@ private function getRoute(string $path, string $controller, string $resourceClas
302302
'_controller' => $controller,
303303
'_format' => null,
304304
'_api_resource_class' => $resourceClass,
305+
'_api_identified_by' => ['id'],
305306
sprintf('_api_%s_operation_name', $collection ? 'collection' : 'item') => $operationName,
306307
] + $extraDefaults,
307308
$requirements,

tests/EventListener/DeserializeListenerTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ public function testLegacyDeserializeResourceClassSupportedFormat(string $method
200200
$formatsProviderProphecy->getFormatsFromAttributes([
201201
'resource_class' => 'Foo',
202202
'collection_operation_name' => 'post',
203+
'identified_by' => null,
203204
'receive' => true,
204205
'respond' => true,
205206
'persist' => true,
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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\Tests\Fixtures\TestBundle\Document;
15+
16+
use ApiPlatform\Core\Annotation\ApiResource;
17+
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
18+
19+
/**
20+
* Book.
21+
*
22+
* @author Antoine Bluchet <[email protected]>
23+
*
24+
* @ApiResource(collectionOperations={}, itemOperations={
25+
* "get",
26+
* "get_by_isbn"={"method"="GET", "path"="/books/by_isbn/{isbn}.{_format}", "requirements"={"isbn"=".+"}, "identifiedBy"="isbn"}
27+
* })
28+
* @ODM\Document
29+
*/
30+
class Book
31+
{
32+
/**
33+
* @ODM\Id(strategy="INCREMENT", type="integer")
34+
*/
35+
private $id;
36+
37+
/**
38+
* @ODM\Field(type="string", nullable=true)
39+
*/
40+
public $name;
41+
42+
/**
43+
* @ODM\Field(type="string")
44+
*/
45+
public $isbn;
46+
47+
public function getId()
48+
{
49+
return $this->id;
50+
}
51+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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\Tests\Fixtures\TestBundle\Entity;
15+
16+
use ApiPlatform\Core\Annotation\ApiResource;
17+
use Doctrine\ORM\Mapping as ORM;
18+
19+
/**
20+
* Book.
21+
*
22+
* @author Antoine Bluchet <[email protected]>
23+
*
24+
* @ApiResource(collectionOperations={}, itemOperations={
25+
* "get",
26+
* "get_by_isbn"={"method"="GET", "path"="/books/by_isbn/{isbn}.{_format}", "requirements"={"isbn"=".+"}, "identifiedBy"="isbn"}
27+
* })
28+
* @ORM\Entity
29+
*/
30+
class Book
31+
{
32+
/**
33+
* @ORM\Column(type="integer")
34+
* @ORM\Id
35+
* @ORM\GeneratedValue(strategy="AUTO")
36+
*/
37+
private $id;
38+
39+
/**
40+
* @ORM\Column
41+
*/
42+
public $name;
43+
44+
/**
45+
* @ORM\Column(unique=true)
46+
*/
47+
public $isbn;
48+
49+
public function getId()
50+
{
51+
return $this->id;
52+
}
53+
}

tests/Serializer/SerializerFilterContextBuilderTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ public function testCreateFromRequestWithoutAttributes()
147147
$attributes = [
148148
'resource_class' => DummyGroup::class,
149149
'collection_operation_name' => 'get',
150+
'identified_by' => null,
150151
'receive' => true,
151152
'respond' => true,
152153
'persist' => true,

0 commit comments

Comments
 (0)