Skip to content

Commit 142f7da

Browse files
committed
Ability to declare empty item operations
1 parent 1088a5d commit 142f7da

File tree

8 files changed

+191
-22
lines changed

8 files changed

+191
-22
lines changed

features/main/operation.feature

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,11 @@ Feature: Operation support
5555
}
5656
}
5757
"""
58+
59+
Scenario: Get the collection of a resource that doesn't have a defined item operation
60+
When I send a "GET" request to "/disable_item_operations"
61+
Then the response status code should be 200
62+
63+
Scenario: Do not get a resource that doesn't have a defined item operation
64+
When I send a "GET" request to "/disable_item_operations/1"
65+
Then the response status code should be 404

src/Action/NotFoundAction.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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\Action;
15+
16+
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
17+
18+
/**
19+
* Not found action.
20+
*
21+
* @author Antoine Bluchet <[email protected]>
22+
*/
23+
final class NotFoundAction
24+
{
25+
public function __invoke()
26+
{
27+
throw new NotFoundHttpException();
28+
}
29+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,8 @@
230230
<service id="api_platform.action.put_item" alias="api_platform.action.placeholder" public="true" />
231231
<service id="api_platform.action.delete_item" alias="api_platform.action.placeholder" public="true" />
232232
<service id="api_platform.action.get_subresource" alias="api_platform.action.placeholder" public="true" />
233+
<service id="api_platform.action.not_found" class="ApiPlatform\Core\Action\NotFoundAction" public="true" />
234+
<service id="ApiPlatform\Core\Action\NotFoundAction" alias="api_platform.action.not_found" public="true" />
233235

234236
<service id="api_platform.action.entrypoint" class="ApiPlatform\Core\Action\EntrypointAction" public="true">
235237
<argument type="service" id="api_platform.metadata.resource.name_collection_factory" />

src/Metadata/Resource/Factory/OperationResourceMetadataFactory.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
namespace ApiPlatform\Core\Metadata\Resource\Factory;
1515

16+
use ApiPlatform\Core\Action\NotFoundAction;
1617
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
1718

1819
/**
@@ -81,6 +82,13 @@ public function create(string $resourceClass): ResourceMetadata
8182
$resourceMetadata = $this->normalize(false, $resourceClass, $resourceMetadata, $itemOperations);
8283
}
8384

85+
if ($this->needsDefaultGetOperation($resourceMetadata)) {
86+
$resourceMetadata = $resourceMetadata->withItemOperations(array_merge(
87+
$resourceMetadata->getItemOperations(),
88+
['get' => ['method' => 'GET', 'read' => false, 'output' => ['class' => false], 'controller' => NotFoundAction::class]])
89+
);
90+
}
91+
8492
$graphql = $resourceMetadata->getGraphql();
8593
if (null === $graphql) {
8694
$resourceMetadata = $resourceMetadata->withGraphql(['item_query' => [], 'collection_query' => [], 'delete' => [], 'update' => [], 'create' => []]);
@@ -148,4 +156,17 @@ private function normalizeGraphQl(ResourceMetadata $resourceMetadata, array $ope
148156

149157
return $resourceMetadata->withGraphql($operations);
150158
}
159+
160+
private function needsDefaultGetOperation(ResourceMetadata $resourceMetadata): bool
161+
{
162+
$itemOperations = $resourceMetadata->getItemOperations();
163+
164+
foreach ($itemOperations as $itemOperation) {
165+
if ('GET' === ($itemOperation['method'] ?? false)) {
166+
return false;
167+
}
168+
}
169+
170+
return true;
171+
}
151172
}

tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
namespace ApiPlatform\Core\Tests\Bridge\Symfony\Bundle\DependencyInjection;
1515

16+
use ApiPlatform\Core\Action\NotFoundAction;
1617
use ApiPlatform\Core\Api\FilterInterface;
1718
use ApiPlatform\Core\Api\IdentifiersExtractorInterface;
1819
use ApiPlatform\Core\Api\IriConverterInterface;
@@ -840,6 +841,7 @@ private function getPartialContainerBuilderProphecy()
840841
'api_platform.action.documentation',
841842
'api_platform.action.entrypoint',
842843
'api_platform.action.exception',
844+
'api_platform.action.not_found',
843845
'api_platform.action.placeholder',
844846
'api_platform.cache.identifiers_extractor',
845847
'api_platform.cache.metadata.property',
@@ -935,23 +937,24 @@ private function getPartialContainerBuilderProphecy()
935937
'api_platform.property_accessor' => 'property_accessor',
936938
'api_platform.property_info' => 'property_info',
937939
'api_platform.serializer' => 'serializer',
938-
Pagination::class => 'api_platform.pagination',
939-
IriConverterInterface::class => 'api_platform.iri_converter',
940-
UrlGeneratorInterface::class => 'api_platform.router',
941-
SerializerContextBuilderInterface::class => 'api_platform.serializer.context_builder',
942940
CollectionDataProviderInterface::class => 'api_platform.collection_data_provider',
943-
ItemDataProviderInterface::class => 'api_platform.item_data_provider',
944-
SubresourceDataProviderInterface::class => 'api_platform.subresource_data_provider',
945941
DataPersisterInterface::class => 'api_platform.data_persister',
946-
ResourceNameCollectionFactoryInterface::class => 'api_platform.metadata.resource.name_collection_factory',
947-
ResourceMetadataFactoryInterface::class => 'api_platform.metadata.resource.metadata_factory',
942+
GroupFilter::class => 'api_platform.serializer.group_filter',
943+
IdentifiersExtractorInterface::class => 'api_platform.identifiers_extractor.cached',
944+
IriConverterInterface::class => 'api_platform.iri_converter',
945+
ItemDataProviderInterface::class => 'api_platform.item_data_provider',
946+
NotFoundAction::class => 'api_platform.action.not_found',
947+
OperationAwareFormatsProviderInterface::class => 'api_platform.formats_provider',
948+
Pagination::class => 'api_platform.pagination',
949+
PropertyFilter::class => 'api_platform.serializer.property_filter',
948950
PropertyNameCollectionFactoryInterface::class => 'api_platform.metadata.property.name_collection_factory',
949951
PropertyMetadataFactoryInterface::class => 'api_platform.metadata.property.metadata_factory',
950952
ResourceClassResolverInterface::class => 'api_platform.resource_class_resolver',
951-
PropertyFilter::class => 'api_platform.serializer.property_filter',
952-
GroupFilter::class => 'api_platform.serializer.group_filter',
953-
OperationAwareFormatsProviderInterface::class => 'api_platform.formats_provider',
954-
IdentifiersExtractorInterface::class => 'api_platform.identifiers_extractor.cached',
953+
ResourceNameCollectionFactoryInterface::class => 'api_platform.metadata.resource.name_collection_factory',
954+
ResourceMetadataFactoryInterface::class => 'api_platform.metadata.resource.metadata_factory',
955+
SerializerContextBuilderInterface::class => 'api_platform.serializer.context_builder',
956+
SubresourceDataProviderInterface::class => 'api_platform.subresource_data_provider',
957+
UrlGeneratorInterface::class => 'api_platform.router',
955958
];
956959

957960
foreach ($aliases as $alias => $service) {
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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+
* DisableItemOperation.
21+
*
22+
* @author Antoine Bluchet <[email protected]>
23+
*
24+
* @ApiResource(itemOperations={}, collectionOperations={"get"})
25+
* @ODM\Document
26+
*/
27+
class DisableItemOperation
28+
{
29+
/**
30+
* @ODM\Id(strategy="INCREMENT", type="integer")
31+
*/
32+
private $id;
33+
34+
/**
35+
* @var string The dummy name
36+
*
37+
* @ODM\Field
38+
*/
39+
public $name;
40+
41+
public function getId()
42+
{
43+
return $this->id;
44+
}
45+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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+
* DisableItemOperation.
21+
*
22+
* @author Antoine Bluchet <[email protected]>
23+
*
24+
* @ApiResource(itemOperations={}, collectionOperations={"get", "post"})
25+
* @ORM\Entity
26+
*/
27+
class DisableItemOperation
28+
{
29+
/**
30+
* @var int The id
31+
*
32+
* @ORM\Column(type="integer", nullable=true)
33+
* @ORM\Id
34+
* @ORM\GeneratedValue(strategy="AUTO")
35+
*/
36+
private $id;
37+
38+
/**
39+
* @var string The dummy name
40+
*
41+
* @ORM\Column
42+
*/
43+
public $name;
44+
45+
public function getId()
46+
{
47+
return $this->id;
48+
}
49+
}

tests/Metadata/Resource/Factory/OperationResourceMetadataFactoryTest.php

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313

1414
namespace ApiPlatform\Core\Tests\Metadata\Resource\Factory;
1515

16+
use ApiPlatform\Core\Action\NotFoundAction;
17+
use ApiPlatform\Core\Api\OperationType;
1618
use ApiPlatform\Core\Metadata\Resource\Factory\OperationResourceMetadataFactory;
1719
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
1820
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
@@ -43,29 +45,39 @@ public function getMetadata(): iterable
4345
yield [new ResourceMetadata(null, null, null, null, [], null, [], []), new ResourceMetadata(null, null, null, $this->getOperations(['get', 'put', 'delete']), [], null, [], [])];
4446
yield [new ResourceMetadata(null, null, null, null, [], null, [], []), new ResourceMetadata(null, null, null, $this->getOperations(['get', 'put', 'patch', 'delete']), [], null, [], []), $jsonapi];
4547
yield [new ResourceMetadata(null, null, null, ['get'], [], null, [], []), new ResourceMetadata(null, null, null, $this->getOperations(['get']), [], null, [], [])];
48+
yield [new ResourceMetadata(null, null, null, [], [], null, [], []), new ResourceMetadata(null, null, null, $this->getPlaceholderOperation(), [], null, [], [])];
4649
yield [new ResourceMetadata(null, null, null, ['put'], [], null, [], []), new ResourceMetadata(null, null, null, $this->getOperations(['put']), [], null, [], [])];
4750
yield [new ResourceMetadata(null, null, null, ['delete'], [], null, [], []), new ResourceMetadata(null, null, null, $this->getOperations(['delete']), [], null, [], [])];
48-
yield [new ResourceMetadata(null, null, null, ['patch' => ['method' => 'PATCH', 'route_name' => 'patch']], [], null, [], []), new ResourceMetadata(null, null, null, ['patch' => ['method' => 'PATCH', 'route_name' => 'patch']], [], null, [], [])];
49-
yield [new ResourceMetadata(null, null, null, ['patch' => ['method' => 'PATCH', 'route_name' => 'patch']], [], null, [], []), new ResourceMetadata(null, null, null, ['patch' => ['method' => 'PATCH', 'route_name' => 'patch']], [], null, [], []), $jsonapi];
51+
yield [new ResourceMetadata(null, null, null, ['patch' => ['method' => 'PATCH', 'route_name' => 'patch']], [], null, [], []), new ResourceMetadata(null, null, null, array_merge(['patch' => ['method' => 'PATCH', 'route_name' => 'patch']], $this->getPlaceholderOperation()), [], null, [], [])];
52+
yield [new ResourceMetadata(null, null, null, ['patch' => ['method' => 'PATCH', 'route_name' => 'patch']], [], null, [], []), new ResourceMetadata(null, null, null, array_merge(['patch' => ['method' => 'PATCH', 'route_name' => 'patch']], $this->getPlaceholderOperation()), [], null, [], []), $jsonapi];
5053
yield [new ResourceMetadata(null, null, null, ['untouched' => ['method' => 'GET']], [], null, [], []), new ResourceMetadata(null, null, null, ['untouched' => ['method' => 'GET']], [], null, [], []), $jsonapi];
51-
yield [new ResourceMetadata(null, null, null, ['untouched_custom' => ['route_name' => 'custom_route']], [], null, [], []), new ResourceMetadata(null, null, null, ['untouched_custom' => ['route_name' => 'custom_route']], [], null, [], []), $jsonapi];
54+
yield [new ResourceMetadata(null, null, null, ['untouched_custom' => ['route_name' => 'custom_route']], [], null, [], []), new ResourceMetadata(null, null, null, array_merge(['untouched_custom' => ['route_name' => 'custom_route']], $this->getPlaceholderOperation()), [], null, [], []), $jsonapi];
5255

5356
// Collection operations
54-
yield [new ResourceMetadata(null, null, null, [], null, null, [], []), new ResourceMetadata(null, null, null, [], $this->getOperations(['get', 'post']), null, [], [])];
55-
yield [new ResourceMetadata(null, null, null, [], ['get'], null, [], []), new ResourceMetadata(null, null, null, [], $this->getOperations(['get']), null, [], [])];
56-
yield [new ResourceMetadata(null, null, null, [], ['post'], null, [], []), new ResourceMetadata(null, null, null, [], $this->getOperations(['post']), null, [], [])];
57-
yield [new ResourceMetadata(null, null, null, [], ['options' => ['method' => 'OPTIONS', 'route_name' => 'options']], null, [], []), new ResourceMetadata(null, null, null, [], ['options' => ['route_name' => 'options', 'method' => 'OPTIONS']], null, [], [])];
58-
yield [new ResourceMetadata(null, null, null, [], ['untouched' => ['method' => 'GET']], null, [], []), new ResourceMetadata(null, null, null, [], ['untouched' => ['method' => 'GET']], null, [], [])];
59-
yield [new ResourceMetadata(null, null, null, [], ['untouched_custom' => ['route_name' => 'custom_route']], null, [], []), new ResourceMetadata(null, null, null, [], ['untouched_custom' => ['route_name' => 'custom_route']], null, [], [])];
57+
yield [new ResourceMetadata(null, null, null, [], null, null, [], []), new ResourceMetadata(null, null, null, $this->getPlaceholderOperation(), $this->getOperations(['get', 'post'], OperationType::COLLECTION), null, [], [])];
58+
yield [new ResourceMetadata(null, null, null, [], ['get'], null, [], []), new ResourceMetadata(null, null, null, $this->getPlaceholderOperation(), $this->getOperations(['get'], OperationType::COLLECTION), null, [], [])];
59+
yield [new ResourceMetadata(null, null, null, [], ['post'], null, [], []), new ResourceMetadata(null, null, null, $this->getPlaceholderOperation(), $this->getOperations(['post'], OperationType::COLLECTION), null, [], [])];
60+
yield [new ResourceMetadata(null, null, null, [], ['options' => ['method' => 'OPTIONS', 'route_name' => 'options']], null, [], []), new ResourceMetadata(null, null, null, $this->getPlaceholderOperation(), ['options' => ['route_name' => 'options', 'method' => 'OPTIONS']], null, [], [])];
61+
yield [new ResourceMetadata(null, null, null, [], ['untouched' => ['method' => 'GET']], null, [], []), new ResourceMetadata(null, null, null, $this->getPlaceholderOperation(), ['untouched' => ['method' => 'GET']], null, [], [])];
62+
yield [new ResourceMetadata(null, null, null, [], ['untouched_custom' => ['route_name' => 'custom_route']], null, [], []), new ResourceMetadata(null, null, null, $this->getPlaceholderOperation(), ['untouched_custom' => ['route_name' => 'custom_route']], null, [], [])];
6063
}
6164

62-
private function getOperations(array $names): array
65+
private function getOperations(array $names, $operationType = OperationType::ITEM): array
6366
{
6467
$operations = [];
6568
foreach ($names as $name) {
6669
$operations[$name] = ['method' => strtoupper($name)];
6770
}
6871

72+
if (OperationType::ITEM === $operationType && !isset($operations['get'])) {
73+
return array_merge($operations, $this->getPlaceholderOperation());
74+
}
75+
6976
return $operations;
7077
}
78+
79+
private function getPlaceholderOperation(): array
80+
{
81+
return ['get' => ['method' => 'GET', 'read' => false, 'output' => ['class' => false], 'controller' => NotFoundAction::class]];
82+
}
7183
}

0 commit comments

Comments
 (0)