diff --git a/CHANGELOG.md b/CHANGELOG.md index f79dfc3efc0..e15cb0905a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -89,6 +89,17 @@ Breaking changes: * Serializer: `skip_null_values` now defaults to `true` * Metadata: `Patch` is added to the automatic CRUD +## v2.7.5 + +### Bug fixes + +* [096ac119a](https://github.com/api-platform/core/commit/096ac119a5126bdc5e7877172a033d7cdaa28983) fix(metadata): keep configured uri variables (#5217) +* [2b2d468f0](https://github.com/api-platform/core/commit/2b2d468f06a63ecfa4928d5d631953acb624c181) fix(metadata): operations must inherit from resource and defaults +* [2cb3b4272](https://github.com/api-platform/core/commit/2cb3b42725105aaf34dc9d71d2c03e156acd5833) fix(serializer): use iri from $context if defined (#5201) +* [39398579e](https://github.com/api-platform/core/commit/39398579e32976b5b4b0219da98fdb35629a35ad) fix(symfony): definition when mercure is not installed (#5206) +* [e9c7e4abb](https://github.com/api-platform/core/commit/e9c7e4abb683bb830a61712a8b63b8063e015b13) fix(serializer): avoid call to legacy iri converter with non-resource class (#5219) +* [ebaad51b2](https://github.com/api-platform/core/commit/ebaad51b2ce173b6c59582dcc6fb311f1f4b7fa9) fix(serializer): read groups off the root operation (#5196) + ## v2.7.4 ### Bug fixes diff --git a/features/main/attribute_resource.feature b/features/main/attribute_resource.feature index eae47b2689b..4cc02cf053d 100644 --- a/features/main/attribute_resource.feature +++ b/features/main/attribute_resource.feature @@ -1,11 +1,12 @@ +@php8 +@v3 +@!mysql +@!mongodb Feature: Resource attributes In order to use the Resource attribute As a developer I should be able to fetch data from a state provider - @php8 - @!mysql - @!mongodb Scenario: Retrieve a Resource collection When I add "Content-Type" header equal to "application/ld+json" And I send a "GET" request to "/attribute_resources" @@ -35,9 +36,6 @@ Feature: Resource attributes } """ - @php8 - @!mysql - @!mongodb Scenario: Retrieve the first resource When I add "Content-Type" header equal to "application/ld+json" And I send a "GET" request to "/attribute_resources/1" @@ -55,9 +53,6 @@ Feature: Resource attributes } """ - @php8 - @!mysql - @!mongodb Scenario: Retrieve the aliased resource When I add "Content-Type" header equal to "application/ld+json" And I send a "GET" request to "/dummy/1/attribute_resources/2" @@ -77,9 +72,6 @@ Feature: Resource attributes } """ - @php8 - @!mysql - @!mongodb Scenario: Patch the aliased resource When I add "Content-Type" header equal to "application/merge-patch+json" And I send a "PATCH" request to "/dummy/1/attribute_resources/2" with body: @@ -101,3 +93,10 @@ Feature: Resource attributes "name": "Patched" } """ + + Scenario: Uri variables should be configured properly + When I send a "GET" request to "/photos/1/resize/300/100" + Then the response status code should be 400 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON node "hydra:description" should be equal to 'Unable to generate an IRI for the item of type "ApiPlatform\Tests\Fixtures\TestBundle\Entity\IncompleteUriVariableConfigured"' diff --git a/src/Doctrine/Odm/State/LinksHandlerTrait.php b/src/Doctrine/Odm/State/LinksHandlerTrait.php index dcaac93c0cc..287ba13dac8 100644 --- a/src/Doctrine/Odm/State/LinksHandlerTrait.php +++ b/src/Doctrine/Odm/State/LinksHandlerTrait.php @@ -37,6 +37,12 @@ private function handleLinks(Builder $aggregationBuilder, array $identifiers, ar return; } + foreach ($links as $i => $link) { + if (null !== $link->getExpandedValue()) { + unset($links[$i]); + } + } + $executeOptions = $operation->getExtraProperties()['doctrine_mongodb']['execute_options'] ?? []; $this->buildAggregation($resourceClass, array_reverse($links), array_reverse($identifiers), $context, $executeOptions, $resourceClass, $aggregationBuilder); diff --git a/src/Doctrine/Orm/State/LinksHandlerTrait.php b/src/Doctrine/Orm/State/LinksHandlerTrait.php index db9c87991e1..c27c2f122d9 100644 --- a/src/Doctrine/Orm/State/LinksHandlerTrait.php +++ b/src/Doctrine/Orm/State/LinksHandlerTrait.php @@ -45,7 +45,7 @@ private function handleLinks(QueryBuilder $queryBuilder, array $identifiers, Que $identifiers = array_reverse($identifiers); foreach (array_reverse($links) as $link) { - if ($link->getExpandedValue() || !$link->getFromClass()) { + if (null !== $link->getExpandedValue() || !$link->getFromClass()) { continue; } diff --git a/src/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactory.php index 3f8cb93688c..0c62bf81693 100644 --- a/src/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactory.php @@ -137,7 +137,9 @@ private function configureUriVariables(ApiResource|HttpOperation $operation): Ap return $this->normalizeUriVariables($operation); } + $hasUserConfiguredUriVariables = !($operation->getExtraProperties['is_legacy_resource_metadata'] ?? false); if (!$operation->getUriVariables()) { + $hasUserConfiguredUriVariables = false; $operation = $operation->withUriVariables($this->transformLinksToUriVariables($this->linkFactory->createLinksFromIdentifiers($operation))); } @@ -164,6 +166,10 @@ private function configureUriVariables(ApiResource|HttpOperation $operation): Ap $variables = $route->getPathVariables(); if (\count($variables) !== \count($uriVariables)) { + if ($hasUserConfiguredUriVariables) { + return $operation; + } + $newUriVariables = []; foreach ($variables as $variable) { if (isset($uriVariables[$variable])) { diff --git a/src/OpenApi/Factory/OpenApiFactory.php b/src/OpenApi/Factory/OpenApiFactory.php index 75f1d2ee8bb..70bda3d3131 100644 --- a/src/OpenApi/Factory/OpenApiFactory.php +++ b/src/OpenApi/Factory/OpenApiFactory.php @@ -189,6 +189,10 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection // Set up parameters foreach ($uriVariables ?? [] as $parameterName => $uriVariable) { + if ($uriVariable->getExpandedValue() ?? false) { + continue; + } + $parameter = new Parameter($parameterName, 'path', (new \ReflectionClass($uriVariable->getFromClass()))->getShortName().' identifier', true, false, false, ['type' => 'string']); if ($this->hasParameter($parameter, $parameters)) { continue; diff --git a/tests/Fixtures/TestBundle/Entity/Company.php b/tests/Fixtures/TestBundle/Entity/Company.php index 208f6681952..26abb965a28 100644 --- a/tests/Fixtures/TestBundle/Entity/Company.php +++ b/tests/Fixtures/TestBundle/Entity/Company.php @@ -25,13 +25,7 @@ #[GetCollection] #[Get] #[Post] -#[Get( - uriTemplate: '/employees/{employeeId}/rooms/{roomId}/company/{companyId}', - uriVariables: [ - 'employeeId' => ['from_class' => Employee::class, 'from_property' => 'company'], - ], -)] -#[Get( +#[ApiResource( uriTemplate: '/employees/{employeeId}/company', uriVariables: [ 'employeeId' => ['from_class' => Employee::class, 'from_property' => 'company'], diff --git a/tests/Fixtures/TestBundle/Entity/IncompleteUriVariableConfigured.php b/tests/Fixtures/TestBundle/Entity/IncompleteUriVariableConfigured.php new file mode 100644 index 00000000000..346f1df20f3 --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/IncompleteUriVariableConfigured.php @@ -0,0 +1,34 @@ + + * + * 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\Tests\Fixtures\TestBundle\Entity; + +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\Operation; + +#[Get(uriTemplate: '/photos/{id}/resize/{width}/{height}', uriVariables: ['id'], provider: [IncompleteUriVariableConfigured::class, 'provide'], openapi: false)] +final class IncompleteUriVariableConfigured +{ + public function __construct(public string $id) + { + } + + public static function provide(Operation $operation, array $uriVariables = [], array $context = []): self + { + if (isset($uriVariables['width'])) { + throw new \LogicException('URI variable "width" should not exist'); + } + + return new self($uriVariables['id']); + } +} diff --git a/tests/Serializer/ItemNormalizerTest.php b/tests/Serializer/ItemNormalizerTest.php index 3a36e10bbdc..102f99f036f 100644 --- a/tests/Serializer/ItemNormalizerTest.php +++ b/tests/Serializer/ItemNormalizerTest.php @@ -201,6 +201,45 @@ public function testDenormalizeWithIdAndUpdateNotAllowed(): void $normalizer->denormalize(['id' => '12', 'name' => 'hello'], Dummy::class, null, $context); } + public function testDenormalizeWithDefinedIri(): void + { + $dummy = new Dummy(); + $dummy->setName('hello'); + + $propertyNameCollection = new PropertyNameCollection(['name']); + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->willReturn($propertyNameCollection); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true); + + $propertyMetadata = (new ApiProperty())->withReadable(true); + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', [])->willReturn($propertyMetadata); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + $iriConverterProphecy->getIriFromResource($dummy)->shouldNotBeCalled(); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass($dummy, null)->willReturn(Dummy::class); + $resourceClassResolverProphecy->getResourceClass(null, Dummy::class)->willReturn(Dummy::class); + $resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true); + + $serializerProphecy = $this->prophesize(SerializerInterface::class); + $serializerProphecy->willImplement(NormalizerInterface::class); + $serializerProphecy->normalize('hello', null, Argument::type('array'))->willReturn('hello'); + + $normalizer = new ItemNormalizer( + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal() + ); + $normalizer->setSerializer($serializerProphecy->reveal()); + + $this->assertEquals(['name' => 'hello'], $normalizer->normalize($dummy, null, ['resources' => [], 'iri' => '/custom'])); + } + public function testDenormalizeWithIdAndNoResourceClass(): void { $context = [];