Skip to content

Commit 4fa2e4a

Browse files
committed
Make it easier to configure operations
1 parent c7e2cda commit 4fa2e4a

File tree

8 files changed

+125
-25
lines changed

8 files changed

+125
-25
lines changed

src/Bridge/Symfony/Routing/ApiLoader.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ private function addRoute(RouteCollection $routeCollection, string $resourceClas
175175
}
176176

177177
if (!isset($operation['method'])) {
178-
throw new RuntimeException('Either a "route_name" or a "method" operation attribute must exist.');
178+
throw new RuntimeException(sprintf('Either a "route_name" or a "method" operation attribute must exist for the operation "%s" of the resource "%s".', $operationName, $resourceClass));
179179
}
180180

181181
if (null === $controller = $operation['controller'] ?? null) {

src/Bridge/Symfony/Routing/OperationMethodResolver.php

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -96,14 +96,7 @@ private function getOperationMethod(string $resourceClass, string $operationName
9696
throw new RuntimeException(sprintf('Either a "route_name" or a "method" operation attribute must exist for the operation "%s" of the resource "%s".', $operationName, $resourceClass));
9797
}
9898

99-
$route = $this->getRoute($routeName);
100-
$methods = $route->getMethods();
101-
102-
if (empty($methods)) {
103-
return 'GET';
104-
}
105-
106-
return $methods[0];
99+
return $this->getRoute($routeName)->getMethods()[0] ?? 'GET';
107100
}
108101

109102
/**

src/Metadata/Resource/Factory/OperationResourceMetadataFactory.php

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,23 @@
2222
*/
2323
final class OperationResourceMetadataFactory implements ResourceMetadataFactoryInterface
2424
{
25+
/**
26+
* @internal
27+
*/
28+
const SUPPORTED_COLLECTION_OPERATION_METHODS = [
29+
'GET' => true,
30+
'POST' => true,
31+
];
32+
33+
/**
34+
* @internal
35+
*/
36+
const SUPPORTED_ITEM_OPERATION_METHODS = [
37+
'GET' => true,
38+
'PUT' => true,
39+
'DELETE' => true,
40+
];
41+
2542
private $decorated;
2643
private $formats;
2744

@@ -37,16 +54,19 @@ public function __construct(ResourceMetadataFactoryInterface $decorated, array $
3754
public function create(string $resourceClass): ResourceMetadata
3855
{
3956
$resourceMetadata = $this->decorated->create($resourceClass);
40-
$reflectionClass = new \ReflectionClass($resourceClass);
41-
$isAbstract = $reflectionClass->isAbstract();
57+
$isAbstract = (new \ReflectionClass($resourceClass))->isAbstract();
4258

43-
if (null === $resourceMetadata->getCollectionOperations()) {
59+
$collectionOperations = $resourceMetadata->getCollectionOperations();
60+
if (null === $collectionOperations) {
4461
$resourceMetadata = $resourceMetadata->withCollectionOperations($this->createOperations(
4562
$isAbstract ? ['GET'] : ['GET', 'POST']
4663
));
64+
} else {
65+
$resourceMetadata = $this->normalize(true, $resourceMetadata, $collectionOperations);
4766
}
4867

49-
if (null === $resourceMetadata->getItemOperations()) {
68+
$itemOperations = $resourceMetadata->getItemOperations();
69+
if (null === $itemOperations) {
5070
$methods = ['GET', 'DELETE'];
5171

5272
if (!$isAbstract) {
@@ -58,6 +78,8 @@ public function create(string $resourceClass): ResourceMetadata
5878
}
5979

6080
$resourceMetadata = $resourceMetadata->withItemOperations($this->createOperations($methods));
81+
} else {
82+
$resourceMetadata = $this->normalize(false, $resourceMetadata, $itemOperations);
6183
}
6284

6385
return $resourceMetadata;
@@ -72,4 +94,31 @@ private function createOperations(array $methods): array
7294

7395
return $operations;
7496
}
97+
98+
private function normalize(bool $collection, ResourceMetadata $resourceMetadata, array $operations): ResourceMetadata
99+
{
100+
$newOperations = [];
101+
foreach ($operations as $operationName => $operation) {
102+
// e.g.: @ApiResource(itemOperations={"get"})
103+
if (is_int($operationName) && is_string($operation)) {
104+
$operationName = $operation;
105+
$operation = [];
106+
}
107+
108+
$upperOperationName = strtoupper($operationName);
109+
if ($collection) {
110+
$supported = isset(self::SUPPORTED_COLLECTION_OPERATION_METHODS[$upperOperationName]);
111+
} else {
112+
$supported = isset(self::SUPPORTED_ITEM_OPERATION_METHODS[$upperOperationName]) || (isset($this->formats['jsonapi']) && 'PATCH' === $upperOperationName);
113+
}
114+
115+
if ($supported && !isset($operation['method']) && !isset($operation['route_name'])) {
116+
$operation['method'] = $upperOperationName;
117+
}
118+
119+
$newOperations[$operationName] = $operation;
120+
}
121+
122+
return $collection ? $resourceMetadata->withCollectionOperations($newOperations) : $resourceMetadata->withItemOperations($newOperations);
123+
}
75124
}

tests/Fixtures/TestBundle/Entity/CustomActionDummy.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@
1919
/**
2020
* @ORM\Entity
2121
* @ApiResource(itemOperations={
22-
* "get"={"method"="GET"},
22+
* "get",
2323
* "custom_normalization"={"route_name"="custom_normalization"}
2424
* }, collectionOperations={
25-
* "get"={"method"="GET"},
25+
* "get",
2626
* "custom_denormalization"={"route_name"="custom_denormalization"}
2727
* })
2828
*

tests/Fixtures/TestBundle/Entity/OverriddenOperationDummy.php

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,17 @@
3030
* "denormalization_context"={"groups"={"overridden_operation_dummy_write"}}
3131
* },
3232
* collectionOperations={
33-
*
3433
* "get"={"method"="GET"},
3534
* "post"={"method"="POST"},
3635
* "swagger"= {
3736
* "path"="/override/swagger",
3837
* "method"="GET",
39-
* }
38+
* }
4039
* },
4140
* itemOperations={
4241
* "swagger"= {
43-
* "method"="GET",
44-
* },
42+
* "method"="GET",
43+
* },
4544
* "get"={
4645
* "method"="GET",
4746
* "normalization_context"={"groups"={"overridden_operation_dummy_get"}},

tests/Fixtures/TestBundle/Entity/RelationEmbedder.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@
2929
* "hydra_context"={"@type"="hydra:Operation", "hydra:title"="A custom operation", "returns"="xmls:string"}
3030
* },
3131
* itemOperations={
32-
* "get"={"method"="GET"},
33-
* "put"={"method"="PUT"},
34-
* "delete"={"method"="DELETE"},
32+
* "get",
33+
* "put"={},
34+
* "delete",
3535
* "custom_get"={"route_name"="relation_embedded.custom_get"},
3636
* "custom1"={"path"="/api/custom-call/{id}", "method"="GET"},
3737
* "custom2"={"path"="/api/custom-call/{id}", "method"="PUT"},

tests/Fixtures/TestBundle/Entity/SecuredDummy.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,11 @@
2525
* @ApiResource(
2626
* attributes={"access_control"="has_role('ROLE_USER')"},
2727
* collectionOperations={
28-
* "get"={"method"="GET"},
29-
* "post"={"method"="POST", "access_control"="has_role('ROLE_ADMIN')"}
28+
* "get",
29+
* "post"={"access_control"="has_role('ROLE_ADMIN')"}
3030
* },
3131
* itemOperations={
32-
* "get"={"method"="GET", "access_control"="has_role('ROLE_USER') and object.getOwner() == user"}
32+
* "get"={"access_control"="has_role('ROLE_USER') and object.getOwner() == user"}
3333
* }
3434
* )
3535
* @ORM\Entity
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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\Metadata\Resource\Factory;
15+
16+
use ApiPlatform\Core\Metadata\Resource\Factory\OperationResourceMetadataFactory;
17+
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
18+
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
19+
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy;
20+
use PHPUnit\Framework\TestCase;
21+
22+
/**
23+
* @author Kévin Dunglas <[email protected]>
24+
*/
25+
class OperationResourceMetadataFactoryTest extends TestCase
26+
{
27+
/**
28+
* @dataProvider getMetadata
29+
*/
30+
public function testCreateOperation(ResourceMetadata $before, ResourceMetadata $after, array $formats = [])
31+
{
32+
$decoratedProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
33+
$decoratedProphecy->create(Dummy::class)->shouldBeCalled()->willReturn($before);
34+
35+
$this->assertEquals($after, (new OperationResourceMetadataFactory($decoratedProphecy->reveal(), $formats))->create(Dummy::class));
36+
}
37+
38+
public function getMetadata()
39+
{
40+
$jsonapi = ['jsonapi' => ['application/vnd.api+json']];
41+
42+
return [
43+
// Item operations
44+
[new ResourceMetadata(null, null, null, null, []), new ResourceMetadata(null, null, null, ['get' => ['method' => 'GET'], 'put' => ['method' => 'PUT'], 'delete' => ['method' => 'DELETE']], [])],
45+
[new ResourceMetadata(null, null, null, null, []), new ResourceMetadata(null, null, null, ['get' => ['method' => 'GET'], 'put' => ['method' => 'PUT'], 'patch' => ['method' => 'PATCH'], 'delete' => ['method' => 'DELETE']], []), $jsonapi],
46+
[new ResourceMetadata(null, null, null, ['get'], []), new ResourceMetadata(null, null, null, ['get' => ['method' => 'GET']], [])],
47+
[new ResourceMetadata(null, null, null, ['put'], []), new ResourceMetadata(null, null, null, ['put' => ['method' => 'PUT']], [])],
48+
[new ResourceMetadata(null, null, null, ['delete'], []), new ResourceMetadata(null, null, null, ['delete' => ['method' => 'DELETE']], [])],
49+
[new ResourceMetadata(null, null, null, ['patch'], []), new ResourceMetadata(null, null, null, ['patch' => []], [])],
50+
[new ResourceMetadata(null, null, null, ['patch'], []), new ResourceMetadata(null, null, null, ['patch' => ['method' => 'PATCH']], []), $jsonapi],
51+
52+
// Collection operations
53+
[new ResourceMetadata(null, null, null, []), new ResourceMetadata(null, null, null, [], ['get' => ['method' => 'GET'], 'post' => ['method' => 'POST']])],
54+
[new ResourceMetadata(null, null, null, [], ['get']), new ResourceMetadata(null, null, null, [], ['get' => ['method' => 'GET']])],
55+
[new ResourceMetadata(null, null, null, [], ['post']), new ResourceMetadata(null, null, null, [], ['post' => ['method' => 'POST']])],
56+
[new ResourceMetadata(null, null, null, [], ['options']), new ResourceMetadata(null, null, null, [], ['options' => []])],
57+
];
58+
}
59+
}

0 commit comments

Comments
 (0)