From 4e65c648d69ea1524cbb9c2c971594ea2f0c506d Mon Sep 17 00:00:00 2001 From: soyuka Date: Tue, 30 Jun 2026 16:36:02 +0200 Subject: [PATCH 01/11] feat!: remove deprecated APIs scheduled for 5.0 Drop deprecated code paths kept as BC layers until 5.0 (TODO bucket A). LegacyType PropertyInfo removals are handled separately; the getDescription() and OpenApi swagger field removals stay deferred to 6.0. - Remove SerializerAwareProviderInterface / SerializerAwareProviderTrait and the DataProviderPass compiler pass (deprecated 4.2). - Remove ObjectMapperProcessor (deprecated 4.3); use ObjectMapperInputProcessor and ObjectMapperOutputProcessor. - DefinitionNameFactory: drop the $distinctFormats constructor argument; drop it from JsonSchema\SchemaFactory and its wiring. - ValidationException: the first constructor argument is now a ConstraintViolationListInterface (string messages no longer accepted). - DeserializeProvider: ASSIGN_OBJECT_TO_POPULATE must be set explicitly (it is set upstream by MainController / DeserializeListener); drop the implicit per-method fallback. - ApiTestCase::$alwaysBootKernel now defaults to false. - Configuration: remove deprecated nodes query_parameter_validation, enable_link_security, resource_class_directories, graphql_playground, varnish_urls and xkey. - Resource short name deduplication is now automatic; drop the opt-in extra_properties.deduplicate_resource_short_names flag. --- ...ReservedAttributeNameSchemaFactoryTest.php | 2 +- .../Tests/JsonSchema/SchemaFactoryTest.php | 6 +- src/JsonSchema/DefinitionNameFactory.php | 12 +-- src/JsonSchema/SchemaFactory.php | 4 +- src/Laravel/ApiPlatformProvider.php | 4 - .../MetadataCollectionFactoryTrait.php | 9 -- ...sResourceMetadataCollectionFactoryTest.php | 26 ++---- .../Tests/Factory/OpenApiFactoryTest.php | 6 +- .../Serializer/OpenApiNormalizerTest.php | 2 +- src/State/Processor/ObjectMapperProcessor.php | 87 ------------------- src/State/Provider/DeserializeProvider.php | 12 --- .../SerializerAwareProviderInterface.php | 28 ------ src/State/SerializerAwareProviderTrait.php | 47 ---------- src/Symfony/Bundle/ApiPlatformBundle.php | 3 - .../ApiPlatformExtension.php | 13 +-- .../Compiler/DataProviderPass.php | 47 ---------- .../DependencyInjection/Configuration.php | 33 ------- .../Bundle/Resources/config/json_schema.php | 1 - src/Symfony/Bundle/Test/ApiTestCase.php | 17 +--- .../Exception/ValidationException.php | 17 +--- ...iderResourceMetadatatCollectionFactory.php | 6 -- .../TestBundle/State/SerializableProvider.php | 43 --------- .../SerializableItemDataProviderTest.php | 48 ---------- tests/Functional/MappingTest.php | 2 +- .../Symfony/Bundle/ApiPlatformBundleTest.php | 3 - .../DependencyInjection/ConfigurationTest.php | 9 -- 26 files changed, 26 insertions(+), 461 deletions(-) delete mode 100644 src/State/Processor/ObjectMapperProcessor.php delete mode 100644 src/State/SerializerAwareProviderInterface.php delete mode 100644 src/State/SerializerAwareProviderTrait.php delete mode 100644 src/Symfony/Bundle/DependencyInjection/Compiler/DataProviderPass.php delete mode 100644 tests/Fixtures/TestBundle/State/SerializableProvider.php delete mode 100644 tests/Functional/JsonLd/SerializableItemDataProviderTest.php diff --git a/src/JsonApi/Tests/JsonSchema/ReservedAttributeNameSchemaFactoryTest.php b/src/JsonApi/Tests/JsonSchema/ReservedAttributeNameSchemaFactoryTest.php index 0b0d46b6452..a1865ab1942 100644 --- a/src/JsonApi/Tests/JsonSchema/ReservedAttributeNameSchemaFactoryTest.php +++ b/src/JsonApi/Tests/JsonSchema/ReservedAttributeNameSchemaFactoryTest.php @@ -62,7 +62,7 @@ protected function setUp(): void ); } - $definitionNameFactory = new DefinitionNameFactory(null); + $definitionNameFactory = new DefinitionNameFactory(); $baseSchemaFactory = new BaseSchemaFactory( resourceMetadataFactory: $resourceMetadataFactory->reveal(), diff --git a/src/JsonApi/Tests/JsonSchema/SchemaFactoryTest.php b/src/JsonApi/Tests/JsonSchema/SchemaFactoryTest.php index 648cfd87701..2c11df844d5 100644 --- a/src/JsonApi/Tests/JsonSchema/SchemaFactoryTest.php +++ b/src/JsonApi/Tests/JsonSchema/SchemaFactoryTest.php @@ -59,7 +59,7 @@ protected function setUp(): void $propertyNameCollectionFactory->create(Dummy::class, ['enable_getter_setter_extraction' => true, 'schema_type' => Schema::TYPE_INPUT])->willReturn(new PropertyNameCollection()); $propertyMetadataFactory = $this->prophesize(PropertyMetadataFactoryInterface::class); - $definitionNameFactory = new DefinitionNameFactory(null); + $definitionNameFactory = new DefinitionNameFactory(); $baseSchemaFactory = new BaseSchemaFactory( resourceMetadataFactory: $resourceMetadataFactory->reveal(), @@ -316,7 +316,7 @@ private function buildSchemaFactoryWithPolymorphicRelation(): SchemaFactory $resourceClassResolver->isResourceClass(RelatedDummy::class)->willReturn(true); $resourceClassResolver->isResourceClass(OtherRelatedDummy::class)->willReturn(true); - $definitionNameFactory = new DefinitionNameFactory(null); + $definitionNameFactory = new DefinitionNameFactory(); $baseSchemaFactory = new BaseSchemaFactory( resourceMetadataFactory: $resourceMetadataFactory->reveal(), @@ -377,7 +377,7 @@ private function buildSchemaFactoryWithRelation(): SchemaFactory $resourceClassResolver->isResourceClass(Dummy::class)->willReturn(true); $resourceClassResolver->isResourceClass(RelatedDummy::class)->willReturn(true); - $definitionNameFactory = new DefinitionNameFactory(null); + $definitionNameFactory = new DefinitionNameFactory(); $baseSchemaFactory = new BaseSchemaFactory( resourceMetadataFactory: $resourceMetadataFactory->reveal(), diff --git a/src/JsonSchema/DefinitionNameFactory.php b/src/JsonSchema/DefinitionNameFactory.php index 2396f9424d5..4c993ebdec8 100644 --- a/src/JsonSchema/DefinitionNameFactory.php +++ b/src/JsonSchema/DefinitionNameFactory.php @@ -26,13 +26,6 @@ final class DefinitionNameFactory implements DefinitionNameFactoryInterface private array $prefixCache = []; - public function __construct(private ?array $distinctFormats = null) - { - if ($distinctFormats) { - trigger_deprecation('api-platform/json-schema', '4.2', 'The distinctFormats argument is deprecated and will be removed in 5.0.'); - } - } - public function create(string $className, string $format = 'json', ?string $inputOrOutputClass = null, ?Operation $operation = null, array $serializerContext = []): string { if ($operation) { @@ -50,10 +43,7 @@ public function create(string $className, string $format = 'json', ?string $inpu $prefix .= self::GLUE.$this->createPrefixFromClass($inputOrOutputClass); } - // TODO: remove in 5.0 - $v = $this->distinctFormats ? ($this->distinctFormats[$format] ?? false) : true; - - if (!\in_array($format, ['json', 'merge-patch+json'], true) && $v) { + if (!\in_array($format, ['json', 'merge-patch+json'], true)) { // JSON is the default, and so isn't included in the definition name // JSON merge patch is postfixed at the end $prefix .= self::GLUE.$format; diff --git a/src/JsonSchema/SchemaFactory.php b/src/JsonSchema/SchemaFactory.php index 1f5d5d67f11..a643f6d7fa4 100644 --- a/src/JsonSchema/SchemaFactory.php +++ b/src/JsonSchema/SchemaFactory.php @@ -47,10 +47,10 @@ final class SchemaFactory implements SchemaFactoryInterface, SchemaFactoryAwareI // Edge case where the related resource is not readable (for example: NotExposed) but we have groups to read the whole related object public const OPENAPI_DEFINITION_NAME = 'openapi_definition_name'; - public function __construct(ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ?NameConverterInterface $nameConverter = null, ?ResourceClassResolverInterface $resourceClassResolver = null, ?array $distinctFormats = null, private ?DefinitionNameFactoryInterface $definitionNameFactory = null) + public function __construct(ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ?NameConverterInterface $nameConverter = null, ?ResourceClassResolverInterface $resourceClassResolver = null, private ?DefinitionNameFactoryInterface $definitionNameFactory = null) { if (!$definitionNameFactory) { - $this->definitionNameFactory = new DefinitionNameFactory($distinctFormats); + $this->definitionNameFactory = new DefinitionNameFactory(); } $this->resourceMetadataFactory = $resourceMetadataFactory; diff --git a/src/Laravel/ApiPlatformProvider.php b/src/Laravel/ApiPlatformProvider.php index 9a87f5a6fd6..99d1384442b 100644 --- a/src/Laravel/ApiPlatformProvider.php +++ b/src/Laravel/ApiPlatformProvider.php @@ -935,16 +935,12 @@ public function register(): void }); $this->app->singleton(SchemaFactory::class, static function (Application $app) { - /** @var ConfigRepository */ - $config = $app['config']; - return new SchemaFactory( $app->make(ResourceMetadataCollectionFactoryInterface::class), $app->make(PropertyNameCollectionFactoryInterface::class), $app->make(PropertyMetadataFactoryInterface::class), $app->make(NameConverterInterface::class), $app->make(ResourceClassResolverInterface::class), - $config->get('api-platform.formats'), $app->make(DefinitionNameFactoryInterface::class), ); }); diff --git a/src/Metadata/Resource/Factory/MetadataCollectionFactoryTrait.php b/src/Metadata/Resource/Factory/MetadataCollectionFactoryTrait.php index cd74e7776df..ae0fd13f366 100644 --- a/src/Metadata/Resource/Factory/MetadataCollectionFactoryTrait.php +++ b/src/Metadata/Resource/Factory/MetadataCollectionFactoryTrait.php @@ -239,7 +239,6 @@ private function hasSameOperation(ApiResource $resource, string $operationClass, */ private function deduplicateShortNames(array $resources): array { - $enabled = $this->defaults['extra_properties']['deduplicate_resource_short_names'] ?? false; $shortNameCounts = []; foreach ($resources as $index => $resource) { @@ -249,14 +248,6 @@ private function deduplicateShortNames(array $resources): array continue; } - if (!$enabled) { - if (1 === $shortNameCounts[$shortName]) { - trigger_deprecation('api-platform/core', '4.2', 'Having multiple "#[ApiResource]" attributes with the same "shortName" "%s" on class "%s" is deprecated and will result in automatic short name deduplication in API Platform 5.x. Set "defaults.extra_properties.deduplicate_resource_short_names" to "true" in the API Platform configuration to enable it now.', $shortName, $resource->getClass()); - } - ++$shortNameCounts[$shortName]; - continue; - } - $newShortName = $shortName.(++$shortNameCounts[$shortName]); $resource = $resource->withShortName($newShortName); diff --git a/src/Metadata/Tests/Resource/Factory/AttributesResourceMetadataCollectionFactoryTest.php b/src/Metadata/Tests/Resource/Factory/AttributesResourceMetadataCollectionFactoryTest.php index 07af77d035c..a8d6de53511 100644 --- a/src/Metadata/Tests/Resource/Factory/AttributesResourceMetadataCollectionFactoryTest.php +++ b/src/Metadata/Tests/Resource/Factory/AttributesResourceMetadataCollectionFactoryTest.php @@ -99,14 +99,14 @@ class: AttributeResource::class, graphQlOperations: $this->getDefaultGraphqlOperations('AttributeResource', AttributeResource::class, AttributeResourceProvider::class) ), new ApiResource( - shortName: 'AttributeResource', + shortName: 'AttributeResource2', class: AttributeResource::class, uriTemplate: '/dummy/{dummyId}/attribute_resources/{identifier}{._format}', operations: [ '_api_/dummy/{dummyId}/attribute_resources/{identifier}{._format}_get' => new Get( class: AttributeResource::class, uriTemplate: '/dummy/{dummyId}/attribute_resources/{identifier}{._format}', - shortName: 'AttributeResource', + shortName: 'AttributeResource2', inputFormats: ['json' => ['application/merge-patch+json']], priority: 4, status: 301, @@ -116,7 +116,7 @@ class: AttributeResource::class, '_api_/dummy/{dummyId}/attribute_resources/{identifier}{._format}_patch' => new Patch( class: AttributeResource::class, uriTemplate: '/dummy/{dummyId}/attribute_resources/{identifier}{._format}', - shortName: 'AttributeResource', + shortName: 'AttributeResource2', inputFormats: ['json' => ['application/merge-patch+json']], priority: 5, status: 301, @@ -272,11 +272,9 @@ public function testNameDeclarationShouldNotBeRemoved(): void $this->assertTrue($operations->has('password_reset')); } - public function testDeduplicateShortNamesWhenEnabled(): void + public function testDeduplicateShortNames(): void { - $factory = new AttributesResourceMetadataCollectionFactory(defaults: [ - 'extra_properties' => ['deduplicate_resource_short_names' => true], - ], graphQlEnabled: true); + $factory = new AttributesResourceMetadataCollectionFactory(graphQlEnabled: true); $collection = $factory->create(AttributeResource::class); @@ -292,20 +290,6 @@ public function testDeduplicateShortNamesWhenEnabled(): void } } - /** @group legacy */ - public function testDeduplicateShortNamesTriggersDeprecationWhenDisabled(): void - { - $factory = new AttributesResourceMetadataCollectionFactory(graphQlEnabled: true); - - $this->expectUserDeprecationMessage('Since api-platform/core 4.2: Having multiple "#[ApiResource]" attributes with the same "shortName" "AttributeResource" on class "ApiPlatform\Metadata\Tests\Fixtures\ApiResource\AttributeResource" is deprecated and will result in automatic short name deduplication in API Platform 5.x. Set "defaults.extra_properties.deduplicate_resource_short_names" to "true" in the API Platform configuration to enable it now.'); - - $collection = $factory->create(AttributeResource::class); - - // Without the flag, shortNames are NOT deduplicated - $this->assertSame('AttributeResource', $collection[0]->getShortName()); - $this->assertSame('AttributeResource', $collection[1]->getShortName()); - } - public function testWithParameters(): void { $attributeResourceMetadataCollectionFactory = new AttributesResourceMetadataCollectionFactory(); diff --git a/src/OpenApi/Tests/Factory/OpenApiFactoryTest.php b/src/OpenApi/Tests/Factory/OpenApiFactoryTest.php index 89463d4fc63..c556998dbb1 100644 --- a/src/OpenApi/Tests/Factory/OpenApiFactoryTest.php +++ b/src/OpenApi/Tests/Factory/OpenApiFactoryTest.php @@ -531,7 +531,7 @@ public function testInvoke(): void $propertyMetadataFactory = $propertyMetadataFactoryProphecy->reveal(); - $definitionNameFactory = new DefinitionNameFactory([]); + $definitionNameFactory = new DefinitionNameFactory(); $schemaFactory = new SchemaFactory( resourceMetadataFactory: $resourceCollectionMetadataFactory, @@ -1397,7 +1397,7 @@ public function testGetExtensionPropertiesWithFalseValue(): void $resourceCollectionMetadataFactory = $this->createMock(ResourceMetadataCollectionFactoryInterface::class); $propertyNameCollectionFactory = $this->createMock(PropertyNameCollectionFactoryInterface::class); $propertyMetadataFactory = $this->createMock(PropertyMetadataFactoryInterface::class); - $definitionNameFactory = new DefinitionNameFactory([]); + $definitionNameFactory = new DefinitionNameFactory(); $resourceCollectionMetadata = new ResourceMetadataCollection(Dummy::class, [(new ApiResource(operations: [ (new Get())->withOpenapi(true)->withShortName('Dummy')->withName('api_dummies_get_collection')->withRouteName('api_dummies_get_collection'), @@ -1447,7 +1447,7 @@ public function testMetadataParameterInOpenApiOperationParametersThrows(): void $resourceCollectionMetadataFactory = $this->createMock(ResourceMetadataCollectionFactoryInterface::class); $propertyNameCollectionFactory = $this->createMock(PropertyNameCollectionFactoryInterface::class); $propertyMetadataFactory = $this->createMock(PropertyMetadataFactoryInterface::class); - $definitionNameFactory = new DefinitionNameFactory([]); + $definitionNameFactory = new DefinitionNameFactory(); $resourceCollectionMetadata = new ResourceMetadataCollection(Dummy::class, [(new ApiResource(operations: [ (new GetCollection()) diff --git a/src/OpenApi/Tests/Serializer/OpenApiNormalizerTest.php b/src/OpenApi/Tests/Serializer/OpenApiNormalizerTest.php index efe1f25df25..4e5000e9c97 100644 --- a/src/OpenApi/Tests/Serializer/OpenApiNormalizerTest.php +++ b/src/OpenApi/Tests/Serializer/OpenApiNormalizerTest.php @@ -239,7 +239,7 @@ public function testNormalize(): void $propertyNameCollectionFactory = $propertyNameCollectionFactoryProphecy->reveal(); $propertyMetadataFactory = $propertyMetadataFactoryProphecy->reveal(); - $definitionNameFactory = new DefinitionNameFactory(null); + $definitionNameFactory = new DefinitionNameFactory(); $schemaFactory = new SchemaFactory( resourceMetadataFactory: $resourceMetadataFactory, diff --git a/src/State/Processor/ObjectMapperProcessor.php b/src/State/Processor/ObjectMapperProcessor.php deleted file mode 100644 index f7bb34a367e..00000000000 --- a/src/State/Processor/ObjectMapperProcessor.php +++ /dev/null @@ -1,87 +0,0 @@ - - * - * 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\State\Processor; - -use ApiPlatform\Metadata\Operation; -use ApiPlatform\State\ProcessorInterface; -use ApiPlatform\State\Util\StateOptionsTrait; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\ObjectMapper\ObjectMapperInterface; - -/** - * @deprecated since API Platform 4.3, use {@see ObjectMapperInputProcessor} and {@see ObjectMapperOutputProcessor} instead - * - * @implements ProcessorInterface - */ -final class ObjectMapperProcessor implements ProcessorInterface -{ - use StateOptionsTrait; - - /** - * @param ProcessorInterface $decorated - */ - public function __construct( - private readonly ?ObjectMapperInterface $objectMapper, - private readonly ProcessorInterface $decorated, - ) { - trigger_deprecation('api-platform/core', '4.3', 'The "%s" class is deprecated, use "%s" and "%s" instead.', self::class, ObjectMapperInputProcessor::class, ObjectMapperOutputProcessor::class); - } - - public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): object|array|null - { - $class = $operation->getInput()['class'] ?? $operation->getClass(); - - if ( - $data instanceof Response - || !$this->objectMapper - || !$operation->canWrite() - || null === $data - || !is_a($data, $class, true) - || !$operation->canMap() - ) { - return $this->decorated->process($data, $operation, $uriVariables, $context); - } - - $request = $context['request'] ?? null; - - // maps the Resource to an Entity - if ($request?->attributes->get('mapped_data')) { - $mappedData = $this->objectMapper->map($data, $request->attributes->get('mapped_data')); - } else { - $mappedData = $this->objectMapper->map($data, $this->getStateOptionsClass($operation, $operation->getClass())); - } - $request?->attributes->set('mapped_data', $mappedData); - - $persisted = $this->decorated->process( - $mappedData, - $operation, - $uriVariables, - $context, - ); - - // in some cases (delete operation), the decoration may return a null object - if (null === $persisted) { - return $persisted; - } - - $request?->attributes->set('persisted_data', $persisted); - - // return the Resource representation of the persisted entity - return $this->objectMapper->map( - // persist the entity - $persisted, - $operation->getClass() - ); - } -} diff --git a/src/State/Provider/DeserializeProvider.php b/src/State/Provider/DeserializeProvider.php index 02572ac9b1a..9ae282691ff 100644 --- a/src/State/Provider/DeserializeProvider.php +++ b/src/State/Provider/DeserializeProvider.php @@ -74,18 +74,6 @@ public function provide(Operation $operation, array $uriVariables = [], array $c throw new UnsupportedMediaTypeHttpException('Format not supported.'); } - if (null === ($serializerContext[SerializerContextBuilderInterface::ASSIGN_OBJECT_TO_POPULATE] ?? null)) { - $method = $operation->getMethod(); - $assignObjectToPopulate = 'POST' === $method - || 'PATCH' === $method - || ('PUT' === $method && !($operation->getExtraProperties()['standard_put'] ?? true)); - - if ($assignObjectToPopulate) { - $serializerContext[SerializerContextBuilderInterface::ASSIGN_OBJECT_TO_POPULATE] = true; - trigger_deprecation('api-platform/core', '5.0', 'To assign an object to populate you should set "%s" in your denormalizationContext, not defining it is deprecated.', SerializerContextBuilderInterface::ASSIGN_OBJECT_TO_POPULATE); - } - } - if (null !== $data && ($serializerContext[SerializerContextBuilderInterface::ASSIGN_OBJECT_TO_POPULATE] ?? false)) { $serializerContext[AbstractNormalizer::OBJECT_TO_POPULATE] = $data; } diff --git a/src/State/SerializerAwareProviderInterface.php b/src/State/SerializerAwareProviderInterface.php deleted file mode 100644 index 6aada8eba41..00000000000 --- a/src/State/SerializerAwareProviderInterface.php +++ /dev/null @@ -1,28 +0,0 @@ - - * - * 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\State; - -use Psr\Container\ContainerInterface; - -/** - * Injects serializer in providers. - * - * @author Vincent Chalamon - * - * @deprecated in 4.2, to be removed in 5.0 because it violates the dependency injection principle. - */ -interface SerializerAwareProviderInterface -{ - public function setSerializerLocator(ContainerInterface $serializerLocator): void; -} diff --git a/src/State/SerializerAwareProviderTrait.php b/src/State/SerializerAwareProviderTrait.php deleted file mode 100644 index bba3665f467..00000000000 --- a/src/State/SerializerAwareProviderTrait.php +++ /dev/null @@ -1,47 +0,0 @@ - - * - * 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\State; - -use Psr\Container\ContainerInterface; -use Symfony\Component\Serializer\SerializerInterface; - -/** - * Injects serializer in providers. - * - * @author Vincent Chalamon - */ -trait SerializerAwareProviderTrait -{ - /** - * @internal - */ - private ContainerInterface $serializerLocator; - - public function setSerializerLocator(ContainerInterface $serializerLocator): void - { - trigger_deprecation( - 'api-platform/core', - '4.2', - 'The "%s" interface is deprecated and will be removed in 5.0. It violates the dependency injection principle.', - SerializerAwareProviderInterface::class - ); - - $this->serializerLocator = $serializerLocator; - } - - private function getSerializer(): SerializerInterface - { - return $this->serializerLocator->get('serializer'); - } -} diff --git a/src/Symfony/Bundle/ApiPlatformBundle.php b/src/Symfony/Bundle/ApiPlatformBundle.php index 3b034ecbfef..5fde037f2d4 100644 --- a/src/Symfony/Bundle/ApiPlatformBundle.php +++ b/src/Symfony/Bundle/ApiPlatformBundle.php @@ -16,7 +16,6 @@ use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\AttributeFilterPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\AttributeResourcePass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\AuthenticatorManagerPass; -use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\DataProviderPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\ElasticsearchClientPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\ErrorResourceAttributeLoaderPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\FilterPass; @@ -47,8 +46,6 @@ public function build(ContainerBuilder $container): void { parent::build($container); - // TODO: remove in 5.x - $container->addCompilerPass(new DataProviderPass()); // Run the compiler pass before the {@see ResolveInstanceofConditionalsPass} to allow autoconfiguration of generated filter definitions. $container->addCompilerPass(new AttributeFilterPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 101); $container->addCompilerPass(new AttributeResourcePass()); diff --git a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index c103878d3e6..daed2199cdf 100644 --- a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php +++ b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php @@ -399,7 +399,7 @@ private function registerCommonConfiguration(ContainerBuilder $container, array $container->setParameter('api_platform.http_cache.stale_while_revalidate', $config['defaults']['cache_headers']['stale_while_revalidate'] ?? null); $container->setParameter('api_platform.http_cache.stale_if_error', $config['defaults']['cache_headers']['stale_if_error'] ?? null); $container->setParameter('api_platform.http_cache.invalidation.max_header_length', $config['defaults']['cache_headers']['invalidation']['max_header_length'] ?? $config['http_cache']['invalidation']['max_header_length']); - $container->setParameter('api_platform.http_cache.invalidation.xkey.glue', $config['defaults']['cache_headers']['invalidation']['xkey']['glue'] ?? $config['http_cache']['invalidation']['xkey']['glue']); + $container->setParameter('api_platform.http_cache.invalidation.xkey.glue', $config['defaults']['cache_headers']['invalidation']['xkey']['glue'] ?? ' '); $container->setAlias('api_platform.path_segment_name_generator', $config['path_segment_name_generator']); $container->setAlias('api_platform.inflector', $config['inflector']); @@ -470,13 +470,6 @@ private function registerMetadataConfiguration(ContainerBuilder $container, arra $loader->load('metadata/resource_name.php'); $loader->load('metadata/property_name.php'); - if (!empty($config['resource_class_directories'])) { - $container->setParameter('api_platform.resource_class_directories', array_merge( - $config['resource_class_directories'], - $container->getParameter('api_platform.resource_class_directories') - )); - } - // V3 metadata $loader->load('metadata/php.php'); $loader->load('metadata/xml.php'); @@ -909,9 +902,7 @@ private function registerHttpCacheConfiguration(ContainerBuilder $container, arr $definition->addTag('api_platform.http_cache.http_client'); } - if (!($urls = $config['http_cache']['invalidation']['urls'])) { - $urls = $config['http_cache']['invalidation']['varnish_urls']; - } + $urls = $config['http_cache']['invalidation']['urls']; foreach ($urls as $key => $url) { $definition = new Definition(ScopingHttpClient::class, [new Reference('http_client'), $url, ['base_uri' => $url] + $config['http_cache']['invalidation']['request_options']]); diff --git a/src/Symfony/Bundle/DependencyInjection/Compiler/DataProviderPass.php b/src/Symfony/Bundle/DependencyInjection/Compiler/DataProviderPass.php deleted file mode 100644 index 78e3c47afb5..00000000000 --- a/src/Symfony/Bundle/DependencyInjection/Compiler/DataProviderPass.php +++ /dev/null @@ -1,47 +0,0 @@ - - * - * 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\Symfony\Bundle\DependencyInjection\Compiler; - -use ApiPlatform\State\SerializerAwareProviderInterface; -use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Reference; - -/** - * Registers data providers. - * - * @internal since 4.2 - * - * @author Kévin Dunglas - * @author Vincent Chalamon - * - * TODO: remove in 5.x - */ -final class DataProviderPass implements CompilerPassInterface -{ - /** - * {@inheritdoc} - */ - public function process(ContainerBuilder $container): void - { - $services = $container->findTaggedServiceIds('api_platform.state_provider', true); - - foreach ($services as $id => $tags) { - $definition = $container->getDefinition((string) $id); - if (is_a($definition->getClass(), SerializerAwareProviderInterface::class, true)) { - $definition->addMethodCall('setSerializerLocator', [new Reference('api_platform.serializer_locator')]); - } - } - } -} diff --git a/src/Symfony/Bundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/DependencyInjection/Configuration.php index 27f5cfbd2be..480043544b9 100644 --- a/src/Symfony/Bundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/DependencyInjection/Configuration.php @@ -94,10 +94,6 @@ public function getConfigTreeBuilder(): TreeBuilder ->addDefaultsIfNotSet() ->children() ->variableNode('serialize_payload_fields')->defaultValue([])->info('Set to null to serialize all payload fields when a validation error is thrown, or set the fields you want to include explicitly.')->end() - ->booleanNode('query_parameter_validation') - ->defaultValue(true) - ->setDeprecated('api-platform/symfony', '4.2', 'Will be removed in API Platform 5.0.') - ->end() ->end() ->end() ->arrayNode('jsonapi') @@ -132,11 +128,6 @@ public function getConfigTreeBuilder(): TreeBuilder ->booleanNode('enable_docs')->defaultTrue()->info('Enable the docs')->end() ->booleanNode('enable_profiler')->defaultTrue()->info('Enable the data collector and the WebProfilerBundle integration.')->end() ->booleanNode('enable_phpdoc_parser')->defaultTrue()->info('Enable resource metadata collector using PHPStan PhpDocParser.')->end() - ->booleanNode('enable_link_security') - ->defaultTrue() - ->info('Enable security for Links (sub resources).') - ->setDeprecated('api-platform/symfony', '4.2', 'This option is always enabled and will be removed in API Platform 5.0.') - ->end() ->arrayNode('collection') ->addDefaultsIfNotSet() ->children() @@ -167,10 +158,6 @@ public function getConfigTreeBuilder(): TreeBuilder ->end() ->end() ->end() - ->arrayNode('resource_class_directories') - ->prototype('scalar')->end() - ->setDeprecated('api-platform/symfony', '4.1', 'The "resource_class_directories" configuration is deprecated, classes using #[ApiResource] attribute are autoconfigured by the dependency injection container.') - ->end() ->arrayNode('serializer') ->addDefaultsIfNotSet() ->children() @@ -297,10 +284,6 @@ private function addGraphQlSection(ArrayNodeDefinition $rootNode): void ->end() ->integerNode('max_query_depth')->defaultValue(20) ->end() - ->arrayNode('graphql_playground') - ->setDeprecated('api-platform/core', '4.0', 'The "graphql_playground" configuration is deprecated and will be ignored.') - ->canBeEnabled() - ->end() ->integerNode('max_query_complexity')->defaultValue(500) ->end() ->scalarNode('nesting_separator')->defaultValue('_')->info('The separator to use to filter nested fields.')->end() @@ -411,12 +394,6 @@ private function addHttpCacheSection(ArrayNodeDefinition $rootNode): void ->info('Enable the tags-based cache invalidation system.') ->canBeEnabled() ->children() - ->arrayNode('varnish_urls') - ->setDeprecated('api-platform/core', '3.0', 'The "varnish_urls" configuration is deprecated, use "urls" or "scoped_clients".') - ->defaultValue([]) - ->prototype('scalar')->end() - ->info('URLs of the Varnish servers to purge using cache tags when a resource is updated.') - ->end() ->arrayNode('urls') ->defaultValue([]) ->prototype('scalar')->end() @@ -443,16 +420,6 @@ private function addHttpCacheSection(ArrayNodeDefinition $rootNode): void ->defaultValue('api_platform.http_cache.purger.varnish') ->info('Specify a purger to use (available values: "api_platform.http_cache.purger.varnish.ban", "api_platform.http_cache.purger.varnish.xkey", "api_platform.http_cache.purger.souin").') ->end() - ->arrayNode('xkey') - ->setDeprecated('api-platform/core', '3.0', 'The "xkey" configuration is deprecated, use your own purger to customize surrogate keys or the appropriate parameters.') - ->addDefaultsIfNotSet() - ->children() - ->scalarNode('glue') - ->defaultValue(' ') - ->info('xkey glue between keys') - ->end() - ->end() - ->end() ->end() ->end() ->end() diff --git a/src/Symfony/Bundle/Resources/config/json_schema.php b/src/Symfony/Bundle/Resources/config/json_schema.php index ab64a31e40d..b1a21cf8c1f 100644 --- a/src/Symfony/Bundle/Resources/config/json_schema.php +++ b/src/Symfony/Bundle/Resources/config/json_schema.php @@ -30,7 +30,6 @@ service('api_platform.metadata.property.metadata_factory'), service('api_platform.name_converter')->ignoreOnInvalid(), service('api_platform.resource_class_resolver'), - [], service('api_platform.json_schema.definition_name_factory')->ignoreOnInvalid(), ]); diff --git a/src/Symfony/Bundle/Test/ApiTestCase.php b/src/Symfony/Bundle/Test/ApiTestCase.php index 90bd2a7db96..e1076128deb 100644 --- a/src/Symfony/Bundle/Test/ApiTestCase.php +++ b/src/Symfony/Bundle/Test/ApiTestCase.php @@ -33,13 +33,12 @@ abstract class ApiTestCase extends KernelTestCase /** * If you're using RecreateDatabaseTrait, RefreshDatabaseTrait, ReloadDatabaseTrait from theofidry/AliceBundle, you - * probably need to set this property to false in your test class to avoid recreating the database on each client creation. + * probably need to keep this property to false in your test class to avoid recreating the database on each client creation. * - * - `null` triggers a deprecation message and always boots the kernel * - `false` does not boot the kernel if it's already booted - * - `true` always boots the kernel without any deprecation message + * - `true` always boots the kernel */ - protected static ?bool $alwaysBootKernel = null; + protected static ?bool $alwaysBootKernel = false; private bool $symfonyErrorHandlerWasRegistered = false; @@ -80,15 +79,7 @@ private static function isSymfonyErrorHandlerRegistered(): bool */ protected static function createClient(array $kernelOptions = [], array $defaultOptions = []): Client { - if (null === static::$alwaysBootKernel) { - trigger_deprecation( - 'api-platform/symfony', - '4.1.0', - 'Currently, the kernel will always be booted when a new client is created, but in API Platform 5.0, it will not be booted unless you set `static::$alwaysBootKernel` to `true` (the default will be `false`). See https://github.com/api-platform/core/issues/6971 for more information.', - ); - } - - if (static::$alwaysBootKernel || null === static::$alwaysBootKernel) { + if (static::$alwaysBootKernel) { static::bootKernel($kernelOptions); } diff --git a/src/Validator/Exception/ValidationException.php b/src/Validator/Exception/ValidationException.php index 19a3d129d54..d1e70899719 100644 --- a/src/Validator/Exception/ValidationException.php +++ b/src/Validator/Exception/ValidationException.php @@ -104,22 +104,11 @@ class ValidationException extends RuntimeException implements ConstraintViolatio protected ?string $errorTitle = null; private ConstraintViolationListInterface $constraintViolationList; - public function __construct(string|ConstraintViolationListInterface $message = new ConstraintViolationList(), string|int|null $code = null, int|\Throwable|null $previous = null, \Throwable|string|null $errorTitle = null) + public function __construct(ConstraintViolationListInterface $message = new ConstraintViolationList(), string|int|null $code = null, int|\Throwable|null $previous = null, \Throwable|string|null $errorTitle = null) { $this->errorTitle = $errorTitle; - - if ($message instanceof ConstraintViolationListInterface) { - $this->constraintViolationList = $message; - parent::__construct($this->__toString(), $code ?? 0, $previous); - $this->detail = $this->getMessage(); - - return; - } - - $this->constraintViolationList = new ConstraintViolationList(); - - trigger_deprecation('api_platform/core', '5.0', \sprintf('The "%s" exception will have a "%s" first argument in 5.x.', self::class, ConstraintViolationListInterface::class)); - parent::__construct($message ?: $this->__toString(), $code ?? 0, $previous); + $this->constraintViolationList = $message; + parent::__construct($this->__toString(), $code ?? 0, $previous); $this->detail = $this->getMessage(); } diff --git a/tests/Fixtures/TestBundle/Metadata/ProviderResourceMetadatatCollectionFactory.php b/tests/Fixtures/TestBundle/Metadata/ProviderResourceMetadatatCollectionFactory.php index 95bcbea3682..3b8f2359128 100644 --- a/tests/Fixtures/TestBundle/Metadata/ProviderResourceMetadatatCollectionFactory.php +++ b/tests/Fixtures/TestBundle/Metadata/ProviderResourceMetadatatCollectionFactory.php @@ -21,11 +21,9 @@ use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ResourceInterface; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Taxon; use ApiPlatform\Tests\Fixtures\TestBundle\Model\ResourceInterface as ResourceInterfaceDocument; -use ApiPlatform\Tests\Fixtures\TestBundle\Model\SerializableResource; use ApiPlatform\Tests\Fixtures\TestBundle\Model\TaxonInterface; use ApiPlatform\Tests\Fixtures\TestBundle\State\ContainNonResourceProvider; use ApiPlatform\Tests\Fixtures\TestBundle\State\ResourceInterfaceImplementationProvider; -use ApiPlatform\Tests\Fixtures\TestBundle\State\SerializableProvider; use ApiPlatform\Tests\Fixtures\TestBundle\State\TaxonItemProvider; class ProviderResourceMetadatatCollectionFactory implements ResourceMetadataCollectionFactoryInterface @@ -49,10 +47,6 @@ public function create(string $resourceClass): ResourceMetadataCollection return $this->setProvider($resourceMetadataCollection, ContainNonResourceProvider::class); } - if (SerializableResource::class === $resourceClass) { - return $this->setProvider($resourceMetadataCollection, SerializableProvider::class); - } - if (Taxon::class === $resourceClass || TaxonDocument::class === $resourceClass || TaxonInterface::class === $resourceClass) { return $this->setProvider($resourceMetadataCollection, TaxonItemProvider::class); } diff --git a/tests/Fixtures/TestBundle/State/SerializableProvider.php b/tests/Fixtures/TestBundle/State/SerializableProvider.php deleted file mode 100644 index 3eca79faf54..00000000000 --- a/tests/Fixtures/TestBundle/State/SerializableProvider.php +++ /dev/null @@ -1,43 +0,0 @@ - - * - * 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\State; - -use ApiPlatform\Metadata\Operation; -use ApiPlatform\State\ProviderInterface; -use ApiPlatform\State\SerializerAwareProviderInterface; -use ApiPlatform\State\SerializerAwareProviderTrait; - -/** - * @author Vincent Chalamon - * - * @deprecated in 4.2, to be removed in 5.0 because it violates the dependency injection principle. - */ -class SerializableProvider implements ProviderInterface, SerializerAwareProviderInterface -{ - use SerializerAwareProviderTrait; - - /** - * {@inheritDoc} - */ - public function provide(Operation $operation, array $uriVariables = [], array $context = []): object - { - return $this->getSerializer()->deserialize(<<<'JSON' -{ - "id": 1, - "foo": "Lorem", - "bar": "Ipsum" -} -JSON, $operation->getClass(), 'json'); - } -} diff --git a/tests/Functional/JsonLd/SerializableItemDataProviderTest.php b/tests/Functional/JsonLd/SerializableItemDataProviderTest.php deleted file mode 100644 index d20032d311c..00000000000 --- a/tests/Functional/JsonLd/SerializableItemDataProviderTest.php +++ /dev/null @@ -1,48 +0,0 @@ - - * - * 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\Functional\JsonLd; - -use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; -use ApiPlatform\Tests\Fixtures\TestBundle\Model\SerializableResource; -use ApiPlatform\Tests\SetupClassResourcesTrait; - -final class SerializableItemDataProviderTest extends ApiTestCase -{ - use SetupClassResourcesTrait; - - protected static ?bool $alwaysBootKernel = false; - - /** - * @return class-string[] - */ - public static function getResources(): array - { - return [SerializableResource::class]; - } - - public function testGetSerializableResource(): void - { - self::createClient()->request('GET', '/serializable_resources/1'); - - $this->assertResponseStatusCodeSame(200); - $this->assertJsonEquals([ - '@context' => '/contexts/SerializableResource', - '@id' => '/serializable_resources/1', - '@type' => 'SerializableResource', - 'id' => 1, - 'foo' => 'Lorem', - 'bar' => 'Ipsum', - ]); - } -} diff --git a/tests/Functional/MappingTest.php b/tests/Functional/MappingTest.php index 0b3571a0e9c..35922ff4cff 100644 --- a/tests/Functional/MappingTest.php +++ b/tests/Functional/MappingTest.php @@ -108,7 +108,7 @@ public function testShouldMapBetweenResourceAndEntity(): void /** * When an API resource has multiple #[Map] targets (e.g. MappedEntity + AnotherMappedObject), - * the ObjectMapperProcessor must resolve the correct target using stateOptions during POST. + * the ObjectMapperInputProcessor must resolve the correct target using stateOptions during POST. */ public function testPostWithMultipleMapTargetsResolvesCorrectEntity(): void { diff --git a/tests/Symfony/Bundle/ApiPlatformBundleTest.php b/tests/Symfony/Bundle/ApiPlatformBundleTest.php index a8b85a8e4c9..45af8e628d8 100644 --- a/tests/Symfony/Bundle/ApiPlatformBundleTest.php +++ b/tests/Symfony/Bundle/ApiPlatformBundleTest.php @@ -17,7 +17,6 @@ use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\AttributeFilterPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\AttributeResourcePass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\AuthenticatorManagerPass; -use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\DataProviderPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\ElasticsearchClientPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\ErrorResourceAttributeLoaderPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\FilterPass; @@ -47,8 +46,6 @@ public function testBuild(): void $passes = $container->getCompilerPassConfig()->getBeforeOptimizationPasses(); $passClasses = array_map(static fn (object $p): string => $p::class, $passes); - // TODO: remove in 5.x - $this->assertContains(DataProviderPass::class, $passClasses); $this->assertContains(AttributeFilterPass::class, $passClasses); $this->assertContains(AttributeResourcePass::class, $passClasses); $this->assertContains(FilterPass::class, $passClasses); diff --git a/tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php b/tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php index a31121d8a19..0e5d5dde025 100644 --- a/tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php +++ b/tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php @@ -106,7 +106,6 @@ private function runDefaultConfigTests(array $doctrineIntegrationsToLoad = ['orm 'inflector' => 'api_platform.metadata.inflector', 'validator' => [ 'serialize_payload_fields' => [], - 'query_parameter_validation' => true, ], 'name_converter' => null, 'enable_swagger' => true, @@ -132,9 +131,6 @@ private function runDefaultConfigTests(array $doctrineIntegrationsToLoad = ['orm 'enabled' => true, ], ], - 'graphql_playground' => [ - 'enabled' => false, - ], ], 'elasticsearch' => [ 'enabled' => false, @@ -189,11 +185,9 @@ private function runDefaultConfigTests(array $doctrineIntegrationsToLoad = ['orm 'http_cache' => [ 'invalidation' => [ 'enabled' => false, - 'varnish_urls' => [], 'request_options' => [], 'max_header_length' => 7500, 'purger' => 'api_platform.http_cache.purger.varnish', - 'xkey' => ['glue' => ' '], 'urls' => [], 'scoped_clients' => [], ], @@ -213,7 +207,6 @@ private function runDefaultConfigTests(array $doctrineIntegrationsToLoad = ['orm 'hub_url' => null, 'include_type' => false, ], - 'resource_class_directories' => [], 'asset_package' => null, 'openapi' => [ 'contact' => [ @@ -240,8 +233,6 @@ private function runDefaultConfigTests(array $doctrineIntegrationsToLoad = ['orm ], 'use_symfony_listeners' => false, 'handle_symfony_errors' => false, - // TODO: remove in 5.0 - 'enable_link_security' => true, 'serializer' => [ 'hydra_prefix' => null, ], From 17053ffcb00cb05cdef100f19cd466db47b13067 Mon Sep 17 00:00:00 2001 From: soyuka Date: Tue, 30 Jun 2026 19:34:02 +0200 Subject: [PATCH 02/11] test(symfony): remove enable_link_security from test app config enable_link_security was removed from Configuration in the 5.0 cleanup, but config_common.yml still set it, so cache:warmup failed with "Unrecognized option" and broke every job booting the test container. --- tests/Fixtures/app/config/config_common.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/Fixtures/app/config/config_common.yml b/tests/Fixtures/app/config/config_common.yml index b046bc20e2f..2f4c0601fe0 100644 --- a/tests/Fixtures/app/config/config_common.yml +++ b/tests/Fixtures/app/config/config_common.yml @@ -85,8 +85,6 @@ api_platform: http_cache: invalidation: enabled: true - # TODO: remove in 5.0 - enable_link_security: true # see also defaults in AppKernel doctrine_mongodb_odm: false mapping: From f2b672233f01bad6cc5268f1a294b5f5fc1529af Mon Sep 17 00:00:00 2001 From: soyuka Date: Tue, 30 Jun 2026 19:34:03 +0200 Subject: [PATCH 03/11] test(state): drop obsolete object_to_populate deprecation test DeserializeProvider no longer triggers the object_to_populate deprecation in 5.0; the assertions for it are dead and the test now fails. --- .../Provider/DeserializeProviderTest.php | 44 +------------------ 1 file changed, 1 insertion(+), 43 deletions(-) diff --git a/src/State/Tests/Provider/DeserializeProviderTest.php b/src/State/Tests/Provider/DeserializeProviderTest.php index 608df3bf09c..6332e30f9ea 100644 --- a/src/State/Tests/Provider/DeserializeProviderTest.php +++ b/src/State/Tests/Provider/DeserializeProviderTest.php @@ -14,15 +14,11 @@ namespace ApiPlatform\State\Tests\Provider; use ApiPlatform\Metadata\Get; -use ApiPlatform\Metadata\HttpOperation; -use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Post; -use ApiPlatform\Metadata\Put; use ApiPlatform\State\DenormalizationViolationFactoryInterface; use ApiPlatform\State\Provider\DeserializeProvider; use ApiPlatform\State\ProviderInterface; use ApiPlatform\State\SerializerContextBuilderInterface; -use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; @@ -34,10 +30,8 @@ class DeserializeProviderTest extends TestCase { - #[IgnoreDeprecations] public function testDeserialize(): void { - $this->expectUserDeprecationMessage('Since api-platform/core 5.0: To assign an object to populate you should set "api_assign_object_to_populate" in your denormalizationContext, not defining it is deprecated.'); $objectToPopulate = new \stdClass(); $serializerContext = []; $operation = new Post(deserialize: true, class: \stdClass::class); @@ -47,7 +41,7 @@ public function testDeserialize(): void $serializerContextBuilder = $this->createMock(SerializerContextBuilderInterface::class); $serializerContextBuilder->expects($this->once())->method('createFromRequest')->willReturn($serializerContext); $serializer = $this->createMock(SerializerInterface::class); - $serializer->expects($this->once())->method('deserialize')->with('test', \stdClass::class, 'format', ['uri_variables' => ['id' => 1], AbstractNormalizer::OBJECT_TO_POPULATE => $objectToPopulate] + $serializerContext)->willReturn(new \stdClass()); + $serializer->expects($this->once())->method('deserialize')->with('test', \stdClass::class, 'format', ['uri_variables' => ['id' => 1]] + $serializerContext)->willReturn(new \stdClass()); $provider = new DeserializeProvider($decorated, $serializer, $serializerContextBuilder); $request = new Request(content: 'test'); @@ -139,42 +133,6 @@ public function testRequestWithEmptyContentType(): void $provider->provide($operation, [], $context); } - #[DataProvider('provideMethodsTriggeringDeprecation')] - #[IgnoreDeprecations] - public function testDeserializeTriggersDeprecationWhenContextNotSet(HttpOperation $operation): void - { - $this->expectUserDeprecationMessage('Since api-platform/core 5.0: To assign an object to populate you should set "api_assign_object_to_populate" in your denormalizationContext, not defining it is deprecated.'); - - $objectToPopulate = new \stdClass(); - $serializerContext = []; - $decorated = $this->createStub(ProviderInterface::class); - $decorated->method('provide')->willReturn($objectToPopulate); - - $serializerContextBuilder = $this->createMock(SerializerContextBuilderInterface::class); - $serializerContextBuilder->method('createFromRequest')->willReturn($serializerContext); - - $serializer = $this->createMock(SerializerInterface::class); - $serializer->expects($this->once())->method('deserialize')->with( - 'test', - \stdClass::class, - 'format', - ['uri_variables' => ['id' => 1], 'object_to_populate' => $objectToPopulate] + $serializerContext - )->willReturn(new \stdClass()); - - $provider = new DeserializeProvider($decorated, $serializer, $serializerContextBuilder); - $request = new Request(content: 'test'); - $request->headers->set('CONTENT_TYPE', 'ok'); - $request->attributes->set('input_format', 'format'); - $provider->provide($operation, ['id' => 1], ['request' => $request]); - } - - public static function provideMethodsTriggeringDeprecation(): iterable - { - yield 'POST method' => [new Post(deserialize: true, class: \stdClass::class)]; - yield 'PATCH method' => [new Patch(deserialize: true, class: \stdClass::class)]; - yield 'PUT method (non-standard)' => [new Put(deserialize: true, class: \stdClass::class, extraProperties: ['standard_put' => false])]; - } - public function testDeserializeSetsObjectToPopulateWhenContextIsTrue(): void { $objectToPopulate = new \stdClass(); From 58f28b8e721928cc89d0f4e5202a75768c38c381 Mon Sep 17 00:00:00 2001 From: soyuka Date: Tue, 30 Jun 2026 19:34:03 +0200 Subject: [PATCH 04/11] fix(laravel): assign object to populate on write operations DeserializeProvider only populates the loaded object when api_assign_object_to_populate is set in the denormalization context. The Symfony path sets it in MainController/DeserializeListener, but the Laravel controller did not, so PATCH re-inserted a fresh model and hit NOT NULL constraint violations. Mirror the Symfony logic: set it for POST, PATCH and non-standard PUT. --- src/Laravel/Controller/ApiPlatformController.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Laravel/Controller/ApiPlatformController.php b/src/Laravel/Controller/ApiPlatformController.php index d7c59b6f8f7..7e507bf2ce3 100644 --- a/src/Laravel/Controller/ApiPlatformController.php +++ b/src/Laravel/Controller/ApiPlatformController.php @@ -19,6 +19,7 @@ use ApiPlatform\Metadata\Operation\Factory\OperationMetadataFactoryInterface; use ApiPlatform\State\ProcessorInterface; use ApiPlatform\State\ProviderInterface; +use ApiPlatform\State\SerializerContextBuilderInterface; use Illuminate\Http\Request; use Illuminate\Routing\Controller; use Illuminate\Support\Facades\Event; @@ -77,6 +78,16 @@ public function __invoke(Request $request): Response $operation = $operation->withDeserialize(\in_array($operation->getMethod(), ['POST', 'PUT', 'PATCH'], true)); } + $denormalizationContext = $operation->getDenormalizationContext() ?? []; + if ($operation->canDeserialize() && !isset($denormalizationContext[SerializerContextBuilderInterface::ASSIGN_OBJECT_TO_POPULATE])) { + $method = $operation->getMethod(); + $assignObjectToPopulate = 'POST' === $method + || 'PATCH' === $method + || ('PUT' === $method && !($operation->getExtraProperties()['standard_put'] ?? true)); + + $operation = $operation->withDenormalizationContext($denormalizationContext + [SerializerContextBuilderInterface::ASSIGN_OBJECT_TO_POPULATE => $assignObjectToPopulate]); + } + $body = $this->provider->provide($operation, $uriVariables, $context); // The provider can change the Operation, extract it again from the Request attributes From f12699b7e8cbcdde425da0c77e98d47d32e5c110 Mon Sep 17 00:00:00 2001 From: soyuka Date: Wed, 1 Jul 2026 07:32:09 +0200 Subject: [PATCH 05/11] test(symfony): drop dead SerializableProvider service definition The SerializableProvider class was removed with the 5.0 deprecation cleanup; config_common.yml still registered it as a service, failing lint:container with "class does not exist". --- tests/Fixtures/app/config/config_common.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/Fixtures/app/config/config_common.yml b/tests/Fixtures/app/config/config_common.yml index 2f4c0601fe0..7e3e79312d1 100644 --- a/tests/Fixtures/app/config/config_common.yml +++ b/tests/Fixtures/app/config/config_common.yml @@ -159,11 +159,6 @@ services: tags: - name: 'api_platform.state_provider' - ApiPlatform\Tests\Fixtures\TestBundle\State\SerializableProvider: - class: 'ApiPlatform\Tests\Fixtures\TestBundle\State\SerializableProvider' - tags: - - name: 'api_platform.state_provider' - ApiPlatform\Tests\Fixtures\TestBundle\State\FakeProvider: class: 'ApiPlatform\Tests\Fixtures\TestBundle\State\FakeProvider' tags: From 45f271cf33162ac17403bd190dc0354b4435b020 Mon Sep 17 00:00:00 2001 From: soyuka Date: Wed, 1 Jul 2026 07:32:10 +0200 Subject: [PATCH 06/11] ci: remove stale "Service is private" phpstan ignore The code paths fetching private services were removed in the 5.0 cleanup, leaving the ignore pattern unmatched and failing PHPStan with reportUnmatchedIgnoredErrors. --- phpstan.neon.dist | 4 ---- 1 file changed, 4 deletions(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 2015d5831b1..ee3f9221f42 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -93,10 +93,6 @@ parameters: - "#Call to function method_exists\\(\\) with Doctrine\\\\ODM\\\\MongoDB\\\\Mapping\\\\ClassMetadata\\|Doctrine\\\\ORM\\\\Mapping\\\\ClassMetadata and 'isChangeTrackingDef…' will always evaluate to true\\.#" - "#Call to function method_exists\\(\\) with Symfony\\\\Component\\\\Serializer\\\\Exception\\\\PartialDenormalizationException and 'getNotNormalizableV…' will always evaluate to true\\.#" - # See https://github.com/phpstan/phpstan-symfony/issues/27 - - - message: '#^Service "[^"]+" is private.$#' - path: src # Allow extra assertions in tests: https://github.com/phpstan/phpstan-strict-rules/issues/130 From f806ae6c4adedb7442ffd84792d2ebaa07c9773c Mon Sep 17 00:00:00 2001 From: soyuka Date: Wed, 1 Jul 2026 07:32:10 +0200 Subject: [PATCH 07/11] fix(jsonld): align @type with deduplicated short name With automatic short name deduplication, the operation serving a request carries the deduplicated short name (e.g. AttributeResource2), so @context follows it while @type used the first resource's short name, producing a mismatch. Use the operation short name for @type when the operation serves the normalized class; keep the resource-level lookup for embedded resources whose operation belongs to another class. --- src/JsonLd/Serializer/ItemNormalizer.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/JsonLd/Serializer/ItemNormalizer.php b/src/JsonLd/Serializer/ItemNormalizer.php index 905b70ff657..65436d63955 100644 --- a/src/JsonLd/Serializer/ItemNormalizer.php +++ b/src/JsonLd/Serializer/ItemNormalizer.php @@ -158,13 +158,15 @@ private function resolveType(string $resourceClass, bool $isResourceClass, array $types = $operation instanceof HttpOperation ? $operation->getTypes() : null; if (null === $types) { - if (isset($context['item_uri_template'])) { - // The members carry the item resource's @type to match their @id, which dereferences - // to the item_uri_template operation rather than to the collection's own resource. + $typeClass = $isResourceClass ? $resourceClass : ($operation->getClass() ?? $resourceClass); + if (isset($context['item_uri_template']) || $operation->getClass() === $typeClass) { + // The operation serves the class being normalized: use its shortName so @type matches the + // (possibly deduplicated) @context. For item_uri_template, $resourceClass is the collection + // resource, so the operation remains authoritative for the item type. $types = [$operation->getShortName()]; } else { - // Use resource-level shortName to avoid operation-specific overrides - $typeClass = $isResourceClass ? $resourceClass : ($operation->getClass() ?? $resourceClass); + // Embedded/related resource: the operation belongs to another class, so fall back to its + // resource-level shortName instead of an operation-specific override. try { $types = [$this->resourceMetadataCollectionFactory->create($typeClass)[0]->getShortName()]; } catch (\Exception) { From fc4b055017925ec962594170b134069aaac085ba Mon Sep 17 00:00:00 2001 From: soyuka Date: Wed, 1 Jul 2026 08:14:55 +0200 Subject: [PATCH 08/11] test: update expectations for automatic short-name deduplication Resources sharing a short name now get a deduplicated suffix (e.g. AttributeResource2, Employee3), so @context and @type carry the deduplicated name. Update the functional expectations accordingly. --- tests/Functional/AttributeResourceTest.php | 8 +++--- tests/Functional/CrudUriVariablesTest.php | 10 +++---- .../CustomIdentifierWithSubresourceTest.php | 6 ++--- .../Functional/JsonLd/InheritanceIriTest.php | 4 +-- tests/Functional/OpenApiTest.php | 2 +- .../SubResource/SubResourceTest.php | 26 +++++++++---------- 6 files changed, 28 insertions(+), 28 deletions(-) diff --git a/tests/Functional/AttributeResourceTest.php b/tests/Functional/AttributeResourceTest.php index 9c959ff4044..753fc8a39c7 100644 --- a/tests/Functional/AttributeResourceTest.php +++ b/tests/Functional/AttributeResourceTest.php @@ -89,9 +89,9 @@ public function testAliasedResourceRedirectsAndShowsTarget(): void $this->assertResponseHeaderSame('Location', '/attribute_resources/2'); $this->assertResponseHeaderSame('Content-Type', 'application/ld+json; charset=utf-8'); $this->assertJsonEquals([ - '@context' => '/contexts/AttributeResource', + '@context' => '/contexts/AttributeResource2', '@id' => '/attribute_resources/2', - '@type' => 'AttributeResource', + '@type' => 'AttributeResource2', 'identifier' => 2, 'dummy' => '/dummies/1', 'name' => 'Foo', @@ -109,9 +109,9 @@ public function testPatchAliasedResource(): void $this->assertResponseHeaderSame('Location', '/attribute_resources/2'); $this->assertResponseHeaderSame('Content-Type', 'application/ld+json; charset=utf-8'); $this->assertJsonEquals([ - '@context' => '/contexts/AttributeResource', + '@context' => '/contexts/AttributeResource2', '@id' => '/attribute_resources/2', - '@type' => 'AttributeResource', + '@type' => 'AttributeResource2', 'identifier' => 2, 'dummy' => '/dummies/1', 'name' => 'Patched', diff --git a/tests/Functional/CrudUriVariablesTest.php b/tests/Functional/CrudUriVariablesTest.php index 695d3b6938c..27aed1c63aa 100644 --- a/tests/Functional/CrudUriVariablesTest.php +++ b/tests/Functional/CrudUriVariablesTest.php @@ -112,7 +112,7 @@ public function testGetEmployeesCollectionByCompany(): void $this->assertResponseStatusCodeSame(200); $this->assertResponseHeaderSame('Content-Type', 'application/ld+json; charset=utf-8'); $this->assertJsonEquals([ - '@context' => '/contexts/Employee', + '@context' => '/contexts/Employee3', '@id' => '/companies/2/employees', '@type' => 'hydra:Collection', 'hydra:member' => [ @@ -143,9 +143,9 @@ public function testGetCompanyOfEmployee(): void $this->assertResponseStatusCodeSame(200); $this->assertJsonEquals([ - '@context' => '/contexts/Company', + '@context' => '/contexts/Company2', '@id' => '/employees/1/company', - '@type' => 'Company', + '@type' => 'Company2', 'id' => 1, 'name' => 'Foo Company 1', 'employees' => [], @@ -162,9 +162,9 @@ public function testGetEmployeeWithCompanyUriVariable(): void $this->assertResponseStatusCodeSame(200); $this->assertJsonEquals([ - '@context' => '/contexts/Employee', + '@context' => '/contexts/Employee2', '@id' => '/companies/1/employees/1', - '@type' => 'Employee', + '@type' => 'Employee2', 'id' => 1, 'name' => 'foo', 'company' => '/companies/1', diff --git a/tests/Functional/CustomIdentifierWithSubresourceTest.php b/tests/Functional/CustomIdentifierWithSubresourceTest.php index fd91785325b..42699db37d4 100644 --- a/tests/Functional/CustomIdentifierWithSubresourceTest.php +++ b/tests/Functional/CustomIdentifierWithSubresourceTest.php @@ -101,7 +101,7 @@ public function testGetChildDummiesOfParentBySlug(): void $this->assertResponseStatusCodeSame(200); $this->assertResponseHeaderSame('Content-Type', 'application/ld+json; charset=utf-8'); $this->assertJsonEquals([ - '@context' => '/contexts/SlugChildDummy', + '@context' => '/contexts/SlugChildDummy2', '@id' => '/slug_parent_dummies/parent-dummy/child_dummies', '@type' => 'hydra:Collection', 'hydra:member' => [ @@ -126,9 +126,9 @@ public function testGetParentOfChildBySlug(): void $this->assertResponseStatusCodeSame(200); $this->assertResponseHeaderSame('Content-Type', 'application/ld+json; charset=utf-8'); $this->assertJsonEquals([ - '@context' => '/contexts/SlugParentDummy', + '@context' => '/contexts/SlugParentDummy3', '@id' => '/slug_child_dummies/child-dummy/parent_dummy', - '@type' => 'SlugParentDummy', + '@type' => 'SlugParentDummy3', 'id' => 1, 'slug' => 'parent-dummy', 'childDummies' => ['/slug_child_dummies/child-dummy'], diff --git a/tests/Functional/JsonLd/InheritanceIriTest.php b/tests/Functional/JsonLd/InheritanceIriTest.php index 1e1e415d610..36580c4d365 100644 --- a/tests/Functional/JsonLd/InheritanceIriTest.php +++ b/tests/Functional/JsonLd/InheritanceIriTest.php @@ -49,13 +49,13 @@ public function testCollectionItemsUseConcreteSubtypeIris(): void $this->assertSame([ [ '@id' => '/contractor_5438/1', - '@type' => 'Contractor', + '@type' => 'Contractor5438', 'id' => 1, 'name' => 'a', ], [ '@id' => '/employee_5438/2', - '@type' => 'Employee', + '@type' => 'Employee5438', 'id' => 2, 'name' => 'b', ], diff --git a/tests/Functional/OpenApiTest.php b/tests/Functional/OpenApiTest.php index ada32910f10..3e411c256c4 100644 --- a/tests/Functional/OpenApiTest.php +++ b/tests/Functional/OpenApiTest.php @@ -466,7 +466,7 @@ public function testRetrieveTheOpenApiDocumentation(): void $this->assertCount(7, $json['paths']['/related_dummies/{id}/related_to_dummy_friends']['get']['parameters']); // Subcollection - check schema - $this->assertSame('#/components/schemas/RelatedToDummyFriend.jsonld-fakemanytomany', $json['paths']['/related_dummies/{id}/related_to_dummy_friends']['get']['responses']['200']['content']['application/ld+json']['schema']['allOf'][1]['properties']['hydra:member']['items']['$ref']); + $this->assertSame('#/components/schemas/RelatedToDummyFriend4.jsonld-fakemanytomany', $json['paths']['/related_dummies/{id}/related_to_dummy_friends']['get']['responses']['200']['content']['application/ld+json']['schema']['allOf'][1]['properties']['hydra:member']['items']['$ref']); // Deprecations $this->assertTrue($json['paths']['/deprecated_resources']['get']['deprecated']); diff --git a/tests/Functional/SubResource/SubResourceTest.php b/tests/Functional/SubResource/SubResourceTest.php index cfc7c2328cf..9bfa9c8bede 100644 --- a/tests/Functional/SubResource/SubResourceTest.php +++ b/tests/Functional/SubResource/SubResourceTest.php @@ -164,9 +164,9 @@ public function testGetOneToOneSubResource(): void $this->assertResponseStatusCodeSame(200); $this->assertResponseHeaderSame('Content-Type', 'application/ld+json; charset=utf-8'); $this->assertJsonEquals([ - '@context' => '/contexts/Answer', + '@context' => '/contexts/Answer3', '@id' => '/questions/1/answer', - '@type' => 'Answer', + '@type' => 'Answer3', 'id' => 1, 'content' => '42', 'relatedQuestions' => ['/questions/1'], @@ -188,9 +188,9 @@ public function testOneToOneSubresourceExposesInverseSideBackIri(): void $this->assertResponseStatusCodeSame(200); $this->assertResponseHeaderSame('Content-Type', 'application/ld+json; charset=utf-8'); $this->assertJsonEquals([ - '@context' => '/contexts/OneToOneSubresourceAnswer', + '@context' => '/contexts/OneToOneSubresourceAnswer2', '@id' => '/one_to_one_subresource_questions/1/answer', - '@type' => 'OneToOneSubresourceAnswer', + '@type' => 'OneToOneSubresourceAnswer2', 'id' => 1, 'content' => '42', 'question' => '/one_to_one_subresource_questions/1', @@ -216,7 +216,7 @@ public function testGetRecursiveSubResource(): void $this->assertResponseStatusCodeSame(200); $this->assertJsonEquals([ - '@context' => '/contexts/Question', + '@context' => '/contexts/Question3', '@id' => '/questions/1/answer/related_questions', '@type' => 'hydra:Collection', 'hydra:member' => [[ @@ -268,7 +268,7 @@ public function testGetSubResourceItem(): void $this->assertResponseStatusCodeSame(200); $this->assertResponseHeaderSame('Content-Type', 'application/ld+json; charset=utf-8'); $this->assertJsonContains([ - '@context' => '/contexts/RelatedDummy', + '@context' => '/contexts/RelatedDummy3', '@id' => '/dummies/1/related_dummies/2', '@type' => 'https://schema.org/Product', 'id' => 2, @@ -297,7 +297,7 @@ public function testGetEmbeddedRelationAtThirdLevel(): void $this->assertResponseStatusCodeSame(200); $data = $response->toArray(); - $this->assertSame('/contexts/ThirdLevel', $data['@context']); + $this->assertSame('/contexts/ThirdLevel2', $data['@context']); $this->assertSame('/dummies/1/related_dummies/1/third_level', $data['@id']); $this->assertSame('ThirdLevel', $data['@type']); $this->assertSame('/fourth_levels/1', $data['fourthLevel']); @@ -317,9 +317,9 @@ public function testGetEmbeddedRelationAtFourthLevel(): void $this->assertResponseStatusCodeSame(200); $this->assertResponseHeaderSame('Content-Type', 'application/ld+json; charset=utf-8'); $this->assertJsonEquals([ - '@context' => '/contexts/FourthLevel', + '@context' => '/contexts/FourthLevel2', '@id' => '/dummies/1/related_dummies/1/third_level/fourth_level', - '@type' => 'FourthLevel', + '@type' => 'FourthLevel2', 'badThirdLevel' => [], 'id' => 1, 'level' => 4, @@ -485,9 +485,9 @@ public function testOneToOneFromOwnedSide(): void $this->assertResponseStatusCodeSame(200); $this->assertJsonContains([ - '@context' => '/contexts/Dummy', + '@context' => '/contexts/Dummy2', '@id' => '/related_owned_dummies/1/owning_dummy', - '@type' => 'Dummy', + '@type' => 'Dummy2', 'name' => 'plop', 'relatedOwnedDummy' => '/related_owned_dummies/1', 'relatedOwningDummy' => null, @@ -517,9 +517,9 @@ public function testOneToOneFromOwningSide(): void $this->assertResponseStatusCodeSame(200); $this->assertJsonContains([ - '@context' => '/contexts/Dummy', + '@context' => '/contexts/Dummy2', '@id' => '/related_owning_dummies/1/owned_dummy', - '@type' => 'Dummy', + '@type' => 'Dummy2', 'name' => 'plop', 'relatedOwningDummy' => '/related_owning_dummies/1', 'relatedOwnedDummy' => null, From 93bf48704531bc06a3618f361c5f764546507ea7 Mon Sep 17 00:00:00 2001 From: soyuka Date: Wed, 1 Jul 2026 08:14:55 +0200 Subject: [PATCH 09/11] ci: drop unmatched phpstan-ignore in HAL CollectionNormalizerTest The getLastPage/getTotalItems float return errors no longer fire, so the inline ignores were reported as unmatched. --- src/Hal/Tests/Serializer/CollectionNormalizerTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Hal/Tests/Serializer/CollectionNormalizerTest.php b/src/Hal/Tests/Serializer/CollectionNormalizerTest.php index 73095b1cd60..1f5bc63e3f6 100644 --- a/src/Hal/Tests/Serializer/CollectionNormalizerTest.php +++ b/src/Hal/Tests/Serializer/CollectionNormalizerTest.php @@ -125,8 +125,8 @@ private function normalizePaginator(bool $partial = false): array $paginator->method('current')->willReturn('foo'); // @phpstan-ignore-line if (!$partial) { - $paginator->method('getLastPage')->willReturn(7.); // @phpstan-ignore-line - $paginator->method('getTotalItems')->willReturn(1312.); // @phpstan-ignore-line + $paginator->method('getLastPage')->willReturn(7.); + $paginator->method('getTotalItems')->willReturn(1312.); } else { $paginator->method('count')->willReturn(12); } From 58813e3b4ed9d67fbb3effb1f96cf8f57587b861 Mon Sep 17 00:00:00 2001 From: soyuka Date: Wed, 1 Jul 2026 08:28:29 +0200 Subject: [PATCH 10/11] test: correct remaining dedup expectations in SubResourceTest testGetEmbeddedRelationAtThirdLevel @type and testOneToOneFromOwningSide (@context/@type Dummy3) were missed/mis-suffixed in the previous pass. --- tests/Functional/SubResource/SubResourceTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Functional/SubResource/SubResourceTest.php b/tests/Functional/SubResource/SubResourceTest.php index 9bfa9c8bede..f9bc7d0363c 100644 --- a/tests/Functional/SubResource/SubResourceTest.php +++ b/tests/Functional/SubResource/SubResourceTest.php @@ -299,7 +299,7 @@ public function testGetEmbeddedRelationAtThirdLevel(): void $data = $response->toArray(); $this->assertSame('/contexts/ThirdLevel2', $data['@context']); $this->assertSame('/dummies/1/related_dummies/1/third_level', $data['@id']); - $this->assertSame('ThirdLevel', $data['@type']); + $this->assertSame('ThirdLevel2', $data['@type']); $this->assertSame('/fourth_levels/1', $data['fourthLevel']); $this->assertSame(1, $data['id']); $this->assertSame(3, $data['level']); @@ -517,9 +517,9 @@ public function testOneToOneFromOwningSide(): void $this->assertResponseStatusCodeSame(200); $this->assertJsonContains([ - '@context' => '/contexts/Dummy2', + '@context' => '/contexts/Dummy3', '@id' => '/related_owning_dummies/1/owned_dummy', - '@type' => 'Dummy2', + '@type' => 'Dummy3', 'name' => 'plop', 'relatedOwningDummy' => '/related_owning_dummies/1', 'relatedOwnedDummy' => null, From 588095f6d50c86c88a800859a2e00ffc2812b1b1 Mon Sep 17 00:00:00 2001 From: soyuka Date: Wed, 1 Jul 2026 08:49:14 +0200 Subject: [PATCH 11/11] test(doctrine): give ODM Company /rooms subresource a distinct short name Company shares its short name across three ODM #[ApiResource] blocks vs two on ORM, so automatic dedup yielded Company3 on ODM but Company2 on ORM for /employees/{employeeId}/company. Naming the ODM-only /rooms block keeps that endpoint at Company2 on both configs. --- tests/Fixtures/TestBundle/Document/Company.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Fixtures/TestBundle/Document/Company.php b/tests/Fixtures/TestBundle/Document/Company.php index aa6b3e7ae7d..98000290b8c 100644 --- a/tests/Fixtures/TestBundle/Document/Company.php +++ b/tests/Fixtures/TestBundle/Document/Company.php @@ -26,6 +26,7 @@ #[Get] #[Post] #[ApiResource( + shortName: 'CompanyByRoom', uriTemplate: '/employees/{employeeId}/rooms/{roomId}/company/{companyId}', uriVariables: ['employeeId' => ['from_class' => Employee::class, 'from_property' => 'company']] )]