From c5c0d7e68e65519cc044616c03b8c5ed56873c7b Mon Sep 17 00:00:00 2001 From: soyuka Date: Thu, 26 Jun 2025 17:04:09 +0200 Subject: [PATCH] refactor(metadata): cascade resource to operation --- ...DbOdmResourceCollectionMetadataFactory.php | 3 - ...mResourceCollectionMetadataFactoryTest.php | 6 +- ...neOrmResourceCollectionMetadataFactory.php | 3 - ...mResourceCollectionMetadataFactoryTest.php | 6 +- src/Hydra/Serializer/EntrypointNormalizer.php | 2 +- .../Serializer/EntrypointNormalizer.php | 2 +- src/Metadata/ApiResource.php | 29 +++++--- src/Metadata/CascadeFromResource.php | 29 ++++++++ src/Metadata/CascadeToOperationsTrait.php | 70 +++++++++++++++++++ src/Metadata/Operation.php | 3 +- src/Metadata/Operations.php | 12 +++- ...rmatsResourceMetadataCollectionFactory.php | 4 ++ ...ationResourceMetadataCollectionFactory.php | 3 +- .../Factory/OperationDefaultsTrait.php | 39 ++++------- .../Resource/ResourceMetadataCollection.php | 2 +- ...sResourceMetadataCollectionFactoryTest.php | 18 ++++- src/Metadata/WithResourceTrait.php | 6 +- src/OpenApi/Factory/OpenApiFactory.php | 4 +- src/Symfony/Routing/ApiLoader.php | 2 +- .../TestBundle/Entity/AttributeResource.php | 2 +- 20 files changed, 184 insertions(+), 61 deletions(-) create mode 100644 src/Metadata/CascadeFromResource.php create mode 100644 src/Metadata/CascadeToOperationsTrait.php diff --git a/src/Doctrine/Odm/Metadata/Resource/DoctrineMongoDbOdmResourceCollectionMetadataFactory.php b/src/Doctrine/Odm/Metadata/Resource/DoctrineMongoDbOdmResourceCollectionMetadataFactory.php index db79c8f4360..796f0089fd8 100644 --- a/src/Doctrine/Odm/Metadata/Resource/DoctrineMongoDbOdmResourceCollectionMetadataFactory.php +++ b/src/Doctrine/Odm/Metadata/Resource/DoctrineMongoDbOdmResourceCollectionMetadataFactory.php @@ -16,7 +16,6 @@ use ApiPlatform\Doctrine\Odm\State\CollectionProvider; use ApiPlatform\Doctrine\Odm\State\ItemProvider; use ApiPlatform\Doctrine\Odm\State\Options; -use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\CollectionOperationInterface; use ApiPlatform\Metadata\DeleteOperationInterface; use ApiPlatform\Metadata\Operation; @@ -41,12 +40,10 @@ public function create(string $resourceClass): ResourceMetadataCollection { $resourceMetadataCollection = $this->decorated->create($resourceClass); - /** @var ApiResource $resourceMetadata */ foreach ($resourceMetadataCollection as $i => $resourceMetadata) { $operations = $resourceMetadata->getOperations(); if ($operations) { - /** @var Operation $operation */ foreach ($resourceMetadata->getOperations() as $operationName => $operation) { $documentClass = $this->getStateOptionsClass($operation, $operation->getClass(), Options::class); if (!$this->managerRegistry->getManagerForClass($documentClass) instanceof DocumentManager) { diff --git a/src/Doctrine/Odm/Tests/Metadata/Resource/DoctrineMongoDbOdmResourceCollectionMetadataFactoryTest.php b/src/Doctrine/Odm/Tests/Metadata/Resource/DoctrineMongoDbOdmResourceCollectionMetadataFactoryTest.php index 6b36f99053a..f29bf0684d0 100644 --- a/src/Doctrine/Odm/Tests/Metadata/Resource/DoctrineMongoDbOdmResourceCollectionMetadataFactoryTest.php +++ b/src/Doctrine/Odm/Tests/Metadata/Resource/DoctrineMongoDbOdmResourceCollectionMetadataFactoryTest.php @@ -21,7 +21,7 @@ use ApiPlatform\Metadata\Delete; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; -use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\HttpOperation; use ApiPlatform\Metadata\Operations; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; @@ -34,7 +34,7 @@ final class DoctrineMongoDbOdmResourceCollectionMetadataFactoryTest extends Test { use ProphecyTrait; - private function getResourceMetadataCollectionFactory(Operation $operation) + private function getResourceMetadataCollectionFactory(HttpOperation $operation) { if (!class_exists(DocumentManager::class)) { $this->markTestSkipped('ODM not installed'); @@ -71,7 +71,7 @@ public function testWithoutManager(): void } #[\PHPUnit\Framework\Attributes\DataProvider('operationProvider')] - public function testWithProvider(Operation $operation, ?string $expectedProvider = null, ?string $expectedProcessor = null): void + public function testWithProvider(HttpOperation $operation, ?string $expectedProvider = null, ?string $expectedProcessor = null): void { if (!class_exists(DocumentManager::class)) { $this->markTestSkipped('ODM not installed'); diff --git a/src/Doctrine/Orm/Metadata/Resource/DoctrineOrmResourceCollectionMetadataFactory.php b/src/Doctrine/Orm/Metadata/Resource/DoctrineOrmResourceCollectionMetadataFactory.php index 77b7737f6e0..77155723a89 100644 --- a/src/Doctrine/Orm/Metadata/Resource/DoctrineOrmResourceCollectionMetadataFactory.php +++ b/src/Doctrine/Orm/Metadata/Resource/DoctrineOrmResourceCollectionMetadataFactory.php @@ -16,7 +16,6 @@ use ApiPlatform\Doctrine\Orm\State\CollectionProvider; use ApiPlatform\Doctrine\Orm\State\ItemProvider; use ApiPlatform\Doctrine\Orm\State\Options; -use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\CollectionOperationInterface; use ApiPlatform\Metadata\DeleteOperationInterface; use ApiPlatform\Metadata\Operation; @@ -41,12 +40,10 @@ public function create(string $resourceClass): ResourceMetadataCollection { $resourceMetadataCollection = $this->decorated->create($resourceClass); - /** @var ApiResource $resourceMetadata */ foreach ($resourceMetadataCollection as $i => $resourceMetadata) { $operations = $resourceMetadata->getOperations(); if ($operations) { - /** @var Operation $operation */ foreach ($resourceMetadata->getOperations() as $operationName => $operation) { $entityClass = $this->getStateOptionsClass($operation, $operation->getClass(), Options::class); diff --git a/src/Doctrine/Orm/Tests/Metadata/Resource/DoctrineOrmResourceCollectionMetadataFactoryTest.php b/src/Doctrine/Orm/Tests/Metadata/Resource/DoctrineOrmResourceCollectionMetadataFactoryTest.php index 3e371af885a..966b8982cf4 100644 --- a/src/Doctrine/Orm/Tests/Metadata/Resource/DoctrineOrmResourceCollectionMetadataFactoryTest.php +++ b/src/Doctrine/Orm/Tests/Metadata/Resource/DoctrineOrmResourceCollectionMetadataFactoryTest.php @@ -21,7 +21,7 @@ use ApiPlatform\Metadata\Delete; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; -use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\HttpOperation; use ApiPlatform\Metadata\Operations; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; @@ -34,7 +34,7 @@ class DoctrineOrmResourceCollectionMetadataFactoryTest extends TestCase { use ProphecyTrait; - private function getResourceMetadataCollectionFactory(Operation $operation) + private function getResourceMetadataCollectionFactory(HttpOperation $operation) { $resourceMetadataCollectionFactory = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); $resourceMetadataCollectionFactory->create($operation->getClass())->willReturn(new ResourceMetadataCollection($operation->getClass(), [ @@ -63,7 +63,7 @@ public function testWithoutManager(): void } #[\PHPUnit\Framework\Attributes\DataProvider('operationProvider')] - public function testWithProvider(Operation $operation, ?string $expectedProvider = null, ?string $expectedProcessor = null): void + public function testWithProvider(HttpOperation $operation, ?string $expectedProvider = null, ?string $expectedProcessor = null): void { $objectManager = $this->prophesize(EntityManagerInterface::class); $managerRegistry = $this->prophesize(ManagerRegistry::class); diff --git a/src/Hydra/Serializer/EntrypointNormalizer.php b/src/Hydra/Serializer/EntrypointNormalizer.php index 199ca64ff42..92eb44a6406 100644 --- a/src/Hydra/Serializer/EntrypointNormalizer.php +++ b/src/Hydra/Serializer/EntrypointNormalizer.php @@ -61,7 +61,7 @@ public function normalize(mixed $object, ?string $format = null, array $context } try { - $entrypoint[$key] = $this->iriConverter->getIriFromResource($resourceClass, UrlGeneratorInterface::ABS_PATH, $operation); // @phpstan-ignore-line phpstan issue as type is CollectionOperationInterface & Operation + $entrypoint[$key] = $this->iriConverter->getIriFromResource($resourceClass, UrlGeneratorInterface::ABS_PATH, $operation); } catch (InvalidArgumentException|OperationNotFoundException) { // Ignore resources without GET operations } diff --git a/src/JsonApi/Serializer/EntrypointNormalizer.php b/src/JsonApi/Serializer/EntrypointNormalizer.php index 1dd6b67e1a3..7d6dd6ab95f 100644 --- a/src/JsonApi/Serializer/EntrypointNormalizer.php +++ b/src/JsonApi/Serializer/EntrypointNormalizer.php @@ -53,7 +53,7 @@ public function normalize(mixed $object, ?string $format = null, array $context } try { - $iri = $this->iriConverter->getIriFromResource($resourceClass, UrlGeneratorInterface::ABS_URL, $operation); // @phpstan-ignore-line phpstan issue as type is CollectionOperationInterface & Operation + $iri = $this->iriConverter->getIriFromResource($resourceClass, UrlGeneratorInterface::ABS_URL, $operation); $entrypoint['links'][lcfirst($resource->getShortName())] = $iri; } catch (InvalidArgumentException) { // Ignore resources without GET operations diff --git a/src/Metadata/ApiResource.php b/src/Metadata/ApiResource.php index c4c295f5830..e85a996e14c 100644 --- a/src/Metadata/ApiResource.php +++ b/src/Metadata/ApiResource.php @@ -31,20 +31,24 @@ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)] class ApiResource extends Metadata { + use CascadeToOperationsTrait; use WithResourceTrait; + /** + * @var Operations + */ protected ?Operations $operations; /** - * @param array|array|Operations|null $operations Operations is a list of HttpOperation - * @param array|array|string[]|string|null $uriVariables - * @param array $headers - * @param string|callable|null $provider - * @param string|callable|null $processor - * @param mixed|null $mercure - * @param mixed|null $messenger - * @param mixed|null $input - * @param mixed|null $output + * @param list|array|Operations|null $operations Operations is a list of HttpOperation + * @param array|array|string[]|string|null $uriVariables + * @param array $headers + * @param string|callable|null $provider + * @param string|callable|null $processor + * @param mixed|null $mercure + * @param mixed|null $messenger + * @param mixed|null $input + * @param mixed|null $output */ public function __construct( /** @@ -1012,6 +1016,7 @@ class: $class, extraProperties: $extraProperties ); + /* @var Operations $operations> */ $this->operations = null === $operations ? null : new Operations($operations); $this->provider = $provider; $this->processor = $processor; @@ -1020,11 +1025,17 @@ class: $class, } } + /** + * @return Operations + */ public function getOperations(): ?Operations { return $this->operations; } + /** + * @param Operations $operations + */ public function withOperations(Operations $operations): static { $self = clone $this; diff --git a/src/Metadata/CascadeFromResource.php b/src/Metadata/CascadeFromResource.php new file mode 100644 index 00000000000..ad132f1cd81 --- /dev/null +++ b/src/Metadata/CascadeFromResource.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Metadata; + +/** + * @internal + * + * @phpstan-require-extends Operation + */ +trait CascadeFromResource +{ + use WithResourceTrait; + + public function cascadeFromResource(ApiResource $apiResource, array $ignoredOptions = []): static + { + return $this->copyFrom($apiResource, $ignoredOptions); + } +} diff --git a/src/Metadata/CascadeToOperationsTrait.php b/src/Metadata/CascadeToOperationsTrait.php new file mode 100644 index 00000000000..3d7c1daf835 --- /dev/null +++ b/src/Metadata/CascadeToOperationsTrait.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Metadata; + +use ApiPlatform\Metadata\Exception\RuntimeException; +use ApiPlatform\Metadata\GraphQl\Operation as GraphQlOperation; + +/** + * @internal + * + * @phpstan-require-extends ApiResource + */ +trait CascadeToOperationsTrait +{ + public function cascadeToOperations(): static + { + if (!$this instanceof ApiResource) { + throw new RuntimeException('Not an API resource'); + } + + if (!($operations = $this->getOperations() ?? [])) { + return $this; + } + + return (clone $this)->withOperations( + new Operations($this->getMutatedOperations($operations, $this)), + ); + } + + public function cascadeToGraphQlOperations(): static + { + if (!$this instanceof ApiResource) { + throw new RuntimeException('Not an API resource'); + } + + if (!($operations = $this->getGraphQlOperations() ?? [])) { + return $this; + } + + return (clone $this)->withGraphQlOperations( + $this->getMutatedOperations($operations, $this), + ); + } + + /** + * @param Operations|list $operations + * + * @return array[string, HttpOperation]|list + */ + private function getMutatedOperations(iterable $operations, ApiResource $apiResource): iterable + { + $modifiedOperations = []; + foreach ($operations as $key => $operation) { + $modifiedOperations[$key] = $operation->cascadeFromResource($apiResource); + } + + return $modifiedOperations; + } +} diff --git a/src/Metadata/Operation.php b/src/Metadata/Operation.php index 59b4de4abd9..f8d91c9f135 100644 --- a/src/Metadata/Operation.php +++ b/src/Metadata/Operation.php @@ -20,6 +20,7 @@ */ abstract class Operation extends Metadata { + use CascadeFromResource; use WithResourceTrait; /** @@ -861,7 +862,7 @@ class: $class, ); } - public function withOperation($operation) + public function withOperation(self $operation): static { return $this->copyFrom($operation); } diff --git a/src/Metadata/Operations.php b/src/Metadata/Operations.php index 83b5e3ab443..215218bc8a8 100644 --- a/src/Metadata/Operations.php +++ b/src/Metadata/Operations.php @@ -15,13 +15,18 @@ /** * An Operation dictionnary. + * + * @template-covariant T of Operation */ final class Operations implements \IteratorAggregate, \Countable { + /** + * @var list + */ private array $operations = []; /** - * @param array $operations + * @param list|array $operations */ public function __construct(array $operations = []) { @@ -42,6 +47,9 @@ public function __construct(array $operations = []) $this->sort(); } + /** + * @return \Iterator + */ public function getIterator(): \Traversable { return (function (): \Generator { @@ -97,7 +105,7 @@ public function count(): int public function sort(): self { - usort($this->operations, fn ($a, $b): int|float => $a[1]->getPriority() - $b[1]->getPriority()); + usort($this->operations, fn ($a, $b): int => $a[1]->getPriority() - $b[1]->getPriority()); return $this; } diff --git a/src/Metadata/Resource/Factory/FormatsResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/FormatsResourceMetadataCollectionFactory.php index b090fddfc12..df787da2b4e 100644 --- a/src/Metadata/Resource/Factory/FormatsResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/FormatsResourceMetadataCollectionFactory.php @@ -17,6 +17,7 @@ use ApiPlatform\Metadata\ErrorResource; use ApiPlatform\Metadata\Exception\InvalidArgumentException; use ApiPlatform\Metadata\Exception\ResourceClassNotFoundException; +use ApiPlatform\Metadata\HttpOperation; use ApiPlatform\Metadata\Operations; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; @@ -67,6 +68,9 @@ public function create(string $resourceClass): ResourceMetadataCollection return $resourceMetadataCollection; } + /** + * @param Operations $operations + */ private function normalize(array $resourceInputFormats, array $resourceOutputFormats, Operations $operations): Operations { $newOperations = []; diff --git a/src/Metadata/Resource/Factory/NotExposedOperationResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/NotExposedOperationResourceMetadataCollectionFactory.php index 0c35d28486c..f5fc104fc7e 100644 --- a/src/Metadata/Resource/Factory/NotExposedOperationResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/NotExposedOperationResourceMetadataCollectionFactory.php @@ -70,8 +70,7 @@ public function create(string $resourceClass): ResourceMetadataCollection // No item operation has been found on all resources for resource class: generate one on the last resource // Helpful to generate an IRI for a resource without declaring the Get operation - /** @var HttpOperation $operation */ - [$key, $operation] = $this->getOperationWithDefaults(resource: $resource, operation: new NotExposed(), generated: true, ignoredOptions: ['uriTemplate', 'uriVariables']); // @phpstan-ignore-line $resource is defined if count > 0 + [$key, $operation] = $this->getOperationWithDefaults($resource, new NotExposed(), true, ['uriTemplate', 'uriVariables']); // @phpstan-ignore-line $resource is defined if count > 0 if (!$this->linkFactory->createLinksFromIdentifiers($operation)) { $operation = $operation->withUriTemplate(self::$skolemUriTemplate); diff --git a/src/Metadata/Resource/Factory/OperationDefaultsTrait.php b/src/Metadata/Resource/Factory/OperationDefaultsTrait.php index 601fd19974b..fe705960d44 100644 --- a/src/Metadata/Resource/Factory/OperationDefaultsTrait.php +++ b/src/Metadata/Resource/Factory/OperationDefaultsTrait.php @@ -36,6 +36,9 @@ use ApiPlatform\Validator\Exception\ValidationException; use Psr\Log\LoggerInterface; +/** + * @internal since api-platform 4.2 + */ trait OperationDefaultsTrait { private CamelCaseToSnakeCaseNameConverter $camelCaseToSnakeCaseNameConverter; @@ -96,7 +99,10 @@ private function getResourceWithDefaults(string $resourceClass, string $shortNam return $this->addGlobalDefaults($resource); } - private function getDefaultHttpOperations($resource): iterable + /** + * @return Operations|array + */ + private function getDefaultHttpOperations(ApiResource $resource): iterable { if (enum_exists($resource->getClass())) { return new Operations([new GetCollection(paginationEnabled: false), new Get()]); @@ -175,30 +181,14 @@ private function completeGraphQlOperations(ApiResource $resource): ApiResource return $resource->withGraphQlOperations($graphQlOperations); } + /** + * @param list $ignoredOptions + * + * @return array + */ private function getOperationWithDefaults(ApiResource $resource, Operation $operation, bool $generated = false, array $ignoredOptions = []): array { - // Inherit from resource defaults - foreach (get_class_methods($resource) as $methodName) { - if (!str_starts_with($methodName, 'get')) { - continue; - } - - if (\in_array(lcfirst(substr($methodName, 3)), $ignoredOptions, true)) { - continue; - } - - if (!method_exists($operation, $methodName) || null !== $operation->{$methodName}()) { - continue; - } - - if (null === ($value = $resource->{$methodName}())) { - continue; - } - - $operation = $operation->{'with'.substr($methodName, 3)}($value); - } - - $operation = $operation->withExtraProperties(array_merge( + $operation = $operation->cascadeFromResource($resource, $ignoredOptions)->withExtraProperties(array_merge( $resource->getExtraProperties(), $operation->getExtraProperties(), $generated ? ['generated_operation' => true] : [] @@ -249,6 +239,7 @@ private function getDefaultOperationName(HttpOperation $operation, string $resou '_api_%s_%s%s', $path ?: ($operation->getShortName() ?? $this->getDefaultShortname($resourceClass)), strtolower($operation->getMethod()), - $operation instanceof CollectionOperationInterface ? '_collection' : ''); + $operation instanceof CollectionOperationInterface ? '_collection' : '' + ); } } diff --git a/src/Metadata/Resource/ResourceMetadataCollection.php b/src/Metadata/Resource/ResourceMetadataCollection.php index e6c0523eff5..f55f83dc1c7 100644 --- a/src/Metadata/Resource/ResourceMetadataCollection.php +++ b/src/Metadata/Resource/ResourceMetadataCollection.php @@ -59,7 +59,7 @@ public function getOperation(?string $operationName = null, bool $forceCollectio if (!$forceGraphQl) { foreach ($metadata->getOperations() ?? [] as $name => $operation) { $isCollection = $operation instanceof CollectionOperationInterface; - $method = $operation->getMethod() ?? 'GET'; + $method = $operation->getMethod(); $isGetOperation = 'GET' === $method || 'OPTIONS' === $method || 'HEAD' === $method; if ('' === $operationName && $isGetOperation && ($forceCollection ? $isCollection : !$isCollection)) { return $this->operationCache[$httpCacheKey] = $operation; diff --git a/src/Metadata/Tests/Resource/Factory/AttributesResourceMetadataCollectionFactoryTest.php b/src/Metadata/Tests/Resource/Factory/AttributesResourceMetadataCollectionFactoryTest.php index 2b38b63067c..b732c16b25d 100644 --- a/src/Metadata/Tests/Resource/Factory/AttributesResourceMetadataCollectionFactoryTest.php +++ b/src/Metadata/Tests/Resource/Factory/AttributesResourceMetadataCollectionFactoryTest.php @@ -71,13 +71,25 @@ class: AttributeResource::class, provider: AttributeResourceProvider::class, operations: [ '_api_AttributeResource_get' => new Get( - shortName: 'AttributeResource', class: AttributeResource::class, normalizationContext: ['skip_null_values' => true], priority: 1, provider: AttributeResourceProvider::class, + shortName: 'AttributeResource', + class: AttributeResource::class, + normalizationContext: ['skip_null_values' => true], + priority: 1, + provider: AttributeResourceProvider::class, ), '_api_AttributeResource_put' => new Put( - shortName: 'AttributeResource', class: AttributeResource::class, normalizationContext: ['skip_null_values' => true], priority: 2, provider: AttributeResourceProvider::class, + shortName: 'AttributeResource', + class: AttributeResource::class, + normalizationContext: ['skip_null_values' => true], + priority: 2, + provider: AttributeResourceProvider::class, ), '_api_AttributeResource_delete' => new Delete( - shortName: 'AttributeResource', class: AttributeResource::class, normalizationContext: ['skip_null_values' => true], priority: 3, provider: AttributeResourceProvider::class, + shortName: 'AttributeResource', + class: AttributeResource::class, + normalizationContext: ['skip_null_values' => true], + priority: 3, + provider: AttributeResourceProvider::class, ), ], graphQlOperations: $this->getDefaultGraphqlOperations('AttributeResource', AttributeResource::class, AttributeResourceProvider::class) diff --git a/src/Metadata/WithResourceTrait.php b/src/Metadata/WithResourceTrait.php index 60d4939f8c3..3acceb92e28 100644 --- a/src/Metadata/WithResourceTrait.php +++ b/src/Metadata/WithResourceTrait.php @@ -13,15 +13,19 @@ namespace ApiPlatform\Metadata; +/** + * @internal since api-platform/metadata 4.2 + */ trait WithResourceTrait { - protected function copyFrom(Metadata $resource): static + protected function copyFrom(Metadata $resource, array $ignoredOptions = []): static { $self = clone $this; foreach (get_class_methods($resource) as $method) { if ( method_exists($self, $method) && preg_match('/^(?:get|is|can)(.*)/', (string) $method, $matches) + && (!$ignoredOptions || !\in_array(lcfirst($matches[1]), $ignoredOptions, true)) && null === $self->{$method}() && null !== $val = $resource->{$method}() ) { diff --git a/src/OpenApi/Factory/OpenApiFactory.php b/src/OpenApi/Factory/OpenApiFactory.php index ccd6c530b67..e8b3cdeef4b 100644 --- a/src/OpenApi/Factory/OpenApiFactory.php +++ b/src/OpenApi/Factory/OpenApiFactory.php @@ -214,7 +214,7 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection } $path = $this->getPath($path); - $method = $operation->getMethod() ?? 'GET'; + $method = $operation->getMethod(); if (!\in_array($method, PathItem::$methods, true)) { continue; @@ -618,7 +618,7 @@ private function getLinks(ResourceMetadataCollection $resourceMetadataCollection foreach ($resourceMetadataCollection as $resource) { foreach ($resource->getOperations() as $operationName => $operation) { $parameters = []; - $method = $operation instanceof HttpOperation ? $operation->getMethod() : 'GET'; + $method = $operation->getMethod(); if ( $operationName === $operation->getName() || isset($links[$operationName]) diff --git a/src/Symfony/Routing/ApiLoader.php b/src/Symfony/Routing/ApiLoader.php index 3c3440c5ae6..01d0e3b1529 100644 --- a/src/Symfony/Routing/ApiLoader.php +++ b/src/Symfony/Routing/ApiLoader.php @@ -103,7 +103,7 @@ public function load(mixed $data, ?string $type = null): RouteCollection $operation->getOptions() ?? [], $operation->getHost() ?? '', $operation->getSchemes() ?? [], - [$operation->getMethod() ?? 'GET'], + [$operation->getMethod()], $operation->getCondition() ?? '' ); diff --git a/tests/Fixtures/TestBundle/Entity/AttributeResource.php b/tests/Fixtures/TestBundle/Entity/AttributeResource.php index 5e5d2620954..db2e695ee59 100644 --- a/tests/Fixtures/TestBundle/Entity/AttributeResource.php +++ b/tests/Fixtures/TestBundle/Entity/AttributeResource.php @@ -31,7 +31,7 @@ #[Put] #[Delete] #[ApiResource( - '/dummy/{dummyId}/attribute_resources/{identifier}{._format}', + uriTemplate: '/dummy/{dummyId}/attribute_resources/{identifier}{._format}', inputFormats: ['json' => ['application/merge-patch+json']], status: 301, provider: AttributeResourceProvider::class,