From f7f605dc8b798b975d2286c970c9091436d7f890 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Mon, 7 Oct 2024 10:48:31 +0200 Subject: [PATCH 1/8] fix: check that api-platform/ramsey-uuid is installed before registering related services (#6696) * fix: check that api-platform/ramsey-uuid is installed before registering related services * fix: suggest in Symfony's package composer.json --- .../Bundle/DependencyInjection/ApiPlatformExtension.php | 3 ++- src/Symfony/composer.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index c927b28e7c..f8f233fa69 100644 --- a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php +++ b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php @@ -38,6 +38,7 @@ use ApiPlatform\Metadata\UriVariableTransformerInterface; use ApiPlatform\Metadata\UrlGeneratorInterface; use ApiPlatform\Problem\Serializer\ConstraintViolationListNormalizer; +use ApiPlatform\RamseyUuid\Serializer\UuidDenormalizer; use ApiPlatform\State\ApiResource\Error; use ApiPlatform\State\ParameterProviderInterface; use ApiPlatform\State\ProcessorInterface; @@ -221,7 +222,7 @@ private function registerCommonConfiguration(ContainerBuilder $container, array $loader->load('api.xml'); $loader->load('filter.xml'); - if (class_exists(Uuid::class)) { + if (class_exists(UuidDenormalizer::class) && class_exists(Uuid::class)) { $loader->load('ramsey_uuid.xml'); } diff --git a/src/Symfony/composer.json b/src/Symfony/composer.json index acbf0d6be9..97a6fba5b2 100644 --- a/src/Symfony/composer.json +++ b/src/Symfony/composer.json @@ -60,10 +60,10 @@ "api-platform/doctrine-odm": "To support MongoDB. Only versions 4.0 and later are supported.", "api-platform/elasticsearch": "To support Elasticsearch.", "api-platform/graphql": "To support GraphQL.", + "api-platform/ramsey-uuid": "To support Ramsey's UUID identifiers.", "ocramius/package-versions": "To display the API Platform's version in the debug bar.", "phpstan/phpdoc-parser": "To support extracting metadata from PHPDoc.", "psr/cache-implementation": "To use metadata caching.", - "ramsey/uuid": "To support Ramsey's UUID identifiers.", "symfony/cache": "To have metadata caching when using Symfony integration.", "symfony/config": "To load XML configuration files.", "symfony/expression-language": "To use authorization and mercure advanced features.", From 69049235ad2684c2d2ccdba057ca6f69cd2f0c8c Mon Sep 17 00:00:00 2001 From: Danny v W Date: Mon, 7 Oct 2024 17:28:06 +0200 Subject: [PATCH 2/8] fix: remove hydra prefix (#6699) Co-authored-by: Danny van Wijk --- src/Hydra/Serializer/ConstraintViolationListNormalizer.php | 4 ++-- src/Symfony/Bundle/Resources/config/hydra.xml | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Hydra/Serializer/ConstraintViolationListNormalizer.php b/src/Hydra/Serializer/ConstraintViolationListNormalizer.php index e6b47af934..c4e43b667d 100644 --- a/src/Hydra/Serializer/ConstraintViolationListNormalizer.php +++ b/src/Hydra/Serializer/ConstraintViolationListNormalizer.php @@ -29,7 +29,7 @@ final class ConstraintViolationListNormalizer extends AbstractConstraintViolatio use HydraPrefixTrait; public const FORMAT = 'jsonld'; - public function __construct(private readonly UrlGeneratorInterface|LegacyUrlGeneratorInterface $urlGenerator, ?array $serializePayloadFields = null, ?NameConverterInterface $nameConverter = null) + public function __construct(private readonly UrlGeneratorInterface|LegacyUrlGeneratorInterface $urlGenerator, ?array $serializePayloadFields = null, ?NameConverterInterface $nameConverter = null, private readonly ?array $defaultContext = []) { parent::__construct($serializePayloadFields, $nameConverter); } @@ -46,7 +46,7 @@ public function normalize(mixed $object, ?string $format = null, array $context return $violations; } - $hydraPrefix = $this->getHydraPrefix($context); + $hydraPrefix = $this->getHydraPrefix($context + $this->defaultContext); return [ '@context' => $this->urlGenerator->generate('api_jsonld_context', ['shortName' => 'ConstraintViolationList']), diff --git a/src/Symfony/Bundle/Resources/config/hydra.xml b/src/Symfony/Bundle/Resources/config/hydra.xml index 22e8f84def..b052591432 100644 --- a/src/Symfony/Bundle/Resources/config/hydra.xml +++ b/src/Symfony/Bundle/Resources/config/hydra.xml @@ -26,6 +26,7 @@ %api_platform.validator.serialize_payload_fields% + %api_platform.serializer.default_context% From 3ca599158139d56fbd6ee66f2de3e586120d735c Mon Sep 17 00:00:00 2001 From: Antoine Bluchet Date: Wed, 9 Oct 2024 11:00:06 +0200 Subject: [PATCH 3/8] fix(hydra): hydra_prefix on errors (#6704) * fix(hydra): hydra_prefix on errors * doc: remove warning from changelog * review --- CHANGELOG.md | 3 - features/hydra/error.feature | 4 ++ src/JsonLd/ContextBuilder.php | 3 +- src/JsonLd/Serializer/ErrorNormalizer.php | 58 +++++++++++++++++++ .../Bundle/Resources/config/jsonld.xml | 8 ++- 5 files changed, 70 insertions(+), 6 deletions(-) create mode 100644 src/JsonLd/Serializer/ErrorNormalizer.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 1dc75f10a5..e62036d851 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,9 +12,6 @@ * [afe7d47d7](https://github.com/api-platform/core/commit/afe7d47d7b7ba6c8591bfb60137a65d1fa1fe38f) fix(metadata): passing class as parameter in XML ApiResource's definition (#6659) * [b93ee467c](https://github.com/api-platform/core/commit/b93ee467c69253e0cfe60e75b48a5c7aa683474a) fix(metadata): overwriting XML ApiResource definition by YAML ApiResource definition (#6660) -> [!WARNING] -> Hydra prefix on errors is breaking, read `title` not `hydra:title`. The `hydra_prefix` flag doesn't apply to errors as it provided redundant information (both `hydra:title` and `title` were available) - ## v3.4.1 ### Bug fixes diff --git a/features/hydra/error.feature b/features/hydra/error.feature index 6b473c7b5b..1663597c76 100644 --- a/features/hydra/error.feature +++ b/features/hydra/error.feature @@ -17,8 +17,10 @@ Feature: Error handling And the header "Link" should contain '; rel="http://www.w3.org/ns/json-ld#error"' And the JSON node "type" should exist And the JSON node "title" should be equal to "An error occurred" + And the JSON node "hydra:title" should be equal to "An error occurred" And the JSON node "detail" should exist And the JSON node "description" should exist + And the JSON node "hydra:description" should exist And the JSON node "trace" should exist And the JSON node "status" should exist And the JSON node "@context" should not exist @@ -47,6 +49,8 @@ Feature: Error handling ], "detail": "name: This value should not be blank.", "title": "An error occurred", + "hydra:title": "An error occurred", + "hydra:description": "name: This value should not be blank.", "description": "name: This value should not be blank.", "type": "/validation_errors/c1051bb4-d103-4f74-8988-acbcafc7fdc3" } diff --git a/src/JsonLd/ContextBuilder.php b/src/JsonLd/ContextBuilder.php index 0fe1063636..5cb591d0fa 100644 --- a/src/JsonLd/ContextBuilder.php +++ b/src/JsonLd/ContextBuilder.php @@ -14,7 +14,6 @@ namespace ApiPlatform\JsonLd; use ApiPlatform\JsonLd\Serializer\HydraPrefixTrait; -use ApiPlatform\Metadata\Error; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\HttpOperation; use ApiPlatform\Metadata\IriConverterInterface; @@ -185,7 +184,7 @@ private function getResourceContextWithShortname(string $resourceClass, int $ref } } - if (false === ($this->defaultContext[self::HYDRA_CONTEXT_HAS_PREFIX] ?? true) || $operation instanceof Error) { + if (false === ($this->defaultContext[self::HYDRA_CONTEXT_HAS_PREFIX] ?? true)) { return ['http://www.w3.org/ns/hydra/context.jsonld', $context]; } diff --git a/src/JsonLd/Serializer/ErrorNormalizer.php b/src/JsonLd/Serializer/ErrorNormalizer.php new file mode 100644 index 0000000000..b5bf3eec6c --- /dev/null +++ b/src/JsonLd/Serializer/ErrorNormalizer.php @@ -0,0 +1,58 @@ + + * + * 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\JsonLd\Serializer; + +use ApiPlatform\State\ApiResource\Error; +use ApiPlatform\Symfony\Validator\Exception\ValidationException as SymfonyValidationException; +use ApiPlatform\Validator\Exception\ValidationException; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; + +final class ErrorNormalizer implements NormalizerInterface +{ + use HydraPrefixTrait; + + public function __construct(private readonly NormalizerInterface $inner, private readonly array $defaultContext = []) + { + } + + public function normalize(mixed $object, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null + { + $normalized = $this->inner->normalize($object, $format, $context); + $hydraPrefix = $this->getHydraPrefix($context + $this->defaultContext); + if (!$hydraPrefix) { + return $normalized; + } + + if (isset($normalized['description'])) { + $normalized['hydra:description'] = $normalized['description']; + } + + if (isset($normalized['title'])) { + $normalized['hydra:title'] = $normalized['title']; + } + + return $normalized; + } + + public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool + { + return $this->inner->supportsNormalization($data, $format, $context) + && (is_a($data, Error::class) || is_a($data, ValidationException::class) || is_a($data, SymfonyValidationException::class)); + } + + public function getSupportedTypes(?string $format): array + { + return $this->inner->getSupportedTypes($format); + } +} diff --git a/src/Symfony/Bundle/Resources/config/jsonld.xml b/src/Symfony/Bundle/Resources/config/jsonld.xml index 0a1b8eb052..6bb7ea31d1 100644 --- a/src/Symfony/Bundle/Resources/config/jsonld.xml +++ b/src/Symfony/Bundle/Resources/config/jsonld.xml @@ -36,6 +36,12 @@ + + + %api_platform.serializer.default_context% + + + @@ -46,7 +52,7 @@ - + From fbb53e5e35ca0ec3de26ddc7de7ea4d1dda5c20b Mon Sep 17 00:00:00 2001 From: Antoine Bluchet Date: Thu, 10 Oct 2024 12:00:18 +0200 Subject: [PATCH 4/8] fix(symfony): metadata aware name converter has 0 arguments by default (#6711) Since https://github.com/symfony/symfony/commit/ce228d3a7a6448e5f214033a0cd2710177085f29#diff-f53370286252d2326a377eafd4bba05112518e0c8bf3aedda670fd76a254e9fa the serializer.name_converter.metadata_aware is extending an abstract service. Therefore there are no arguments by default. We relied on the number of arguments to inject API Platform's configured name converter. --- .../MetadataAwareNameConverterPass.php | 14 ++++++++++---- .../MetadataAwareNameConverterPassTest.php | 19 +++++++++++++++++++ 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Bundle/DependencyInjection/Compiler/MetadataAwareNameConverterPass.php b/src/Symfony/Bundle/DependencyInjection/Compiler/MetadataAwareNameConverterPass.php index a1b93b067f..a611e305ec 100644 --- a/src/Symfony/Bundle/DependencyInjection/Compiler/MetadataAwareNameConverterPass.php +++ b/src/Symfony/Bundle/DependencyInjection/Compiler/MetadataAwareNameConverterPass.php @@ -39,14 +39,20 @@ public function process(ContainerBuilder $container): void } $definition = $container->getDefinition('serializer.name_converter.metadata_aware'); - $num = \count($definition->getArguments()); + $key = '$fallbackNameConverter'; + $arguments = $definition->getArguments(); + if (false === \array_key_exists($key, $arguments)) { + $key = 1; + } if ($container->hasAlias('api_platform.name_converter')) { $nameConverter = new Reference((string) $container->getAlias('api_platform.name_converter')); - if (1 === $num) { + + // old symfony versions + if (false === \array_key_exists($key, $arguments)) { $definition->addArgument($nameConverter); - } elseif (1 < $num && null === $definition->getArgument(1)) { - $definition->setArgument(1, $nameConverter); + } elseif (null === $definition->getArgument($key)) { + $definition->setArgument($key, $nameConverter); } } diff --git a/tests/Symfony/Bundle/DependencyInjection/Compiler/MetadataAwareNameConverterPassTest.php b/tests/Symfony/Bundle/DependencyInjection/Compiler/MetadataAwareNameConverterPassTest.php index 2ecfd99bc4..d069cc8b39 100644 --- a/tests/Symfony/Bundle/DependencyInjection/Compiler/MetadataAwareNameConverterPassTest.php +++ b/tests/Symfony/Bundle/DependencyInjection/Compiler/MetadataAwareNameConverterPassTest.php @@ -101,4 +101,23 @@ public function testProcessOnlyOneArg(): void $pass->process($containerBuilderProphecy->reveal()); } + + public function testProcessWithAbstractMetadataAware(): void + { + $pass = new MetadataAwareNameConverterPass(); + + $definition = $this->prophesize(Definition::class); + $definition->getArguments()->willReturn(['$metadataFactory' => [], '$fallbackNameConverter' => null])->shouldBeCalled(); + $definition->getArgument('$fallbackNameConverter')->willReturn(null)->shouldBeCalled(); + $definition->setArgument('$fallbackNameConverter', new Reference('app.name_converter'))->willReturn($definition)->shouldBeCalled(); + + $containerBuilderProphecy = $this->prophesize(ContainerBuilder::class); + $containerBuilderProphecy->hasDefinition('serializer.name_converter.metadata_aware')->willReturn(true)->shouldBeCalled(); + $containerBuilderProphecy->hasAlias('api_platform.name_converter')->shouldBeCalled()->willReturn(true); + $containerBuilderProphecy->getAlias('api_platform.name_converter')->shouldBeCalled()->willReturn(new Alias('app.name_converter')); + $containerBuilderProphecy->setAlias('api_platform.name_converter', 'serializer.name_converter.metadata_aware')->shouldBeCalled(); + $containerBuilderProphecy->getDefinition('serializer.name_converter.metadata_aware')->shouldBeCalled()->willReturn($definition); + + $pass->process($containerBuilderProphecy->reveal()); + } } From 439c188ea1685676d5e705a49a4b835f35a84d72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20H=C3=A9bert?= Date: Fri, 11 Oct 2024 10:40:13 +0200 Subject: [PATCH 5/8] fix(laravel): match integer type (#6715) --- .../Factory/Property/EloquentPropertyMetadataFactory.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Laravel/Eloquent/Metadata/Factory/Property/EloquentPropertyMetadataFactory.php b/src/Laravel/Eloquent/Metadata/Factory/Property/EloquentPropertyMetadataFactory.php index 83906610a7..b0aee59ee8 100644 --- a/src/Laravel/Eloquent/Metadata/Factory/Property/EloquentPropertyMetadataFactory.php +++ b/src/Laravel/Eloquent/Metadata/Factory/Property/EloquentPropertyMetadataFactory.php @@ -71,6 +71,7 @@ public function create(string $resourceClass, string $property, array $options = // see https://laravel.com/docs/11.x/eloquent-mutators#attribute-casting $builtinType = $p['cast'] ?? $p['type']; $type = match ($builtinType) { + 'integer' => new Type(Type::BUILTIN_TYPE_INT, $p['nullable']), 'double', 'real' => new Type(Type::BUILTIN_TYPE_FLOAT, $p['nullable']), 'datetime', 'date', 'timestamp' => new Type(Type::BUILTIN_TYPE_OBJECT, $p['nullable'], \DateTime::class), 'immutable_datetime', 'immutable_date' => new Type(Type::BUILTIN_TYPE_OBJECT, $p['nullable'], \DateTimeImmutable::class), From 4ad7a50aaabf0d85e2eb5bb3a6d4ef8d5b7b39a7 Mon Sep 17 00:00:00 2001 From: cay89 Date: Fri, 11 Oct 2024 10:56:06 +0200 Subject: [PATCH 6/8] fix(laravel): openapi Options binding If the settings are not passed, the UI elements required for login will not appear on the API documentation page. --- src/Laravel/ApiPlatformProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Laravel/ApiPlatformProvider.php b/src/Laravel/ApiPlatformProvider.php index 09eaf07c8a..933c14642b 100644 --- a/src/Laravel/ApiPlatformProvider.php +++ b/src/Laravel/ApiPlatformProvider.php @@ -799,7 +799,7 @@ public function register(): void $app->make(SchemaFactoryInterface::class), null, $config->get('api-platform.formats'), - null, // ?Options $openApiOptions = null, + $app->make(Options::class), $app->make(PaginationOptions::class), // ?PaginationOptions $paginationOptions = null, // ?RouterInterface $router = null ); From 2e8287dad0c0315dd6527279a6359c0a22f40d93 Mon Sep 17 00:00:00 2001 From: soyuka Date: Fri, 27 Sep 2024 17:20:10 +0200 Subject: [PATCH 7/8] fix(laravel): allow serializer attributes through ApiProperty --- src/Laravel/ApiPlatformProvider.php | 13 +- ...tPropertyNameCollectionMetadataFactory.php | 6 +- src/Laravel/Tests/JsonApiTest.php | 12 ++ src/Laravel/Tests/JsonLdTest.php | 10 ++ .../workbench/app/Models/WithAccessor.php | 12 +- .../app/Models/WithAccessorRelation.php | 26 +++ .../factories/WithAccessorFactory.php | 1 + .../factories/WithAccessorRelationFactory.php | 44 +++++ ..._24_065934_create_with_accessors_table.php | 9 + src/Metadata/ApiProperty.php | 68 +++++--- .../Extractor/Adapter/XmlPropertyAdapter.php | 3 +- .../Mapping/Loader/PropertyMetadataLoader.php | 161 ++++++++++++++++++ .../Tests/Fixtures/Model/HasRelation.php | 26 +++ .../Tests/Fixtures/Model/Relation.php | 21 +++ .../Loader/PropertyMetadataLoaderTest.php | 47 +++++ src/Symfony/Bundle/ApiPlatformBundle.php | 2 + .../Compiler/SerializerMappingLoaderPass.php | 28 +++ src/Symfony/Bundle/Resources/config/api.xml | 4 + .../Symfony/Bundle/ApiPlatformBundleTest.php | 2 + 19 files changed, 469 insertions(+), 26 deletions(-) create mode 100644 src/Laravel/workbench/app/Models/WithAccessorRelation.php create mode 100644 src/Laravel/workbench/database/factories/WithAccessorRelationFactory.php create mode 100644 src/Serializer/Mapping/Loader/PropertyMetadataLoader.php create mode 100644 src/Serializer/Tests/Fixtures/Model/HasRelation.php create mode 100644 src/Serializer/Tests/Fixtures/Model/Relation.php create mode 100644 src/Serializer/Tests/Mapping/Loader/PropertyMetadataLoaderTest.php create mode 100644 src/Symfony/Bundle/DependencyInjection/Compiler/SerializerMappingLoaderPass.php diff --git a/src/Laravel/ApiPlatformProvider.php b/src/Laravel/ApiPlatformProvider.php index 933c14642b..0b9f991c37 100644 --- a/src/Laravel/ApiPlatformProvider.php +++ b/src/Laravel/ApiPlatformProvider.php @@ -165,6 +165,7 @@ use ApiPlatform\Serializer\ItemNormalizer; use ApiPlatform\Serializer\JsonEncoder; use ApiPlatform\Serializer\Mapping\Factory\ClassMetadataFactory as SerializerClassMetadataFactory; +use ApiPlatform\Serializer\Mapping\Loader\PropertyMetadataLoader; use ApiPlatform\Serializer\Parameter\SerializerFilterParameterProvider; use ApiPlatform\Serializer\SerializerContextBuilder; use ApiPlatform\State\CallableProcessor; @@ -206,6 +207,7 @@ use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader; +use Symfony\Component\Serializer\Mapping\Loader\LoaderChain; use Symfony\Component\Serializer\Mapping\Loader\LoaderInterface; use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter; @@ -244,8 +246,15 @@ public function register(): void $this->app->bind(LoaderInterface::class, AttributeLoader::class); $this->app->bind(ClassMetadataFactoryInterface::class, ClassMetadataFactory::class); - $this->app->singleton(ClassMetadataFactory::class, function () { - return new ClassMetadataFactory(new AttributeLoader()); + $this->app->singleton(ClassMetadataFactory::class, function (Application $app) { + return new ClassMetadataFactory( + new LoaderChain([ + new PropertyMetadataLoader( + $app->make(PropertyNameCollectionFactoryInterface::class), + ), + new AttributeLoader(), + ]) + ); }); $this->app->singleton(SerializerClassMetadataFactory::class, function (Application $app) { diff --git a/src/Laravel/Eloquent/Metadata/Factory/Property/EloquentPropertyNameCollectionMetadataFactory.php b/src/Laravel/Eloquent/Metadata/Factory/Property/EloquentPropertyNameCollectionMetadataFactory.php index 5f0811d23e..2db75b3972 100644 --- a/src/Laravel/Eloquent/Metadata/Factory/Property/EloquentPropertyNameCollectionMetadataFactory.php +++ b/src/Laravel/Eloquent/Metadata/Factory/Property/EloquentPropertyNameCollectionMetadataFactory.php @@ -39,8 +39,12 @@ public function create(string $resourceClass, array $options = []): PropertyName return $this->decorated?->create($resourceClass, $options) ?? new PropertyNameCollection(); } - $refl = new \ReflectionClass($resourceClass); try { + $refl = new \ReflectionClass($resourceClass); + if ($refl->isAbstract()) { + return $this->decorated?->create($resourceClass, $options) ?? new PropertyNameCollection(); + } + $model = $refl->newInstanceWithoutConstructor(); } catch (\ReflectionException) { return $this->decorated?->create($resourceClass, $options) ?? new PropertyNameCollection(); diff --git a/src/Laravel/Tests/JsonApiTest.php b/src/Laravel/Tests/JsonApiTest.php index 1c690dbabc..74833c3ebc 100644 --- a/src/Laravel/Tests/JsonApiTest.php +++ b/src/Laravel/Tests/JsonApiTest.php @@ -23,6 +23,7 @@ use Workbench\App\Models\Book; use Workbench\Database\Factories\AuthorFactory; use Workbench\Database\Factories\BookFactory; +use Workbench\Database\Factories\WithAccessorFactory; class JsonApiTest extends TestCase { @@ -197,4 +198,15 @@ public function testDeleteBook(): void $response->assertStatus(204); $this->assertNull(Book::find($book->id)); } + + public function testRelationWithGroups(): void + { + WithAccessorFactory::new()->create(); + $response = $this->get('/api/with_accessors/1', ['accept' => 'application/vnd.api+json']); + $content = $response->json(); + $this->assertArrayHasKey('data', $content); + $this->assertArrayHasKey('relationships', $content['data']); + $this->assertArrayHasKey('relation', $content['data']['relationships']); + $this->assertArrayHasKey('data', $content['data']['relationships']['relation']); + } } diff --git a/src/Laravel/Tests/JsonLdTest.php b/src/Laravel/Tests/JsonLdTest.php index a803df9da0..46039ebe68 100644 --- a/src/Laravel/Tests/JsonLdTest.php +++ b/src/Laravel/Tests/JsonLdTest.php @@ -26,6 +26,7 @@ use Workbench\Database\Factories\CommentFactory; use Workbench\Database\Factories\PostFactory; use Workbench\Database\Factories\SluggableFactory; +use Workbench\Database\Factories\WithAccessorFactory; class JsonLdTest extends TestCase { @@ -327,4 +328,13 @@ public function testError(): void $content = $response->json(); $this->assertArrayHasKey('trace', $content); } + + public function testRelationWithGroups(): void + { + WithAccessorFactory::new()->create(); + $response = $this->get('/api/with_accessors/1', ['accept' => 'application/ld+json']); + $content = $response->json(); + $this->assertArrayHasKey('relation', $content); + $this->assertArrayHasKey('name', $content['relation']); + } } diff --git a/src/Laravel/workbench/app/Models/WithAccessor.php b/src/Laravel/workbench/app/Models/WithAccessor.php index 74d4d4f282..0c19809fa8 100644 --- a/src/Laravel/workbench/app/Models/WithAccessor.php +++ b/src/Laravel/workbench/app/Models/WithAccessor.php @@ -13,18 +13,28 @@ namespace Workbench\App\Models; +use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\ApiResource; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Symfony\Component\Serializer\Attribute\Groups; -#[ApiResource] +#[ApiResource(normalizationContext: ['groups' => ['read']])] class WithAccessor extends Model { use HasFactory; protected $hidden = ['created_at', 'updated_at', 'id']; + #[ApiProperty(serialize: [new Groups(['read'])])] + public function relation(): BelongsTo + { + return $this->belongsTo(WithAccessorRelation::class); + } + + #[ApiProperty(serialize: [new Groups(['read'])])] protected function name(): Attribute { return Attribute::make( diff --git a/src/Laravel/workbench/app/Models/WithAccessorRelation.php b/src/Laravel/workbench/app/Models/WithAccessorRelation.php new file mode 100644 index 0000000000..7cad5bbd45 --- /dev/null +++ b/src/Laravel/workbench/app/Models/WithAccessorRelation.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Workbench\App\Models; + +use ApiPlatform\Metadata\ApiResource; +use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Model; +use Symfony\Component\Serializer\Attribute\Groups; + +#[Groups(['read'])] +#[ApiResource(operations: [])] +class WithAccessorRelation extends Model +{ + use HasFactory; +} diff --git a/src/Laravel/workbench/database/factories/WithAccessorFactory.php b/src/Laravel/workbench/database/factories/WithAccessorFactory.php index f5c026199d..9420af363d 100644 --- a/src/Laravel/workbench/database/factories/WithAccessorFactory.php +++ b/src/Laravel/workbench/database/factories/WithAccessorFactory.php @@ -39,6 +39,7 @@ public function definition(): array { return [ 'name' => strtolower(fake()->name()), + 'relation_id' => WithAccessorRelationFactory::new(), ]; } } diff --git a/src/Laravel/workbench/database/factories/WithAccessorRelationFactory.php b/src/Laravel/workbench/database/factories/WithAccessorRelationFactory.php new file mode 100644 index 0000000000..7352776cb2 --- /dev/null +++ b/src/Laravel/workbench/database/factories/WithAccessorRelationFactory.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Workbench\Database\Factories; + +use Illuminate\Database\Eloquent\Factories\Factory; +use Workbench\App\Models\WithAccessorRelation; + +/** + * @template TModel of \Workbench\App\Models\WithAccessorRelation + * + * @extends \Illuminate\Database\Eloquent\Factories\Factory + */ +class WithAccessorRelationFactory extends Factory +{ + /** + * The name of the factory's corresponding model. + * + * @var class-string + */ + protected $model = WithAccessorRelation::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'name' => strtolower(fake()->name()), + ]; + } +} diff --git a/src/Laravel/workbench/database/migrations/2024_09_24_065934_create_with_accessors_table.php b/src/Laravel/workbench/database/migrations/2024_09_24_065934_create_with_accessors_table.php index 58b640ecc9..7636f04818 100644 --- a/src/Laravel/workbench/database/migrations/2024_09_24_065934_create_with_accessors_table.php +++ b/src/Laravel/workbench/database/migrations/2024_09_24_065934_create_with_accessors_table.php @@ -21,9 +21,17 @@ */ public function up(): void { + Schema::create('with_accessor_relations', function (Blueprint $table): void { + $table->id(); + $table->string('name'); + $table->timestamps(); + }); + Schema::create('with_accessors', function (Blueprint $table): void { $table->id(); $table->string('name'); + $table->integer('relation_id')->unsigned(); + $table->foreign('relation_id')->references('id')->on('with_accessor_relations'); $table->timestamps(); }); } @@ -34,5 +42,6 @@ public function up(): void public function down(): void { Schema::dropIfExists('with_accessors'); + Schema::dropIfExists('with_accessors_relation'); } }; diff --git a/src/Metadata/ApiProperty.php b/src/Metadata/ApiProperty.php index b2cbca4eec..1839f220b2 100644 --- a/src/Metadata/ApiProperty.php +++ b/src/Metadata/ApiProperty.php @@ -14,6 +14,12 @@ namespace ApiPlatform\Metadata; use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\Serializer\Attribute\Context; +use Symfony\Component\Serializer\Attribute\Groups; +use Symfony\Component\Serializer\Attribute\Ignore; +use Symfony\Component\Serializer\Attribute\MaxDepth; +use Symfony\Component\Serializer\Attribute\SerializedName; +use Symfony\Component\Serializer\Attribute\SerializedPath; /** * ApiProperty annotation. @@ -23,24 +29,28 @@ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::TARGET_PARAMETER | \Attribute::TARGET_CLASS_CONSTANT | \Attribute::TARGET_CLASS)] final class ApiProperty { + private ?array $types; + private ?array $serialize; + /** - * @param bool|null $readableLink https://api-platform.com/docs/core/serialization/#force-iri-with-relations-of-the-same-type-parentchilds-relations - * @param bool|null $writableLink https://api-platform.com/docs/core/serialization/#force-iri-with-relations-of-the-same-type-parentchilds-relations - * @param bool|null $required https://api-platform.com/docs/admin/validation/#client-side-validation - * @param bool|null $identifier https://api-platform.com/docs/core/identifiers/ - * @param mixed $example https://api-platform.com/docs/core/openapi/#using-the-openapi-and-swagger-contexts - * @param string|null $deprecationReason https://api-platform.com/docs/core/deprecations/#deprecating-resource-classes-operations-and-properties - * @param bool|null $fetchEager https://api-platform.com/docs/core/performance/#eager-loading - * @param array|null $jsonldContext https://api-platform.com/docs/core/extending-jsonld-context/#extending-json-ld-and-hydra-contexts - * @param array|null $openapiContext https://api-platform.com/docs/core/openapi/#using-the-openapi-and-swagger-contexts - * @param bool|null $push https://api-platform.com/docs/core/push-relations/ - * @param string|\Stringable|null $security https://api-platform.com/docs/core/security - * @param string|\Stringable|null $securityPostDenormalize https://api-platform.com/docs/core/security/#executing-access-control-rules-after-denormalization - * @param string[] $types the RDF types of this property - * @param string[] $iris - * @param Type[] $builtinTypes - * @param string|null $uriTemplate (experimental) whether to return the subRessource collection IRI instead of an iterable of IRI - * @param string|null $property The property name + * @param bool|null $readableLink https://api-platform.com/docs/core/serialization/#force-iri-with-relations-of-the-same-type-parentchilds-relations + * @param bool|null $writableLink https://api-platform.com/docs/core/serialization/#force-iri-with-relations-of-the-same-type-parentchilds-relations + * @param bool|null $required https://api-platform.com/docs/admin/validation/#client-side-validation + * @param bool|null $identifier https://api-platform.com/docs/core/identifiers/ + * @param mixed $example https://api-platform.com/docs/core/openapi/#using-the-openapi-and-swagger-contexts + * @param string|null $deprecationReason https://api-platform.com/docs/core/deprecations/#deprecating-resource-classes-operations-and-properties + * @param bool|null $fetchEager https://api-platform.com/docs/core/performance/#eager-loading + * @param array|null $jsonldContext https://api-platform.com/docs/core/extending-jsonld-context/#extending-json-ld-and-hydra-contexts + * @param array|null $openapiContext https://api-platform.com/docs/core/openapi/#using-the-openapi-and-swagger-contexts + * @param bool|null $push https://api-platform.com/docs/core/push-relations/ + * @param string|\Stringable|null $security https://api-platform.com/docs/core/security + * @param string|\Stringable|null $securityPostDenormalize https://api-platform.com/docs/core/security/#executing-access-control-rules-after-denormalization + * @param string[] $types the RDF types of this property + * @param string[] $iris + * @param Type[] $builtinTypes + * @param string|null $uriTemplate (experimental) whether to return the subRessource collection IRI instead of an iterable of IRI + * @param string|null $property The property name + * @param Context|Groups|Ignore|SerializedName|SerializedPath|MaxDepth|array $serialize Serializer attributes */ public function __construct( private ?string $description = null, @@ -193,7 +203,7 @@ public function __construct( * */ private string|\Stringable|null $securityPostDenormalize = null, - private array|string|null $types = null, + array|string|null $types = null, /* * The related php types. */ @@ -205,11 +215,11 @@ public function __construct( private ?string $uriTemplate = null, private ?string $property = null, private ?string $policy = null, + array|Context|Groups|Ignore|SerializedName|SerializedPath|MaxDepth|null $serialize = null, private array $extraProperties = [], ) { - if (\is_string($types)) { - $this->types = (array) $types; - } + $this->types = \is_string($types) ? (array) $types : $types; + $this->serialize = \is_array($serialize) ? $serialize : (array) $serialize; } public function getProperty(): ?string @@ -600,4 +610,20 @@ public function withPolicy(?string $policy): static return $self; } + + public function getSerialize(): ?array + { + return $this->serialize; + } + + /** + * @param Context|Groups|Ignore|SerializedName|SerializedPath|MaxDepth|array $serialize + */ + public function withSerialize(array|Context|Groups|Ignore|SerializedName|SerializedPath|MaxDepth $serialize): static + { + $self = clone $this; + $self->serialize = (array) $serialize; + + return $self; + } } diff --git a/src/Metadata/Tests/Extractor/Adapter/XmlPropertyAdapter.php b/src/Metadata/Tests/Extractor/Adapter/XmlPropertyAdapter.php index 950ba56fd4..5af03c7119 100644 --- a/src/Metadata/Tests/Extractor/Adapter/XmlPropertyAdapter.php +++ b/src/Metadata/Tests/Extractor/Adapter/XmlPropertyAdapter.php @@ -47,7 +47,8 @@ final class XmlPropertyAdapter implements PropertyAdapterInterface 'property', ]; - private const EXCLUDE = ['policy']; + // TODO: add serialize support for XML (policy is Laravel-only) + private const EXCLUDE = ['policy', 'serialize']; /** * {@inheritdoc} diff --git a/src/Serializer/Mapping/Loader/PropertyMetadataLoader.php b/src/Serializer/Mapping/Loader/PropertyMetadataLoader.php new file mode 100644 index 0000000000..87d144a7a6 --- /dev/null +++ b/src/Serializer/Mapping/Loader/PropertyMetadataLoader.php @@ -0,0 +1,161 @@ + + * + * 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\Serializer\Mapping\Loader; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; +use Illuminate\Database\Eloquent\Model; +use Symfony\Component\Serializer\Attribute\Context; +use Symfony\Component\Serializer\Attribute\DiscriminatorMap; +use Symfony\Component\Serializer\Attribute\Groups; +use Symfony\Component\Serializer\Attribute\Ignore; +use Symfony\Component\Serializer\Attribute\MaxDepth; +use Symfony\Component\Serializer\Attribute\SerializedName; +use Symfony\Component\Serializer\Attribute\SerializedPath; +use Symfony\Component\Serializer\Mapping\AttributeMetadata; +use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface; +use Symfony\Component\Serializer\Mapping\ClassDiscriminatorMapping; +use Symfony\Component\Serializer\Mapping\ClassMetadataInterface; +use Symfony\Component\Serializer\Mapping\Loader\LoaderInterface; + +/** + * Loader for PHP attributes using ApiProperty. + */ +final class PropertyMetadataLoader implements LoaderInterface +{ + public function __construct(private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory) + { + } + + public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool + { + $attributesMetadata = $classMetadata->getAttributesMetadata(); + // It's very weird to grab Eloquent's properties in that case as they're never serialized + // the Serializer makes a call on the abstract class, let's save some unneeded work with a condition + if (Model::class === $classMetadata->getName()) { + return false; + } + + $refl = $classMetadata->getReflectionClass(); + $attributes = []; + $classGroups = []; + $classContextAnnotation = null; + + foreach ($refl->getAttributes(ApiProperty::class) as $clAttr) { + $this->addAttributeMetadata($clAttr->newInstance(), $attributes); + } + + $attributesMetadata = $classMetadata->getAttributesMetadata(); + + foreach ($refl->getAttributes() as $a) { + $attribute = $a->newInstance(); + if ($attribute instanceof DiscriminatorMap) { + $classMetadata->setClassDiscriminatorMapping(new ClassDiscriminatorMapping( + $attribute->getTypeProperty(), + $attribute->getMapping() + )); + continue; + } + + if ($attribute instanceof Groups) { + $classGroups = $attribute->getGroups(); + + continue; + } + + if ($attribute instanceof Context) { + $classContextAnnotation = $attribute; + } + } + + foreach ($refl->getProperties() as $reflProperty) { + foreach ($reflProperty->getAttributes(ApiProperty::class) as $propAttr) { + $this->addAttributeMetadata($propAttr->newInstance()->withProperty($reflProperty->name), $attributes); + } + } + + foreach ($refl->getMethods() as $reflMethod) { + foreach ($reflMethod->getAttributes(ApiProperty::class) as $methodAttr) { + $this->addAttributeMetadata($methodAttr->newInstance()->withProperty($reflMethod->getName()), $attributes); + } + } + + foreach ($this->propertyNameCollectionFactory->create($classMetadata->getName()) as $propertyName) { + if (!isset($attributesMetadata[$propertyName])) { + $attributesMetadata[$propertyName] = new AttributeMetadata($propertyName); + $classMetadata->addAttributeMetadata($attributesMetadata[$propertyName]); + } + + foreach ($classGroups as $group) { + $attributesMetadata[$propertyName]->addGroup($group); + } + + if ($classContextAnnotation) { + $this->setAttributeContextsForGroups($classContextAnnotation, $attributesMetadata[$propertyName]); + } + + if (!isset($attributes[$propertyName])) { + continue; + } + + $attributeMetadata = $attributesMetadata[$propertyName]; + + // This code is adapted from Symfony\Component\Serializer\Mapping\Loader\AttributeLoader + foreach ($attributes[$propertyName] as $attr) { + if ($attr instanceof Groups) { + foreach ($attr->getGroups() as $group) { + $attributeMetadata->addGroup($group); + } + continue; + } + + match (true) { + $attr instanceof MaxDepth => $attributeMetadata->setMaxDepth($attr->getMaxDepth()), + $attr instanceof SerializedName => $attributeMetadata->setSerializedName($attr->getSerializedName()), + $attr instanceof SerializedPath => $attributeMetadata->setSerializedPath($attr->getSerializedPath()), + $attr instanceof Ignore => $attributeMetadata->setIgnore(true), + $attr instanceof Context => $this->setAttributeContextsForGroups($attr, $attributeMetadata), + default => null, + }; + } + } + + return true; + } + + /** + * @param ApiProperty[] $attributes + */ + private function addAttributeMetadata(ApiProperty $attribute, array &$attributes): void + { + if (($prop = $attribute->getProperty()) && ($value = $attribute->getSerialize())) { + $attributes[$prop] = $value; + } + } + + private function setAttributeContextsForGroups(Context $annotation, AttributeMetadataInterface $attributeMetadata): void + { + $context = $annotation->getContext(); + $groups = $annotation->getGroups(); + $normalizationContext = $annotation->getNormalizationContext(); + $denormalizationContext = $annotation->getDenormalizationContext(); + if ($normalizationContext || $context) { + $attributeMetadata->setNormalizationContextForGroups($normalizationContext ?: $context, $groups); + } + + if ($denormalizationContext || $context) { + $attributeMetadata->setDenormalizationContextForGroups($denormalizationContext ?: $context, $groups); + } + } +} diff --git a/src/Serializer/Tests/Fixtures/Model/HasRelation.php b/src/Serializer/Tests/Fixtures/Model/HasRelation.php new file mode 100644 index 0000000000..833fee5549 --- /dev/null +++ b/src/Serializer/Tests/Fixtures/Model/HasRelation.php @@ -0,0 +1,26 @@ + + * + * 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\Serializer\Tests\Fixtures\Model; + +use ApiPlatform\Metadata\ApiProperty; +use Symfony\Component\Serializer\Attribute\Groups; + +class HasRelation +{ + #[ApiProperty(serialize: [new Groups(['read'])])] + public function relation(): Relation + { + return new Relation(); + } +} diff --git a/src/Serializer/Tests/Fixtures/Model/Relation.php b/src/Serializer/Tests/Fixtures/Model/Relation.php new file mode 100644 index 0000000000..4d06da7d1b --- /dev/null +++ b/src/Serializer/Tests/Fixtures/Model/Relation.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\Serializer\Tests\Fixtures\Model; + +use Symfony\Component\Serializer\Attribute\Groups; + +#[Groups(['read'])] +class Relation +{ +} diff --git a/src/Serializer/Tests/Mapping/Loader/PropertyMetadataLoaderTest.php b/src/Serializer/Tests/Mapping/Loader/PropertyMetadataLoaderTest.php new file mode 100644 index 0000000000..27a8fc5593 --- /dev/null +++ b/src/Serializer/Tests/Mapping/Loader/PropertyMetadataLoaderTest.php @@ -0,0 +1,47 @@ + + * + * 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\Serializer\Tests\Mapping\Loader; + +use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; +use ApiPlatform\Metadata\Property\PropertyNameCollection; +use ApiPlatform\Serializer\Mapping\Loader\PropertyMetadataLoader; +use ApiPlatform\Serializer\Tests\Fixtures\Model\HasRelation; +use ApiPlatform\Serializer\Tests\Fixtures\Model\Relation; +use PHPUnit\Framework\TestCase; +use Symfony\Component\Serializer\Mapping\ClassMetadata; + +final class PropertyMetadataLoaderTest extends TestCase +{ + public function testCreateMappingForASetOfProperties(): void + { + $coll = $this->createStub(PropertyNameCollectionFactoryInterface::class); + $coll->method('create')->willReturn(new PropertyNameCollection(['relation'])); + $loader = new PropertyMetadataLoader($coll); + $classMetadata = new ClassMetadata(HasRelation::class); + $loader->loadClassMetadata($classMetadata); + $this->assertArrayHasKey('relation', $classMetadata->attributesMetadata); + $this->assertEquals(['read'], $classMetadata->attributesMetadata['relation']->getGroups()); + } + + public function testCreateMappingForAClass(): void + { + $coll = $this->createStub(PropertyNameCollectionFactoryInterface::class); + $coll->method('create')->willReturn(new PropertyNameCollection(['name'])); + $loader = new PropertyMetadataLoader($coll); + $classMetadata = new ClassMetadata(Relation::class); + $loader->loadClassMetadata($classMetadata); + $this->assertArrayHasKey('name', $classMetadata->attributesMetadata); + $this->assertEquals(['read'], $classMetadata->attributesMetadata['name']->getGroups()); + } +} diff --git a/src/Symfony/Bundle/ApiPlatformBundle.php b/src/Symfony/Bundle/ApiPlatformBundle.php index cd5d08e974..f7fc270f08 100644 --- a/src/Symfony/Bundle/ApiPlatformBundle.php +++ b/src/Symfony/Bundle/ApiPlatformBundle.php @@ -23,6 +23,7 @@ use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\GraphQlResolverPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\GraphQlTypePass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\MetadataAwareNameConverterPass; +use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\SerializerMappingLoaderPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\TestClientPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\TestMercureHubPass; use Symfony\Component\DependencyInjection\Compiler\PassConfig; @@ -58,5 +59,6 @@ public function build(ContainerBuilder $container): void $container->addCompilerPass(new TestClientPass()); $container->addCompilerPass(new TestMercureHubPass()); $container->addCompilerPass(new AuthenticatorManagerPass()); + $container->addCompilerPass(new SerializerMappingLoaderPass()); } } diff --git a/src/Symfony/Bundle/DependencyInjection/Compiler/SerializerMappingLoaderPass.php b/src/Symfony/Bundle/DependencyInjection/Compiler/SerializerMappingLoaderPass.php new file mode 100644 index 0000000000..7a9cc0d5c1 --- /dev/null +++ b/src/Symfony/Bundle/DependencyInjection/Compiler/SerializerMappingLoaderPass.php @@ -0,0 +1,28 @@ + + * + * 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 Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +final class SerializerMappingLoaderPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container): void + { + $chainLoader = $container->getDefinition('serializer.mapping.chain_loader'); + $loaders = $chainLoader->getArgument(0); + $loaders[] = $container->getDefinition('api_platform.serializer.property_metadata_loader'); + $container->getDefinition('serializer.mapping.cache_warmer')->replaceArgument(0, $loaders); + } +} diff --git a/src/Symfony/Bundle/Resources/config/api.xml b/src/Symfony/Bundle/Resources/config/api.xml index 275bef9278..7bce6a8f3a 100644 --- a/src/Symfony/Bundle/Resources/config/api.xml +++ b/src/Symfony/Bundle/Resources/config/api.xml @@ -175,5 +175,9 @@ + + + + diff --git a/tests/Symfony/Bundle/ApiPlatformBundleTest.php b/tests/Symfony/Bundle/ApiPlatformBundleTest.php index 49b423959f..d6b1643e57 100644 --- a/tests/Symfony/Bundle/ApiPlatformBundleTest.php +++ b/tests/Symfony/Bundle/ApiPlatformBundleTest.php @@ -24,6 +24,7 @@ use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\GraphQlResolverPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\GraphQlTypePass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\MetadataAwareNameConverterPass; +use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\SerializerMappingLoaderPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\TestClientPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\TestMercureHubPass; use PHPUnit\Framework\TestCase; @@ -54,6 +55,7 @@ public function testBuild(): void $containerProphecy->addCompilerPass(Argument::type(TestClientPass::class))->willReturn($containerProphecy->reveal())->shouldBeCalled(); $containerProphecy->addCompilerPass(Argument::type(TestMercureHubPass::class))->willReturn($containerProphecy->reveal())->shouldBeCalled(); $containerProphecy->addCompilerPass(Argument::type(AuthenticatorManagerPass::class))->willReturn($containerProphecy->reveal())->shouldBeCalled(); + $containerProphecy->addCompilerPass(Argument::type(SerializerMappingLoaderPass::class))->willReturn($containerProphecy->reveal())->shouldBeCalled(); $bundle = new ApiPlatformBundle(); $bundle->build($containerProphecy->reveal()); From 4bbfff394daec40d0643e68b75924c405f3184be Mon Sep 17 00:00:00 2001 From: soyuka Date: Fri, 11 Oct 2024 12:57:19 +0200 Subject: [PATCH 8/8] fix(jsonld): remove deprecated class --- src/JsonLd/Serializer/ErrorNormalizer.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/JsonLd/Serializer/ErrorNormalizer.php b/src/JsonLd/Serializer/ErrorNormalizer.php index b5bf3eec6c..ee2e87f063 100644 --- a/src/JsonLd/Serializer/ErrorNormalizer.php +++ b/src/JsonLd/Serializer/ErrorNormalizer.php @@ -14,7 +14,6 @@ namespace ApiPlatform\JsonLd\Serializer; use ApiPlatform\State\ApiResource\Error; -use ApiPlatform\Symfony\Validator\Exception\ValidationException as SymfonyValidationException; use ApiPlatform\Validator\Exception\ValidationException; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; @@ -48,7 +47,7 @@ public function normalize(mixed $object, ?string $format = null, array $context public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool { return $this->inner->supportsNormalization($data, $format, $context) - && (is_a($data, Error::class) || is_a($data, ValidationException::class) || is_a($data, SymfonyValidationException::class)); + && (is_a($data, Error::class) || is_a($data, ValidationException::class)); } public function getSupportedTypes(?string $format): array