Skip to content

Commit fb96f33

Browse files
authored
refactor(metadata): cascade resource to operation (#7246)
1 parent 4ecde01 commit fb96f33

20 files changed

+184
-61
lines changed

src/Doctrine/Odm/Metadata/Resource/DoctrineMongoDbOdmResourceCollectionMetadataFactory.php

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
use ApiPlatform\Doctrine\Odm\State\CollectionProvider;
1717
use ApiPlatform\Doctrine\Odm\State\ItemProvider;
1818
use ApiPlatform\Doctrine\Odm\State\Options;
19-
use ApiPlatform\Metadata\ApiResource;
2019
use ApiPlatform\Metadata\CollectionOperationInterface;
2120
use ApiPlatform\Metadata\DeleteOperationInterface;
2221
use ApiPlatform\Metadata\Operation;
@@ -41,12 +40,10 @@ public function create(string $resourceClass): ResourceMetadataCollection
4140
{
4241
$resourceMetadataCollection = $this->decorated->create($resourceClass);
4342

44-
/** @var ApiResource $resourceMetadata */
4543
foreach ($resourceMetadataCollection as $i => $resourceMetadata) {
4644
$operations = $resourceMetadata->getOperations();
4745

4846
if ($operations) {
49-
/** @var Operation $operation */
5047
foreach ($resourceMetadata->getOperations() as $operationName => $operation) {
5148
$documentClass = $this->getStateOptionsClass($operation, $operation->getClass(), Options::class);
5249
if (!$this->managerRegistry->getManagerForClass($documentClass) instanceof DocumentManager) {

src/Doctrine/Odm/Tests/Metadata/Resource/DoctrineMongoDbOdmResourceCollectionMetadataFactoryTest.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
use ApiPlatform\Metadata\Delete;
2222
use ApiPlatform\Metadata\Get;
2323
use ApiPlatform\Metadata\GetCollection;
24-
use ApiPlatform\Metadata\Operation;
24+
use ApiPlatform\Metadata\HttpOperation;
2525
use ApiPlatform\Metadata\Operations;
2626
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
2727
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
@@ -34,7 +34,7 @@ final class DoctrineMongoDbOdmResourceCollectionMetadataFactoryTest extends Test
3434
{
3535
use ProphecyTrait;
3636

37-
private function getResourceMetadataCollectionFactory(Operation $operation)
37+
private function getResourceMetadataCollectionFactory(HttpOperation $operation)
3838
{
3939
if (!class_exists(DocumentManager::class)) {
4040
$this->markTestSkipped('ODM not installed');
@@ -71,7 +71,7 @@ public function testWithoutManager(): void
7171
}
7272

7373
#[\PHPUnit\Framework\Attributes\DataProvider('operationProvider')]
74-
public function testWithProvider(Operation $operation, ?string $expectedProvider = null, ?string $expectedProcessor = null): void
74+
public function testWithProvider(HttpOperation $operation, ?string $expectedProvider = null, ?string $expectedProcessor = null): void
7575
{
7676
if (!class_exists(DocumentManager::class)) {
7777
$this->markTestSkipped('ODM not installed');

src/Doctrine/Orm/Metadata/Resource/DoctrineOrmResourceCollectionMetadataFactory.php

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
use ApiPlatform\Doctrine\Orm\State\CollectionProvider;
1717
use ApiPlatform\Doctrine\Orm\State\ItemProvider;
1818
use ApiPlatform\Doctrine\Orm\State\Options;
19-
use ApiPlatform\Metadata\ApiResource;
2019
use ApiPlatform\Metadata\CollectionOperationInterface;
2120
use ApiPlatform\Metadata\DeleteOperationInterface;
2221
use ApiPlatform\Metadata\Operation;
@@ -41,12 +40,10 @@ public function create(string $resourceClass): ResourceMetadataCollection
4140
{
4241
$resourceMetadataCollection = $this->decorated->create($resourceClass);
4342

44-
/** @var ApiResource $resourceMetadata */
4543
foreach ($resourceMetadataCollection as $i => $resourceMetadata) {
4644
$operations = $resourceMetadata->getOperations();
4745

4846
if ($operations) {
49-
/** @var Operation $operation */
5047
foreach ($resourceMetadata->getOperations() as $operationName => $operation) {
5148
$entityClass = $this->getStateOptionsClass($operation, $operation->getClass(), Options::class);
5249

src/Doctrine/Orm/Tests/Metadata/Resource/DoctrineOrmResourceCollectionMetadataFactoryTest.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
use ApiPlatform\Metadata\Delete;
2222
use ApiPlatform\Metadata\Get;
2323
use ApiPlatform\Metadata\GetCollection;
24-
use ApiPlatform\Metadata\Operation;
24+
use ApiPlatform\Metadata\HttpOperation;
2525
use ApiPlatform\Metadata\Operations;
2626
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
2727
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
@@ -34,7 +34,7 @@ class DoctrineOrmResourceCollectionMetadataFactoryTest extends TestCase
3434
{
3535
use ProphecyTrait;
3636

37-
private function getResourceMetadataCollectionFactory(Operation $operation)
37+
private function getResourceMetadataCollectionFactory(HttpOperation $operation)
3838
{
3939
$resourceMetadataCollectionFactory = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class);
4040
$resourceMetadataCollectionFactory->create($operation->getClass())->willReturn(new ResourceMetadataCollection($operation->getClass(), [
@@ -63,7 +63,7 @@ public function testWithoutManager(): void
6363
}
6464

6565
#[\PHPUnit\Framework\Attributes\DataProvider('operationProvider')]
66-
public function testWithProvider(Operation $operation, ?string $expectedProvider = null, ?string $expectedProcessor = null): void
66+
public function testWithProvider(HttpOperation $operation, ?string $expectedProvider = null, ?string $expectedProcessor = null): void
6767
{
6868
$objectManager = $this->prophesize(EntityManagerInterface::class);
6969
$managerRegistry = $this->prophesize(ManagerRegistry::class);

src/Hydra/Serializer/EntrypointNormalizer.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public function normalize(mixed $object, ?string $format = null, array $context
6161
}
6262

6363
try {
64-
$entrypoint[$key] = $this->iriConverter->getIriFromResource($resourceClass, UrlGeneratorInterface::ABS_PATH, $operation); // @phpstan-ignore-line phpstan issue as type is CollectionOperationInterface & Operation
64+
$entrypoint[$key] = $this->iriConverter->getIriFromResource($resourceClass, UrlGeneratorInterface::ABS_PATH, $operation);
6565
} catch (InvalidArgumentException|OperationNotFoundException) {
6666
// Ignore resources without GET operations
6767
}

src/JsonApi/Serializer/EntrypointNormalizer.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public function normalize(mixed $object, ?string $format = null, array $context
5353
}
5454

5555
try {
56-
$iri = $this->iriConverter->getIriFromResource($resourceClass, UrlGeneratorInterface::ABS_URL, $operation); // @phpstan-ignore-line phpstan issue as type is CollectionOperationInterface & Operation
56+
$iri = $this->iriConverter->getIriFromResource($resourceClass, UrlGeneratorInterface::ABS_URL, $operation);
5757
$entrypoint['links'][lcfirst($resource->getShortName())] = $iri;
5858
} catch (InvalidArgumentException) {
5959
// Ignore resources without GET operations

src/Metadata/ApiResource.php

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,20 +31,24 @@
3131
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
3232
class ApiResource extends Metadata
3333
{
34+
use CascadeToOperationsTrait;
3435
use WithResourceTrait;
3536

37+
/**
38+
* @var Operations<HttpOperation>
39+
*/
3640
protected ?Operations $operations;
3741

3842
/**
39-
* @param array<int, HttpOperation>|array<string, HttpOperation>|Operations|null $operations Operations is a list of HttpOperation
40-
* @param array<string, Link>|array<string, mixed[]>|string[]|string|null $uriVariables
41-
* @param array<string, string> $headers
42-
* @param string|callable|null $provider
43-
* @param string|callable|null $processor
44-
* @param mixed|null $mercure
45-
* @param mixed|null $messenger
46-
* @param mixed|null $input
47-
* @param mixed|null $output
43+
* @param list<HttpOperation>|array<string, HttpOperation>|Operations<HttpOperation>|null $operations Operations is a list of HttpOperation
44+
* @param array<string, Link>|array<string, mixed[]>|string[]|string|null $uriVariables
45+
* @param array<string, string> $headers
46+
* @param string|callable|null $provider
47+
* @param string|callable|null $processor
48+
* @param mixed|null $mercure
49+
* @param mixed|null $messenger
50+
* @param mixed|null $input
51+
* @param mixed|null $output
4852
*/
4953
public function __construct(
5054
/**
@@ -1012,6 +1016,7 @@ class: $class,
10121016
extraProperties: $extraProperties
10131017
);
10141018

1019+
/* @var Operations<HttpOperation> $operations> */
10151020
$this->operations = null === $operations ? null : new Operations($operations);
10161021
$this->provider = $provider;
10171022
$this->processor = $processor;
@@ -1020,11 +1025,17 @@ class: $class,
10201025
}
10211026
}
10221027

1028+
/**
1029+
* @return Operations<HttpOperation>
1030+
*/
10231031
public function getOperations(): ?Operations
10241032
{
10251033
return $this->operations;
10261034
}
10271035

1036+
/**
1037+
* @param Operations<HttpOperation> $operations
1038+
*/
10281039
public function withOperations(Operations $operations): static
10291040
{
10301041
$self = clone $this;

src/Metadata/CascadeFromResource.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 <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\Metadata;
15+
16+
/**
17+
* @internal
18+
*
19+
* @phpstan-require-extends Operation
20+
*/
21+
trait CascadeFromResource
22+
{
23+
use WithResourceTrait;
24+
25+
public function cascadeFromResource(ApiResource $apiResource, array $ignoredOptions = []): static
26+
{
27+
return $this->copyFrom($apiResource, $ignoredOptions);
28+
}
29+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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\Metadata;
15+
16+
use ApiPlatform\Metadata\Exception\RuntimeException;
17+
use ApiPlatform\Metadata\GraphQl\Operation as GraphQlOperation;
18+
19+
/**
20+
* @internal
21+
*
22+
* @phpstan-require-extends ApiResource
23+
*/
24+
trait CascadeToOperationsTrait
25+
{
26+
public function cascadeToOperations(): static
27+
{
28+
if (!$this instanceof ApiResource) {
29+
throw new RuntimeException('Not an API resource');
30+
}
31+
32+
if (!($operations = $this->getOperations() ?? [])) {
33+
return $this;
34+
}
35+
36+
return (clone $this)->withOperations(
37+
new Operations($this->getMutatedOperations($operations, $this)),
38+
);
39+
}
40+
41+
public function cascadeToGraphQlOperations(): static
42+
{
43+
if (!$this instanceof ApiResource) {
44+
throw new RuntimeException('Not an API resource');
45+
}
46+
47+
if (!($operations = $this->getGraphQlOperations() ?? [])) {
48+
return $this;
49+
}
50+
51+
return (clone $this)->withGraphQlOperations(
52+
$this->getMutatedOperations($operations, $this),
53+
);
54+
}
55+
56+
/**
57+
* @param Operations<HttpOperation>|list<GraphQlOperation> $operations
58+
*
59+
* @return array[string, HttpOperation]|list<GraphQlOperation>
60+
*/
61+
private function getMutatedOperations(iterable $operations, ApiResource $apiResource): iterable
62+
{
63+
$modifiedOperations = [];
64+
foreach ($operations as $key => $operation) {
65+
$modifiedOperations[$key] = $operation->cascadeFromResource($apiResource);
66+
}
67+
68+
return $modifiedOperations;
69+
}
70+
}

src/Metadata/Operation.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
*/
2121
abstract class Operation extends Metadata
2222
{
23+
use CascadeFromResource;
2324
use WithResourceTrait;
2425

2526
/**
@@ -861,7 +862,7 @@ class: $class,
861862
);
862863
}
863864

864-
public function withOperation($operation)
865+
public function withOperation(self $operation): static
865866
{
866867
return $this->copyFrom($operation);
867868
}

src/Metadata/Operations.php

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,18 @@
1515

1616
/**
1717
* An Operation dictionnary.
18+
*
19+
* @template-covariant T of Operation
1820
*/
1921
final class Operations implements \IteratorAggregate, \Countable
2022
{
23+
/**
24+
* @var list<array{0: string, 1: T}>
25+
*/
2126
private array $operations = [];
2227

2328
/**
24-
* @param array<string|int, Operation> $operations
29+
* @param list<T>|array<string, T> $operations
2530
*/
2631
public function __construct(array $operations = [])
2732
{
@@ -42,6 +47,9 @@ public function __construct(array $operations = [])
4247
$this->sort();
4348
}
4449

50+
/**
51+
* @return \Iterator<string, T>
52+
*/
4553
public function getIterator(): \Traversable
4654
{
4755
return (function (): \Generator {
@@ -97,7 +105,7 @@ public function count(): int
97105

98106
public function sort(): self
99107
{
100-
usort($this->operations, fn ($a, $b): int|float => $a[1]->getPriority() - $b[1]->getPriority());
108+
usort($this->operations, fn ($a, $b): int => $a[1]->getPriority() - $b[1]->getPriority());
101109

102110
return $this;
103111
}

src/Metadata/Resource/Factory/FormatsResourceMetadataCollectionFactory.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use ApiPlatform\Metadata\ErrorResource;
1818
use ApiPlatform\Metadata\Exception\InvalidArgumentException;
1919
use ApiPlatform\Metadata\Exception\ResourceClassNotFoundException;
20+
use ApiPlatform\Metadata\HttpOperation;
2021
use ApiPlatform\Metadata\Operations;
2122
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
2223

@@ -67,6 +68,9 @@ public function create(string $resourceClass): ResourceMetadataCollection
6768
return $resourceMetadataCollection;
6869
}
6970

71+
/**
72+
* @param Operations<HttpOperation> $operations
73+
*/
7074
private function normalize(array $resourceInputFormats, array $resourceOutputFormats, Operations $operations): Operations
7175
{
7276
$newOperations = [];

src/Metadata/Resource/Factory/NotExposedOperationResourceMetadataCollectionFactory.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,7 @@ public function create(string $resourceClass): ResourceMetadataCollection
7070

7171
// No item operation has been found on all resources for resource class: generate one on the last resource
7272
// Helpful to generate an IRI for a resource without declaring the Get operation
73-
/** @var HttpOperation $operation */
74-
[$key, $operation] = $this->getOperationWithDefaults(resource: $resource, operation: new NotExposed(), generated: true, ignoredOptions: ['uriTemplate', 'uriVariables']); // @phpstan-ignore-line $resource is defined if count > 0
73+
[$key, $operation] = $this->getOperationWithDefaults($resource, new NotExposed(), true, ['uriTemplate', 'uriVariables']); // @phpstan-ignore-line $resource is defined if count > 0
7574

7675
if (!$this->linkFactory->createLinksFromIdentifiers($operation)) {
7776
$operation = $operation->withUriTemplate(self::$skolemUriTemplate);

0 commit comments

Comments
 (0)