diff --git a/src/Doctrine/Odm/Metadata/Resource/DoctrineMongoDbOdmParameterResourceMetadataCollectionFactory.php b/src/Doctrine/Odm/Metadata/Resource/DoctrineMongoDbOdmParameterResourceMetadataCollectionFactory.php index 8bce8b8ddb5..617951967d3 100644 --- a/src/Doctrine/Odm/Metadata/Resource/DoctrineMongoDbOdmParameterResourceMetadataCollectionFactory.php +++ b/src/Doctrine/Odm/Metadata/Resource/DoctrineMongoDbOdmParameterResourceMetadataCollectionFactory.php @@ -13,7 +13,6 @@ namespace ApiPlatform\Doctrine\Odm\Metadata\Resource; -use ApiPlatform\Doctrine\Odm\State\Options; use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; @@ -79,7 +78,7 @@ private function enrichOperation(Operation $operation, string $resourceClass): O return $operation; } - $documentClass = $this->getStateOptionsClass($operation, $operation->getClass(), Options::class); + $documentClass = $operation->getDataClass(); if (!$this->managerRegistry->getManagerForClass($documentClass) instanceof DocumentManager) { return $operation; } diff --git a/src/Doctrine/Odm/Metadata/Resource/DoctrineMongoDbOdmResourceCollectionMetadataFactory.php b/src/Doctrine/Odm/Metadata/Resource/DoctrineMongoDbOdmResourceCollectionMetadataFactory.php index c5ceb77c329..4715f201c99 100644 --- a/src/Doctrine/Odm/Metadata/Resource/DoctrineMongoDbOdmResourceCollectionMetadataFactory.php +++ b/src/Doctrine/Odm/Metadata/Resource/DoctrineMongoDbOdmResourceCollectionMetadataFactory.php @@ -45,7 +45,7 @@ public function create(string $resourceClass): ResourceMetadataCollection if ($operations) { foreach ($resourceMetadata->getOperations() as $operationName => $operation) { - $documentClass = $this->getStateOptionsClass($operation, $operation->getClass(), Options::class); + $documentClass = $operation->getDataClass(); if (!$this->managerRegistry->getManagerForClass($documentClass) instanceof DocumentManager) { continue; } @@ -62,7 +62,7 @@ public function create(string $resourceClass): ResourceMetadataCollection if ($graphQlOperations) { foreach ($graphQlOperations as $operationName => $graphQlOperation) { - $documentClass = $this->getStateOptionsClass($graphQlOperation, $graphQlOperation->getClass(), Options::class); + $documentClass = $operation->getDataClass(); if (!$this->managerRegistry->getManagerForClass($documentClass) instanceof DocumentManager) { continue; } diff --git a/src/Doctrine/Odm/State/CollectionProvider.php b/src/Doctrine/Odm/State/CollectionProvider.php index 6c68b663f3e..e7bd1b66d19 100644 --- a/src/Doctrine/Odm/State/CollectionProvider.php +++ b/src/Doctrine/Odm/State/CollectionProvider.php @@ -47,7 +47,7 @@ public function __construct(ResourceMetadataCollectionFactoryInterface $resource public function provide(Operation $operation, array $uriVariables = [], array $context = []): iterable { - $documentClass = $this->getStateOptionsClass($operation, $operation->getClass(), Options::class); + $documentClass = $operation->getDataClass(); /** @var DocumentManager $manager */ $manager = $this->managerRegistry->getManagerForClass($documentClass); diff --git a/src/Doctrine/Odm/State/ItemProvider.php b/src/Doctrine/Odm/State/ItemProvider.php index b1b8999e38a..1b3537d5eaa 100644 --- a/src/Doctrine/Odm/State/ItemProvider.php +++ b/src/Doctrine/Odm/State/ItemProvider.php @@ -50,7 +50,7 @@ public function __construct(ResourceMetadataCollectionFactoryInterface $resource public function provide(Operation $operation, array $uriVariables = [], array $context = []): ?object { - $documentClass = $this->getStateOptionsClass($operation, $operation->getClass(), Options::class); + $documentClass = $operation->getDataClass(); /** @var DocumentManager $manager */ $manager = $this->managerRegistry->getManagerForClass($documentClass); diff --git a/src/Doctrine/Odm/State/LinksHandlerTrait.php b/src/Doctrine/Odm/State/LinksHandlerTrait.php index 215f0cbb899..324652019c5 100644 --- a/src/Doctrine/Odm/State/LinksHandlerTrait.php +++ b/src/Doctrine/Odm/State/LinksHandlerTrait.php @@ -133,7 +133,7 @@ private function buildAggregation(string $toClass, array $links, array $identifi private function getLinkFromClass(Link $link, Operation $operation): string { - $documentClass = $this->getStateOptionsClass($operation, $operation->getClass(), Options::class); + $documentClass = $operation->getDataClass(); $fromClass = $link->getFromClass(); if ($fromClass === $operation->getClass() && $documentClass) { return $documentClass; @@ -141,7 +141,7 @@ private function getLinkFromClass(Link $link, Operation $operation): string $operation = $this->resourceMetadataCollectionFactory->create($fromClass)->getOperation(); - if ($documentClass = $this->getStateOptionsClass($operation, null, Options::class)) { + if ($documentClass = $operation->getDataClass()) { return $documentClass; } diff --git a/src/Doctrine/Odm/State/Options.php b/src/Doctrine/Odm/State/Options.php index 459d6bc49ec..aec5fd4d22e 100644 --- a/src/Doctrine/Odm/State/Options.php +++ b/src/Doctrine/Odm/State/Options.php @@ -14,9 +14,9 @@ namespace ApiPlatform\Doctrine\Odm\State; use ApiPlatform\Doctrine\Common\State\Options as CommonOptions; -use ApiPlatform\State\OptionsInterface; +use ApiPlatform\State\DataOptionsInterface; -class Options extends CommonOptions implements OptionsInterface +class Options extends CommonOptions implements DataOptionsInterface { /** * @param mixed $handleLinks experimental callable, typed mixed as we may want a service name in the future @@ -30,6 +30,11 @@ public function __construct( parent::__construct(handleLinks: $handleLinks); } + public function getDataClass(): ?string + { + return $this->documentClass; + } + public function getDocumentClass(): ?string { return $this->documentClass; diff --git a/src/Doctrine/Orm/Metadata/Resource/DoctrineOrmParameterResourceMetadataCollectionFactory.php b/src/Doctrine/Orm/Metadata/Resource/DoctrineOrmParameterResourceMetadataCollectionFactory.php index be342c758ce..d0f7881d1a5 100644 --- a/src/Doctrine/Orm/Metadata/Resource/DoctrineOrmParameterResourceMetadataCollectionFactory.php +++ b/src/Doctrine/Orm/Metadata/Resource/DoctrineOrmParameterResourceMetadataCollectionFactory.php @@ -13,7 +13,6 @@ namespace ApiPlatform\Doctrine\Orm\Metadata\Resource; -use ApiPlatform\Doctrine\Orm\State\Options; use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; @@ -77,7 +76,7 @@ private function enrichOperation(Operation $operation, string $resourceClass): O return $operation; } - $entityClass = $this->getStateOptionsClass($operation, $operation->getClass(), Options::class); + $entityClass = $operation->getDataClass(); if (!$this->managerRegistry->getManagerForClass($entityClass) instanceof EntityManagerInterface) { return $operation; } diff --git a/src/Doctrine/Orm/Metadata/Resource/DoctrineOrmResourceCollectionMetadataFactory.php b/src/Doctrine/Orm/Metadata/Resource/DoctrineOrmResourceCollectionMetadataFactory.php index e34e7c27491..6510c15274a 100644 --- a/src/Doctrine/Orm/Metadata/Resource/DoctrineOrmResourceCollectionMetadataFactory.php +++ b/src/Doctrine/Orm/Metadata/Resource/DoctrineOrmResourceCollectionMetadataFactory.php @@ -47,7 +47,7 @@ public function create(string $resourceClass): ResourceMetadataCollection if ($operations) { foreach ($operations as $operationName => $operation) { - $entityClass = $this->getStateOptionsClass($operation, $operation->getClass(), Options::class); + $entityClass = $operation->getDataClass(); $manager = $this->managerRegistry->getManagerForClass($entityClass); if (!$manager instanceof EntityManagerInterface) { @@ -74,7 +74,7 @@ public function create(string $resourceClass): ResourceMetadataCollection if ($graphQlOperations) { foreach ($graphQlOperations as $operationName => $graphQlOperation) { - $entityClass = $this->getStateOptionsClass($graphQlOperation, $graphQlOperation->getClass(), Options::class); + $entityClass = $operation->getDataClass(); if (!$this->managerRegistry->getManagerForClass($entityClass) instanceof EntityManagerInterface) { continue; diff --git a/src/Doctrine/Orm/State/CollectionProvider.php b/src/Doctrine/Orm/State/CollectionProvider.php index 3815447a8d3..c482b2146c8 100644 --- a/src/Doctrine/Orm/State/CollectionProvider.php +++ b/src/Doctrine/Orm/State/CollectionProvider.php @@ -50,7 +50,7 @@ public function __construct(ResourceMetadataCollectionFactoryInterface $resource public function provide(Operation $operation, array $uriVariables = [], array $context = []): iterable { - $entityClass = $this->getStateOptionsClass($operation, $operation->getClass(), Options::class); + $entityClass = $operation->getDataClass(); /** @var EntityManagerInterface $manager */ $manager = $this->managerRegistry->getManagerForClass($entityClass); diff --git a/src/Doctrine/Orm/State/ItemProvider.php b/src/Doctrine/Orm/State/ItemProvider.php index b201d03b7d0..ef6ad7c6963 100644 --- a/src/Doctrine/Orm/State/ItemProvider.php +++ b/src/Doctrine/Orm/State/ItemProvider.php @@ -50,7 +50,7 @@ public function __construct(ResourceMetadataCollectionFactoryInterface $resource public function provide(Operation $operation, array $uriVariables = [], array $context = []): ?object { - $entityClass = $this->getStateOptionsClass($operation, $operation->getClass(), Options::class); + $entityClass = $operation->getDataClass(); /** @var EntityManagerInterface|null $manager */ $manager = $this->managerRegistry->getManagerForClass($entityClass); diff --git a/src/Doctrine/Orm/State/LinksHandlerTrait.php b/src/Doctrine/Orm/State/LinksHandlerTrait.php index d44805a07b5..7134e340481 100644 --- a/src/Doctrine/Orm/State/LinksHandlerTrait.php +++ b/src/Doctrine/Orm/State/LinksHandlerTrait.php @@ -161,12 +161,12 @@ private function handleLinks(QueryBuilder $queryBuilder, array $identifiers, Que private function getLinkFromClass(Link $link, Operation $operation): string { $fromClass = $link->getFromClass(); - if ($fromClass === $operation->getClass() && $entityClass = $this->getStateOptionsClass($operation, $operation->getClass(), Options::class)) { + if ($fromClass === $operation->getClass() && $entityClass = $operation->getDataClass()) { return $entityClass; } $operation = $this->resourceMetadataCollectionFactory->create($fromClass)->getOperation(); - return $this->getStateOptionsClass($operation, $operation->getClass(), Options::class); + return $operation->getDataClass(); } } diff --git a/src/Doctrine/Orm/State/Options.php b/src/Doctrine/Orm/State/Options.php index 3a9a46c3825..17157f71426 100644 --- a/src/Doctrine/Orm/State/Options.php +++ b/src/Doctrine/Orm/State/Options.php @@ -14,9 +14,9 @@ namespace ApiPlatform\Doctrine\Orm\State; use ApiPlatform\Doctrine\Common\State\Options as CommonOptions; -use ApiPlatform\State\OptionsInterface; +use ApiPlatform\State\DataOptionsInterface; -class Options extends CommonOptions implements OptionsInterface +class Options extends CommonOptions implements DataOptionsInterface { /** * @param string|callable $handleLinks experimental callable, typed mixed as we may want a service name in the future @@ -30,6 +30,11 @@ public function __construct( parent::__construct(handleLinks: $handleLinks); } + public function getDataClass(): ?string + { + return $this->entityClass; + } + public function getEntityClass(): ?string { return $this->entityClass; diff --git a/src/GraphQl/Serializer/SerializerContextBuilder.php b/src/GraphQl/Serializer/SerializerContextBuilder.php index 571f3dae9b8..d48f21fb83f 100644 --- a/src/GraphQl/Serializer/SerializerContextBuilder.php +++ b/src/GraphQl/Serializer/SerializerContextBuilder.php @@ -40,12 +40,14 @@ public function create(?string $resourceClass, Operation $operation, array $reso } $context['operation'] = $operation; - if ($operation->getInput()) { - $context['input'] = $operation->getInput(); - } - if ($operation->getOutput()) { - $context['output'] = $operation->getOutput(); - } + $context['input'] = $operation->getInputClass() === $operation->getClass() ? null : ['class' => $operation->getInputClass()]; + $context['output'] = $operation->getOutputClass() === $operation->getClass() ? null : ['class' => $operation->getOutputClass()]; + // if (($inputClass = $operation->getInputClass()) !== $operation->getClass()) { + // $context['input'] = ['class' => $inputClass]; + // } + // if (($outputClass = $operation->getOutputClass()) !== $operation->getClass()) { + // $context['output'] = ['class' => $outputClass]; + // } $context = $normalization ? array_merge($operation->getNormalizationContext() ?? [], $context) : array_merge($operation->getDenormalizationContext() ?? [], $context); if ($normalization) { diff --git a/src/GraphQl/Type/FieldsBuilder.php b/src/GraphQl/Type/FieldsBuilder.php index 458f92bb248..63e3f57e759 100644 --- a/src/GraphQl/Type/FieldsBuilder.php +++ b/src/GraphQl/Type/FieldsBuilder.php @@ -616,7 +616,7 @@ private function getFilterArgs(array $args, ?string $resourceClass, string $root continue; } - $entityClass = $this->getStateOptionsClass($resourceOperation, $resourceOperation->getClass()); + $entityClass = $resourceOperation->getDataClass(); foreach ($this->filterLocator->get($filterId)->getDescription($entityClass) as $key => $description) { $filterType = \in_array($description['type'], TypeIdentifier::values(), true) ? Type::builtin($description['type']) : Type::object($description['type']); if (!($description['required'] ?? false)) { diff --git a/src/GraphQl/Type/TypeBuilder.php b/src/GraphQl/Type/TypeBuilder.php index 289bb8762a4..ef1f1149378 100644 --- a/src/GraphQl/Type/TypeBuilder.php +++ b/src/GraphQl/Type/TypeBuilder.php @@ -311,15 +311,12 @@ private function getQueryOperation(ResourceMetadataCollection $resourceMetadataC private function getResourceObjectTypeConfiguration(string $shortName, ResourceMetadataCollection $resourceMetadataCollection, Operation $operation, array $context = []): InputObjectType|ObjectType { $operationName = $operation->getName(); - $resourceClass = $operation->getClass(); $input = $context['input']; $depth = $context['depth'] ?? 0; $wrapped = $context['wrapped'] ?? false; - $ioMetadata = $input ? $operation->getInput() : $operation->getOutput(); - if (null !== $ioMetadata && \array_key_exists('class', $ioMetadata) && null !== $ioMetadata['class']) { - $resourceClass = $ioMetadata['class']; - } + $resourceClass = $input ? $operation->getInputClass() : $operation->getOutputClass(); + $ioMetadata = $resourceClass ? ['class' => $resourceClass] : null; $wrapData = !$wrapped && ($operation instanceof Mutation || $operation instanceof Subscription) && !$input && $depth < 1; diff --git a/src/Hydra/Serializer/CollectionFiltersNormalizer.php b/src/Hydra/Serializer/CollectionFiltersNormalizer.php index edc3fee3a8a..84a2110aa16 100644 --- a/src/Hydra/Serializer/CollectionFiltersNormalizer.php +++ b/src/Hydra/Serializer/CollectionFiltersNormalizer.php @@ -105,7 +105,7 @@ public function normalize(mixed $data, ?string $format = null, array $context = } } - $resourceClass = $this->getStateOptionsClass($operation, $resourceClass); + $resourceClass = $operation->getDataClass(); if ($currentFilters || ($parameters && \count($parameters))) { $hydraPrefix = $this->getHydraPrefix($context + $this->defaultContext); diff --git a/src/Hydra/Serializer/DocumentationNormalizer.php b/src/Hydra/Serializer/DocumentationNormalizer.php index 428c6da7a94..2e61700e075 100644 --- a/src/Hydra/Serializer/DocumentationNormalizer.php +++ b/src/Hydra/Serializer/DocumentationNormalizer.php @@ -216,13 +216,11 @@ private function getHydraProperties(string $resourceClass, ApiResource $resource continue; } - $inputMetadata = $operation->getInput(); - if (null !== $inputClass = $inputMetadata['class'] ?? null) { + if (null !== ($inputClass = $operation->getInputClass())) { $classes[$inputClass] = true; } - $outputMetadata = $operation->getOutput(); - if (null !== $outputClass = $outputMetadata['class'] ?? null) { + if (null !== ($outputClass = $operation->getOutputClass())) { $classes[$outputClass] = true; } } @@ -288,11 +286,8 @@ private function getHydraOperation(HttpOperation $operation, string $prefixedSho } $shortName = $operation->getShortName(); - $inputMetadata = $operation->getInput() ?? []; - $outputMetadata = $operation->getOutput() ?? []; - - $inputClass = \array_key_exists('class', $inputMetadata) ? $inputMetadata['class'] : false; - $outputClass = \array_key_exists('class', $outputMetadata) ? $outputMetadata['class'] : false; + $inputClass = $operation->getInputClass(); + $outputClass = $operation->getOutputClass(); if ('GET' === $method && $operation instanceof CollectionOperationInterface) { $hydraOperation += [ diff --git a/src/JsonSchema/ResourceMetadataTrait.php b/src/JsonSchema/ResourceMetadataTrait.php index 71988820c76..548d9df79d2 100644 --- a/src/JsonSchema/ResourceMetadataTrait.php +++ b/src/JsonSchema/ResourceMetadataTrait.php @@ -29,11 +29,10 @@ trait ResourceMetadataTrait private function findOutputClass(string $className, string $type, Operation $operation, ?array $serializerContext): ?string { - $inputOrOutput = ['class' => $className]; - $inputOrOutput = Schema::TYPE_OUTPUT === $type ? ($operation->getOutput() ?? $inputOrOutput) : ($operation->getInput() ?? $inputOrOutput); + $resourceClass = (Schema::TYPE_OUTPUT === $type ? $operation->getOutputClass() : $operation->getInputClass()) ?? $className; $forceSubschema = $serializerContext[SchemaFactory::FORCE_SUBSCHEMA] ?? false; - return $forceSubschema ? ($inputOrOutput['class'] ?? $inputOrOutput->class ?? $operation->getClass()) : ($inputOrOutput['class'] ?? $inputOrOutput->class ?? null); + return $forceSubschema ? ($resourceClass ?? $operation->getClass()) : $resourceClass; } private function findOperation(string $className, string $type, ?Operation $operation, ?array $serializerContext, ?string $format = null): Operation diff --git a/src/Laravel/Eloquent/Metadata/Factory/Resource/EloquentResourceCollectionMetadataFactory.php b/src/Laravel/Eloquent/Metadata/Factory/Resource/EloquentResourceCollectionMetadataFactory.php index 5b56b34f3f7..79823ea16b0 100644 --- a/src/Laravel/Eloquent/Metadata/Factory/Resource/EloquentResourceCollectionMetadataFactory.php +++ b/src/Laravel/Eloquent/Metadata/Factory/Resource/EloquentResourceCollectionMetadataFactory.php @@ -15,7 +15,6 @@ use ApiPlatform\Laravel\Eloquent\State\CollectionProvider; use ApiPlatform\Laravel\Eloquent\State\ItemProvider; -use ApiPlatform\Laravel\Eloquent\State\Options; use ApiPlatform\Laravel\Eloquent\State\PersistProcessor; use ApiPlatform\Laravel\Eloquent\State\RemoveProcessor; use ApiPlatform\Metadata\CollectionOperationInterface; @@ -81,7 +80,7 @@ public function create(string $resourceClass): ResourceMetadataCollection $operations = $resourceMetadata->getOperations(); foreach ($operations ?? [] as $operationName => $operation) { // Check if this operation uses Eloquent via stateOptions - $modelClass = $this->getStateOptionsClass($operation, $resourceClass, Options::class); + $modelClass = $operation->getDataClass(); $usesEloquent = $isModel || ($modelClass !== $resourceClass); if (!$usesEloquent) { @@ -115,7 +114,7 @@ public function create(string $resourceClass): ResourceMetadataCollection $graphQlOperations = $resourceMetadata->getGraphQlOperations(); foreach ($graphQlOperations ?? [] as $operationName => $graphQlOperation) { // Check if this operation uses Eloquent via stateOptions - $modelClass = $this->getStateOptionsClass($graphQlOperation, $resourceClass, Options::class); + $modelClass = $operation->getDataClass(); $usesEloquent = $isModel || ($modelClass !== $resourceClass); if (!$usesEloquent) { diff --git a/src/Laravel/Eloquent/State/CollectionProvider.php b/src/Laravel/Eloquent/State/CollectionProvider.php index b70bb37307d..f7d47b60a74 100644 --- a/src/Laravel/Eloquent/State/CollectionProvider.php +++ b/src/Laravel/Eloquent/State/CollectionProvider.php @@ -48,7 +48,7 @@ public function __construct( public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null { - $resourceClass = $this->getStateOptionsClass($operation, $operation->getClass(), Options::class); + $resourceClass = $operation->getDataClass(); $model = new $resourceClass(); if (!$model instanceof Model) { diff --git a/src/Laravel/Eloquent/State/ItemProvider.php b/src/Laravel/Eloquent/State/ItemProvider.php index 38d2bb1bbb5..d7d7b6aab88 100644 --- a/src/Laravel/Eloquent/State/ItemProvider.php +++ b/src/Laravel/Eloquent/State/ItemProvider.php @@ -43,7 +43,7 @@ public function __construct( public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null { - $resourceClass = $this->getStateOptionsClass($operation, $operation->getClass(), Options::class); + $resourceClass = $operation->getDataClass(); $model = new $resourceClass(); if (!$model instanceof Model) { diff --git a/src/Laravel/Eloquent/State/Options.php b/src/Laravel/Eloquent/State/Options.php index 3e328adf3d2..096e141b17e 100644 --- a/src/Laravel/Eloquent/State/Options.php +++ b/src/Laravel/Eloquent/State/Options.php @@ -13,9 +13,9 @@ namespace ApiPlatform\Laravel\Eloquent\State; -use ApiPlatform\State\OptionsInterface; +use ApiPlatform\State\DataOptionsInterface; -class Options implements OptionsInterface +class Options implements DataOptionsInterface { /** * @param string|callable $handleLinks experimental callable, typed mixed as we may want a service name in the future @@ -28,6 +28,11 @@ public function __construct( ) { } + public function getDataClass(): ?string + { + return $this->modelClass; + } + public function getHandleLinks(): mixed { return $this->handleLinks; diff --git a/src/Mcp/Capability/Registry/Loader.php b/src/Mcp/Capability/Registry/Loader.php index 32bff5b1089..65b113ae30e 100644 --- a/src/Mcp/Capability/Registry/Loader.php +++ b/src/Mcp/Capability/Registry/Loader.php @@ -49,13 +49,13 @@ public function load(RegistryInterface $registry): void foreach ($metadata as $resource) { foreach ($resource->getMcp() ?? [] as $mcp) { if ($mcp instanceof McpTool) { - $inputClass = $mcp->getInput()['class'] ?? $mcp->getClass(); + $inputClass = $mcp->getInputClass(); $inputFormat = array_key_first($mcp->getInputFormats() ?? ['json' => ['application/json']]); $inputSchema = $this->schemaFactory->buildSchema($inputClass, $inputFormat, Schema::TYPE_INPUT, $mcp, null, [SchemaFactory::FORCE_SUBSCHEMA => true]); $outputSchema = null; if (false !== $mcp->getStructuredContent()) { - $outputClass = $mcp->getOutput()['class'] ?? $mcp->getClass(); + $outputClass = $mcp->getOutputClass(); $outputFormat = array_key_first($mcp->getOutputFormats() ?? ['json' => ['application/json']]); $outputSchema = $this->schemaFactory->buildSchema($outputClass, $outputFormat, Schema::TYPE_OUTPUT, $mcp, null, [SchemaFactory::FORCE_SUBSCHEMA => true])->getArrayCopy(); } diff --git a/src/Mcp/State/ToolProvider.php b/src/Mcp/State/ToolProvider.php index dff2e8874eb..59411e746e6 100644 --- a/src/Mcp/State/ToolProvider.php +++ b/src/Mcp/State/ToolProvider.php @@ -35,7 +35,7 @@ public function provide(Operation $operation, array $uriVariables = [], array $c } $data = (object) $context['mcp_data']; - $class = $operation->getInput()['class'] ?? $operation->getClass(); + $class = $operation->getInputClass(); return $this->objectMapper->map($data, $class); } diff --git a/src/Metadata/ApiResource.php b/src/Metadata/ApiResource.php index eabfdda8fc0..591e9f0e7a7 100644 --- a/src/Metadata/ApiResource.php +++ b/src/Metadata/ApiResource.php @@ -430,8 +430,16 @@ public function __construct( * @var string|bool|null */ protected $messenger = null, + /** + * @deprecated use inputClass instead + */ protected $input = null, + protected ?string $inputClass = null, + /** + * @deprecated use outputClass instead + */ protected $output = null, + protected ?string $outputClass = null, /** * Override the default order of items in your collection. Note that this is handled by our doctrine filters such as * the [OrderFilter](/docs/reference/Doctrine/Orm/Filter/OrderFilter). @@ -973,6 +981,7 @@ public function __construct( protected array $extraProperties = [], ?bool $map = null, protected ?array $mcp = null, + protected ?string $dataClass = null, ) { parent::__construct( shortName: $shortName, @@ -988,7 +997,9 @@ class: $class, mercure: $mercure, messenger: $messenger, input: $input, + inputClass: $inputClass, output: $output, + outputClass: $outputClass, order: $order, fetchPartial: $fetchPartial, forceEager: $forceEager, @@ -1020,6 +1031,7 @@ class: $class, jsonStream: $jsonStream, extraProperties: $extraProperties, map: $map, + dataClass: $dataClass, ); /* @var Operations $operations> */ diff --git a/src/Metadata/Get.php b/src/Metadata/Get.php index 4babd54eb27..17890235abf 100644 --- a/src/Metadata/Get.php +++ b/src/Metadata/Get.php @@ -103,6 +103,7 @@ public function __construct( ?bool $jsonStream = null, array $extraProperties = [], ?bool $map = null, + ?string $dataClass = null, ) { parent::__construct( uriTemplate: $uriTemplate, @@ -185,7 +186,8 @@ class: $class, hideHydraOperation: $hideHydraOperation, jsonStream: $jsonStream, extraProperties: $extraProperties, - map: $map + map: $map, + dataClass: $dataClass ); } } diff --git a/src/Metadata/HttpOperation.php b/src/Metadata/HttpOperation.php index 58d4cf98c7f..8dec070dfb2 100644 --- a/src/Metadata/HttpOperation.php +++ b/src/Metadata/HttpOperation.php @@ -223,6 +223,7 @@ public function __construct( ?bool $jsonStream = null, array $extraProperties = [], ?bool $map = null, + ?string $dataClass = null, ) { $this->formats = (null === $formats || \is_array($formats)) ? $formats : [$formats]; $this->inputFormats = (null === $inputFormats || \is_array($inputFormats)) ? $inputFormats : [$inputFormats]; @@ -283,7 +284,8 @@ class: $class, hideHydraOperation: $hideHydraOperation, jsonStream: $jsonStream, extraProperties: $extraProperties, - map: $map + map: $map, + dataClass: $dataClass ); } diff --git a/src/Metadata/Metadata.php b/src/Metadata/Metadata.php index 009612c9900..1be8177477f 100644 --- a/src/Metadata/Metadata.php +++ b/src/Metadata/Metadata.php @@ -13,6 +13,7 @@ namespace ApiPlatform\Metadata; +use ApiPlatform\State\DataOptionsInterface; use ApiPlatform\State\OptionsInterface; /** @@ -29,8 +30,10 @@ abstract class Metadata * @param string|\Stringable|null $securityPostDenormalize https://api-platform.com/docs/core/security/#executing-access-control-rules-after-denormalization * @param mixed|null $mercure * @param mixed|null $messenger - * @param mixed|null $input - * @param mixed|null $output + * @param mixed|null $input @deprecated use inputClass instead + * @param mixed|null $inputClass + * @param mixed|null $output @deprecated use outputClass instead + * @param mixed|null $outputClass * @param mixed|null $provider * @param mixed|null $processor * @param Parameters|array $parameters @@ -50,7 +53,9 @@ public function __construct( protected $mercure = null, protected $messenger = null, protected $input = null, + protected ?string $inputClass = null, protected $output = null, + protected ?string $outputClass = null, protected ?array $order = null, protected ?bool $fetchPartial = null, protected ?bool $forceEager = null, @@ -83,12 +88,17 @@ public function __construct( protected ?bool $jsonStream = null, protected ?bool $map = null, protected array $extraProperties = [], + protected ?string $dataClass = null, ) { if (\is_array($parameters) && $parameters) { $parameters = new Parameters($parameters); } $this->parameters = $parameters; + + if (!$this->dataClass && $this->stateOptions instanceof DataOptionsInterface) { + $this->dataClass = $this->stateOptions->getDataClass(); + } } public function canMap(): ?bool @@ -272,26 +282,109 @@ public function withMessenger(mixed $messenger): static return $self; } + /** + * @deprecated use getInputClass() instead + */ public function getInput(): mixed { + trigger_deprecation('api-platform/metadata', '4.3', 'The method "getInput()" is deprecated, use "getInputClass()" instead.'); + return $this->input; } + /** + * @deprecated use withInputClass() instead + */ public function withInput(mixed $input): static { + trigger_deprecation('api-platform/metadata', '4.3', 'The method "withInput()" is deprecated, use "withInputClass()" instead.'); + $self = clone $this; $self->input = $input; return $self; } + public function getInputClass(): ?string + { + if (null !== $this->inputClass) { + return $this->inputClass; + } + + if (false === $this->input) { + return null; + } + + if (\is_array($this->input) && \is_string($this->input['class'] ?? null)) { + return $this->input['class']; + } + + if (null === $this->input) { + return $this->getClass(); + } + + return null; + } + + /** + * @param class-string|null $inputClass + * + * @return $this + */ + public function withInputClass(?string $inputClass): static + { + $self = clone $this; + $self->inputClass = $inputClass; + + return $self; + } + + public function getOutputClass(): ?string + { + if (null !== $this->outputClass) { + return $this->outputClass; + } + + if (false === $this->output) { + return null; + } + + if (\is_array($this->output) && \is_string($this->output['class'] ?? null)) { + return $this->output['class']; + } + + if (null === $this->output) { + return $this->getClass(); + } + + return null; + } + + public function withOutputClass(?string $outputClass): static + { + $self = clone $this; + $self->outputClass = $outputClass; + + return $self; + } + + /** + * @deprecated use getOutputClass() instead + */ public function getOutput(): mixed { + trigger_deprecation('api-platform/metadata', '4.3', 'The method "getOutput()" is deprecated, use "getOutputClass()" instead.'); + return $this->output; } + /** + * @deprecated use withOutputClass() instead + */ public function withOutput(mixed $output): static { + trigger_deprecation('api-platform/metadata', '4.3', 'The method "withOutput()" is deprecated, use "withOutputClass()" instead.'); + $self = clone $this; $self->output = $output; @@ -581,6 +674,33 @@ public function withStateOptions(?OptionsInterface $stateOptions): static $self = clone $this; $self->stateOptions = $stateOptions; + if ($stateOptions instanceof DataOptionsInterface) { + $self->dataClass = $stateOptions->getDataClass(); + } + + return $self; + } + + /** + * @return class-string|null + */ + public function getDataClass(): ?string + { + if (null === $this->dataClass) { + return $this->class; + } + + return $this->dataClass; + } + + /** + * @param class-string|null $dataClass + */ + public function withDataClass(?string $dataClass): static + { + $self = clone $this; + $self->dataClass = $dataClass; + return $self; } diff --git a/src/Metadata/Operation.php b/src/Metadata/Operation.php index cbd53751e59..acefc7957f1 100644 --- a/src/Metadata/Operation.php +++ b/src/Metadata/Operation.php @@ -729,8 +729,16 @@ public function __construct( * For more examples, read our guide on [validation](/guides/validation). */ protected ?array $validationContext = null, + /** + * @deprecated use inputClass instead + */ protected $input = null, + protected ?string $inputClass = null, + /** + * @deprecated use outputClass instead + */ protected $output = null, + protected ?string $outputClass = null, protected $mercure = null, /** * The `messenger` option dispatches the current resource through the Message Bus. @@ -816,6 +824,7 @@ public function __construct( protected ?bool $jsonStream = null, protected array $extraProperties = [], ?bool $map = null, + protected ?string $dataClass = null, ) { parent::__construct( shortName: $shortName, @@ -831,7 +840,9 @@ class: $class, mercure: $mercure, messenger: $messenger, input: $input, + inputClass: $inputClass, output: $output, + outputClass: $outputClass, order: $order, fetchPartial: $fetchPartial, forceEager: $forceEager, @@ -863,7 +874,8 @@ class: $class, hideHydraOperation: $hideHydraOperation, jsonStream: $jsonStream, extraProperties: $extraProperties, - map: $map + map: $map, + dataClass: $dataClass ); } diff --git a/src/Metadata/Resource/Factory/InputOutputResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/InputOutputResourceMetadataCollectionFactory.php index 00d2c6ea252..5832df05ccb 100644 --- a/src/Metadata/Resource/Factory/InputOutputResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/InputOutputResourceMetadataCollectionFactory.php @@ -37,8 +37,8 @@ public function create(string $resourceClass): ResourceMetadataCollection $resourceMetadataCollection = $this->decorated->create($resourceClass); foreach ($resourceMetadataCollection as $key => $resourceMetadata) { - $resourceMetadata = $resourceMetadata->withInput($this->transformInputOutput($resourceMetadata->getInput())); - $resourceMetadata = $resourceMetadata->withOutput($this->transformInputOutput($resourceMetadata->getOutput())); + $resourceMetadata = $resourceMetadata->withInputClass($this->transformInputOutput($resourceMetadata->getInput())['class'] ?? null); + $resourceMetadata = $resourceMetadata->withOutputClass($this->transformInputOutput($resourceMetadata->getOutput())['class'] ?? null); if ($resourceMetadata->getOperations()) { $resourceMetadata = $resourceMetadata->withOperations($this->getTransformedOperations($resourceMetadata->getOperations(), $resourceMetadata)); @@ -61,13 +61,11 @@ public function create(string $resourceClass): ResourceMetadataCollection private function getTransformedOperations(Operations|array $operations, ApiResource $resourceMetadata): Operations|array { foreach ($operations as $key => $operation) { - $operation = $operation->withInput(null !== $operation->getInput() ? $this->transformInputOutput($operation->getInput()) : $resourceMetadata->getInput()); - $operation = $operation->withOutput(null !== $operation->getOutput() ? $this->transformInputOutput($operation->getOutput()) : $resourceMetadata->getOutput()); + $operation = $operation->withInputClass(null !== $operation->getInput() ? ($this->transformInputOutput($operation->getInput())['class'] ?? null) : $resourceMetadata->getInputClass()); + $operation = $operation->withOutputClass(null !== $operation->getOutput() ? ($this->transformInputOutput($operation->getOutput())['class'] ?? null) : $resourceMetadata->getOutputClass()); if ( - $operation->getInput() - && \array_key_exists('class', $operation->getInput()) - && null === $operation->getInput()['class'] + null === $operation->getInputClass() ) { $operation = $operation->withDeserialize(null === $operation->canDeserialize() ? false : $operation->canDeserialize()); $operation = $operation->withValidate(null === $operation->canValidate() ? false : $operation->canValidate()); @@ -75,9 +73,7 @@ private function getTransformedOperations(Operations|array $operations, ApiResou if ( $operation instanceof HttpOperation - && $operation->getOutput() - && \array_key_exists('class', $operation->getOutput()) - && null === $operation->getOutput()['class'] + && null === $operation->getOutputClass() && null === $operation->getStatus() ) { $operation = $operation->withStatus(204); diff --git a/src/Metadata/Resource/Factory/ObjectMapperMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/ObjectMapperMetadataCollectionFactory.php index 37204def6a4..51465e4ac73 100644 --- a/src/Metadata/Resource/Factory/ObjectMapperMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/ObjectMapperMetadataCollectionFactory.php @@ -57,8 +57,8 @@ public function create(string $resourceClass): ResourceMetadataCollection $entityClass = $options->getModelClass(); } - $class = $operation->getInput()['class'] ?? $operation->getClass(); - $outputClass = $operation->getOutput()['class'] ?? null; + $class = $operation->getInputClass(); + $outputClass = $operation->getOutputClass(); $entityMap = null; // Look for Mapping metadata @@ -89,8 +89,12 @@ public function create(string $resourceClass): ResourceMetadataCollection /** * @return bool|list */ - private function canBeMapped(string $class): bool|array + private function canBeMapped(?string $class): bool|array { + if (!$class) { + return false; + } + try { $r = new \ReflectionClass($class); if (!$r->isInstantiable() || !($mapping = $this->objectMapperMetadata->create($r->newInstanceWithoutConstructor(), null, ['_api_check_can_be_mapped' => true]))) { diff --git a/src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php index 5c0db818d70..187089e41d7 100644 --- a/src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php @@ -103,7 +103,7 @@ private function getProperties(string $resourceClass, ?Parameter $parameter = nu { $filterClass = $parameter?->getFilterClass(); if (null === $filterClass && null !== $operation) { - $filterClass = $this->getStateOptionsClass($operation, $resourceClass); + $filterClass = $operation->getDataClass(); } $filterClass ??= $resourceClass; @@ -427,7 +427,7 @@ private function setDefaults(string $key, Parameter $parameter, ?object $filter, private function getLegacyFilterMetadata(Parameter $parameter, Operation $operation, FilterInterface $filter): Parameter { - $description = $filter->getDescription($this->getStateOptionsClass($operation, $operation->getClass())); + $description = $operation->getDataClass(); $key = $parameter->getKey(); if (($schema = $description[$key]['schema'] ?? null) && null === $parameter->getSchema()) { $parameter = $parameter->withSchema($schema); diff --git a/src/OpenApi/Factory/OpenApiFactory.php b/src/OpenApi/Factory/OpenApiFactory.php index 351b9d68193..635053e52bb 100644 --- a/src/OpenApi/Factory/OpenApiFactory.php +++ b/src/OpenApi/Factory/OpenApiFactory.php @@ -313,7 +313,7 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection } } - $entityClass = $this->getStateOptionsClass($operation, $operation->getClass()); + $entityClass = $operation->getDataClass(); $openapiParameters = $openapiOperation->getParameters(); foreach ($operation->getParameters() ?? [] as $key => $p) { if (false === $p->getOpenApi()) { @@ -453,7 +453,7 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection if ( \in_array($method, ['PATCH', 'PUT', 'POST'], true) - && !(false === ($input = $operation->getInput()) || (\is_array($input) && null === $input['class'])) + && $operation->getInputClass() ) { $content = $openapiOperation->getRequestBody()?->getContent(); if (null === $content) { @@ -501,7 +501,7 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection */ private function buildOpenApiResponse(array $existingResponses, int|string $status, string $description, Operation $openapiOperation, ?HttpOperation $operation = null, ?array $responseMimeTypes = null, ?array $operationOutputSchemas = null, ?ResourceMetadataCollection $resourceMetadataCollection = null, bool $isErrorResponse = false): Operation { - $noOutput = !$isErrorResponse && \is_array($operation?->getOutput()) && null === $operation->getOutput()['class']; + $noOutput = !$isErrorResponse && null === $operation?->getOutputClass(); $response = $existingResponses[$status] ?? new Response($description); if (null === $response->getDescription()) { @@ -680,7 +680,7 @@ private function getFiltersParameters(CollectionOperationInterface|HttpOperation { $parameters = []; $resourceFilters = $operation->getFilters(); - $entityClass = $this->getStateOptionsClass($operation, $operation->getClass()); + $entityClass = $operation->getDataClass(); foreach ($resourceFilters ?? [] as $filterId) { if (!$this->filterLocator->has($filterId)) { diff --git a/src/Serializer/InputOutputMetadataTrait.php b/src/Serializer/InputOutputMetadataTrait.php index 03b604f97fe..16d16f9b673 100644 --- a/src/Serializer/InputOutputMetadataTrait.php +++ b/src/Serializer/InputOutputMetadataTrait.php @@ -21,6 +21,10 @@ trait InputOutputMetadataTrait protected function getInputClass(array $context = []): ?string { + if (\is_string($context['input'] ?? null)) { + return $context['input']; + } + if (!$this->resourceMetadataCollectionFactory) { return $context['input']['class'] ?? null; } @@ -34,6 +38,10 @@ protected function getInputClass(array $context = []): ?string protected function getOutputClass(array $context = []): ?string { + if (\is_string($context['output'] ?? null)) { + return $context['output']; + } + if (!$this->resourceMetadataCollectionFactory) { return $context['output']['class'] ?? null; } diff --git a/src/Serializer/SerializerContextBuilder.php b/src/Serializer/SerializerContextBuilder.php index 63ee797f426..215f81aa026 100644 --- a/src/Serializer/SerializerContextBuilder.php +++ b/src/Serializer/SerializerContextBuilder.php @@ -64,8 +64,8 @@ public function createFromRequest(Request $request, bool $normalization, ?array $context['iri_only'] ??= false; $context['request_uri'] = $request->getRequestUri(); $context['uri'] = $request->getUri(); - $context['input'] = $operation->getInput(); - $context['output'] = $operation->getOutput(); + $context['input'] = $operation->getInputClass() === $operation->getClass() ? null : ['class' => $operation->getInputClass()]; + $context['output'] = $operation->getOutputClass() === $operation->getClass() ? null : ['class' => $operation->getOutputClass()]; // Special case as this is usually handled by our OperationContextTrait, here we want to force the IRI in the response if (!$operation instanceof CollectionOperationInterface && method_exists($operation, 'getItemUriTemplate') && $operation->getItemUriTemplate()) { diff --git a/src/State/DataOptionsInterface.php b/src/State/DataOptionsInterface.php new file mode 100644 index 00000000000..52d00d4d352 --- /dev/null +++ b/src/State/DataOptionsInterface.php @@ -0,0 +1,19 @@ + + * + * 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; + +interface DataOptionsInterface extends OptionsInterface +{ + public function getDataClass(): ?string; +} diff --git a/src/State/Processor/ObjectMapperInputProcessor.php b/src/State/Processor/ObjectMapperInputProcessor.php index 845be27e098..411af5952e7 100644 --- a/src/State/Processor/ObjectMapperInputProcessor.php +++ b/src/State/Processor/ObjectMapperInputProcessor.php @@ -39,7 +39,7 @@ public function __construct( public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): object|array|null { - $class = $operation->getInput()['class'] ?? $operation->getClass(); + $class = $operation->getInputClass(); if ( $data instanceof Response @@ -54,7 +54,7 @@ public function process(mixed $data, Operation $operation, array $uriVariables = } $request = $context['request'] ?? null; - $mapped = $this->objectMapper->map($data, $request?->attributes->get('mapped_data') ?? $this->getStateOptionsClass($operation, $operation->getClass())); + $mapped = $this->objectMapper->map($data, $request?->attributes->get('mapped_data') ?? $operation->getDataClass()); $request?->attributes->set('mapped_data', $mapped); return $this->decorated ? $this->decorated->process($mapped, $operation, $uriVariables, $context) : $mapped; diff --git a/src/State/Processor/ObjectMapperProcessor.php b/src/State/Processor/ObjectMapperProcessor.php index f7bb34a367e..63171402ee1 100644 --- a/src/State/Processor/ObjectMapperProcessor.php +++ b/src/State/Processor/ObjectMapperProcessor.php @@ -40,7 +40,7 @@ public function __construct( public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): object|array|null { - $class = $operation->getInput()['class'] ?? $operation->getClass(); + $class = $operation->getInputClass(); if ( $data instanceof Response @@ -52,14 +52,13 @@ public function process(mixed $data, Operation $operation, array $uriVariables = ) { 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())); + $mappedData = $this->objectMapper->map($data, $operation->getDataClass()); } $request?->attributes->set('mapped_data', $mappedData); diff --git a/src/State/Processor/SerializeProcessor.php b/src/State/Processor/SerializeProcessor.php index 8047a384899..092bf58864c 100644 --- a/src/State/Processor/SerializeProcessor.php +++ b/src/State/Processor/SerializeProcessor.php @@ -68,7 +68,7 @@ public function process(mixed $data, Operation $operation, array $uriVariables = $serializerContext['uri_variables'] = $uriVariables; - if (isset($serializerContext['output']) && \array_key_exists('class', $serializerContext['output']) && null === $serializerContext['output']['class']) { + if (isset($serializerContext['output']) && null === $serializerContext['output']) { $this->stopwatch?->stop('api_platform.processor.serialize'); return $this->processor ? $this->processor->process(null, $operation, $uriVariables, $context) : null; diff --git a/src/State/Provider/ContentNegotiationProvider.php b/src/State/Provider/ContentNegotiationProvider.php index 42e81252282..05f292b8994 100644 --- a/src/State/Provider/ContentNegotiationProvider.php +++ b/src/State/Provider/ContentNegotiationProvider.php @@ -98,8 +98,7 @@ private function flattenMimeTypes(array $formats): array private function getInputFormat(HttpOperation $operation, Request $request): ?string { if ( - false === ($input = $operation->getInput()) - || (\is_array($input) && null === $input['class']) + null === $operation->getInputClass() || false === $operation->canDeserialize() ) { return null; diff --git a/src/State/Provider/ObjectMapperProvider.php b/src/State/Provider/ObjectMapperProvider.php index 9986f43954f..0896ea77a55 100644 --- a/src/State/Provider/ObjectMapperProvider.php +++ b/src/State/Provider/ObjectMapperProvider.php @@ -41,7 +41,7 @@ public function __construct( public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null { $data = $this->decorated->provide($operation, $uriVariables, $context); - $class = $operation->getOutput()['class'] ?? $operation->getClass(); + $class = $operation->getOutputClass(); if (!$this->objectMapper || !$operation->canMap()) { return $data; diff --git a/src/State/SerializerAwareProviderInterface.php b/src/State/SerializerAwareProviderInterface.php index f8962331dcf..6aada8eba41 100644 --- a/src/State/SerializerAwareProviderInterface.php +++ b/src/State/SerializerAwareProviderInterface.php @@ -24,8 +24,5 @@ */ interface SerializerAwareProviderInterface { - /** - * @return void - */ - public function setSerializerLocator(ContainerInterface $serializerLocator); + public function setSerializerLocator(ContainerInterface $serializerLocator): void; }