diff --git a/composer.json b/composer.json index 1dcab68224f..99311392c34 100644 --- a/composer.json +++ b/composer.json @@ -23,6 +23,7 @@ "doctrine/inflector": "^1.0 || ^2.0", "psr/cache": "^1.0 || ^2.0 || ^3.0", "psr/container": "^1.0 || ^2.0", + "symfony/deprecation-contracts": "^3.1", "symfony/http-foundation": "^6.1", "symfony/http-kernel": "^6.1", "symfony/property-access": "^6.1", diff --git a/src/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactory.php index 7b4a5ca7c70..3f8cb93688c 100644 --- a/src/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactory.php @@ -27,6 +27,8 @@ */ final class UriTemplateResourceMetadataCollectionFactory implements ResourceMetadataCollectionFactoryInterface { + private $triggerLegacyFormatOnce = []; + public function __construct(private readonly LinkFactoryInterface $linkFactory, private readonly PathSegmentNameGeneratorInterface $pathSegmentNameGenerator, private readonly ?ResourceMetadataCollectionFactoryInterface $decorated = null) { } @@ -95,6 +97,16 @@ private function generateUriTemplate(HttpOperation $operation): string { $uriTemplate = $operation->getUriTemplate() ?? sprintf('/%s', $this->pathSegmentNameGenerator->getSegmentName($operation->getShortName())); $uriVariables = $operation->getUriVariables() ?? []; + $legacyFormat = null; + + if (str_ends_with($uriTemplate, '{._format}') || ($legacyFormat = str_ends_with($uriTemplate, '.{_format}'))) { + $uriTemplate = substr($uriTemplate, 0, -10); + } + + if ($legacyFormat && ($this->triggerLegacyFormatOnce[$operation->getClass()] ?? true)) { + $this->triggerLegacyFormatOnce[$operation->getClass()] = false; + trigger_deprecation('api-platform/core', '3.0', sprintf('The special Symfony parameter ".{_format}" in your URI Template is deprecated, use an RFC6570 variable "{._format}" on the class "%s" instead. We will only use the RFC6570 compatible variable in 4.0.', $operation->getClass())); + } if ($parameters = array_keys($uriVariables)) { foreach ($parameters as $parameterName) { @@ -105,7 +117,7 @@ private function generateUriTemplate(HttpOperation $operation): string } } - return sprintf('%s{._format}', $uriTemplate); + return sprintf('%s%s', $uriTemplate, $legacyFormat ? '.{_format}' : '{._format}'); } private function configureUriVariables(ApiResource|HttpOperation $operation): ApiResource|HttpOperation @@ -144,7 +156,7 @@ private function configureUriVariables(ApiResource|HttpOperation $operation): Ap } $operation = $operation->withUriVariables($uriVariables); - if (str_ends_with($uriTemplate, '{._format}')) { + if (str_ends_with($uriTemplate, '{._format}') || str_ends_with($uriTemplate, '.{_format}')) { $uriTemplate = substr($uriTemplate, 0, -10); } diff --git a/tests/Fixtures/TestBundle/ApiResourceNotLoaded/SymfonyFormatParameterLegacy.php b/tests/Fixtures/TestBundle/ApiResourceNotLoaded/SymfonyFormatParameterLegacy.php new file mode 100644 index 00000000000..29c69250aa3 --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResourceNotLoaded/SymfonyFormatParameterLegacy.php @@ -0,0 +1,21 @@ + + * + * 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\ApiResourceNotLoaded; + +use ApiPlatform\Metadata\ApiResource; + +#[ApiResource('/format_not_rfc.{_format}')] +class SymfonyFormatParameterLegacy +{ +} diff --git a/tests/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactoryTest.php b/tests/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactoryTest.php index 7603509b3a9..22948b75d22 100644 --- a/tests/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactoryTest.php +++ b/tests/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactoryTest.php @@ -23,22 +23,26 @@ use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Metadata\Property\PropertyNameCollection; use ApiPlatform\Metadata\Put; +use ApiPlatform\Metadata\Resource\Factory\AttributesResourceMetadataCollectionFactory; use ApiPlatform\Metadata\Resource\Factory\LinkFactory; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\Factory\UriTemplateResourceMetadataCollectionFactory; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; use ApiPlatform\Operation\PathSegmentNameGeneratorInterface; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResourceNotLoaded\SymfonyFormatParameterLegacy; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\AttributeResource; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; /** * @author Antoine Bluchet */ class UriTemplateResourceMetadataCollectionFactoryTest extends TestCase { + use ExpectDeprecationTrait; use ProphecyTrait; public function testCreate(): void @@ -169,4 +173,25 @@ class: AttributeResource::class, $uriTemplateResourceMetadataCollectionFactory->create(AttributeResource::class) ); } + + /** + * @group legacy + */ + public function testCreateWithLegacyFormat(): void + { + $this->expectDeprecation('Since api-platform/core 3.0: The special Symfony parameter ".{_format}" in your URI Template is deprecated, use an RFC6570 variable "{._format}" on the class "ApiPlatform\Tests\Fixtures\TestBundle\ApiResourceNotLoaded\SymfonyFormatParameterLegacy" instead. We will only use the RFC6570 compatible variable in 4.0.'); + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(Argument::cetera())->willReturn(new PropertyNameCollection()); + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $linkFactory = new LinkFactory($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $resourceClassResolverProphecy->reveal()); + $pathSegmentNameGeneratorProphecy = $this->prophesize(PathSegmentNameGeneratorInterface::class); + $pathSegmentNameGeneratorProphecy->getSegmentName('SymfonyFormatParameterLegacy')->willReturn('attribute_resources'); + $resourceCollectionMetadataFactoryProphecy = new AttributesResourceMetadataCollectionFactory(); + + $linkFactory = new LinkFactory($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $resourceClassResolverProphecy->reveal()); + $uriTemplateResourceMetadataCollectionFactory = new UriTemplateResourceMetadataCollectionFactory($linkFactory, $pathSegmentNameGeneratorProphecy->reveal(), $resourceCollectionMetadataFactoryProphecy); + $uriTemplateResourceMetadataCollectionFactory->create(SymfonyFormatParameterLegacy::class); + } }