From 84f9713686ce351f44f7f8bcdce4f98a6af396d0 Mon Sep 17 00:00:00 2001 From: soyuka Date: Fri, 19 Jul 2024 09:54:42 +0200 Subject: [PATCH 1/4] fix(symfony): deprecate actions --- src/Symfony/Action/NotExposedAction.php | 33 +++++++++++++++++++ src/Symfony/Action/NotFoundAction.php | 27 +++++++++++++++ src/Symfony/Action/PlaceholderAction.php | 32 ++++++++++++++++++ src/Symfony/Bundle/Resources/config/api.xml | 12 ++++--- .../Resources/config/symfony/symfony.xml | 7 ++-- src/deprecation.php | 25 +++++++++++++- 6 files changed, 129 insertions(+), 7 deletions(-) create mode 100644 src/Symfony/Action/NotExposedAction.php create mode 100644 src/Symfony/Action/NotFoundAction.php create mode 100644 src/Symfony/Action/PlaceholderAction.php diff --git a/src/Symfony/Action/NotExposedAction.php b/src/Symfony/Action/NotExposedAction.php new file mode 100644 index 00000000000..1692e702aed --- /dev/null +++ b/src/Symfony/Action/NotExposedAction.php @@ -0,0 +1,33 @@ + + * + * 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\Action; + +use ApiPlatform\Metadata\Exception\NotExposedHttpException; +use Symfony\Component\HttpFoundation\Request; + +/** + * An action which always returns HTTP 404 Not Found with an explanation for why the operation is not exposed. + */ +final class NotExposedAction +{ + public function __invoke(Request $request): never + { + $message = 'This route does not aim to be called.'; + if ('api_genid' === $request->attributes->get('_route')) { + $message = 'This route is not exposed on purpose. It generates an IRI for a collection resource without identifier nor item operation.'; + } + + throw new NotExposedHttpException($message); + } +} diff --git a/src/Symfony/Action/NotFoundAction.php b/src/Symfony/Action/NotFoundAction.php new file mode 100644 index 00000000000..8d183f3c9b2 --- /dev/null +++ b/src/Symfony/Action/NotFoundAction.php @@ -0,0 +1,27 @@ + + * + * 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\Action; + +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; + +/** + * An action which always returns HTTP 404 Not Found. Useful for disabling an operation. + */ +final class NotFoundAction +{ + public function __invoke(): void + { + throw new NotFoundHttpException(); + } +} diff --git a/src/Symfony/Action/PlaceholderAction.php b/src/Symfony/Action/PlaceholderAction.php new file mode 100644 index 00000000000..44b5f34e6f6 --- /dev/null +++ b/src/Symfony/Action/PlaceholderAction.php @@ -0,0 +1,32 @@ + + * + * 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\Action; + +/** + * Placeholder returning the data passed in parameter. + * + * @author Kévin Dunglas + */ +final class PlaceholderAction +{ + /** + * @param object $data + * + * @return object + */ + public function __invoke($data) + { + return $data; + } +} diff --git a/src/Symfony/Bundle/Resources/config/api.xml b/src/Symfony/Bundle/Resources/config/api.xml index f9801b3f82e..49da060fe58 100644 --- a/src/Symfony/Bundle/Resources/config/api.xml +++ b/src/Symfony/Bundle/Resources/config/api.xml @@ -5,15 +5,19 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> - - - - + + + + + + + + diff --git a/src/Symfony/Bundle/Resources/config/symfony/symfony.xml b/src/Symfony/Bundle/Resources/config/symfony/symfony.xml index e5c2dec65cd..897b158b804 100644 --- a/src/Symfony/Bundle/Resources/config/symfony/symfony.xml +++ b/src/Symfony/Bundle/Resources/config/symfony/symfony.xml @@ -5,8 +5,8 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> - - + + @@ -14,6 +14,9 @@ + + + %api_platform.handle_symfony_errors% diff --git a/src/deprecation.php b/src/deprecation.php index ba82266a811..37732b5a6c3 100644 --- a/src/deprecation.php +++ b/src/deprecation.php @@ -24,7 +24,30 @@ ApiPlatform\Api\QueryParameterValidator\Validator\Required::class => ApiPlatform\ParameterValidator\Validator\Required::class, ]; -spl_autoload_register(function ($className) use ($deprecatedClassesWithAliases): void { +$movedClasses = [ + ApiPlatform\Action\EntrypointAction::class => ApiPlatform\Symfony\Action\EntrypointAction::class, + ApiPlatform\Action\NotExposedAction::class => ApiPlatform\Symfony\Action\NotExposedAction::class, + ApiPlatform\Action\NotFoundAction::class => ApiPlatform\Symfony\Action\NotFoundAction::class, + ApiPlatform\Action\PlaceholderAction::class => ApiPlatform\Symfony\Action\PlaceholderAction::class, +]; + +$removedClasses = [ + ApiPlatform\Action\ExceptionAction::class => true, +]; + +spl_autoload_register(function ($className) use ($deprecatedClassesWithAliases, $movedClasses, $removedClasses): void { + if (isset($removedClasses[$className])) { + trigger_deprecation('api-platform/core', '4.0', sprintf('The class %s is deprecated and will be removed.', $className)); + + return; + } + + if (isset($movedClasses[$className])) { + trigger_deprecation('api-platform/core', '4.0', sprintf('The class %s is deprecated, use %s instead.', $className, $movedClasses[$className])); + + return; + } + if (isset($deprecatedClassesWithAliases[$className])) { trigger_deprecation('api-platform/core', '4.0', sprintf('The class %s is deprecated, use %s instead.', $className, $deprecatedClassesWithAliases[$className])); From 3132711de5d59512e4d5bb24e5ea481ef6a569e5 Mon Sep 17 00:00:00 2001 From: soyuka Date: Fri, 19 Jul 2024 10:34:34 +0200 Subject: [PATCH 2/4] exception deprecations --- .../Exception/NotExposedHttpException.php | 23 +++++++++++++++++++ src/deprecation.php | 18 +++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 src/Metadata/Exception/NotExposedHttpException.php diff --git a/src/Metadata/Exception/NotExposedHttpException.php b/src/Metadata/Exception/NotExposedHttpException.php new file mode 100644 index 00000000000..3951dd6aa4f --- /dev/null +++ b/src/Metadata/Exception/NotExposedHttpException.php @@ -0,0 +1,23 @@ + + * + * 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\Metadata\Exception; + +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; + +/** + * @author Vincent Chalamon + */ +class NotExposedHttpException extends NotFoundHttpException +{ +} diff --git a/src/deprecation.php b/src/deprecation.php index 37732b5a6c3..d6e43c1b5a8 100644 --- a/src/deprecation.php +++ b/src/deprecation.php @@ -29,10 +29,28 @@ ApiPlatform\Action\NotExposedAction::class => ApiPlatform\Symfony\Action\NotExposedAction::class, ApiPlatform\Action\NotFoundAction::class => ApiPlatform\Symfony\Action\NotFoundAction::class, ApiPlatform\Action\PlaceholderAction::class => ApiPlatform\Symfony\Action\PlaceholderAction::class, + ApiPlatform\Api\Entrypoint::class => ApiPlatform\Documentation\Entrypoint::class, + ApiPlatform\Exception\ExceptionInterface::class => ApiPlatform\Metadata\Exception\ExceptionInterface::class, + ApiPlatform\Exception\InvalidArgumentException::class => ApiPlatform\Metadata\Exception\InvalidArgumentException::class, + ApiPlatform\Exception\InvalidIdentifierException::class => ApiPlatform\Metadata\Exception\InvalidIdentifierException::class, + ApiPlatform\Exception\InvalidUriVariableException::class => ApiPlatform\Metadata\Exception\InvalidUriVariableException::class, + ApiPlatform\Exception\ItemNotFoundException::class => ApiPlatform\Metadata\Exception\ItemNotFoundException::class, + ApiPlatform\Exception\NotExposedHttpException::class => ApiPlatform\Metadata\Exception\NotExposedHttpException::class, + ApiPlatform\Exception\OperationNotFoundException::class => ApiPlatform\Metadata\Exception\OperationNotFoundException::class, + ApiPlatform\Exception\PropertyNotFoundException::class => ApiPlatform\Metadata\Exception\PropertyNotFoundException::class, + ApiPlatform\Exception\ResourceClassNotFoundException::class => ApiPlatform\Metadata\Exception\ResourceClassNotFoundException::class, + ApiPlatform\Exception\RuntimeException::class=> ApiPlatform\Metadata\Exception\RuntimeException::class, + ]; $removedClasses = [ ApiPlatform\Action\ExceptionAction::class => true, + ApiPlatform\Exception\DeserializationException::class => true, + ApiPlatform\Exception\ErrorCodeSerializableInterface::class => true, + ApiPlatform\Exception\FilterValidationException::class => true, + ApiPlatform\Exception\InvalidResourceException::class => true, + ApiPlatform\Exception\InvalidValueException::class => true, + ApiPlatform\Exception\ResourceClassNotSupportedException::class ]; spl_autoload_register(function ($className) use ($deprecatedClassesWithAliases, $movedClasses, $removedClasses): void { From ed16132889c3165604c2c0070ae7294f4c302a92 Mon Sep 17 00:00:00 2001 From: soyuka Date: Fri, 19 Jul 2024 15:51:53 +0200 Subject: [PATCH 3/4] deprecations --- src/Action/NotExposedAction.php | 2 + src/Action/NotFoundAction.php | 2 + src/Action/PlaceholderAction.php | 2 + ...viderResourceMetadataCollectionFactory.php | 6 +- src/Exception/DeserializationException.php | 2 + .../ErrorCodeSerializableInterface.php | 2 + src/Exception/ExceptionInterface.php | 2 + src/Exception/InvalidArgumentException.php | 2 + src/Exception/InvalidIdentifierException.php | 2 + src/Exception/InvalidResourceException.php | 2 + src/Exception/InvalidUriVariableException.php | 2 + src/Exception/InvalidValueException.php | 3 + src/Exception/ItemNotFoundException.php | 2 + src/Exception/NotExposedHttpException.php | 2 + src/Exception/OperationNotFoundException.php | 2 + src/Exception/PropertyNotFoundException.php | 2 + .../ResourceClassNotFoundException.php | 2 + .../ResourceClassNotSupportedException.php | 2 + src/Exception/RuntimeException.php | 2 + .../Factory/CollectionResolverFactory.php | 2 + .../Factory/ItemMutationResolverFactory.php | 2 + .../Resolver/Factory/ItemResolverFactory.php | 2 + .../ItemSubscriptionResolverFactory.php | 2 + .../Resolver/Stage/DeserializeStage.php | 2 + .../Stage/DeserializeStageInterface.php | 2 + src/GraphQl/Resolver/Stage/ReadStage.php | 2 + .../Resolver/Stage/ReadStageInterface.php | 2 + .../Stage/SecurityPostDenormalizeStage.php | 2 + .../SecurityPostDenormalizeStageInterface.php | 2 + .../SecurityPostValidationStageInterface.php | 2 + src/GraphQl/Resolver/Stage/SecurityStage.php | 2 + .../Resolver/Stage/SecurityStageInterface.php | 2 + src/GraphQl/Resolver/Stage/SerializeStage.php | 2 + .../Stage/SerializeStageInterface.php | 2 + src/GraphQl/Resolver/Stage/ValidateStage.php | 2 + .../Resolver/Stage/ValidateStageInterface.php | 2 + src/GraphQl/Resolver/Stage/WriteStage.php | 2 + .../Resolver/Stage/WriteStageInterface.php | 2 + .../Subscription/SubscriptionManager.php | 5 +- src/Hal/Serializer/CollectionNormalizer.php | 2 +- .../PartialCollectionViewNormalizer.php | 2 +- .../Serializer/CollectionNormalizer.php | 2 +- src/JsonSchema/SchemaFactory.php | 1 + src/JsonSchema/TypeFactoryInterface.php | 2 + src/Metadata/FilterInterface.php | 7 +- src/Metadata/Util/IriHelper.php | 108 ++++++++++++++ src/OpenApi/Factory/OpenApiFactory.php | 51 ++++++- src/OpenApi/Factory/TypeFactoryTrait.php | 132 ++++++++++++++++++ src/Problem/Serializer/ErrorNormalizer.php | 1 + .../ApiPlatformExtension.php | 8 ++ .../DependencyInjection/Configuration.php | 1 + .../Bundle/Resources/config/graphql.xml | 3 +- .../Bundle/Resources/config/json_schema.xml | 2 +- .../Bundle/Resources/config/openapi.xml | 2 +- src/Symfony/Bundle/Test/ClientTrait.php | 25 ++++ src/Symfony/EventListener/ErrorListener.php | 4 +- .../QueryParameterValidateListener.php | 2 + .../Security/State/AccessCheckerProvider.php | 2 +- .../State/QueryParameterValidateProvider.php | 3 + src/Util/IriHelper.php | 2 + src/deprecation.php | 54 ++++++- tests/Fixtures/app/AppKernel.php | 1 + .../Command/JsonSchemaGenerateCommandTest.php | 6 +- 63 files changed, 483 insertions(+), 26 deletions(-) create mode 100644 src/Metadata/Util/IriHelper.php create mode 100644 src/OpenApi/Factory/TypeFactoryTrait.php create mode 100644 src/Symfony/Bundle/Test/ClientTrait.php diff --git a/src/Action/NotExposedAction.php b/src/Action/NotExposedAction.php index cfee48b3ffe..183ad439443 100644 --- a/src/Action/NotExposedAction.php +++ b/src/Action/NotExposedAction.php @@ -18,6 +18,8 @@ /** * An action which always returns HTTP 404 Not Found with an explanation for why the operation is not exposed. + * + * @deprecated use ApiPlatform\Symfony\Action\NotExposedAction */ final class NotExposedAction { diff --git a/src/Action/NotFoundAction.php b/src/Action/NotFoundAction.php index 164cc63cb85..8005756e426 100644 --- a/src/Action/NotFoundAction.php +++ b/src/Action/NotFoundAction.php @@ -17,6 +17,8 @@ /** * An action which always returns HTTP 404 Not Found. Useful for disabling an operation. + * + * @deprecated use ApiPlatform\Symfony\Action\NotFoundAction */ final class NotFoundAction { diff --git a/src/Action/PlaceholderAction.php b/src/Action/PlaceholderAction.php index 79cc83f8371..1a2acd2e6a8 100644 --- a/src/Action/PlaceholderAction.php +++ b/src/Action/PlaceholderAction.php @@ -17,6 +17,8 @@ * Placeholder returning the data passed in parameter. * * @author Kévin Dunglas + * + * @deprecated use ApiPlatform\Symfony\Action\PlaceholderAction */ final class PlaceholderAction { diff --git a/src/Elasticsearch/Metadata/Resource/Factory/ElasticsearchProviderResourceMetadataCollectionFactory.php b/src/Elasticsearch/Metadata/Resource/Factory/ElasticsearchProviderResourceMetadataCollectionFactory.php index 624f44ff891..00f44193b72 100644 --- a/src/Elasticsearch/Metadata/Resource/Factory/ElasticsearchProviderResourceMetadataCollectionFactory.php +++ b/src/Elasticsearch/Metadata/Resource/Factory/ElasticsearchProviderResourceMetadataCollectionFactory.php @@ -21,15 +21,17 @@ use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; -use ApiPlatform\Metadata\Util\Inflector; use Elasticsearch\Client; use Elasticsearch\Common\Exceptions\Missing404Exception; use Elasticsearch\Common\Exceptions\NoNodesAvailableException; final class ElasticsearchProviderResourceMetadataCollectionFactory implements ResourceMetadataCollectionFactoryInterface { - public function __construct(private readonly ?Client $client, private readonly ResourceMetadataCollectionFactoryInterface $decorated, private readonly bool $triggerDeprecation = true, private readonly ?InflectorInterface $inflector = new Inflector()) // @phpstan-ignore-line + public function __construct(private readonly ?Client $client, private readonly ResourceMetadataCollectionFactoryInterface $decorated, private readonly bool $triggerDeprecation = true, private readonly ?InflectorInterface $inflector = null) // @phpstan-ignore-line { + if ($client) { + trigger_deprecation('api-platform/core', '4.0', sprintf('Using $client at "%s" is deprecated and the argument will be removed.', self::class)); + } } /** diff --git a/src/Exception/DeserializationException.php b/src/Exception/DeserializationException.php index 37dfcb55772..1cc3584ad48 100644 --- a/src/Exception/DeserializationException.php +++ b/src/Exception/DeserializationException.php @@ -20,6 +20,8 @@ * * @author Samuel ROZE * @author Kévin Dunglas + * + * @deprecated */ class DeserializationException extends \Exception implements ExceptionInterface, SerializerExceptionInterface { diff --git a/src/Exception/ErrorCodeSerializableInterface.php b/src/Exception/ErrorCodeSerializableInterface.php index 7d2fd3fcf17..5a3550c8c3a 100644 --- a/src/Exception/ErrorCodeSerializableInterface.php +++ b/src/Exception/ErrorCodeSerializableInterface.php @@ -15,6 +15,8 @@ /** * An exception which has a serializable application-specific error code. + * + * @deprecated */ interface ErrorCodeSerializableInterface { diff --git a/src/Exception/ExceptionInterface.php b/src/Exception/ExceptionInterface.php index 99fcdca6992..c094cc44ed4 100644 --- a/src/Exception/ExceptionInterface.php +++ b/src/Exception/ExceptionInterface.php @@ -17,6 +17,8 @@ * Base exception interface. * * @author Kévin Dunglas + * + * @deprecated use ApiPlatform\Metadata\Exception\ExceptionInterface */ interface ExceptionInterface extends \Throwable { diff --git a/src/Exception/InvalidArgumentException.php b/src/Exception/InvalidArgumentException.php index bddf0a57112..57cf4bb3d9b 100644 --- a/src/Exception/InvalidArgumentException.php +++ b/src/Exception/InvalidArgumentException.php @@ -17,6 +17,8 @@ * Invalid argument exception. * * @author Kévin Dunglas + * + * @deprecated use ApiPlatform\Metadata\Exception\InvalidArgumentException */ class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface { diff --git a/src/Exception/InvalidIdentifierException.php b/src/Exception/InvalidIdentifierException.php index d77354ae30a..3d22df6058e 100644 --- a/src/Exception/InvalidIdentifierException.php +++ b/src/Exception/InvalidIdentifierException.php @@ -17,6 +17,8 @@ * Identifier is not valid exception. * * @author Antoine Bluchet + * + * @deprecated use ApiPlatform\Metadata\Exception\InvalidIdentifierException */ class InvalidIdentifierException extends \Exception implements ExceptionInterface { diff --git a/src/Exception/InvalidResourceException.php b/src/Exception/InvalidResourceException.php index a69543355d7..d1c0b56f72d 100644 --- a/src/Exception/InvalidResourceException.php +++ b/src/Exception/InvalidResourceException.php @@ -17,6 +17,8 @@ * Invalid resource exception. * * @author Paul Le Corre + * + * @deprecated */ class InvalidResourceException extends \Exception implements ExceptionInterface { diff --git a/src/Exception/InvalidUriVariableException.php b/src/Exception/InvalidUriVariableException.php index 5aedb5839a7..e686cec932a 100644 --- a/src/Exception/InvalidUriVariableException.php +++ b/src/Exception/InvalidUriVariableException.php @@ -17,6 +17,8 @@ * Identifier is not valid exception. * * @author Antoine Bluchet + * + * @deprecated use ApiPlatform\Metadata\Exception\InvalidUriVariableException */ class InvalidUriVariableException extends \Exception implements ExceptionInterface { diff --git a/src/Exception/InvalidValueException.php b/src/Exception/InvalidValueException.php index 96b023fdbae..dc9d834a644 100644 --- a/src/Exception/InvalidValueException.php +++ b/src/Exception/InvalidValueException.php @@ -13,6 +13,9 @@ namespace ApiPlatform\Exception; +/** + * @deprecated + */ class InvalidValueException extends InvalidArgumentException { } diff --git a/src/Exception/ItemNotFoundException.php b/src/Exception/ItemNotFoundException.php index a66f39965d8..b21d2732ecc 100644 --- a/src/Exception/ItemNotFoundException.php +++ b/src/Exception/ItemNotFoundException.php @@ -17,6 +17,8 @@ * Item not found exception. * * @author Amrouche Hamza + * + * @deprecated use ApiPlatform\Metadata\Exception\ItemNotFoundException */ class ItemNotFoundException extends InvalidArgumentException { diff --git a/src/Exception/NotExposedHttpException.php b/src/Exception/NotExposedHttpException.php index 5092738d046..08d50f22982 100644 --- a/src/Exception/NotExposedHttpException.php +++ b/src/Exception/NotExposedHttpException.php @@ -17,6 +17,8 @@ /** * @author Vincent Chalamon + * + * @deprecated use ApiPlatform\Metadata\Exception\NotExposedHttpException */ class NotExposedHttpException extends NotFoundHttpException { diff --git a/src/Exception/OperationNotFoundException.php b/src/Exception/OperationNotFoundException.php index a112ef15da5..855ba110b2e 100644 --- a/src/Exception/OperationNotFoundException.php +++ b/src/Exception/OperationNotFoundException.php @@ -15,6 +15,8 @@ /** * Operation not found exception. + * + * @deprecated use ApiPlatform\Metadata\Exception\OperationNotFoundException */ class OperationNotFoundException extends \InvalidArgumentException implements ExceptionInterface { diff --git a/src/Exception/PropertyNotFoundException.php b/src/Exception/PropertyNotFoundException.php index 44df7ee03ec..be1f406c97a 100644 --- a/src/Exception/PropertyNotFoundException.php +++ b/src/Exception/PropertyNotFoundException.php @@ -17,6 +17,8 @@ * Property not found exception. * * @author Kévin Dunglas + * + * @deprecated use ApiPlatform\Metadata\Exception\PropertyNotFoundException */ class PropertyNotFoundException extends \Exception implements ExceptionInterface { diff --git a/src/Exception/ResourceClassNotFoundException.php b/src/Exception/ResourceClassNotFoundException.php index 0a0f0eaa9c4..408d7dd1b4c 100644 --- a/src/Exception/ResourceClassNotFoundException.php +++ b/src/Exception/ResourceClassNotFoundException.php @@ -17,6 +17,8 @@ * Resource class not found exception. * * @author Kévin Dunglas + * + * @deprecated use ApiPlatform\Metadata\Exception\ResourceClassNotFoundException */ class ResourceClassNotFoundException extends \Exception implements ExceptionInterface { diff --git a/src/Exception/ResourceClassNotSupportedException.php b/src/Exception/ResourceClassNotSupportedException.php index 9d291d1ae09..f8d0cc0fa6b 100644 --- a/src/Exception/ResourceClassNotSupportedException.php +++ b/src/Exception/ResourceClassNotSupportedException.php @@ -16,6 +16,8 @@ /** * Resource class not supported exception. * + * @deprecated + * * @author Kévin Dunglas */ class ResourceClassNotSupportedException extends \Exception implements ExceptionInterface diff --git a/src/Exception/RuntimeException.php b/src/Exception/RuntimeException.php index 7eba6481b10..eab66a88207 100644 --- a/src/Exception/RuntimeException.php +++ b/src/Exception/RuntimeException.php @@ -17,6 +17,8 @@ * Runtime exception. * * @author Kévin Dunglas + * + * @deprecated use ApiPlatform\Metadata\Exception\RuntimeException */ class RuntimeException extends \RuntimeException implements ExceptionInterface { diff --git a/src/GraphQl/Resolver/Factory/CollectionResolverFactory.php b/src/GraphQl/Resolver/Factory/CollectionResolverFactory.php index 5f7e31f2af0..2ab6032923a 100644 --- a/src/GraphQl/Resolver/Factory/CollectionResolverFactory.php +++ b/src/GraphQl/Resolver/Factory/CollectionResolverFactory.php @@ -32,6 +32,8 @@ * @author Alan Poulain * @author Kévin Dunglas * @author Vincent Chalamon + * + * @deprecated */ final class CollectionResolverFactory implements ResolverFactoryInterface { diff --git a/src/GraphQl/Resolver/Factory/ItemMutationResolverFactory.php b/src/GraphQl/Resolver/Factory/ItemMutationResolverFactory.php index ffe1e3d04c2..380efee4b73 100644 --- a/src/GraphQl/Resolver/Factory/ItemMutationResolverFactory.php +++ b/src/GraphQl/Resolver/Factory/ItemMutationResolverFactory.php @@ -35,6 +35,8 @@ * * @author Alan Poulain * @author Vincent Chalamon + * + * @deprecated */ final class ItemMutationResolverFactory implements ResolverFactoryInterface { diff --git a/src/GraphQl/Resolver/Factory/ItemResolverFactory.php b/src/GraphQl/Resolver/Factory/ItemResolverFactory.php index 45820aaeed4..197662a2e0e 100644 --- a/src/GraphQl/Resolver/Factory/ItemResolverFactory.php +++ b/src/GraphQl/Resolver/Factory/ItemResolverFactory.php @@ -32,6 +32,8 @@ * @author Alan Poulain * @author Kévin Dunglas * @author Vincent Chalamon + * + * @deprecated */ final class ItemResolverFactory implements ResolverFactoryInterface { diff --git a/src/GraphQl/Resolver/Factory/ItemSubscriptionResolverFactory.php b/src/GraphQl/Resolver/Factory/ItemSubscriptionResolverFactory.php index b182ba528ff..3fcfff6499d 100644 --- a/src/GraphQl/Resolver/Factory/ItemSubscriptionResolverFactory.php +++ b/src/GraphQl/Resolver/Factory/ItemSubscriptionResolverFactory.php @@ -28,6 +28,8 @@ * Creates a function resolving a GraphQL subscription of an item. * * @author Alan Poulain + * + * @deprecated */ final class ItemSubscriptionResolverFactory implements ResolverFactoryInterface { diff --git a/src/GraphQl/Resolver/Stage/DeserializeStage.php b/src/GraphQl/Resolver/Stage/DeserializeStage.php index 0e7f7daec1e..7cfcf08b7f8 100644 --- a/src/GraphQl/Resolver/Stage/DeserializeStage.php +++ b/src/GraphQl/Resolver/Stage/DeserializeStage.php @@ -23,6 +23,8 @@ * Deserialize stage of GraphQL resolvers. * * @author Alan Poulain + * + * @deprecated */ final class DeserializeStage implements DeserializeStageInterface { diff --git a/src/GraphQl/Resolver/Stage/DeserializeStageInterface.php b/src/GraphQl/Resolver/Stage/DeserializeStageInterface.php index 586104961c4..146ec0aa143 100644 --- a/src/GraphQl/Resolver/Stage/DeserializeStageInterface.php +++ b/src/GraphQl/Resolver/Stage/DeserializeStageInterface.php @@ -19,6 +19,8 @@ * Deserialize stage of GraphQL resolvers. * * @author Alan Poulain + * + * @deprecated */ interface DeserializeStageInterface { diff --git a/src/GraphQl/Resolver/Stage/ReadStage.php b/src/GraphQl/Resolver/Stage/ReadStage.php index 9ab1007eab0..e46ee0225ec 100644 --- a/src/GraphQl/Resolver/Stage/ReadStage.php +++ b/src/GraphQl/Resolver/Stage/ReadStage.php @@ -27,6 +27,8 @@ * Read stage of GraphQL resolvers. * * @author Alan Poulain + * + * @deprecated */ final class ReadStage implements ReadStageInterface { diff --git a/src/GraphQl/Resolver/Stage/ReadStageInterface.php b/src/GraphQl/Resolver/Stage/ReadStageInterface.php index fa0e941ea20..0c1670257ed 100644 --- a/src/GraphQl/Resolver/Stage/ReadStageInterface.php +++ b/src/GraphQl/Resolver/Stage/ReadStageInterface.php @@ -19,6 +19,8 @@ * Read stage of GraphQL resolvers. * * @author Alan Poulain + * + * @deprecated */ interface ReadStageInterface { diff --git a/src/GraphQl/Resolver/Stage/SecurityPostDenormalizeStage.php b/src/GraphQl/Resolver/Stage/SecurityPostDenormalizeStage.php index f87d48ed545..8610ee781d1 100644 --- a/src/GraphQl/Resolver/Stage/SecurityPostDenormalizeStage.php +++ b/src/GraphQl/Resolver/Stage/SecurityPostDenormalizeStage.php @@ -22,6 +22,8 @@ * Security post denormalize stage of GraphQL resolvers. * * @author Vincent Chalamon + * + * @deprecated */ final class SecurityPostDenormalizeStage implements SecurityPostDenormalizeStageInterface { diff --git a/src/GraphQl/Resolver/Stage/SecurityPostDenormalizeStageInterface.php b/src/GraphQl/Resolver/Stage/SecurityPostDenormalizeStageInterface.php index a62ba52731d..5a9bfae3b19 100644 --- a/src/GraphQl/Resolver/Stage/SecurityPostDenormalizeStageInterface.php +++ b/src/GraphQl/Resolver/Stage/SecurityPostDenormalizeStageInterface.php @@ -20,6 +20,8 @@ * Security post deserialization stage of GraphQL resolvers. * * @author Vincent Chalamon + * + * @deprecated */ interface SecurityPostDenormalizeStageInterface { diff --git a/src/GraphQl/Resolver/Stage/SecurityPostValidationStageInterface.php b/src/GraphQl/Resolver/Stage/SecurityPostValidationStageInterface.php index bbbbf7d4d05..2e23c0393c0 100644 --- a/src/GraphQl/Resolver/Stage/SecurityPostValidationStageInterface.php +++ b/src/GraphQl/Resolver/Stage/SecurityPostValidationStageInterface.php @@ -21,6 +21,8 @@ * * @author Vincent Chalamon * @author Grégoire Pineau + * + * @deprecated */ interface SecurityPostValidationStageInterface { diff --git a/src/GraphQl/Resolver/Stage/SecurityStage.php b/src/GraphQl/Resolver/Stage/SecurityStage.php index b577fc1c8a0..e1db9550826 100644 --- a/src/GraphQl/Resolver/Stage/SecurityStage.php +++ b/src/GraphQl/Resolver/Stage/SecurityStage.php @@ -22,6 +22,8 @@ * Security stage of GraphQL resolvers. * * @author Alan Poulain + * + * @deprecated */ final class SecurityStage implements SecurityStageInterface { diff --git a/src/GraphQl/Resolver/Stage/SecurityStageInterface.php b/src/GraphQl/Resolver/Stage/SecurityStageInterface.php index a1f47fad9bf..3ee436365bc 100644 --- a/src/GraphQl/Resolver/Stage/SecurityStageInterface.php +++ b/src/GraphQl/Resolver/Stage/SecurityStageInterface.php @@ -21,6 +21,8 @@ * * @author Alan Poulain * @author Vincent Chalamon + * + * @deprecated */ interface SecurityStageInterface { diff --git a/src/GraphQl/Resolver/Stage/SerializeStage.php b/src/GraphQl/Resolver/Stage/SerializeStage.php index e10298f0a1e..81e98971524 100644 --- a/src/GraphQl/Resolver/Stage/SerializeStage.php +++ b/src/GraphQl/Resolver/Stage/SerializeStage.php @@ -30,6 +30,8 @@ * Serialize stage of GraphQL resolvers. * * @author Alan Poulain + * + * @deprecated */ final class SerializeStage implements SerializeStageInterface { diff --git a/src/GraphQl/Resolver/Stage/SerializeStageInterface.php b/src/GraphQl/Resolver/Stage/SerializeStageInterface.php index ac67e9b97f0..eda72060f80 100644 --- a/src/GraphQl/Resolver/Stage/SerializeStageInterface.php +++ b/src/GraphQl/Resolver/Stage/SerializeStageInterface.php @@ -19,6 +19,8 @@ * Serialize stage of GraphQL resolvers. * * @author Alan Poulain + * + * @deprecated */ interface SerializeStageInterface { diff --git a/src/GraphQl/Resolver/Stage/ValidateStage.php b/src/GraphQl/Resolver/Stage/ValidateStage.php index c7c2103b4b3..54a67f29b1b 100644 --- a/src/GraphQl/Resolver/Stage/ValidateStage.php +++ b/src/GraphQl/Resolver/Stage/ValidateStage.php @@ -20,6 +20,8 @@ * Validate stage of GraphQL resolvers. * * @author Alan Poulain + * + * @deprecated */ final class ValidateStage implements ValidateStageInterface { diff --git a/src/GraphQl/Resolver/Stage/ValidateStageInterface.php b/src/GraphQl/Resolver/Stage/ValidateStageInterface.php index 092a1d5aee4..8d349bf8292 100644 --- a/src/GraphQl/Resolver/Stage/ValidateStageInterface.php +++ b/src/GraphQl/Resolver/Stage/ValidateStageInterface.php @@ -20,6 +20,8 @@ * Validate stage of GraphQL resolvers. * * @author Alan Poulain + * + * @deprecated */ interface ValidateStageInterface { diff --git a/src/GraphQl/Resolver/Stage/WriteStage.php b/src/GraphQl/Resolver/Stage/WriteStage.php index 35bd780154b..fb430005309 100644 --- a/src/GraphQl/Resolver/Stage/WriteStage.php +++ b/src/GraphQl/Resolver/Stage/WriteStage.php @@ -21,6 +21,8 @@ * Write stage of GraphQL resolvers. * * @author Alan Poulain + * + * @deprecated */ final class WriteStage implements WriteStageInterface { diff --git a/src/GraphQl/Resolver/Stage/WriteStageInterface.php b/src/GraphQl/Resolver/Stage/WriteStageInterface.php index 9d090ce59c9..b02501ed80a 100644 --- a/src/GraphQl/Resolver/Stage/WriteStageInterface.php +++ b/src/GraphQl/Resolver/Stage/WriteStageInterface.php @@ -19,6 +19,8 @@ * Write stage of GraphQL resolvers. * * @author Alan Poulain + * + * @deprecated */ interface WriteStageInterface { diff --git a/src/GraphQl/Subscription/SubscriptionManager.php b/src/GraphQl/Subscription/SubscriptionManager.php index 2cd2c2c6873..c7b7e9662b2 100644 --- a/src/GraphQl/Subscription/SubscriptionManager.php +++ b/src/GraphQl/Subscription/SubscriptionManager.php @@ -37,8 +37,11 @@ final class SubscriptionManager implements OperationAwareSubscriptionManagerInte use ResourceClassInfoTrait; use SortTrait; - public function __construct(private readonly CacheItemPoolInterface $subscriptionsCache, private readonly SubscriptionIdentifierGeneratorInterface $subscriptionIdentifierGenerator, private readonly ?SerializeStageInterface $serializeStage, private readonly IriConverterInterface $iriConverter, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly ?ProcessorInterface $normalizeProcessor = null) + public function __construct(private readonly CacheItemPoolInterface $subscriptionsCache, private readonly SubscriptionIdentifierGeneratorInterface $subscriptionIdentifierGenerator, private readonly SerializeStageInterface|ProcessorInterface|null $serializeStage = null, private readonly IriConverterInterface $iriConverter, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly ?ProcessorInterface $normalizeProcessor = null) { + if (!$serializeStage instanceof ProcessorInterface) { + trigger_deprecation('api-platform/core', '4.0', sprintf('Using an instanceof "%s" is deprecated, use "%s" instead.', SerializeStageInterface::class, ProcessorInterface::class)); + } } public function retrieveSubscriptionId(array $context, ?array $result, ?Operation $operation = null): ?string diff --git a/src/Hal/Serializer/CollectionNormalizer.php b/src/Hal/Serializer/CollectionNormalizer.php index 8cce319c174..f0fdf618f3a 100644 --- a/src/Hal/Serializer/CollectionNormalizer.php +++ b/src/Hal/Serializer/CollectionNormalizer.php @@ -16,8 +16,8 @@ use ApiPlatform\Api\ResourceClassResolverInterface as LegacyResourceClassResolverInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\ResourceClassResolverInterface; +use ApiPlatform\Metadata\Util\IriHelper; use ApiPlatform\Serializer\AbstractCollectionNormalizer; -use ApiPlatform\Util\IriHelper; use Symfony\Component\Serializer\Exception\UnexpectedValueException; /** diff --git a/src/Hydra/Serializer/PartialCollectionViewNormalizer.php b/src/Hydra/Serializer/PartialCollectionViewNormalizer.php index 95fb34245d2..d6c138ba48a 100644 --- a/src/Hydra/Serializer/PartialCollectionViewNormalizer.php +++ b/src/Hydra/Serializer/PartialCollectionViewNormalizer.php @@ -16,10 +16,10 @@ use ApiPlatform\Metadata\HttpOperation; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\UrlGeneratorInterface; +use ApiPlatform\Metadata\Util\IriHelper; use ApiPlatform\Serializer\CacheableSupportsMethodInterface; use ApiPlatform\State\Pagination\PaginatorInterface; use ApiPlatform\State\Pagination\PartialPaginatorInterface; -use ApiPlatform\Util\IriHelper; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; use Symfony\Component\Serializer\Exception\UnexpectedValueException; diff --git a/src/JsonApi/Serializer/CollectionNormalizer.php b/src/JsonApi/Serializer/CollectionNormalizer.php index 8fe2be7c32b..f83264fec7f 100644 --- a/src/JsonApi/Serializer/CollectionNormalizer.php +++ b/src/JsonApi/Serializer/CollectionNormalizer.php @@ -16,8 +16,8 @@ use ApiPlatform\Api\ResourceClassResolverInterface as LegacyResourceClassResolverInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\ResourceClassResolverInterface; +use ApiPlatform\Metadata\Util\IriHelper; use ApiPlatform\Serializer\AbstractCollectionNormalizer; -use ApiPlatform\Util\IriHelper; use Symfony\Component\Serializer\Exception\UnexpectedValueException; /** diff --git a/src/JsonSchema/SchemaFactory.php b/src/JsonSchema/SchemaFactory.php index 097eaddd4b8..a8420d6e091 100644 --- a/src/JsonSchema/SchemaFactory.php +++ b/src/JsonSchema/SchemaFactory.php @@ -42,6 +42,7 @@ final class SchemaFactory implements SchemaFactoryInterface, SchemaFactoryAwareI public function __construct(?TypeFactoryInterface $typeFactory, ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ?NameConverterInterface $nameConverter = null, ?ResourceClassResolverInterface $resourceClassResolver = null, private readonly ?array $distinctFormats = null, private ?DefinitionNameFactoryInterface $definitionNameFactory = null) { if ($typeFactory) { + trigger_deprecation('api-platform/core', '4.0', sprintf('Injecting the "%s" inside "%s" is deprecated and "%s" will be removed in 4.x.', TypeFactoryInterface::class, self::class, TypeFactoryInterface::class)); $this->typeFactory = $typeFactory; } if (!$definitionNameFactory) { diff --git a/src/JsonSchema/TypeFactoryInterface.php b/src/JsonSchema/TypeFactoryInterface.php index b2ba889c14e..70a551fda16 100644 --- a/src/JsonSchema/TypeFactoryInterface.php +++ b/src/JsonSchema/TypeFactoryInterface.php @@ -18,6 +18,8 @@ /** * Factory for creating the JSON Schema document which specifies the data type corresponding to a PHP type. * + * @deprecated + * * @author Kévin Dunglas */ interface TypeFactoryInterface diff --git a/src/Metadata/FilterInterface.php b/src/Metadata/FilterInterface.php index abeb170d4c6..5f393fa4b40 100644 --- a/src/Metadata/FilterInterface.php +++ b/src/Metadata/FilterInterface.php @@ -30,12 +30,7 @@ interface FilterInterface * - description : the description of the filter * - strategy: the used strategy * - is_collection: if this filter is for collection - * - swagger: additional parameters for the path operation, - * e.g. 'swagger' => [ - * 'description' => 'My Description', - * 'name' => 'My Name', - * 'type' => 'integer', - * ] + * - swagger: deprecated, use openapi instead * - openapi: additional parameters for the path operation in the version 3 spec, * e.g. 'openapi' => [ * 'description' => 'My Description', diff --git a/src/Metadata/Util/IriHelper.php b/src/Metadata/Util/IriHelper.php new file mode 100644 index 00000000000..7d36ba732e0 --- /dev/null +++ b/src/Metadata/Util/IriHelper.php @@ -0,0 +1,108 @@ + + * + * 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\Metadata\Util; + +use ApiPlatform\Metadata\Exception\InvalidArgumentException; +use ApiPlatform\Metadata\UrlGeneratorInterface; +use ApiPlatform\State\Util\RequestParser; + +/** + * Parses and creates IRIs. + * + * @author Kévin Dunglas + * + * @internal + */ +final class IriHelper +{ + private function __construct() + { + } + + /** + * Parses and standardizes the request IRI. + * + * @throws InvalidArgumentException + */ + public static function parseIri(string $iri, string $pageParameterName): array + { + $parts = parse_url($iri); + if (false === $parts) { + throw new InvalidArgumentException(sprintf('The request URI "%s" is malformed.', $iri)); + } + + $parameters = []; + if (isset($parts['query'])) { + $parameters = RequestParser::parseRequestParams($parts['query']); + + // Remove existing page parameter + unset($parameters[$pageParameterName]); + } + + return ['parts' => $parts, 'parameters' => $parameters]; + } + + /** + * Gets a collection IRI for the given parameters. + */ + public static function createIri(array $parts, array $parameters, ?string $pageParameterName = null, ?float $page = null, $urlGenerationStrategy = UrlGeneratorInterface::ABS_PATH): string + { + if (null !== $page && null !== $pageParameterName) { + $parameters[$pageParameterName] = $page; + } + + $query = http_build_query($parameters, '', '&', \PHP_QUERY_RFC3986); + $parts['query'] = preg_replace('/%5B\d+%5D/', '%5B%5D', $query); + + $url = ''; + if ((UrlGeneratorInterface::ABS_URL === $urlGenerationStrategy || UrlGeneratorInterface::NET_PATH === $urlGenerationStrategy) && isset($parts['host'])) { + if (isset($parts['scheme'])) { + $scheme = $parts['scheme']; + } elseif (isset($parts['port']) && 443 === $parts['port']) { + $scheme = 'https'; + } else { + $scheme = 'http'; + } + $url .= UrlGeneratorInterface::NET_PATH === $urlGenerationStrategy ? '//' : "$scheme://"; + + if (isset($parts['user'])) { + $url .= $parts['user']; + + if (isset($parts['pass'])) { + $url .= ':'.$parts['pass']; + } + + $url .= '@'; + } + + $url .= $parts['host']; + + if (isset($parts['port'])) { + $url .= ':'.$parts['port']; + } + } + + $url .= $parts['path']; + + if ('' !== $parts['query']) { + $url .= '?'.$parts['query']; + } + + if (isset($parts['fragment'])) { + $url .= '#'.$parts['fragment']; + } + + return $url; + } +} diff --git a/src/OpenApi/Factory/OpenApiFactory.php b/src/OpenApi/Factory/OpenApiFactory.php index 541e54c5521..8465d498da4 100644 --- a/src/OpenApi/Factory/OpenApiFactory.php +++ b/src/OpenApi/Factory/OpenApiFactory.php @@ -60,12 +60,14 @@ final class OpenApiFactory implements OpenApiFactoryInterface { use NormalizeOperationNameTrait; + use TypeFactoryTrait; public const BASE_URL = 'base_url'; public const OVERRIDE_OPENAPI_RESPONSES = 'open_api_override_responses'; private readonly Options $openApiOptions; private readonly PaginationOptions $paginationOptions; private ?RouteCollection $routeCollection = null; + private ?TypeFactoryInterface $jsonSchemaTypeFactory = null; private ?ContainerInterface $filterLocator = null; /** @@ -73,11 +75,16 @@ final class OpenApiFactory implements OpenApiFactoryInterface */ public const OPENAPI_DEFINITION_NAME = 'openapi_definition_name'; - public function __construct(private readonly ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly SchemaFactoryInterface $jsonSchemaFactory, private readonly TypeFactoryInterface $jsonSchemaTypeFactory, ContainerInterface $filterLocator, private readonly array $formats = [], ?Options $openApiOptions = null, ?PaginationOptions $paginationOptions = null, private readonly ?RouterInterface $router = null) + public function __construct(private readonly ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly SchemaFactoryInterface $jsonSchemaFactory, ?TypeFactoryInterface $jsonSchemaTypeFactory, ContainerInterface $filterLocator, private readonly array $formats = [], ?Options $openApiOptions = null, ?PaginationOptions $paginationOptions = null, private readonly ?RouterInterface $router = null) { $this->filterLocator = $filterLocator; $this->openApiOptions = $openApiOptions ?: new Options('API Platform'); $this->paginationOptions = $paginationOptions ?: new PaginationOptions(); + + if ($jsonSchemaTypeFactory) { + trigger_deprecation('api-platform/core', '4.0', sprintf('Injecting the "%s" inside "%s" is deprecated and "%s" will be removed in 4.x.', TypeFactoryInterface::class, self::class, TypeFactoryInterface::class)); + $this->jsonSchemaTypeFactory = $jsonSchemaTypeFactory; + } } /** @@ -638,7 +645,47 @@ private function getFiltersParameters(CollectionOperationInterface|HttpOperation } foreach ($filter->getDescription($entityClass) as $name => $data) { - $schema = $data['schema'] ?? (\in_array($data['type'], Type::$builtinTypes, true) ? $this->jsonSchemaTypeFactory->getType(new Type($data['type'], false, null, $data['is_collection'] ?? false), 'openapi') : ['type' => 'string']); + if (isset($data['swagger'])) { + trigger_deprecation('api-platform/core', '4.0', sprintf('Using the "swagger" field of the %s::getDescription() (%s) is deprecated.', $filter::class, $operation->getShortName())); + } + + if (isset($data['openapi']) && $data['openapi'] instanceof Parameter) { + $schema = $data['schema'] ?? []; + + if (isset($data['type']) && \in_array($data['type'] ?? null, Type::$builtinTypes, true) && !isset($schema['type'])) { + $schema += $this->jsonSchemaTypeFactory ? $this->jsonSchemaTypeFactory->getType(new Type($data['type'], false, null, $data['is_collection'] ?? false)) : $this->getType(new Type($data['type'], false, null, $data['is_collection'] ?? false)); + } + + if (!isset($schema['type'])) { + $schema['type'] = 'string'; + } + + $style = 'array' === ($schema['type'] ?? null) && \in_array( + $data['type'], + [Type::BUILTIN_TYPE_ARRAY, Type::BUILTIN_TYPE_OBJECT], + true + ) ? 'deepObject' : 'form'; + + $parameter = isset($data['openapi']) && $data['openapi'] instanceof Parameter ? $data['openapi'] : new Parameter(in: 'query', name: $name, style: $style, explode: $data['is_collection'] ?? false); + + if ('' === $parameter->getDescription() && ($description = $data['description'] ?? '')) { + $parameter = $parameter->withDescription($description); + } + + if (false === $parameter->getRequired() && false !== ($required = $data['required'] ?? false)) { + $parameter = $parameter->withRequired($required); + } + + $parameters[] = $parameter->withSchema($schema); + continue; + } + + trigger_deprecation('api-platform/core', '4.0', sprintf('Not using "%s" on the "openapi" field of the %s::getDescription() (%s) is deprecated.', Parameter::class, $filter::class, $operation->getShortName())); + if ($this->jsonSchemaTypeFactory) { + $schema = $data['schema'] ?? (\in_array($data['type'], Type::$builtinTypes, true) ? $this->jsonSchemaTypeFactory->getType(new Type($data['type'], false, null, $data['is_collection'] ?? false), 'openapi') : ['type' => 'string']); + } else { + $schema = $data['schema'] ?? (\in_array($data['type'], Type::$builtinTypes, true) ? $this->getType(new Type($data['type'], false, null, $data['is_collection'] ?? false), 'openapi') : ['type' => 'string']); + } $parameters[] = new Parameter( $name, diff --git a/src/OpenApi/Factory/TypeFactoryTrait.php b/src/OpenApi/Factory/TypeFactoryTrait.php new file mode 100644 index 00000000000..6a86070b988 --- /dev/null +++ b/src/OpenApi/Factory/TypeFactoryTrait.php @@ -0,0 +1,132 @@ + + * + * 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\OpenApi\Factory; + +use Ramsey\Uuid\UuidInterface; +use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\Uid\Ulid; +use Symfony\Component\Uid\Uuid; + +/** + * @internal + */ +trait TypeFactoryTrait +{ + private function getType(Type $type): array + { + if ($type->isCollection()) { + $keyType = $type->getCollectionKeyTypes()[0] ?? null; + $subType = ($type->getCollectionValueTypes()[0] ?? null) ?? new Type($type->getBuiltinType(), false, $type->getClassName(), false); + + if (null !== $keyType && Type::BUILTIN_TYPE_STRING === $keyType->getBuiltinType()) { + return $this->addNullabilityToTypeDefinition([ + 'type' => 'object', + 'additionalProperties' => $this->getType($subType), + ], $type); + } + + return $this->addNullabilityToTypeDefinition([ + 'type' => 'array', + 'items' => $this->getType($subType), + ], $type); + } + + return $this->addNullabilityToTypeDefinition($this->makeBasicType($type), $type); + } + + private function makeBasicType(Type $type): array + { + return match ($type->getBuiltinType()) { + Type::BUILTIN_TYPE_INT => ['type' => 'integer'], + Type::BUILTIN_TYPE_FLOAT => ['type' => 'number'], + Type::BUILTIN_TYPE_BOOL => ['type' => 'boolean'], + Type::BUILTIN_TYPE_OBJECT => $this->getClassType($type->getClassName(), $type->isNullable()), + default => ['type' => 'string'], + }; + } + + /** + * Gets the JSON Schema document which specifies the data type corresponding to the given PHP class, and recursively adds needed new schema to the current schema if provided. + */ + private function getClassType(?string $className, bool $nullable): array + { + if (null === $className) { + return ['type' => 'string']; + } + + if (is_a($className, \DateTimeInterface::class, true)) { + return [ + 'type' => 'string', + 'format' => 'date-time', + ]; + } + if (is_a($className, \DateInterval::class, true)) { + return [ + 'type' => 'string', + 'format' => 'duration', + ]; + } + if (is_a($className, UuidInterface::class, true) || is_a($className, Uuid::class, true)) { + return [ + 'type' => 'string', + 'format' => 'uuid', + ]; + } + if (is_a($className, Ulid::class, true)) { + return [ + 'type' => 'string', + 'format' => 'ulid', + ]; + } + if (is_a($className, \SplFileInfo::class, true)) { + return [ + 'type' => 'string', + 'format' => 'binary', + ]; + } + if (is_a($className, \BackedEnum::class, true)) { + $enumCases = array_map(static fn (\BackedEnum $enum): string|int => $enum->value, $className::cases()); + + $type = \is_string($enumCases[0] ?? '') ? 'string' : 'integer'; + + if ($nullable) { + $enumCases[] = null; + } + + return [ + 'type' => $type, + 'enum' => $enumCases, + ]; + } + + return ['type' => 'string']; + } + + /** + * @param array $jsonSchema + * + * @return array + */ + private function addNullabilityToTypeDefinition(array $jsonSchema, Type $type): array + { + if (!$type->isNullable()) { + return $jsonSchema; + } + + $typeDefinition = ['anyOf' => [$jsonSchema]]; + $typeDefinition['anyOf'][] = ['type' => 'null']; + + return $typeDefinition; + } +} diff --git a/src/Problem/Serializer/ErrorNormalizer.php b/src/Problem/Serializer/ErrorNormalizer.php index d62b32e64fb..f56373f2389 100644 --- a/src/Problem/Serializer/ErrorNormalizer.php +++ b/src/Problem/Serializer/ErrorNormalizer.php @@ -23,6 +23,7 @@ * Normalizes errors according to the API Problem spec (RFC 7807). * * @see https://tools.ietf.org/html/rfc7807 + * @deprecated * * @author Kévin Dunglas */ diff --git a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index cbbc8664022..80fcf56bd24 100644 --- a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php +++ b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php @@ -530,6 +530,10 @@ private function registerSwaggerConfiguration(ContainerBuilder $container, array $loader->load('openapi.xml'); + if ($config['use_deprecated_json_schema_type_factory'] ?? false) { + $container->getDefinition('api_platform.openapi.factory')->setArgument('$jsonSchemaTypeFactory', new Reference('api_platform.json_schema.type_factory')); + } + if (class_exists(Yaml::class)) { $loader->load('openapi/yaml.xml'); } @@ -978,6 +982,10 @@ private function registerOpenApiConfiguration(ContainerBuilder $container, array $container->setParameter('api_platform.openapi.overrideResponses', $config['openapi']['overrideResponses']); $loader->load('json_schema.xml'); + + if ($config['use_deprecated_json_schema_type_factory'] ?? false) { + $container->getDefinition('api_platform.json_schema.schema_factory')->setArgument('$typeFactory', new Reference('api_platform.json_schema.type_factory')); + } } private function registerMakerConfiguration(ContainerBuilder $container, array $config, XmlFileLoader $loader): void diff --git a/src/Symfony/Bundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/DependencyInjection/Configuration.php index 4f95ec6f797..fb1bbd01176 100644 --- a/src/Symfony/Bundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/DependencyInjection/Configuration.php @@ -87,6 +87,7 @@ public function getConfigTreeBuilder(): TreeBuilder ->end() ->booleanNode('show_webby')->defaultTrue()->info('If true, show Webby on the documentation page')->end() ->booleanNode('event_listeners_backward_compatibility_layer')->defaultNull()->info('If true API Platform uses Symfony event listeners instead of providers and processors.')->end() + ->booleanNode('use_deprecated_json_schema_type_factory')->defaultNull()->info('Use the deprecated type factory, this option will be removed in 4.0.')->end() ->booleanNode('use_symfony_listeners')->defaultNull()->info(sprintf('Uses Symfony event listeners instead of the %s.', MainController::class))->end() ->scalarNode('name_converter')->defaultNull()->info('Specify a name converter to use.')->end() ->scalarNode('asset_package')->defaultNull()->info('Specify an asset package name to use.')->end() diff --git a/src/Symfony/Bundle/Resources/config/graphql.xml b/src/Symfony/Bundle/Resources/config/graphql.xml index 7e6c730661a..f532c6ab500 100644 --- a/src/Symfony/Bundle/Resources/config/graphql.xml +++ b/src/Symfony/Bundle/Resources/config/graphql.xml @@ -237,10 +237,9 @@ - + - diff --git a/src/Symfony/Bundle/Resources/config/json_schema.xml b/src/Symfony/Bundle/Resources/config/json_schema.xml index c49ffe710e9..d4a740601fc 100644 --- a/src/Symfony/Bundle/Resources/config/json_schema.xml +++ b/src/Symfony/Bundle/Resources/config/json_schema.xml @@ -16,7 +16,7 @@ - + null diff --git a/src/Symfony/Bundle/Resources/config/openapi.xml b/src/Symfony/Bundle/Resources/config/openapi.xml index fb9486cd97d..421cf2cf285 100644 --- a/src/Symfony/Bundle/Resources/config/openapi.xml +++ b/src/Symfony/Bundle/Resources/config/openapi.xml @@ -85,7 +85,7 @@ - + null %api_platform.formats% diff --git a/src/Symfony/Bundle/Test/ClientTrait.php b/src/Symfony/Bundle/Test/ClientTrait.php new file mode 100644 index 00000000000..147236420cf --- /dev/null +++ b/src/Symfony/Bundle/Test/ClientTrait.php @@ -0,0 +1,25 @@ + + * + * 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\Test; + +trait ClientTrait +{ + public function withOptions(array $options): static + { + $clone = clone $this; + $clone->defaultOptions = self::mergeDefaultOptions($options, $this->defaultOptions); + + return $clone; + } +} diff --git a/src/Symfony/EventListener/ErrorListener.php b/src/Symfony/EventListener/ErrorListener.php index e69dd60354a..85103eba284 100644 --- a/src/Symfony/EventListener/ErrorListener.php +++ b/src/Symfony/EventListener/ErrorListener.php @@ -61,7 +61,7 @@ public function __construct( private readonly IdentifiersExtractorInterface|LegacyIdentifiersExtractorInterface|null $identifiersExtractor = null, private readonly ResourceClassResolverInterface|LegacyResourceClassResolverInterface|null $resourceClassResolver = null, ?Negotiator $negotiator = null, - private readonly bool $problemCompliantErrors = true, + private readonly ?bool $problemCompliantErrors = true, ) { parent::__construct($controller, $logger, $debug, $exceptionsMapping); $this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory; @@ -90,7 +90,7 @@ protected function duplicateRequest(\Throwable $exception, Request $request): Re $legacy = $apiOperation ? ($apiOperation->getExtraProperties()['rfc_7807_compliant_errors'] ?? false) : $this->problemCompliantErrors; if (!$this->problemCompliantErrors || !$legacy) { - // TODO: deprecate in API Platform 3.3 + trigger_deprecation('api-platform/core', '3.4', "rfc_7807_compliant_errors flag will be removed in 4.0, to handle errors yourself use extraProperties: ['rfc_7807_compliant_errors' => false]"); $this->controller = 'api_platform.action.exception'; $dup = parent::duplicateRequest($exception, $request); $dup->attributes->set('_api_operation', $apiOperation); diff --git a/src/Symfony/EventListener/QueryParameterValidateListener.php b/src/Symfony/EventListener/QueryParameterValidateListener.php index 662889d1c11..6f799b07a1e 100644 --- a/src/Symfony/EventListener/QueryParameterValidateListener.php +++ b/src/Symfony/EventListener/QueryParameterValidateListener.php @@ -28,6 +28,8 @@ * Validates query parameters depending on filter description. * * @author Julien Deniau + * + * @deprecated */ final class QueryParameterValidateListener { diff --git a/src/Symfony/Security/State/AccessCheckerProvider.php b/src/Symfony/Security/State/AccessCheckerProvider.php index 85c42b002ac..46292c9b31d 100644 --- a/src/Symfony/Security/State/AccessCheckerProvider.php +++ b/src/Symfony/Security/State/AccessCheckerProvider.php @@ -13,7 +13,7 @@ namespace ApiPlatform\Symfony\Security\State; -use ApiPlatform\Exception\RuntimeException; +use ApiPlatform\Metadata\Exception\RuntimeException; use ApiPlatform\Metadata\GraphQl\Operation as GraphQlOperation; use ApiPlatform\Metadata\GraphQl\QueryCollection; use ApiPlatform\Metadata\HttpOperation; diff --git a/src/Symfony/Validator/State/QueryParameterValidateProvider.php b/src/Symfony/Validator/State/QueryParameterValidateProvider.php index dcf5290dc41..1b9dbd44071 100644 --- a/src/Symfony/Validator/State/QueryParameterValidateProvider.php +++ b/src/Symfony/Validator/State/QueryParameterValidateProvider.php @@ -21,6 +21,9 @@ use ApiPlatform\State\ProviderInterface; use ApiPlatform\State\Util\RequestParser; +/** + * @deprecated the query parameter validator is deprecated + */ final class QueryParameterValidateProvider implements ProviderInterface { public function __construct(private readonly ?ProviderInterface $decorated, private readonly ParameterValidator $parameterValidator) diff --git a/src/Util/IriHelper.php b/src/Util/IriHelper.php index d4c8915e6ac..d1c1511bcfa 100644 --- a/src/Util/IriHelper.php +++ b/src/Util/IriHelper.php @@ -22,6 +22,8 @@ * * @author Kévin Dunglas * + * @deprecated use ApiPlatform\Metadata\Util\IriHelper instead + * * @internal */ final class IriHelper diff --git a/src/deprecation.php b/src/deprecation.php index d6e43c1b5a8..e68183ae208 100644 --- a/src/deprecation.php +++ b/src/deprecation.php @@ -39,8 +39,16 @@ ApiPlatform\Exception\OperationNotFoundException::class => ApiPlatform\Metadata\Exception\OperationNotFoundException::class, ApiPlatform\Exception\PropertyNotFoundException::class => ApiPlatform\Metadata\Exception\PropertyNotFoundException::class, ApiPlatform\Exception\ResourceClassNotFoundException::class => ApiPlatform\Metadata\Exception\ResourceClassNotFoundException::class, - ApiPlatform\Exception\RuntimeException::class=> ApiPlatform\Metadata\Exception\RuntimeException::class, - + ApiPlatform\Exception\RuntimeException::class => ApiPlatform\Metadata\Exception\RuntimeException::class, + ApiPlatform\GraphQl\Type\TypeBuilderInterface::class => ApiPlatform\GraphQl\Type\ContextAwareTypeBuilderInterface::class, + ApiPlatform\GraphQl\Type\TypeBuilderEnumInterface::class => ApiPlatform\GraphQl\Type\ContextAwareTypeBuilderInterface::class, + ApiPlatform\Operation\DashPathSegmentNameGenerator::class => ApiPlatform\Metadata\Operation\DashPathSegmentNameGenerator::class, + ApiPlatform\Operation\UnderscorePathSegmentNameGenerator::class => ApiPlatform\Metadata\Operation\UnderscorePathSegmentNameGenerator::class, + ApiPlatform\Operation\PathSegmentNameGeneratorInterface::class => ApiPlatform\Metadata\Operation\PathSegmentNameGeneratorInterface::class, + ApiPlatform\Symfony\Bundle\Command\OpenApiCommand::class => ApiPlatform\OpenApi\Command\OpenApiCommand::class, + ApiPlatform\Util\ClientTrait::class => ApiPlatform\Symfony\Bundle\Test\ClientTrait::class, + ApiPlatform\Util\RequestAttributesExtractor::class => ApiPlatform\State\Util\RequestAttributesExtractor::class, + ApiPlatform\Symfony\Util\RequestAttributesExtractor::class => ApiPlatform\State\Util\RequestAttributesExtractor::class, ]; $removedClasses = [ @@ -50,7 +58,47 @@ ApiPlatform\Exception\FilterValidationException::class => true, ApiPlatform\Exception\InvalidResourceException::class => true, ApiPlatform\Exception\InvalidValueException::class => true, - ApiPlatform\Exception\ResourceClassNotSupportedException::class + ApiPlatform\Exception\ResourceClassNotSupportedException::class => true, + ApiPlatform\GraphQl\Resolver\Factory\CollectionResolverFactory::class => true, + ApiPlatform\GraphQl\Resolver\Factory\ItemMutationResolverFactory::class => true, + ApiPlatform\GraphQl\Resolver\Factory\ItemResolverFactory::class => true, + ApiPlatform\GraphQl\Resolver\Factory\ItemSubscriptionResolverFactory::class => true, + ApiPlatform\GraphQl\Resolver\Stage\DeserializeStage::class => true, + ApiPlatform\GraphQl\Resolver\Stage\DeserializeStageInterface::class => true, + ApiPlatform\GraphQl\Resolver\Stage\ReadStage::class => true, + ApiPlatform\GraphQl\Resolver\Stage\ReadStageInterface::class => true, + ApiPlatform\GraphQl\Resolver\Stage\SecurityPostDenormalizeStage::class => true, + ApiPlatform\GraphQl\Resolver\Stage\SecurityPostDenormalizeStageInterface::class => true, + ApiPlatform\GraphQl\Resolver\Stage\SecurityPostValidationStage::class => true, + ApiPlatform\GraphQl\Resolver\Stage\SecurityPostValidationStageInterface::class => true, + ApiPlatform\GraphQl\Resolver\Stage\SecurityStage::class => true, + ApiPlatform\GraphQl\Resolver\Stage\SecurityStageInterface::class => true, + ApiPlatform\GraphQl\Resolver\Stage\SerializeStage::class => true, + ApiPlatform\GraphQl\Resolver\Stage\SerializeStageInterface::class => true, + ApiPlatform\GraphQl\Resolver\Stage\ValidateStage::class => true, + ApiPlatform\GraphQl\Resolver\Stage\ValidateStageInterface::class => true, + ApiPlatform\GraphQl\Resolver\Stage\WriteStage::class => true, + ApiPlatform\GraphQl\Resolver\Stage\WriteStageInterface::class => true, + ApiPlatform\HttpCache\EventListener\AddHeadersListener::class => true, + ApiPlatform\Symfony\EventListener\AddHeadersListener::class => true, + ApiPlatform\HttpCache\EventListener\AddTagsListener::class => true, + ApiPlatform\Hydra\EventListener\AddLinkHeaderListener::class => true, + ApiPlatform\Hydra\Serializer\ErrorNormalizer::class => true, + ApiPlatform\JsonSchema\TypeFactory::class => true, + ApiPlatform\JsonSchema\TypeFactoryInterface::class => true, + ApiPlatform\Problem\Serializer\ErrorNormalizer::class => true, + ApiPlatform\Serializer\CacheableSupportsMethodInterface::class => true, + ApiPlatform\OpenApi\Serializer\CacheableSupportsMethodInterface::class => true, + ApiPlatform\Symfony\EventListener\AddHeadersListener::class => true, + ApiPlatform\Symfony\EventListener\AddLinkHeaderListener::class => true, + ApiPlatform\Symfony\EventListener\AddTagsListener::class => true, + ApiPlatform\Symfony\EventListener\DenyAccessListener::class => true, + ApiPlatform\Symfony\EventListener\QueryParameterValidateListener::class => true, + ApiPlatform\Symfony\Validator\EventListener\ValidationExceptionListener::class => true, + ApiPlatform\Symfony\Validator\Exception\ConstraintViolationListAwareExceptionInterface::class => true, + ApiPlatform\Symfony\Validator\Exception\ValidationException::class => true, + ApiPlatform\Symfony\Validator\State\QueryParameterValidateProvider::class => true, + ApiPlatform\Util\ErrorFormatGuesser::class => true, ]; spl_autoload_register(function ($className) use ($deprecatedClassesWithAliases, $movedClasses, $removedClasses): void { diff --git a/tests/Fixtures/app/AppKernel.php b/tests/Fixtures/app/AppKernel.php index 5666307434a..c861b9f1450 100644 --- a/tests/Fixtures/app/AppKernel.php +++ b/tests/Fixtures/app/AppKernel.php @@ -248,6 +248,7 @@ class_exists(NativePasswordHasher::class) ? 'password_hashers' : 'encoders' => [ 'graphql' => [ 'graphql_playground' => false, ], + 'use_deprecated_json_schema_type_factory' => true, 'use_symfony_listeners' => $useSymfonyListeners, 'defaults' => [ 'pagination_client_enabled' => true, diff --git a/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php b/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php index ed68a69e50e..e29b170560b 100644 --- a/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php +++ b/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php @@ -179,6 +179,8 @@ public function testArraySchemaWithMultipleUnionTypesJsonHal(): void */ public function testArraySchemaWithTypeFactory(): void { + $container = static::getContainer(); + $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => 'ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue5896\Foo', '--type' => 'output']); $result = $this->tester->getDisplay(); $json = json_decode($result, associative: true); @@ -278,7 +280,7 @@ public function testBackedEnumExamplesAreNotLost(): void $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => 'ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue6317\Issue6317', '--type' => 'output', '--format' => 'jsonld']); $result = $this->tester->getDisplay(); $json = json_decode($result, associative: true); - $properties = $json['definitions']['Issue6317.jsonld']['properties']; + $properties = $json['definitions']['Issue6317.jsonld.output']['properties']; $this->assertArrayHasKey('example', $properties['id']); $this->assertArrayHasKey('example', $properties['name']); @@ -293,7 +295,7 @@ public function testResourceWithEnumPropertiesSchema(): void $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => 'ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\ResourceWithEnumProperty', '--type' => 'output', '--format' => 'jsonld']); $result = $this->tester->getDisplay(); $json = json_decode($result, associative: true); - $properties = $json['definitions']['ResourceWithEnumProperty.jsonld']['properties']; + $properties = $json['definitions']['ResourceWithEnumProperty.jsonld.output']['properties']; $this->assertSame( [ From 1f7fd42c4e039fd4c890fe79ae420c7908261fd2 Mon Sep 17 00:00:00 2001 From: soyuka Date: Fri, 19 Jul 2024 16:19:17 +0200 Subject: [PATCH 4/4] tests --- .../Subscription/SubscriptionManager.php | 9 ++++--- .../Subscription/SubscriptionManagerTest.php | 27 ++++++++++++++----- src/GraphQl/composer.json | 2 +- src/JsonSchema/SchemaFactory.php | 2 +- src/OpenApi/Factory/OpenApiFactory.php | 6 ++--- .../Tests/Factory/OpenApiFactoryTest.php | 11 ++++---- .../Serializer/OpenApiNormalizerTest.php | 2 +- .../Tests/EventListener/ErrorListenerTest.php | 20 -------------- src/deprecation.php | 1 - tests/.ignored-deprecations | 3 ++- tests/.ignored-deprecations-legacy-events | 2 ++ .../Bundle/Command/OpenApiCommandTest.php | 2 +- .../DependencyInjection/ConfigurationTest.php | 1 + 13 files changed, 44 insertions(+), 44 deletions(-) diff --git a/src/GraphQl/Subscription/SubscriptionManager.php b/src/GraphQl/Subscription/SubscriptionManager.php index c7b7e9662b2..3f1b80244a2 100644 --- a/src/GraphQl/Subscription/SubscriptionManager.php +++ b/src/GraphQl/Subscription/SubscriptionManager.php @@ -13,6 +13,7 @@ namespace ApiPlatform\GraphQl\Subscription; +use ApiPlatform\GraphQl\Resolver\Stage\SerializeStage; use ApiPlatform\GraphQl\Resolver\Stage\SerializeStageInterface; use ApiPlatform\GraphQl\Resolver\Util\IdentifierTrait; use ApiPlatform\Metadata\GraphQl\Operation; @@ -37,7 +38,7 @@ final class SubscriptionManager implements OperationAwareSubscriptionManagerInte use ResourceClassInfoTrait; use SortTrait; - public function __construct(private readonly CacheItemPoolInterface $subscriptionsCache, private readonly SubscriptionIdentifierGeneratorInterface $subscriptionIdentifierGenerator, private readonly SerializeStageInterface|ProcessorInterface|null $serializeStage = null, private readonly IriConverterInterface $iriConverter, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly ?ProcessorInterface $normalizeProcessor = null) + public function __construct(private readonly CacheItemPoolInterface $subscriptionsCache, private readonly SubscriptionIdentifierGeneratorInterface $subscriptionIdentifierGenerator, private readonly SerializeStageInterface|ProcessorInterface|null $serializeStage = null, private readonly ?IriConverterInterface $iriConverter = null, private readonly ?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null) { if (!$serializeStage instanceof ProcessorInterface) { trigger_deprecation('api-platform/core', '4.0', sprintf('Using an instanceof "%s" is deprecated, use "%s" instead.', SerializeStageInterface::class, ProcessorInterface::class)); @@ -88,9 +89,9 @@ public function getPushPayloads(object $object): array $resolverContext = ['fields' => $subscriptionFields, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => true]; /** @var Operation */ $operation = (new Subscription())->withName('update_subscription')->withShortName($shortName); - if ($this->normalizeProcessor) { - $data = $this->normalizeProcessor->process($object, $operation, [], $resolverContext); - } elseif ($this->serializeStage) { + if ($this->serializeStage instanceof ProcessorInterface) { + $data = $this->serializeStage->process($object, $operation, [], $resolverContext); + } elseif ($this->serializeStage instanceof SerializeStage) { $data = ($this->serializeStage)($object, $resourceClass, $operation, $resolverContext); } else { throw new \LogicException(); diff --git a/src/GraphQl/Tests/Subscription/SubscriptionManagerTest.php b/src/GraphQl/Tests/Subscription/SubscriptionManagerTest.php index b8fcbd22e3f..7afeaeaef03 100644 --- a/src/GraphQl/Tests/Subscription/SubscriptionManagerTest.php +++ b/src/GraphQl/Tests/Subscription/SubscriptionManagerTest.php @@ -13,7 +13,6 @@ namespace ApiPlatform\GraphQl\Tests\Subscription; -use ApiPlatform\GraphQl\Resolver\Stage\SerializeStageInterface; use ApiPlatform\GraphQl\Subscription\SubscriptionIdentifierGeneratorInterface; use ApiPlatform\GraphQl\Subscription\SubscriptionManager; use ApiPlatform\GraphQl\Tests\Fixtures\ApiResource\Dummy; @@ -24,6 +23,7 @@ use ApiPlatform\Metadata\Operations; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use ApiPlatform\State\ProcessorInterface; use GraphQL\Type\Definition\ResolveInfo; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; @@ -40,7 +40,7 @@ class SubscriptionManagerTest extends TestCase private ObjectProphecy $subscriptionsCacheProphecy; private ObjectProphecy $subscriptionIdentifierGeneratorProphecy; - private ObjectProphecy $serializeStageProphecy; + private ObjectProphecy $normalizeProcessor; private ObjectProphecy $iriConverterProphecy; private SubscriptionManager $subscriptionManager; private ObjectProphecy $resourceMetadataCollectionFactory; @@ -52,10 +52,10 @@ protected function setUp(): void { $this->subscriptionsCacheProphecy = $this->prophesize(CacheItemPoolInterface::class); $this->subscriptionIdentifierGeneratorProphecy = $this->prophesize(SubscriptionIdentifierGeneratorInterface::class); - $this->serializeStageProphecy = $this->prophesize(SerializeStageInterface::class); + $this->normalizeProcessor = $this->prophesize(ProcessorInterface::class); $this->iriConverterProphecy = $this->prophesize(IriConverterInterface::class); $this->resourceMetadataCollectionFactory = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $this->subscriptionManager = new SubscriptionManager($this->subscriptionsCacheProphecy->reveal(), $this->subscriptionIdentifierGeneratorProphecy->reveal(), $this->serializeStageProphecy->reveal(), $this->iriConverterProphecy->reveal(), $this->resourceMetadataCollectionFactory->reveal()); + $this->subscriptionManager = new SubscriptionManager($this->subscriptionsCacheProphecy->reveal(), $this->subscriptionIdentifierGeneratorProphecy->reveal(), $this->normalizeProcessor->reveal(), $this->iriConverterProphecy->reveal(), $this->resourceMetadataCollectionFactory->reveal()); } public function testRetrieveSubscriptionIdNoIdentifier(): void @@ -206,8 +206,23 @@ public function testGetPushPayloadsHit(): void ]); $this->subscriptionsCacheProphecy->getItem('_dummies_2')->willReturn($cacheItemProphecy->reveal()); - $this->serializeStageProphecy->__invoke($object, Dummy::class, (new Subscription())->withName('update_subscription')->withShortName('Dummy'), ['fields' => ['fieldsFoo'], 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => true])->willReturn(['newResultFoo', 'clientSubscriptionId' => 'client-subscription-id']); - $this->serializeStageProphecy->__invoke($object, Dummy::class, (new Subscription())->withName('update_subscription')->withShortName('Dummy'), ['fields' => ['fieldsBar'], 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => true])->willReturn(['resultBar', 'clientSubscriptionId' => 'client-subscription-id']); + $this->normalizeProcessor->process( + $object, + (new Subscription())->withName('update_subscription')->withShortName('Dummy'), + [], + ['fields' => ['fieldsFoo'], 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => true] + )->willReturn( + ['newResultFoo', 'clientSubscriptionId' => 'client-subscription-id'] + ); + + $this->normalizeProcessor->process( + $object, + (new Subscription())->withName('update_subscription')->withShortName('Dummy'), + [], + ['fields' => ['fieldsBar'], 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => true] + )->willReturn( + ['resultBar', 'clientSubscriptionId' => 'client-subscription-id'] + ); $this->assertEquals([['subscriptionIdFoo', ['newResultFoo']]], $this->subscriptionManager->getPushPayloads($object)); } diff --git a/src/GraphQl/composer.json b/src/GraphQl/composer.json index 756f9e8aed1..851c1fe827c 100644 --- a/src/GraphQl/composer.json +++ b/src/GraphQl/composer.json @@ -74,4 +74,4 @@ "scripts": { "test": "./vendor/bin/phpunit" } -} \ No newline at end of file +} diff --git a/src/JsonSchema/SchemaFactory.php b/src/JsonSchema/SchemaFactory.php index a8420d6e091..b1b0814821b 100644 --- a/src/JsonSchema/SchemaFactory.php +++ b/src/JsonSchema/SchemaFactory.php @@ -42,7 +42,7 @@ final class SchemaFactory implements SchemaFactoryInterface, SchemaFactoryAwareI public function __construct(?TypeFactoryInterface $typeFactory, ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ?NameConverterInterface $nameConverter = null, ?ResourceClassResolverInterface $resourceClassResolver = null, private readonly ?array $distinctFormats = null, private ?DefinitionNameFactoryInterface $definitionNameFactory = null) { if ($typeFactory) { - trigger_deprecation('api-platform/core', '4.0', sprintf('Injecting the "%s" inside "%s" is deprecated and "%s" will be removed in 4.x.', TypeFactoryInterface::class, self::class, TypeFactoryInterface::class)); + trigger_deprecation('api-platform/core', '3.4', sprintf('Injecting the "%s" inside "%s" is deprecated and "%s" will be removed in 4.x.', TypeFactoryInterface::class, self::class, TypeFactoryInterface::class)); $this->typeFactory = $typeFactory; } if (!$definitionNameFactory) { diff --git a/src/OpenApi/Factory/OpenApiFactory.php b/src/OpenApi/Factory/OpenApiFactory.php index 8465d498da4..e335532c012 100644 --- a/src/OpenApi/Factory/OpenApiFactory.php +++ b/src/OpenApi/Factory/OpenApiFactory.php @@ -82,7 +82,7 @@ public function __construct(private readonly ResourceNameCollectionFactoryInterf $this->paginationOptions = $paginationOptions ?: new PaginationOptions(); if ($jsonSchemaTypeFactory) { - trigger_deprecation('api-platform/core', '4.0', sprintf('Injecting the "%s" inside "%s" is deprecated and "%s" will be removed in 4.x.', TypeFactoryInterface::class, self::class, TypeFactoryInterface::class)); + trigger_deprecation('api-platform/core', '3.4', sprintf('Injecting the "%s" inside "%s" is deprecated and "%s" will be removed in 4.x.', TypeFactoryInterface::class, self::class, TypeFactoryInterface::class)); $this->jsonSchemaTypeFactory = $jsonSchemaTypeFactory; } } @@ -649,7 +649,7 @@ private function getFiltersParameters(CollectionOperationInterface|HttpOperation trigger_deprecation('api-platform/core', '4.0', sprintf('Using the "swagger" field of the %s::getDescription() (%s) is deprecated.', $filter::class, $operation->getShortName())); } - if (isset($data['openapi']) && $data['openapi'] instanceof Parameter) { + if (!isset($data['openapi']) || $data['openapi'] instanceof Parameter) { $schema = $data['schema'] ?? []; if (isset($data['type']) && \in_array($data['type'] ?? null, Type::$builtinTypes, true) && !isset($schema['type'])) { @@ -684,7 +684,7 @@ private function getFiltersParameters(CollectionOperationInterface|HttpOperation if ($this->jsonSchemaTypeFactory) { $schema = $data['schema'] ?? (\in_array($data['type'], Type::$builtinTypes, true) ? $this->jsonSchemaTypeFactory->getType(new Type($data['type'], false, null, $data['is_collection'] ?? false), 'openapi') : ['type' => 'string']); } else { - $schema = $data['schema'] ?? (\in_array($data['type'], Type::$builtinTypes, true) ? $this->getType(new Type($data['type'], false, null, $data['is_collection'] ?? false), 'openapi') : ['type' => 'string']); + $schema = $data['schema'] ?? (\in_array($data['type'], Type::$builtinTypes, true) ? $this->getType(new Type($data['type'], false, null, $data['is_collection'] ?? false)) : ['type' => 'string']); } $parameters[] = new Parameter( diff --git a/src/OpenApi/Tests/Factory/OpenApiFactoryTest.php b/src/OpenApi/Tests/Factory/OpenApiFactoryTest.php index 924032e2a60..098eb8fe966 100644 --- a/src/OpenApi/Tests/Factory/OpenApiFactoryTest.php +++ b/src/OpenApi/Tests/Factory/OpenApiFactoryTest.php @@ -400,7 +400,7 @@ public function testInvoke(): void 'type' => 'string', 'required' => true, 'strategy' => 'exact', - 'openapi' => ['example' => 'bar', 'deprecated' => true, 'allowEmptyValue' => true, 'allowReserved' => true, 'explode' => true], + 'openapi' => new Parameter(in: 'query', name: 'name', example: 'bar', deprecated: true, allowEmptyValue: true, allowReserved: true, explode: true), ]]), 'f2' => new DummyFilter(['ha' => [ 'property' => 'foo', @@ -458,7 +458,7 @@ public function testInvoke(): void $propertyNameCollectionFactory, $propertyMetadataFactory, $schemaFactory, - $typeFactory, + null, $filterLocatorProphecy->reveal(), [], new Options('Test API', 'This is a test API.', '1.2.3', true, 'oauth2', 'authorizationCode', '/oauth/v2/token', '/oauth/v2/auth', '/oauth/v2/refresh', ['scope param'], [ @@ -787,14 +787,15 @@ public function testInvoke(): void new Parameter('name', 'query', '', true, true, true, [ 'type' => 'string', ], 'form', true, true, 'bar'), - new Parameter('ha', 'query', '', false, false, true, [ + new Parameter('ha', 'query', '', false, false, false, [ 'type' => 'integer', ]), - new Parameter('toto', 'query', '', true, false, true, [ + new Parameter('toto', 'query', '', true, false, false, [ 'type' => 'array', 'items' => ['type' => 'string'], ], 'deepObject', true), - new Parameter('order[name]', 'query', '', false, false, true, [ + + new Parameter('order[name]', 'query', '', false, false, false, [ 'type' => 'string', 'enum' => ['asc', 'desc'], ]), diff --git a/src/OpenApi/Tests/Serializer/OpenApiNormalizerTest.php b/src/OpenApi/Tests/Serializer/OpenApiNormalizerTest.php index a0fcf600ff6..418147a9d1d 100644 --- a/src/OpenApi/Tests/Serializer/OpenApiNormalizerTest.php +++ b/src/OpenApi/Tests/Serializer/OpenApiNormalizerTest.php @@ -224,7 +224,7 @@ public function testNormalize(): void $propertyNameCollectionFactory, $propertyMetadataFactory, $schemaFactory, - $typeFactory, + null, $filterLocatorProphecy->reveal(), [], new Options('Test API', 'This is a test API.', '1.2.3', true, 'oauth2', 'authorizationCode', '/oauth/v2/token', '/oauth/v2/auth', '/oauth/v2/refresh', ['scope param'], [ diff --git a/src/Symfony/Tests/EventListener/ErrorListenerTest.php b/src/Symfony/Tests/EventListener/ErrorListenerTest.php index 3bc622c1bdf..f9f1e9e21cf 100644 --- a/src/Symfony/Tests/EventListener/ErrorListenerTest.php +++ b/src/Symfony/Tests/EventListener/ErrorListenerTest.php @@ -137,24 +137,4 @@ public function testDuplicateExceptionWithErrorResource(): void $errorListener = new ErrorListener('action', null, true, [], $resourceMetadataCollectionFactory, ['jsonld' => ['application/ld+json']], [], $identifiersExtractor, $resourceClassResolver); $errorListener->onKernelException($exceptionEvent); } - - public function testDisableErrorResourceHandling(): void - { - $exception = Error::createFromException(new \Exception(), 400); - $resourceMetadataCollectionFactory = $this->createMock(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataCollectionFactory->expects($this->never())->method('create'); - $resourceClassResolver = $this->createMock(ResourceClassResolverInterface::class); - $resourceClassResolver->expects($this->never())->method('isResourceClass'); - $kernel = $this->createStub(KernelInterface::class); - $kernel->method('handle')->willReturnCallback(function ($request) { - $this->assertEquals($request->attributes->get('_api_operation'), null); - - return new Response(); - }); - - $exceptionEvent = new ExceptionEvent($kernel, Request::create('/'), HttpKernelInterface::SUB_REQUEST, $exception); - $identifiersExtractor = $this->createStub(IdentifiersExtractorInterface::class); - $errorListener = new ErrorListener('action', null, true, [], $resourceMetadataCollectionFactory, ['jsonld' => ['application/ld+json']], [], $identifiersExtractor, $resourceClassResolver, null, false); - $errorListener->onKernelException($exceptionEvent); - } } diff --git a/src/deprecation.php b/src/deprecation.php index e68183ae208..7c5eb72b93c 100644 --- a/src/deprecation.php +++ b/src/deprecation.php @@ -80,7 +80,6 @@ ApiPlatform\GraphQl\Resolver\Stage\WriteStage::class => true, ApiPlatform\GraphQl\Resolver\Stage\WriteStageInterface::class => true, ApiPlatform\HttpCache\EventListener\AddHeadersListener::class => true, - ApiPlatform\Symfony\EventListener\AddHeadersListener::class => true, ApiPlatform\HttpCache\EventListener\AddTagsListener::class => true, ApiPlatform\Hydra\EventListener\AddLinkHeaderListener::class => true, ApiPlatform\Hydra\Serializer\ErrorNormalizer::class => true, diff --git a/tests/.ignored-deprecations b/tests/.ignored-deprecations index b78860273c9..a908818cba6 100644 --- a/tests/.ignored-deprecations +++ b/tests/.ignored-deprecations @@ -23,4 +23,5 @@ %Since api-platform/core 3.4: The "exclusiveMaximum" schema value should be a number not a boolean.% %Since api-platform/core 3.4: The "exclusiveMinimum" schema value should be a number not a boolean.% %Since api-platform/core 3.4: The "allowEmptyValue" option should be declared using an "openapi" parameter.% - +%Since api-platform/core 3.4: Injecting the "ApiPlatform\\JsonSchema\\TypeFactoryInterface" inside "ApiPlatform\\JsonSchema\\SchemaFactory" is deprecated and "ApiPlatform\\JsonSchema\\TypeFactoryInterface" will be removed in 4.x.% +%Since api-platform/core 3.4: Injecting the "ApiPlatform\\JsonSchema\\TypeFactoryInterface" inside "ApiPlatform\\OpenApi\\Factory\\OpenApiFactory" is deprecated and "ApiPlatform\\JsonSchema\\TypeFactoryInterface" will be removed in 4.x.% diff --git a/tests/.ignored-deprecations-legacy-events b/tests/.ignored-deprecations-legacy-events index 38ea17ea2ec..e9cd0f40746 100644 --- a/tests/.ignored-deprecations-legacy-events +++ b/tests/.ignored-deprecations-legacy-events @@ -26,3 +26,5 @@ %The "Symfony\\Bundle\\MakerBundle\\Maker\\MakeAuthenticator" class is deprecated, use any of the Security\\Make\* commands instead% %Since symfony/validator 7.1: Not passing a value for the "requireTld" option to the Url constraint is deprecated. Its default value will change to "true".% +%Since api-platform/core 3.4: Injecting the "ApiPlatform\\JsonSchema\\TypeFactoryInterface" inside "ApiPlatform\\JsonSchema\\SchemaFactory" is deprecated and "ApiPlatform\\JsonSchema\\TypeFactoryInterface" will be removed in 4.x.% +%Since api-platform/core 3.4: Injecting the "ApiPlatform\\JsonSchema\\TypeFactoryInterface" inside "ApiPlatform\\OpenApi\\Factory\\OpenApiFactory" is deprecated and "ApiPlatform\\JsonSchema\\TypeFactoryInterface" will be removed in 4.x.% diff --git a/tests/Symfony/Bundle/Command/OpenApiCommandTest.php b/tests/Symfony/Bundle/Command/OpenApiCommandTest.php index cf68f461997..b09d7ae7f65 100644 --- a/tests/Symfony/Bundle/Command/OpenApiCommandTest.php +++ b/tests/Symfony/Bundle/Command/OpenApiCommandTest.php @@ -134,7 +134,7 @@ public function testBackedEnumExamplesAreNotLost(): void }; $assertExample($json['components']['schemas']['Issue6317']['properties'], 'id'); - $assertExample($json['components']['schemas']['Issue6317.jsonld']['properties'], 'id'); + $assertExample($json['components']['schemas']['Issue6317.jsonld.output']['properties'], 'id'); $assertExample($json['components']['schemas']['Issue6317.jsonapi']['properties']['data']['properties']['attributes']['properties'], '_id'); $assertExample($json['components']['schemas']['Issue6317.jsonhal']['properties'], 'id'); } diff --git a/tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php b/tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php index 2b1773e53a0..49ba4c84185 100644 --- a/tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php +++ b/tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php @@ -231,6 +231,7 @@ private function runDefaultConfigTests(array $doctrineIntegrationsToLoad = ['orm 'keep_legacy_inflector' => true, 'event_listeners_backward_compatibility_layer' => null, 'use_symfony_listeners' => false, + 'use_deprecated_json_schema_type_factory' => null, 'handle_symfony_errors' => false, 'enable_link_security' => false, ], $config);