From dc7601c89bd47fd6a58a2471514800b749e0910d Mon Sep 17 00:00:00 2001 From: Zakhar <45487603+fiveight00@users.noreply.github.com> Date: Wed, 11 Sep 2024 13:10:30 +0300 Subject: [PATCH] Add PHP 8 attributes support (#15) * Add PHP 8 attributes support * Update tests * Refactor annotations * Refactor structure * Refactor test controllers * Add tests for attributes * Modify PHPUnit version * Configure tests * Handle deprecations * Update attributed test controllers * handle BCs * remove PHP version checks * add legacy Paysera\Bundle\ApiBundle\Service\Annotation\ReflectionMethodWrapper to resolve the BC * add BC-related improvements * update README --------- Co-authored-by: Zakhar Shokel --- CHANGELOG.md | 5 + README.md | 102 +++++++- composer.json | 4 +- src/Annotation/Body.php | 110 +-------- src/Annotation/BodyContentType.php | 47 +--- src/Annotation/PathAttribute.php | 115 +-------- src/Annotation/Query.php | 125 +--------- src/Annotation/RequiredPermissions.php | 33 +-- src/Annotation/ResponseNormalization.php | 48 +--- src/Annotation/RestAnnotationInterface.php | 3 +- src/Annotation/Validation.php | 62 +---- src/Attribute/Body.php | 111 +++++++++ src/Attribute/BodyContentType.php | 49 ++++ src/Attribute/PathAttribute.php | 113 +++++++++ src/Attribute/Query.php | 127 ++++++++++ src/Attribute/RequiredPermissions.php | 38 +++ src/Attribute/ResponseNormalization.php | 54 +++++ src/Attribute/RestAttributeInterface.php | 13 + src/Attribute/Validation.php | 73 ++++++ .../PayseraApiExtension.php | 14 +- src/Resources/config/services.xml | 10 + src/Resources/config/services/annotations.xml | 15 +- .../config/services/annotations_legacy.xml | 25 -- src/Resources/config/services/attributes.xml | 23 ++ .../Annotation/ReflectionMethodWrapper.php | 60 +---- .../RoutingLoader/ReflectionMethodWrapper.php | 66 ++++++ .../RestRequestAnnotationOptionsBuilder.php} | 9 +- .../RestRequestAttributeOptionsBuilder.php | 45 ++++ .../RoutingAnnotationLoader.php | 6 +- .../RoutingLoader/RoutingAttributeLoader.php | 109 +++++++++ ...atedClassRequiredPermissionsController.php | 8 +- .../AnnotatedClassValidationController.php | 10 +- .../Controller/AnnotatedController.php | 133 +++-------- ...utedClassRequiredPermissionsController.php | 29 +++ .../AttributedClassValidationController.php | 32 +++ .../Attribute/AttributedController.php | 215 +++++++++++++++++ .../Controller/DefaultController.php | 4 +- .../Controller/PagedQueryController.php | 6 +- .../Controller/PersistedEntityController.php | 10 +- .../PayseraFixtureTestExtension.php | 23 ++ .../Resources/config/annotation_registry.xml | 9 + .../Resources/config/attributed_routing.xml | 8 + .../Resources/config/legacy_routing.xml | 7 +- .../Resources/config/routing.xml | 7 +- .../Resources/config/services.xml | 6 + .../FixtureTestBundle/Service/TestHelper.php | 15 ++ tests/Functional/Fixtures/TestKernel.php | 8 +- .../Fixtures/config/attributed_common.yml | 3 + .../Fixtures/config/attributed_routing.yml | 7 + tests/Functional/Fixtures/config/common.yml | 34 --- .../Fixtures/config/legacy_common.yml | 34 --- tests/Functional/Fixtures/config/services.yml | 37 +++ .../Functional/FunctionalAnnotationsTest.php | 222 ++++++++++++++---- tests/Functional/FunctionalTestCase.php | 1 + .../ReflectionMethodWrapperTest.php | 18 +- ...stRequestAnnotationOptionsBuilderTest.php} | 22 +- ...RestRequestAttributeOptionsBuilderTest.php | 68 ++++++ 57 files changed, 1686 insertions(+), 904 deletions(-) create mode 100644 src/Attribute/Body.php create mode 100644 src/Attribute/BodyContentType.php create mode 100644 src/Attribute/PathAttribute.php create mode 100644 src/Attribute/Query.php create mode 100644 src/Attribute/RequiredPermissions.php create mode 100644 src/Attribute/ResponseNormalization.php create mode 100644 src/Attribute/RestAttributeInterface.php create mode 100644 src/Attribute/Validation.php delete mode 100644 src/Resources/config/services/annotations_legacy.xml create mode 100644 src/Resources/config/services/attributes.xml create mode 100644 src/Service/RoutingLoader/ReflectionMethodWrapper.php rename src/Service/{Annotation/RestRequestOptionsBuilder.php => RoutingLoader/RestRequestAnnotationOptionsBuilder.php} (87%) create mode 100644 src/Service/RoutingLoader/RestRequestAttributeOptionsBuilder.php rename src/Service/{Annotation => RoutingLoader}/RoutingAnnotationLoader.php (88%) create mode 100644 src/Service/RoutingLoader/RoutingAttributeLoader.php create mode 100644 tests/Functional/Fixtures/FixtureTestBundle/Controller/Attribute/AttributedClassRequiredPermissionsController.php create mode 100644 tests/Functional/Fixtures/FixtureTestBundle/Controller/Attribute/AttributedClassValidationController.php create mode 100644 tests/Functional/Fixtures/FixtureTestBundle/Controller/Attribute/AttributedController.php create mode 100644 tests/Functional/Fixtures/FixtureTestBundle/Resources/config/annotation_registry.xml create mode 100644 tests/Functional/Fixtures/FixtureTestBundle/Resources/config/attributed_routing.xml create mode 100644 tests/Functional/Fixtures/FixtureTestBundle/Service/TestHelper.php create mode 100644 tests/Functional/Fixtures/config/attributed_common.yml create mode 100644 tests/Functional/Fixtures/config/attributed_routing.yml create mode 100644 tests/Functional/Fixtures/config/services.yml rename tests/Unit/Service/{Annotation => RoutingLoader}/ReflectionMethodWrapperTest.php (79%) rename tests/Unit/Service/{Annotation/RestRequestOptionsBuilderTest.php => RoutingLoader/RestRequestAnnotationOptionsBuilderTest.php} (78%) create mode 100644 tests/Unit/Service/RoutingLoader/RestRequestAttributeOptionsBuilderTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index e134d17..5f5ed61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.8.0] +### Added +- Support for PHP 8 attributes +- Support for `doctrine/annotations: ^2.0` + ## [1.7.0] ### Added - Support for Symfony 6.4 diff --git a/README.md b/README.md index 02a4168..eec01c8 100644 --- a/README.md +++ b/README.md @@ -114,10 +114,10 @@ class UserNormalizer implements ObjectDenormalizerInterface, NormalizerInterface In this case you'd also need to implement normalizer for `Address` class. -It's easiest to configure REST endpoints using annotations. This requires your routing to be provided in -controller annotations, too. +It's easiest to configure REST endpoints using annotations/attributes. This requires your routing to be provided in +controller annotations/attributes, too. -Controller example: +Controller example using annotations: ```php securityChecker->checkPermissions(Permissions::REGISTER_USER, $user); + + $this->userManager->registerUser($user); + $this->entityManager->flush(); + + return $user; + } +} +``` Don't forget to also import your controller (or `Controller` directory) into routing configuration. For example: @@ -199,7 +224,7 @@ Content-Type: application/json ### Fetching resource -Controller example: +Controller example using annotations: ```php securityChecker->checkPermissions(Permissions::ACCESS_USER, $user); + + return $user; + } +} +``` + For path attributes `PathAttributeResolverInterface` should be implemented, as in this case we receive just a scalar type (ID), not an object. @@ -308,7 +356,7 @@ Content-Type: application/json ### Fetching list of resources -Controller example: +Controller example using annotations: ```php securityChecker->checkPermissions(Permissions::SEARCH_USERS, $filter); + + $configuredQuery = $this->userRepository->buildConfiguredQuery($filter); + + return new PagedQuery($configuredQuery, $pager); + } +} +``` + Denormalizer for `UserFilter`: ```php setParameterName($options['parameterName']); - $this->setDenormalizationType($options['denormalizationType'] ?? null); - $this->setDenormalizationGroup($options['denormalizationGroup'] ?? null); - $this->setOptional($options['optional'] ?? null); - } - - /** - * @param string|null $denormalizationType - * @return $this - */ - private function setDenormalizationType($denormalizationType): self - { - $this->denormalizationType = $denormalizationType; - return $this; - } - - /** - * @param string|null $denormalizationGroup - * @return $this - */ - public function setDenormalizationGroup($denormalizationGroup): self - { - $this->denormalizationGroup = $denormalizationGroup; - return $this; - } - - /** - * @param string $parameterName - * @return $this - */ - private function setParameterName(string $parameterName): self - { - $this->parameterName = $parameterName; - return $this; - } - - /** - * @param bool|null $optional - * @return $this - */ - private function setOptional($optional): self - { - $this->optional = $optional; - return $this; - } - public function isSeveralSupported(): bool { return false; } - - public function apply(RestRequestOptions $options, ReflectionMethodWrapper $reflectionMethod) - { - $options->setBodyParameterName($this->parameterName); - $options->setBodyDenormalizationType($this->resolveDenormalizationType($reflectionMethod)); - $options->setBodyDenormalizationGroup($this->denormalizationGroup); - $options->setBodyOptional($this->resolveIfBodyIsOptional($reflectionMethod)); - } - - private function resolveDenormalizationType(ReflectionMethodWrapper $reflectionMethod): string - { - if ($this->denormalizationType !== null) { - return $this->denormalizationType; - } - - try { - $typeName = $reflectionMethod->getNonBuiltInTypeForParameter($this->parameterName); - } catch (ConfigurationException $exception) { - throw new ConfigurationException(sprintf( - 'Denormalization type could not be guessed for %s in %s', - '$' . $this->parameterName, - $reflectionMethod->getFriendlyName() - )); - } - - return $typeName; - } - - private function resolveIfBodyIsOptional(ReflectionMethodWrapper $reflectionMethod): bool - { - if ($this->optional !== null) { - return $this->optional; - } - - return $reflectionMethod->getParameterByName($this->parameterName)->isDefaultValueAvailable(); - } } diff --git a/src/Annotation/BodyContentType.php b/src/Annotation/BodyContentType.php index 1f0189b..3162757 100644 --- a/src/Annotation/BodyContentType.php +++ b/src/Annotation/BodyContentType.php @@ -1,60 +1,19 @@ setSupportedContentTypes($options['supportedContentTypes']); - $this->setJsonEncodedBody($options['jsonEncodedBody'] ?? false); - } - - /** - * @param array $supportedContentTypes - * @return $this - */ - private function setSupportedContentTypes(array $supportedContentTypes): self - { - $this->supportedContentTypes = $supportedContentTypes; - return $this; - } - - /** - * @param bool $jsonEncodedBody - * @return $this - */ - private function setJsonEncodedBody(bool $jsonEncodedBody): self - { - $this->jsonEncodedBody = $jsonEncodedBody; - return $this; - } - public function isSeveralSupported(): bool { return false; } - - public function apply(RestRequestOptions $options, ReflectionMethodWrapper $reflectionMethod) - { - $options->setSupportedContentTypes($this->supportedContentTypes, $this->jsonEncodedBody); - } } diff --git a/src/Annotation/PathAttribute.php b/src/Annotation/PathAttribute.php index b78d3de..4f5b94f 100644 --- a/src/Annotation/PathAttribute.php +++ b/src/Annotation/PathAttribute.php @@ -1,128 +1,19 @@ setParameterName($options['parameterName']); - $this->setPathPartName($options['pathPartName']); - $this->setResolverType($options['resolverType'] ?? null); - $this->setResolutionMandatory($options['resolutionMandatory'] ?? null); - } - - /** - * @param string $parameterName - * @return $this - */ - private function setParameterName(string $parameterName): self - { - $this->parameterName = $parameterName; - return $this; - } - - /** - * @param string $pathPartName - * @return $this - */ - private function setPathPartName(string $pathPartName): self - { - $this->pathPartName = $pathPartName; - return $this; - } - - /** - * @param string|null $resolverType - * @return $this - */ - private function setResolverType($resolverType): self - { - $this->resolverType = $resolverType; - return $this; - } - - /** - * @param bool|null $resolutionMandatory - * @return $this - */ - private function setResolutionMandatory($resolutionMandatory): self - { - $this->resolutionMandatory = $resolutionMandatory; - return $this; - } - public function isSeveralSupported(): bool { return true; } - - public function apply(RestRequestOptions $options, ReflectionMethodWrapper $reflectionMethod) - { - $options->addPathAttributeResolverOptions( - (new PathAttributeResolverOptions()) - ->setParameterName($this->parameterName) - ->setPathPartName($this->pathPartName) - ->setPathAttributeResolverType($this->resolvePathAttributeResolverType($reflectionMethod)) - ->setResolutionMandatory($this->resolveIfResolutionIsMandatory($reflectionMethod)) - ); - } - - private function resolvePathAttributeResolverType(ReflectionMethodWrapper $reflectionMethod): string - { - if ($this->resolverType !== null) { - return $this->resolverType; - } - - try { - return $reflectionMethod->getNonBuiltInTypeForParameter($this->parameterName); - } catch (ConfigurationException $exception) { - throw new ConfigurationException(sprintf( - 'Denormalization type could not be guessed for %s in %s', - '$' . $this->parameterName, - $reflectionMethod->getFriendlyName() - )); - } - } - - private function resolveIfResolutionIsMandatory(ReflectionMethodWrapper $reflectionMethod): bool - { - if ($this->resolutionMandatory !== null) { - return $this->resolutionMandatory; - } - - $parameter = $reflectionMethod->getParameterByName($this->parameterName); - - return !$parameter->isDefaultValueAvailable(); - } } diff --git a/src/Annotation/Query.php b/src/Annotation/Query.php index c09e3e9..2b6a702 100644 --- a/src/Annotation/Query.php +++ b/src/Annotation/Query.php @@ -1,138 +1,19 @@ setParameterName($options['parameterName']); - $this->setDenormalizationType($options['denormalizationType'] ?? null); - $this->setDenormalizationGroup($options['denormalizationGroup'] ?? null); - $this->setValidation($options['validation'] ?? null); - } - - /** - * @param string $parameterName - * @return $this - */ - private function setParameterName(string $parameterName): self - { - $this->parameterName = $parameterName; - return $this; - } - - /** - * @param string|null $denormalizationType - * @return $this - */ - private function setDenormalizationType($denormalizationType): self - { - $this->denormalizationType = $denormalizationType; - return $this; - } - - /** - * @param string|null $denormalizationGroup - * @return $this - */ - public function setDenormalizationGroup($denormalizationGroup): self - { - $this->denormalizationGroup = $denormalizationGroup; - return $this; - } - - /** - * @param Validation|null $validation - * @return $this - */ - private function setValidation($validation): self - { - $this->validation = $validation; - return $this; - } - public function isSeveralSupported(): bool { return true; } - - public function apply(RestRequestOptions $options, ReflectionMethodWrapper $reflectionMethod) - { - $resolverOptions = (new QueryResolverOptions()) - ->setParameterName($this->parameterName) - ->setDenormalizationType($this->resolveDenormalizationType($reflectionMethod)) - ->setDenormalizationGroup($this->denormalizationGroup) - ; - - $this->setValidationOptions($reflectionMethod, $resolverOptions); - - $options->addQueryResolverOptions($resolverOptions); - } - - private function resolveDenormalizationType(ReflectionMethodWrapper $reflectionMethod): string - { - if ($this->denormalizationType !== null) { - return $this->denormalizationType; - } - - try { - $typeName = $reflectionMethod->getNonBuiltInTypeForParameter($this->parameterName); - } catch (ConfigurationException $exception) { - throw new ConfigurationException(sprintf( - 'Denormalization type could not be guessed for %s in %s', - '$' . $this->parameterName, - $reflectionMethod->getFriendlyName() - )); - } - - return $typeName; - } - - private function setValidationOptions(ReflectionMethodWrapper $reflectionMethod, QueryResolverOptions $options) - { - if ($this->validation === null) { - return; - } - - $restRequestOptions = new RestRequestOptions(); - $this->validation->apply($restRequestOptions, $reflectionMethod); - - if (!$restRequestOptions->isBodyValidationNeeded()) { - $options->disableValidation(); - return; - } - - $options->setValidationOptions($restRequestOptions->getBodyValidationOptions()); - } } diff --git a/src/Annotation/RequiredPermissions.php b/src/Annotation/RequiredPermissions.php index 16424e5..2958a82 100644 --- a/src/Annotation/RequiredPermissions.php +++ b/src/Annotation/RequiredPermissions.php @@ -1,46 +1,19 @@ setPermissions($options['permissions']); - } - - /** - * @param array $permissions - * @return $this - */ - private function setPermissions(array $permissions): self - { - $this->permissions = $permissions; - return $this; - } - public function isSeveralSupported(): bool { return true; } - - public function apply(RestRequestOptions $options, ReflectionMethodWrapper $reflectionMethod) - { - $options->setRequiredPermissions( - array_unique(array_merge($options->getRequiredPermissions(), $this->permissions)) - ); - } } diff --git a/src/Annotation/ResponseNormalization.php b/src/Annotation/ResponseNormalization.php index 01eac36..33c8ddc 100644 --- a/src/Annotation/ResponseNormalization.php +++ b/src/Annotation/ResponseNormalization.php @@ -1,61 +1,19 @@ setNormalizationType($options['normalizationType'] ?? null); - $this->setNormalizationGroup($options['normalizationGroup'] ?? null); - } - - /** - * @param string|null $normalizationType - * @return $this - */ - private function setNormalizationType($normalizationType): self - { - $this->normalizationType = $normalizationType; - return $this; - } - - /** - * @param string|null $normalizationGroup - * @return $this - */ - public function setNormalizationGroup($normalizationGroup): self - { - $this->normalizationGroup = $normalizationGroup; - return $this; - } - public function isSeveralSupported(): bool { return false; } - - public function apply(RestRequestOptions $options, ReflectionMethodWrapper $reflectionMethod) - { - $options->setResponseNormalizationType($this->normalizationType); - $options->setResponseNormalizationGroup($this->normalizationGroup); - } } diff --git a/src/Annotation/RestAnnotationInterface.php b/src/Annotation/RestAnnotationInterface.php index 0ed9fba..89d4f87 100644 --- a/src/Annotation/RestAnnotationInterface.php +++ b/src/Annotation/RestAnnotationInterface.php @@ -1,10 +1,11 @@ setGroups($options['groups'] ?? [Constraint::DEFAULT_GROUP]); - $this->setViolationPathMap($options['violationPathMap'] ?? []); - $this->setEnabled($options['enabled'] ?? true); - } - - private function setGroups(array $groups): self - { - $this->groups = $groups; - return $this; - } - - private function setViolationPathMap(array $violationPathMap): self - { - $this->violationPathMap = $violationPathMap; - return $this; - } - - private function setEnabled(bool $enabled): self - { - $this->enabled = $enabled; - return $this; - } - public function isSeveralSupported(): bool { return true; } - - public function apply(RestRequestOptions $options, ReflectionMethodWrapper $reflectionMethod) - { - if (!$this->enabled) { - $options->disableBodyValidation(); - return; - } - - $options->setBodyValidationOptions( - (new ValidationOptions()) - ->setValidationGroups($this->groups) - ->setViolationPathMap($this->violationPathMap) - ); - } } diff --git a/src/Attribute/Body.php b/src/Attribute/Body.php new file mode 100644 index 0000000..a04ded8 --- /dev/null +++ b/src/Attribute/Body.php @@ -0,0 +1,111 @@ +setParameterName($options['parameterName'] ?? $parameterName); + $this->setDenormalizationType($options['denormalizationType'] ?? $denormalizationType); + $this->setDenormalizationGroup($options['denormalizationGroup'] ?? $denormalizationGroup); + $this->setOptional($options['optional'] ?? $optional); + } + + private function setDenormalizationType(?string $denormalizationType): self + { + $this->denormalizationType = $denormalizationType; + return $this; + } + + /** + * @param string|null $denormalizationGroup + * @return $this + */ + public function setDenormalizationGroup($denormalizationGroup): self + { + $this->denormalizationGroup = $denormalizationGroup; + return $this; + } + + private function setParameterName(string $parameterName): self + { + $this->parameterName = $parameterName; + return $this; + } + + private function setOptional(?bool $optional): self + { + $this->optional = $optional; + return $this; + } + + public function apply(RestRequestOptions $options, ReflectionMethodWrapper $reflectionMethod) + { + $options->setBodyParameterName($this->parameterName); + $options->setBodyDenormalizationType($this->resolveDenormalizationType($reflectionMethod)); + $options->setBodyDenormalizationGroup($this->denormalizationGroup); + $options->setBodyOptional($this->resolveIfBodyIsOptional($reflectionMethod)); + } + + private function resolveDenormalizationType(ReflectionMethodWrapper $reflectionMethod): string + { + if ($this->denormalizationType !== null) { + return $this->denormalizationType; + } + + try { + $typeName = $reflectionMethod->getNonBuiltInTypeForParameter($this->parameterName); + } catch (ConfigurationException $exception) { + throw new ConfigurationException(sprintf( + 'Denormalization type could not be guessed for %s in %s', + '$' . $this->parameterName, + $reflectionMethod->getFriendlyName() + )); + } + + return $typeName; + } + + private function resolveIfBodyIsOptional(ReflectionMethodWrapper $reflectionMethod): bool + { + if ($this->optional !== null) { + return $this->optional; + } + + return $reflectionMethod->getParameterByName($this->parameterName)->isDefaultValueAvailable(); + } +} diff --git a/src/Attribute/BodyContentType.php b/src/Attribute/BodyContentType.php new file mode 100644 index 0000000..d1b0f1a --- /dev/null +++ b/src/Attribute/BodyContentType.php @@ -0,0 +1,49 @@ +setSupportedContentTypes($options['supportedContentTypes'] ?? $supportedContentTypes); + $this->setJsonEncodedBody($options['jsonEncodedBody'] ?? $jsonEncodedBody); + } + + private function setSupportedContentTypes(array $supportedContentTypes): self + { + $this->supportedContentTypes = $supportedContentTypes; + return $this; + } + + private function setJsonEncodedBody(bool $jsonEncodedBody): self + { + $this->jsonEncodedBody = $jsonEncodedBody; + return $this; + } + + public function apply(RestRequestOptions $options, ReflectionMethodWrapper $reflectionMethod) + { + $options->setSupportedContentTypes($this->supportedContentTypes, $this->jsonEncodedBody); + } +} diff --git a/src/Attribute/PathAttribute.php b/src/Attribute/PathAttribute.php new file mode 100644 index 0000000..471f9a7 --- /dev/null +++ b/src/Attribute/PathAttribute.php @@ -0,0 +1,113 @@ +setParameterName($options['parameterName'] ?? $parameterName); + $this->setPathPartName($options['pathPartName'] ?? $pathPartName); + $this->setResolverType($options['resolverType'] ?? $resolverType); + $this->setResolutionMandatory($options['resolutionMandatory'] ?? $resolutionMandatory); + } + + private function setParameterName(string $parameterName): self + { + $this->parameterName = $parameterName; + return $this; + } + + private function setPathPartName(string $pathPartName): self + { + $this->pathPartName = $pathPartName; + return $this; + } + + private function setResolverType(?string $resolverType): self + { + $this->resolverType = $resolverType; + return $this; + } + + private function setResolutionMandatory(?bool $resolutionMandatory): self + { + $this->resolutionMandatory = $resolutionMandatory; + return $this; + } + + public function apply(RestRequestOptions $options, ReflectionMethodWrapper $reflectionMethod) + { + $options->addPathAttributeResolverOptions( + (new PathAttributeResolverOptions()) + ->setParameterName($this->parameterName) + ->setPathPartName($this->pathPartName) + ->setPathAttributeResolverType($this->resolvePathAttributeResolverType($reflectionMethod)) + ->setResolutionMandatory($this->resolveIfResolutionIsMandatory($reflectionMethod)) + ); + } + + private function resolvePathAttributeResolverType(ReflectionMethodWrapper $reflectionMethod): string + { + if ($this->resolverType !== null) { + return $this->resolverType; + } + + try { + return $reflectionMethod->getNonBuiltInTypeForParameter($this->parameterName); + } catch (ConfigurationException $exception) { + throw new ConfigurationException( + sprintf( + 'Denormalization type could not be guessed for %s in %s', + '$' . $this->parameterName, + $reflectionMethod->getFriendlyName() + ) + ); + } + } + + private function resolveIfResolutionIsMandatory(ReflectionMethodWrapper $reflectionMethod): bool + { + if ($this->resolutionMandatory !== null) { + return $this->resolutionMandatory; + } + + $parameter = $reflectionMethod->getParameterByName($this->parameterName); + + return !$parameter->isDefaultValueAvailable(); + } +} diff --git a/src/Attribute/Query.php b/src/Attribute/Query.php new file mode 100644 index 0000000..ea47fb8 --- /dev/null +++ b/src/Attribute/Query.php @@ -0,0 +1,127 @@ +setParameterName($options['parameterName'] ?? $parameterName); + $this->setDenormalizationType($options['denormalizationType'] ?? $denormalizationType); + $this->setDenormalizationGroup($options['denormalizationGroup'] ?? $denormalizationGroup); + $this->setValidation($options['validation'] ?? $validation); + } + + private function setParameterName(string $parameterName): self + { + $this->parameterName = $parameterName; + return $this; + } + + private function setDenormalizationType(?string $denormalizationType): self + { + $this->denormalizationType = $denormalizationType; + return $this; + } + + /** + * @param string|null $denormalizationGroup + * @return $this + */ + public function setDenormalizationGroup($denormalizationGroup): self + { + $this->denormalizationGroup = $denormalizationGroup; + return $this; + } + + private function setValidation(?Validation $validation): self + { + $this->validation = $validation; + return $this; + } + + public function apply(RestRequestOptions $options, ReflectionMethodWrapper $reflectionMethod) + { + $resolverOptions = (new QueryResolverOptions()) + ->setParameterName($this->parameterName) + ->setDenormalizationType($this->resolveDenormalizationType($reflectionMethod)) + ->setDenormalizationGroup($this->denormalizationGroup) + ; + + $this->setValidationOptions($reflectionMethod, $resolverOptions); + + $options->addQueryResolverOptions($resolverOptions); + } + + private function resolveDenormalizationType(ReflectionMethodWrapper $reflectionMethod): string + { + if ($this->denormalizationType !== null) { + return $this->denormalizationType; + } + + try { + $typeName = $reflectionMethod->getNonBuiltInTypeForParameter($this->parameterName); + } catch (ConfigurationException $exception) { + throw new ConfigurationException( + sprintf( + 'Denormalization type could not be guessed for %s in %s', + '$' . $this->parameterName, + $reflectionMethod->getFriendlyName() + ) + ); + } + + return $typeName; + } + + private function setValidationOptions(ReflectionMethodWrapper $reflectionMethod, QueryResolverOptions $options): void + { + if ($this->validation === null) { + return; + } + + $restRequestOptions = new RestRequestOptions(); + $this->validation->apply($restRequestOptions, $reflectionMethod); + + if (!$restRequestOptions->isBodyValidationNeeded()) { + $options->disableValidation(); + return; + } + + $options->setValidationOptions($restRequestOptions->getBodyValidationOptions()); + } +} diff --git a/src/Attribute/RequiredPermissions.php b/src/Attribute/RequiredPermissions.php new file mode 100644 index 0000000..648d0c1 --- /dev/null +++ b/src/Attribute/RequiredPermissions.php @@ -0,0 +1,38 @@ +setPermissions($options['permissions'] ?? $permissions); + } + + private function setPermissions(array $permissions): self + { + $this->permissions = $permissions; + return $this; + } + + public function apply(RestRequestOptions $options, ReflectionMethodWrapper $reflectionMethod) + { + $options->setRequiredPermissions( + array_unique(array_merge($options->getRequiredPermissions(), $this->permissions)) + ); + } +} diff --git a/src/Attribute/ResponseNormalization.php b/src/Attribute/ResponseNormalization.php new file mode 100644 index 0000000..92e6a82 --- /dev/null +++ b/src/Attribute/ResponseNormalization.php @@ -0,0 +1,54 @@ +setNormalizationType($options['normalizationType'] ?? $normalizationType); + $this->setNormalizationGroup($options['normalizationGroup'] ?? $normalizationGroup); + } + + private function setNormalizationType(?string $normalizationType): self + { + $this->normalizationType = $normalizationType; + return $this; + } + + /** + * @param string|null $normalizationGroup + * @return $this + */ + public function setNormalizationGroup($normalizationGroup): self + { + $this->normalizationGroup = $normalizationGroup; + return $this; + } + + public function apply(RestRequestOptions $options, ReflectionMethodWrapper $reflectionMethod) + { + $options->setResponseNormalizationType($this->normalizationType); + $options->setResponseNormalizationGroup($this->normalizationGroup); + } +} diff --git a/src/Attribute/RestAttributeInterface.php b/src/Attribute/RestAttributeInterface.php new file mode 100644 index 0000000..92f3d8d --- /dev/null +++ b/src/Attribute/RestAttributeInterface.php @@ -0,0 +1,13 @@ +setGroups($options['groups'] ?? $groups); + $this->setViolationPathMap($options['violationPathMap'] ?? $violationPathMap); + $this->setEnabled($options['enabled'] ?? $enabled); + } + + private function setGroups(array $groups): self + { + $this->groups = $groups; + return $this; + } + + private function setViolationPathMap(array $violationPathMap): self + { + $this->violationPathMap = $violationPathMap; + return $this; + } + + private function setEnabled(bool $enabled): self + { + $this->enabled = $enabled; + return $this; + } + + public function apply(RestRequestOptions $options, ReflectionMethodWrapper $reflectionMethod) + { + if (!$this->enabled) { + $options->disableBodyValidation(); + return; + } + + $options->setBodyValidationOptions( + (new ValidationOptions()) + ->setValidationGroups($this->groups) + ->setViolationPathMap($this->violationPathMap) + ); + } +} diff --git a/src/DependencyInjection/PayseraApiExtension.php b/src/DependencyInjection/PayseraApiExtension.php index d157b41..2a1db01 100644 --- a/src/DependencyInjection/PayseraApiExtension.php +++ b/src/DependencyInjection/PayseraApiExtension.php @@ -1,4 +1,5 @@ load('services.xml'); $loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config/services')); - if (class_exists(AttributeRouteControllerLoader::class)) { - $loader->load('annotations.xml'); - } else { - $loader->load('annotations_legacy.xml'); - } + class_exists(AttributeRouteControllerLoader::class) + ? $loader->load('attributes.xml') + : $loader->load('annotations.xml') + ; $container->setParameter('paysera_api.locales', $config['locales']); if (count($config['locales']) === 0) { diff --git a/src/Resources/config/services.xml b/src/Resources/config/services.xml index d282980..cccb84d 100644 --- a/src/Resources/config/services.xml +++ b/src/Resources/config/services.xml @@ -18,6 +18,16 @@ + + + + + + + + diff --git a/src/Resources/config/services/annotations.xml b/src/Resources/config/services/annotations.xml index bfd252d..35b69eb 100644 --- a/src/Resources/config/services/annotations.xml +++ b/src/Resources/config/services/annotations.xml @@ -2,13 +2,13 @@ + xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd"> - + @@ -16,10 +16,5 @@ - - - - diff --git a/src/Resources/config/services/annotations_legacy.xml b/src/Resources/config/services/annotations_legacy.xml deleted file mode 100644 index 06531f1..0000000 --- a/src/Resources/config/services/annotations_legacy.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/src/Resources/config/services/attributes.xml b/src/Resources/config/services/attributes.xml new file mode 100644 index 0000000..8ecc323 --- /dev/null +++ b/src/Resources/config/services/attributes.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/Service/Annotation/ReflectionMethodWrapper.php b/src/Service/Annotation/ReflectionMethodWrapper.php index 9e22eb2..f67bb88 100644 --- a/src/Service/Annotation/ReflectionMethodWrapper.php +++ b/src/Service/Annotation/ReflectionMethodWrapper.php @@ -1,61 +1,19 @@ reflectionMethod = $reflectionMethod; - } - - public function getParameterByName(string $name): ReflectionParameter - { - foreach ($this->reflectionMethod->getParameters() as $parameter) { - if ($parameter->getName() === $name) { - return $parameter; - } - } +use Paysera\Bundle\ApiBundle\Service\RoutingLoader\ReflectionMethodWrapper as NewReflectionMethodWrapper; - throw new ConfigurationException(sprintf( - 'Parameter %s is configured but not found in method %s', - '$' . $name, - $this->getFriendlyName() - )); - } - - public function getNonBuiltInTypeForParameter(string $name): string - { - $parameter = $this->getParameterByName($name); - - $type = $parameter->getType(); - if ($type === null || $type->isBuiltin()) { - throw new ConfigurationException(sprintf( - 'Expected non built-in type-hint for %s in %s', - '$' . $parameter->getName(), - $this->getFriendlyName() - )); - } - - return version_compare(PHP_VERSION, '7.1.0') >= 0 ? $type->getName() : (string)$type; - } +class_exists(NewReflectionMethodWrapper::class); - public function getFriendlyName() +if (false) { + /** + * @internal + * @deprecated since 1.8, to be removed in 2.0, use {@link NewReflectionMethodWrapper} instead + */ + class ReflectionMethodWrapper extends NewReflectionMethodWrapper { - return sprintf( - '%s::%s', - $this->reflectionMethod->getDeclaringClass()->getName(), - $this->reflectionMethod->getName() - ); } } diff --git a/src/Service/RoutingLoader/ReflectionMethodWrapper.php b/src/Service/RoutingLoader/ReflectionMethodWrapper.php new file mode 100644 index 0000000..fab0dd3 --- /dev/null +++ b/src/Service/RoutingLoader/ReflectionMethodWrapper.php @@ -0,0 +1,66 @@ +reflectionMethod = $reflectionMethod; + } + + public function getParameterByName(string $name): ReflectionParameter + { + foreach ($this->reflectionMethod->getParameters() as $parameter) { + if ($parameter->getName() === $name) { + return $parameter; + } + } + + throw new ConfigurationException(sprintf( + 'Parameter %s is configured but not found in method %s', + '$' . $name, + $this->getFriendlyName() + )); + } + + public function getNonBuiltInTypeForParameter(string $name): string + { + $parameter = $this->getParameterByName($name); + + $type = $parameter->getType(); + if ($type === null || $type->isBuiltin()) { + throw new ConfigurationException(sprintf( + 'Expected non built-in type-hint for %s in %s', + '$' . $parameter->getName(), + $this->getFriendlyName() + )); + } + + return version_compare(PHP_VERSION, '7.1.0') >= 0 ? $type->getName() : (string)$type; + } + + public function getFriendlyName() + { + return sprintf( + '%s::%s', + $this->reflectionMethod->getDeclaringClass()->getName(), + $this->reflectionMethod->getName() + ); + } +} + +if (!class_exists(LegacyReflectionMethodWrapper::class, false)) { + class_alias(ReflectionMethodWrapper::class, LegacyReflectionMethodWrapper::class); +} diff --git a/src/Service/Annotation/RestRequestOptionsBuilder.php b/src/Service/RoutingLoader/RestRequestAnnotationOptionsBuilder.php similarity index 87% rename from src/Service/Annotation/RestRequestOptionsBuilder.php rename to src/Service/RoutingLoader/RestRequestAnnotationOptionsBuilder.php index eb006c9..3e5d241 100644 --- a/src/Service/Annotation/RestRequestOptionsBuilder.php +++ b/src/Service/RoutingLoader/RestRequestAnnotationOptionsBuilder.php @@ -1,7 +1,7 @@ validator = $validator; + } + + /** + * @param RestAttributeInterface[] $attributes + * @param ReflectionMethod $reflectionMethod + * @return RestRequestOptions + */ + public function buildOptions(array $attributes, ReflectionMethod $reflectionMethod): RestRequestOptions + { + $options = new RestRequestOptions(); + $wrapper = new ReflectionMethodWrapper($reflectionMethod); + + foreach ($attributes as $attribute) { + $attribute->apply($options, $wrapper); + } + + $this->validator->validateRestRequestOptions($options, $wrapper->getFriendlyName()); + + return $options; + } +} diff --git a/src/Service/Annotation/RoutingAnnotationLoader.php b/src/Service/RoutingLoader/RoutingAnnotationLoader.php similarity index 88% rename from src/Service/Annotation/RoutingAnnotationLoader.php rename to src/Service/RoutingLoader/RoutingAnnotationLoader.php index 7ac1f13..da85461 100644 --- a/src/Service/Annotation/RoutingAnnotationLoader.php +++ b/src/Service/RoutingLoader/RoutingAnnotationLoader.php @@ -1,7 +1,7 @@ restRequestHelper = $restRequestHelper; } - public function setRestRequestOptionsBuilder(RestRequestOptionsBuilder $restRequestOptionsBuilder) + public function setRestRequestOptionsBuilder(RestRequestAnnotationOptionsBuilder $restRequestOptionsBuilder) { $this->restRequestOptionsBuilder = $restRequestOptionsBuilder; } diff --git a/src/Service/RoutingLoader/RoutingAttributeLoader.php b/src/Service/RoutingLoader/RoutingAttributeLoader.php new file mode 100644 index 0000000..87eaae8 --- /dev/null +++ b/src/Service/RoutingLoader/RoutingAttributeLoader.php @@ -0,0 +1,109 @@ +restRequestHelper = $restRequestHelper; + } + + public function setRestRequestAnnotationOptionsBuilder(RestRequestAnnotationOptionsBuilder $annotationOptionsBuilder + ) { + $this->annotationOptionsBuilder = $annotationOptionsBuilder; + } + + public function setRestRequestAttributeOptionsBuilder(RestRequestAttributeOptionsBuilder $attributeOptionsBuilder) + { + $this->attributeOptionsBuilder = $attributeOptionsBuilder; + } + + protected function configureRoute( + Route $route, + ReflectionClass $class, + ReflectionMethod $method, + object $annot + ): void { + parent::configureRoute($route, $class, $method, $annot); + + $this->loadAnnotations($route, $class, $method); + $this->loadAttributes($route, $class, $method); + } + + private function loadAnnotations(Route $route, ReflectionClass $class, ReflectionMethod $method): void + { + $annotations = []; + foreach ($this->reader->getClassAnnotations($class) as $annotation) { + if ($annotation instanceof RestAnnotationInterface) { + $annotations[] = $annotation; + } + } + + foreach ($this->reader->getMethodAnnotations($method) as $annotation) { + if ($annotation instanceof RestAnnotationInterface) { + $annotations[] = $annotation; + } + } + + if ($annotations === []) { + return; + } + + $this->restRequestHelper->setOptionsForRoute( + $route, + $this->annotationOptionsBuilder->buildOptions($annotations, $method) + ); + } + + private function loadAttributes(Route $route, ReflectionClass $class, ReflectionMethod $method): void + { + $attributes = array_merge($class->getAttributes(), $method->getAttributes()); + + $restAttributes = []; + foreach ($attributes as $attribute) { + if (is_subclass_of($attribute->getName(), RestAttributeInterface::class)) { + /** @var RestAttributeInterface $instance */ + $instance = $attribute->newInstance(); + $restAttributes[] = $instance; + } + } + + if ($restAttributes === []) { + return; + } + + $this->restRequestHelper->setOptionsForRoute( + $route, + $this->attributeOptionsBuilder->buildOptions($restAttributes, $method) + ); + } +} diff --git a/tests/Functional/Fixtures/FixtureTestBundle/Controller/AnnotatedClassRequiredPermissionsController.php b/tests/Functional/Fixtures/FixtureTestBundle/Controller/AnnotatedClassRequiredPermissionsController.php index 20949b6..82ba493 100644 --- a/tests/Functional/Fixtures/FixtureTestBundle/Controller/AnnotatedClassRequiredPermissionsController.php +++ b/tests/Functional/Fixtures/FixtureTestBundle/Controller/AnnotatedClassRequiredPermissionsController.php @@ -18,20 +18,16 @@ class AnnotatedClassRequiredPermissionsController * @RequiredPermissions(permissions={"ROLE_USER"}) * @RequiredPermissions(permissions={"ROLE_ADMIN"}) * @RequiredPermissions(permissions={"ROLE_USER"}) - * - * @return Response */ - public function test() + public function test(): Response { return new Response('OK'); } /** * @Route(path="/annotated/class/simpleAction", methods={"GET"}) - * - * @return Response */ - public function simpleAction() + public function simpleAction(): Response { return new Response('OK'); } diff --git a/tests/Functional/Fixtures/FixtureTestBundle/Controller/AnnotatedClassValidationController.php b/tests/Functional/Fixtures/FixtureTestBundle/Controller/AnnotatedClassValidationController.php index 43a153e..48971ec 100644 --- a/tests/Functional/Fixtures/FixtureTestBundle/Controller/AnnotatedClassValidationController.php +++ b/tests/Functional/Fixtures/FixtureTestBundle/Controller/AnnotatedClassValidationController.php @@ -19,11 +19,8 @@ class AnnotatedClassValidationController * * @Body(parameterName="resource") * @Validation(groups={"field1_email"}, violationPathMap={"field1": "my_mapped_key"}) - * - * @param MyObject $resource - * @return Response */ - public function testValidation(MyObject $resource) + public function testValidation(MyObject $resource): Response { // should fail validation return new Response('FAIL'); @@ -33,11 +30,8 @@ public function testValidation(MyObject $resource) * @Route(path="/annotated/class/testValidationFromClass", methods={"POST"}) * * @Body(parameterName="resource") - * - * @param MyObject $resource - * @return Response */ - public function testValidationFromClass(MyObject $resource) + public function testValidationFromClass(MyObject $resource): Response { // should fail validation return new Response('FAIL'); diff --git a/tests/Functional/Fixtures/FixtureTestBundle/Controller/AnnotatedController.php b/tests/Functional/Fixtures/FixtureTestBundle/Controller/AnnotatedController.php index 8d81f44..842365d 100644 --- a/tests/Functional/Fixtures/FixtureTestBundle/Controller/AnnotatedController.php +++ b/tests/Functional/Fixtures/FixtureTestBundle/Controller/AnnotatedController.php @@ -21,11 +21,8 @@ class AnnotatedController * @Route(path="/annotated/testBodyNormalizationWithExtractedKeyValue", methods={"POST"}) * * @Body(parameterName="keyValueInBody", denormalizationType="extract:key") - * - * @param string $keyValueInBody - * @return Response */ - public function testBodyNormalizationWithExtractedKeyValue(string $keyValueInBody = 'default') + public function testBodyNormalizationWithExtractedKeyValue(string $keyValueInBody = 'default'): Response { return new Response($keyValueInBody); } @@ -34,11 +31,8 @@ public function testBodyNormalizationWithExtractedKeyValue(string $keyValueInBod * @Route(path="/annotated/testBodyNormalizationWithDenormalizationGroup", methods={"POST"}) * * @Body(parameterName="keyValueInBody", denormalizationType="extract:key", denormalizationGroup="custom") - * - * @param string $keyValueInBody - * @return Response */ - public function testBodyNormalizationWithDenormalizationGroup(string $keyValueInBody = 'default') + public function testBodyNormalizationWithDenormalizationGroup(string $keyValueInBody = 'default'): Response { return new Response($keyValueInBody); } @@ -47,11 +41,8 @@ public function testBodyNormalizationWithDenormalizationGroup(string $keyValueIn * @Route(path="/annotated/testBodyNormalizationWithRequiredBody", methods={"POST"}) * * @Body(parameterName="body", denormalizationType="extract:key") - * - * @param string $body - * @return Response */ - public function testBodyNormalizationWithRequiredBody(string $body) + public function testBodyNormalizationWithRequiredBody(string $body): Response { // should fail as we don't pass any body return new Response('FAIL'); @@ -61,11 +52,8 @@ public function testBodyNormalizationWithRequiredBody(string $body) * @Route(path="/annotated/testBodyAndResponseNormalization", methods={"POST"}) * * @Body(parameterName="resource") - * - * @param MyObject $resource - * @return MyObject */ - public function testBodyAndResponseNormalization(MyObject $resource) + public function testBodyAndResponseNormalization(MyObject $resource): MyObject { return $resource; } @@ -75,11 +63,8 @@ public function testBodyAndResponseNormalization(MyObject $resource) * * @Body(parameterName="body", denormalizationType="prefixed") * @BodyContentType(supportedContentTypes={"text/plain"}) - * - * @param string $body - * @return Response */ - public function testBodyNormalizationWithCustomContentType(string $body) + public function testBodyNormalizationWithCustomContentType(string $body): Response { return new Response($body); } @@ -89,11 +74,8 @@ public function testBodyNormalizationWithCustomContentType(string $body) * * @Body(parameterName="keyValueInBody", denormalizationType="extract:key") * @BodyContentType(supportedContentTypes={"text/plain"}, jsonEncodedBody=true) - * - * @param string $keyValueInBody - * @return Response */ - public function testBodyNormalizationWithCustomContentTypeAndJsonDecode(string $keyValueInBody) + public function testBodyNormalizationWithCustomContentTypeAndJsonDecode(string $keyValueInBody): Response { return new Response($keyValueInBody); } @@ -103,11 +85,8 @@ public function testBodyNormalizationWithCustomContentTypeAndJsonDecode(string $ * * @Body(parameterName="body", denormalizationType="prefixed") * @BodyContentType(supportedContentTypes={"image/jpeg", "text/*"}) - * - * @param string $body - * @return Response */ - public function testBodyNormalizationWithSemiContentTypeRestriction(string $body) + public function testBodyNormalizationWithSemiContentTypeRestriction(string $body): Response { return new Response($body); } @@ -117,11 +96,8 @@ public function testBodyNormalizationWithSemiContentTypeRestriction(string $body * * @Body(parameterName="resource") * @Validation(groups={"field1_email"}, violationPathMap={"field1": "my_mapped_key"}) - * - * @param MyObject $resource - * @return Response */ - public function testBodyNormalizationWithValidation(MyObject $resource) + public function testBodyNormalizationWithValidation(MyObject $resource): Response { // should fail validation return new Response('FAIL'); @@ -132,11 +108,8 @@ public function testBodyNormalizationWithValidation(MyObject $resource) * * @Body(parameterName="resource") * @Validation(groups={"internal_field1_email"}) - * - * @param MyObject $resource - * @return Response */ - public function testBodyNormalizationWithInnerTypeValidation(MyObject $resource) + public function testBodyNormalizationWithInnerTypeValidation(MyObject $resource): Response { // should fail validation return new Response('FAIL'); @@ -147,11 +120,8 @@ public function testBodyNormalizationWithInnerTypeValidation(MyObject $resource) * * @Body(parameterName="resource") * @Validation(enabled=false) - * - * @param MyObject $resource - * @return Response */ - public function testBodyValidationCanBeTurnedOff(MyObject $resource) + public function testBodyValidationCanBeTurnedOff(MyObject $resource): Response { return new Response('OK'); } @@ -161,11 +131,8 @@ public function testBodyValidationCanBeTurnedOff(MyObject $resource) * * @Body(parameterName="resource") * @Validation(groups={}) - * - * @param MyObject $resource - * @return Response */ - public function testBodyValidationCanBeTurnedOffWithEmptyGroups(MyObject $resource) + public function testBodyValidationCanBeTurnedOffWithEmptyGroups(MyObject $resource): Response { return new Response('OK'); } @@ -175,11 +142,8 @@ public function testBodyValidationCanBeTurnedOffWithEmptyGroups(MyObject $resour * @Route(path="/annotated/testPathAttribute", methods={"GET"}) * * @PathAttribute(parameterName="parameter", pathPartName="id", resolverType="prefixed") - * - * @param string $parameter - * @return Response */ - public function testPathAttribute(string $parameter = 'default') + public function testPathAttribute(string $parameter = 'default'): Response { return new Response($parameter); } @@ -188,11 +152,8 @@ public function testPathAttribute(string $parameter = 'default') * @Route(path="/annotated/testPathAttributeWithFindingObject/{id}", methods={"GET"}) * * @PathAttribute(parameterName="myObject", pathPartName="id") - * - * @param MyObject $myObject - * @return Response */ - public function testPathAttributeWithFindingObject(MyObject $myObject) + public function testPathAttributeWithFindingObject(MyObject $myObject): Response { return new Response($myObject->getField1()); } @@ -201,11 +162,8 @@ public function testPathAttributeWithFindingObject(MyObject $myObject) * @Route(path="/annotated/testPathAttributeWithFailedResolution/{id}", methods={"GET"}) * * @PathAttribute(parameterName="myObject", pathPartName="id", resolverType="always_null") - * - * @param MyObject $myObject - * @return Response */ - public function testPathAttributeWithFailedResolution(MyObject $myObject) + public function testPathAttributeWithFailedResolution(MyObject $myObject): Response { // should fail before calling controller return new Response('FAIL'); @@ -215,11 +173,8 @@ public function testPathAttributeWithFailedResolution(MyObject $myObject) * @Route(path="/annotated/testQueryResolver", methods={"GET"}) * * @Query(parameterName="parameter", denormalizationType="extract:parameter") - * - * @param string $parameter - * @return Response */ - public function testQueryResolver(string $parameter) + public function testQueryResolver(string $parameter): Response { return new Response($parameter); } @@ -228,11 +183,8 @@ public function testQueryResolver(string $parameter) * @Route(path="/annotated/testQueryResolverWithDenormalizationGroup", methods={"GET"}) * * @Query(parameterName="parameter", denormalizationType="extract:parameter", denormalizationGroup="custom") - * - * @param string $parameter - * @return Response */ - public function testQueryResolverWithDenormalizationGroup(string $parameter) + public function testQueryResolverWithDenormalizationGroup(string $parameter): Response { return new Response($parameter); } @@ -241,11 +193,8 @@ public function testQueryResolverWithDenormalizationGroup(string $parameter) * @Route(path="/annotated/testQueryResolverPagerLimitIs42", methods={"GET"}) * * @Query(parameterName="pager") - * - * @param Pager $pager - * @return Response */ - public function testQueryResolverPagerLimitIs42(Pager $pager) + public function testQueryResolverPagerLimitIs42(Pager $pager): Response { return new Response($pager->getLimit() === 42 ? 'OK' : 'FAIL'); } @@ -254,11 +203,8 @@ public function testQueryResolverPagerLimitIs42(Pager $pager) * @Route(path="/annotated/testQueryResolverHasDefaultValidation", methods={"GET"}) * * @Query(parameterName="myObject") - * - * @param MyObject $myObject - * @return Response */ - public function testQueryResolverHasDefaultValidation(MyObject $myObject) + public function testQueryResolverHasDefaultValidation(MyObject $myObject): Response { // should fail validation return new Response('FAIL'); @@ -268,11 +214,8 @@ public function testQueryResolverHasDefaultValidation(MyObject $myObject) * @Route(path="/annotated/testQueryResolverCanTurnOffValidation", methods={"GET"}) * * @Query(parameterName="myObject", validation=@Validation(enabled=false)) - * - * @param MyObject $myObject - * @return Response */ - public function testQueryResolverCanTurnOffValidation(MyObject $myObject) + public function testQueryResolverCanTurnOffValidation(MyObject $myObject): Response { return new Response('OK'); } @@ -281,11 +224,8 @@ public function testQueryResolverCanTurnOffValidation(MyObject $myObject) * @Route(path="/annotated/testQueryResolverCanTurnOffValidationWithEmptyGroups", methods={"GET"}) * * @Query(parameterName="myObject", validation=@Validation(groups={})) - * - * @param MyObject $myObject - * @return Response */ - public function testQueryResolverCanTurnOffValidationWithEmptyGroups(MyObject $myObject) + public function testQueryResolverCanTurnOffValidationWithEmptyGroups(MyObject $myObject): Response { return new Response('OK'); } @@ -297,11 +237,8 @@ public function testQueryResolverCanTurnOffValidationWithEmptyGroups(MyObject $m * groups={"field1_email"}, * violationPathMap={"field1": "mapped_key"} * )) - * - * @param MyObject $myObject - * @return Response */ - public function testQueryResolverValidationWithInvalidData(MyObject $myObject) + public function testQueryResolverValidationWithInvalidData(MyObject $myObject): Response { // should fail validation return new Response('FAIL'); @@ -311,10 +248,8 @@ public function testQueryResolverValidationWithInvalidData(MyObject $myObject) * @Route(path="/annotated/testRequiredPermissions", methods={"GET"}) * * @RequiredPermissions(permissions={"ROLE_USER", "ROLE_ADMIN"}) - * - * @return Response */ - public function testRequiredPermissions() + public function testRequiredPermissions(): Response { return new Response('OK'); } @@ -323,35 +258,35 @@ public function testRequiredPermissions() * @Route(path="/annotated/testResponseNormalization", methods={"GET"}) * * @ResponseNormalization(normalizationType="my_object_custom") - * - * @return string */ - public function testResponseNormalization() + public function testResponseNormalization(): MyObject { - return (new MyObject())->setField1('hi'); + return (new MyObject()) + ->setField1('hi') + ; } /** * @Route(path="/annotated/testResponseNormalizationWithNormalizationGroup", methods={"GET"}) * * @ResponseNormalization(normalizationGroup="custom") - * - * @return string */ - public function testResponseNormalizationWithNormalizationGroup() + public function testResponseNormalizationWithNormalizationGroup(): MyObject { - return (new MyObject())->setField1('hi'); + return (new MyObject()) + ->setField1('hi') + ; } /** * @Route(path="/annotated/testResponseNormalizationWithGuessedNormalizer", methods={"GET"}) * * @ResponseNormalization() - * - * @return string */ - public function testResponseNormalizationWithGuessedNormalizer() + public function testResponseNormalizationWithGuessedNormalizer(): MyObject { - return (new MyObject())->setField1('hi'); + return (new MyObject()) + ->setField1('hi') + ; } } diff --git a/tests/Functional/Fixtures/FixtureTestBundle/Controller/Attribute/AttributedClassRequiredPermissionsController.php b/tests/Functional/Fixtures/FixtureTestBundle/Controller/Attribute/AttributedClassRequiredPermissionsController.php new file mode 100644 index 0000000..f93ebd4 --- /dev/null +++ b/tests/Functional/Fixtures/FixtureTestBundle/Controller/Attribute/AttributedClassRequiredPermissionsController.php @@ -0,0 +1,29 @@ + 'internal.field1'])] +class AttributedClassValidationController +{ + #[Route(path: '/attributed/class/testValidation', methods: Request::METHOD_POST)] + #[Body(parameterName: 'resource')] + #[Validation(groups: ['field1_email'], violationPathMap: ['field1' => 'my_mapped_key'])] + public function testValidation(MyObject $resource): Response + { + // should fail validation + return new Response('FAIL'); + } + + #[Route(path: '/attributed/class/testValidationFromClass', methods: Request::METHOD_POST)] + #[Body(parameterName: 'resource')] + public function testValidationFromClass(MyObject $resource): Response + { + // should fail validation + return new Response('FAIL'); + } +} diff --git a/tests/Functional/Fixtures/FixtureTestBundle/Controller/Attribute/AttributedController.php b/tests/Functional/Fixtures/FixtureTestBundle/Controller/Attribute/AttributedController.php new file mode 100644 index 0000000..6544ab7 --- /dev/null +++ b/tests/Functional/Fixtures/FixtureTestBundle/Controller/Attribute/AttributedController.php @@ -0,0 +1,215 @@ + 'my_mapped_key'])] + public function testBodyNormalizationWithValidation(MyObject $resource): Response + { + // should fail validation + return new Response('FAIL'); + } + + #[Route(path: '/attributed/testBodyNormalizationWithInnerTypeValidation', methods: Request::METHOD_POST)] + #[Body(parameterName: 'resource')] + #[Validation(groups: ['internal_field1_email'])] + public function testBodyNormalizationWithInnerTypeValidation(MyObject $resource): Response + { + // should fail validation + return new Response('FAIL'); + } + + #[Route(path: '/attributed/testBodyValidationCanBeTurnedOff', methods: Request::METHOD_POST)] + #[Body(parameterName: 'resource')] + #[Validation(enabled: false)] + public function testBodyValidationCanBeTurnedOff(MyObject $resource): Response + { + return new Response('OK'); + } + + #[Route(path: '/attributed/testBodyValidationCanBeTurnedOffWithEmptyGroups', methods: Request::METHOD_POST)] + #[Body(parameterName: 'resource')] + #[Validation(groups: [])] + public function testBodyValidationCanBeTurnedOffWithEmptyGroups(MyObject $resource): Response + { + return new Response('OK'); + } + + #[Route(path: '/attributed/testPathAttribute/{id}', methods: Request::METHOD_GET)] + #[Route(path: '/attributed/testPathAttribute', methods: Request::METHOD_GET)] + #[PathAttribute(parameterName: 'parameter', pathPartName: 'id', resolverType: 'prefixed')] + public function testPathAttribute(string $parameter = 'default'): Response + { + return new Response($parameter); + } + + #[Route(path: '/attributed/testPathAttributeWithFindingObject/{id}', methods: Request::METHOD_GET)] + #[PathAttribute(parameterName: 'myObject', pathPartName: 'id')] + public function testPathAttributeWithFindingObject(MyObject $myObject): Response + { + return new Response($myObject->getField1()); + } + + #[Route(path: '/attributed/testPathAttributeWithFailedResolution/{id}', methods: Request::METHOD_GET)] + #[PathAttribute(parameterName: 'myObject', pathPartName: 'id', resolverType: 'always_null')] + public function testPathAttributeWithFailedResolution(MyObject $myObject): Response + { + // should fail before calling controller + return new Response('FAIL'); + } + + #[Route(path: '/attributed/testQueryResolver', methods: Request::METHOD_GET)] + #[Query(parameterName: 'parameter', denormalizationType: 'extract:parameter')] + public function testQueryResolver(string $parameter): Response + { + return new Response($parameter); + } + + #[Route(path: '/attributed/testQueryResolverWithDenormalizationGroup', methods: Request::METHOD_GET)] + #[Query(parameterName: 'parameter', denormalizationType: 'extract:parameter', denormalizationGroup: 'custom')] + public function testQueryResolverWithDenormalizationGroup(string $parameter): Response + { + return new Response($parameter); + } + + #[Route(path: '//attributed/testQueryResolverPagerLimitIs42', methods: Request::METHOD_GET)] + #[Query(parameterName: 'pager')] + public function testQueryResolverPagerLimitIs42(Pager $pager): Response + { + return new Response($pager->getLimit() === 42 ? 'OK' : 'FAIL'); + } + + #[Route(path: '/attributed/testQueryResolverHasDefaultValidation', methods: Request::METHOD_GET)] + #[Query(parameterName: 'myObject')] + public function testQueryResolverHasDefaultValidation(MyObject $myObject): Response + { + // should fail validation + return new Response('FAIL'); + } + + #[Route(path: '/attributed/testQueryResolverCanTurnOffValidation', methods: Request::METHOD_GET)] + #[Query(parameterName: 'myObject', validation: new Validation(enabled: false))] + public function testQueryResolverCanTurnOffValidation(MyObject $myObject): Response + { + return new Response('OK'); + } + + #[Route(path: '/attributed/testQueryResolverCanTurnOffValidationWithEmptyGroups', methods: Request::METHOD_GET)] + #[Query(parameterName: 'myObject', validation: new Validation(groups: []))] + public function testQueryResolverCanTurnOffValidationWithEmptyGroups(MyObject $myObject): Response + { + return new Response('OK'); + } + + #[Route(path: '/attributed/testQueryResolverValidationWithInvalidData', methods: Request::METHOD_GET)] + #[Query(parameterName: 'myObject', validation: new Validation(groups: ['field1_email'], violationPathMap: ['field1' => 'mapped_key']))] + public function testQueryResolverValidationWithInvalidData(MyObject $myObject): Response + { + // should fail validation + return new Response('FAIL'); + } + + #[Route(path: '/attributed/testRequiredPermissions', methods: Request::METHOD_GET)] + #[RequiredPermissions(permissions: ['ROLE_USER', 'ROLE_ADMIN'])] + public function testRequiredPermissions(): Response + { + return new Response('OK'); + } + + #[Route(path: '/attributed/testResponseNormalization', methods: Request::METHOD_GET)] + #[ResponseNormalization(normalizationType: 'my_object_custom')] + public function testResponseNormalization(): MyObject + { + return (new MyObject()) + ->setField1('hi') + ; + } + + #[Route(path: '/attributed/testResponseNormalizationWithNormalizationGroup', methods: Request::METHOD_GET)] + #[ResponseNormalization(normalizationGroup: 'custom')] + public function testResponseNormalizationWithNormalizationGroup(): MyObject + { + return (new MyObject()) + ->setField1('hi') + ; + } + + #[Route(path: '/attributed/testResponseNormalizationWithGuessedNormalizer', methods: Request::METHOD_GET)] + #[ResponseNormalization] + public function testResponseNormalizationWithGuessedNormalizer(): MyObject + { + return (new MyObject()) + ->setField1('hi') + ; + } +} diff --git a/tests/Functional/Fixtures/FixtureTestBundle/Controller/DefaultController.php b/tests/Functional/Fixtures/FixtureTestBundle/Controller/DefaultController.php index 41eaaa5..cbbec4b 100644 --- a/tests/Functional/Fixtures/FixtureTestBundle/Controller/DefaultController.php +++ b/tests/Functional/Fixtures/FixtureTestBundle/Controller/DefaultController.php @@ -50,6 +50,8 @@ public function actionWithMultipleParameters($parameter1, $parameter2): Response public function actionWithReturn(): MyObject { - return (new MyObject())->setField1('field from controller'); + return (new MyObject()) + ->setField1('field from controller') + ; } } diff --git a/tests/Functional/Fixtures/FixtureTestBundle/Controller/PagedQueryController.php b/tests/Functional/Fixtures/FixtureTestBundle/Controller/PagedQueryController.php index c8a1167..a9cd224 100644 --- a/tests/Functional/Fixtures/FixtureTestBundle/Controller/PagedQueryController.php +++ b/tests/Functional/Fixtures/FixtureTestBundle/Controller/PagedQueryController.php @@ -26,12 +26,8 @@ public function __construct(EntityManagerInterface $entityManager) * * @Query(parameterName="pager") * @Query(parameterName="filter") - * - * @param Pager $pager - * @param PersistedEntityFilter $filter - * @return PagedQuery */ - public function findSimplePersistedEntities(Pager $pager, PersistedEntityFilter $filter) + public function findSimplePersistedEntities(Pager $pager, PersistedEntityFilter $filter): PagedQuery { /** @var PersistedEntityRepository $repository */ $repository = $this->entityManager->getRepository(PersistedEntity::class); diff --git a/tests/Functional/Fixtures/FixtureTestBundle/Controller/PersistedEntityController.php b/tests/Functional/Fixtures/FixtureTestBundle/Controller/PersistedEntityController.php index dc71748..abc744d 100644 --- a/tests/Functional/Fixtures/FixtureTestBundle/Controller/PersistedEntityController.php +++ b/tests/Functional/Fixtures/FixtureTestBundle/Controller/PersistedEntityController.php @@ -15,11 +15,8 @@ class PersistedEntityController * @Route(path="/persisted-entities/{identifier}", methods={"GET"}) * * @PathAttribute(parameterName="entity", pathPartName="identifier") - * - * @param PersistedEntity $entity - * @return Response */ - public function findPersistedEntity(PersistedEntity $entity) + public function findPersistedEntity(PersistedEntity $entity): Response { return new Response((string)$entity->getId()); } @@ -28,11 +25,8 @@ public function findPersistedEntity(PersistedEntity $entity) * @Route(path="/simple-persisted-entities/{identifier}", methods={"GET"}) * * @PathAttribute(parameterName="entity", pathPartName="identifier") - * - * @param SimplePersistedEntity $entity - * @return Response */ - public function findSimplePersistedEntity(SimplePersistedEntity $entity) + public function findSimplePersistedEntity(SimplePersistedEntity $entity): Response { return new Response((string)$entity->getId()); } diff --git a/tests/Functional/Fixtures/FixtureTestBundle/DependencyInjection/PayseraFixtureTestExtension.php b/tests/Functional/Fixtures/FixtureTestBundle/DependencyInjection/PayseraFixtureTestExtension.php index 0fd5676..a9eb723 100644 --- a/tests/Functional/Fixtures/FixtureTestBundle/DependencyInjection/PayseraFixtureTestExtension.php +++ b/tests/Functional/Fixtures/FixtureTestBundle/DependencyInjection/PayseraFixtureTestExtension.php @@ -3,6 +3,9 @@ namespace Paysera\Bundle\ApiBundle\Tests\Functional\Fixtures\FixtureTestBundle\DependencyInjection; +use Doctrine\Common\Annotations\AnnotationRegistry; +use Doctrine\Common\Persistence\Mapping\Driver\MappingDriverChain as LegacyMappingDriverChain; +use Doctrine\Persistence\Mapping\Driver\MappingDriverChain; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\Config\FileLocator; use Symfony\Component\HttpKernel\DependencyInjection\Extension; @@ -28,5 +31,25 @@ public function load(array $configs, ContainerBuilder $container) $prefix = Kernel::MAJOR_VERSION <= 4 ? 'legacy_' : ''; $loader->load($prefix . 'services.xml'); + $this->handleDeprecations($container, $loader); + } + + public function handleDeprecations(ContainerBuilder $container, Loader\XmlFileLoader $loader): void + { + if (Kernel::VERSION_ID < 40000 || Kernel::VERSION >= 50400) { + return; + } + + // override the dummy registry when doctrine/annotations v2 is used + if ( + !method_exists(AnnotationRegistry::class, 'registerLoader') + || !method_exists(AnnotationRegistry::class, 'registerUniqueLoader') + ) { + $loader->load('annotation_registry.xml'); + } + + if (!class_exists(LegacyMappingDriverChain::class)) { + $container->setParameter('doctrine.orm.metadata.driver_chain.class', MappingDriverChain::class); + } } } diff --git a/tests/Functional/Fixtures/FixtureTestBundle/Resources/config/annotation_registry.xml b/tests/Functional/Fixtures/FixtureTestBundle/Resources/config/annotation_registry.xml new file mode 100644 index 0000000..5742e78 --- /dev/null +++ b/tests/Functional/Fixtures/FixtureTestBundle/Resources/config/annotation_registry.xml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/tests/Functional/Fixtures/FixtureTestBundle/Resources/config/attributed_routing.xml b/tests/Functional/Fixtures/FixtureTestBundle/Resources/config/attributed_routing.xml new file mode 100644 index 0000000..253529a --- /dev/null +++ b/tests/Functional/Fixtures/FixtureTestBundle/Resources/config/attributed_routing.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/tests/Functional/Fixtures/FixtureTestBundle/Resources/config/legacy_routing.xml b/tests/Functional/Fixtures/FixtureTestBundle/Resources/config/legacy_routing.xml index 60c538f..c302e90 100644 --- a/tests/Functional/Fixtures/FixtureTestBundle/Resources/config/legacy_routing.xml +++ b/tests/Functional/Fixtures/FixtureTestBundle/Resources/config/legacy_routing.xml @@ -4,7 +4,12 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd"> - + + + + + + PayseraFixtureTestBundle:Default:action1 diff --git a/tests/Functional/Fixtures/FixtureTestBundle/Resources/config/routing.xml b/tests/Functional/Fixtures/FixtureTestBundle/Resources/config/routing.xml index 705905f..3a06c43 100644 --- a/tests/Functional/Fixtures/FixtureTestBundle/Resources/config/routing.xml +++ b/tests/Functional/Fixtures/FixtureTestBundle/Resources/config/routing.xml @@ -4,7 +4,12 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd"> - + + + + + + Paysera\Bundle\ApiBundle\Tests\Functional\Fixtures\FixtureTestBundle\Controller\DefaultController::action1Action diff --git a/tests/Functional/Fixtures/FixtureTestBundle/Resources/config/services.xml b/tests/Functional/Fixtures/FixtureTestBundle/Resources/config/services.xml index b4ada44..46836bd 100644 --- a/tests/Functional/Fixtures/FixtureTestBundle/Resources/config/services.xml +++ b/tests/Functional/Fixtures/FixtureTestBundle/Resources/config/services.xml @@ -78,10 +78,16 @@ + + + diff --git a/tests/Functional/Fixtures/FixtureTestBundle/Service/TestHelper.php b/tests/Functional/Fixtures/FixtureTestBundle/Service/TestHelper.php new file mode 100644 index 0000000..a3133ff --- /dev/null +++ b/tests/Functional/Fixtures/FixtureTestBundle/Service/TestHelper.php @@ -0,0 +1,15 @@ +load(__DIR__ . '/config/' . $this->commonFile); + $loader->load(__DIR__ . '/config/services.yml'); $loader->load(__DIR__ . '/config/' . $this->configFile); + $loader->load(__DIR__ . '/config/' . $this->commonFile); + + if (TestHelper::phpAttributeSupportExists()) { + $loader->load(__DIR__ . '/config/attributed_common.yml'); + } } } diff --git a/tests/Functional/Fixtures/config/attributed_common.yml b/tests/Functional/Fixtures/config/attributed_common.yml new file mode 100644 index 0000000..d545657 --- /dev/null +++ b/tests/Functional/Fixtures/config/attributed_common.yml @@ -0,0 +1,3 @@ +framework: + router: + resource: '%kernel.project_dir%/tests/Functional/Fixtures/config/attributed_routing.yml' diff --git a/tests/Functional/Fixtures/config/attributed_routing.yml b/tests/Functional/Fixtures/config/attributed_routing.yml new file mode 100644 index 0000000..d2e71f1 --- /dev/null +++ b/tests/Functional/Fixtures/config/attributed_routing.yml @@ -0,0 +1,7 @@ +paysera_fixture_test: + resource: "@PayseraFixtureTestBundle/Resources/config/routing.xml" + prefix: / + +paysera_fixture_attributed_test: + resource: '@PayseraFixtureTestBundle/Resources/config/attributed_routing.xml' + prefix: / diff --git a/tests/Functional/Fixtures/config/common.yml b/tests/Functional/Fixtures/config/common.yml index 6ebbeab..93526c1 100644 --- a/tests/Functional/Fixtures/config/common.yml +++ b/tests/Functional/Fixtures/config/common.yml @@ -1,21 +1,9 @@ framework: - secret: 'secret' router: resource: '%kernel.project_dir%/tests/Functional/Fixtures/config/routing.yml' - validation: ~ security: enable_authenticator_manager: true - providers: - in_memory: - memory: - users: - user: - password: pass - roles: 'ROLE_USER' - admin: - password: pass - roles: ['ROLE_USER', 'ROLE_ADMIN'] firewalls: config: pattern: ^/(config)/ @@ -26,25 +14,3 @@ security: password_hashers: Symfony\Component\Security\Core\User\InMemoryUser: algorithm: plaintext - -doctrine: - orm: - auto_mapping: true - dbal: - driver: pdo_sqlite - memory: true - charset: UTF8 - -services: - logger: - class: Symfony\Component\ErrorHandler\BufferingLogger - public: true - rest_registry: - alias: paysera_api.rest_request_options_registry - public: true - -paysera_api: - path_attribute_resolvers: - Paysera\Bundle\ApiBundle\Tests\Functional\Fixtures\FixtureTestBundle\Entity\PersistedEntity: - field: someField - Paysera\Bundle\ApiBundle\Tests\Functional\Fixtures\FixtureTestBundle\Entity\SimplePersistedEntity: ~ diff --git a/tests/Functional/Fixtures/config/legacy_common.yml b/tests/Functional/Fixtures/config/legacy_common.yml index e684ada..8f0b54f 100644 --- a/tests/Functional/Fixtures/config/legacy_common.yml +++ b/tests/Functional/Fixtures/config/legacy_common.yml @@ -1,20 +1,8 @@ framework: - secret: 'secret' router: resource: '%kernel.root_dir%/config/legacy_routing.yml' - validation: ~ security: - providers: - in_memory: - memory: - users: - user: - password: pass - roles: 'ROLE_USER' - admin: - password: pass - roles: ['ROLE_USER', 'ROLE_ADMIN'] firewalls: main: anonymous: true @@ -22,25 +10,3 @@ security: stateless: true encoders: Symfony\Component\Security\Core\User\User: plaintext - -doctrine: - orm: - auto_mapping: true - dbal: - driver: pdo_sqlite - memory: true - charset: UTF8 - -services: - logger: - class: Symfony\Component\Debug\BufferingLogger - public: true - rest_registry: - alias: paysera_api.rest_request_options_registry - public: true - -paysera_api: - path_attribute_resolvers: - Paysera\Bundle\ApiBundle\Tests\Functional\Fixtures\FixtureTestBundle\Entity\PersistedEntity: - field: someField - Paysera\Bundle\ApiBundle\Tests\Functional\Fixtures\FixtureTestBundle\Entity\SimplePersistedEntity: ~ diff --git a/tests/Functional/Fixtures/config/services.yml b/tests/Functional/Fixtures/config/services.yml new file mode 100644 index 0000000..7b6689a --- /dev/null +++ b/tests/Functional/Fixtures/config/services.yml @@ -0,0 +1,37 @@ +framework: + secret: 'secret' + validation: ~ + +security: + providers: + in_memory: + memory: + users: + user: + password: pass + roles: 'ROLE_USER' + admin: + password: pass + roles: ['ROLE_USER', 'ROLE_ADMIN'] + +doctrine: + orm: + auto_mapping: true + dbal: + driver: pdo_sqlite + memory: true + charset: UTF8 + +services: + logger: + class: Psr\Log\NullLogger # Symfony\Component\ErrorHandler\BufferingLogger + public: true + rest_registry: + alias: paysera_api.rest_request_options_registry + public: true + +paysera_api: + path_attribute_resolvers: + Paysera\Bundle\ApiBundle\Tests\Functional\Fixtures\FixtureTestBundle\Entity\PersistedEntity: + field: someField + Paysera\Bundle\ApiBundle\Tests\Functional\Fixtures\FixtureTestBundle\Entity\SimplePersistedEntity: ~ diff --git a/tests/Functional/FunctionalAnnotationsTest.php b/tests/Functional/FunctionalAnnotationsTest.php index 36fa024..41b6f7f 100644 --- a/tests/Functional/FunctionalAnnotationsTest.php +++ b/tests/Functional/FunctionalAnnotationsTest.php @@ -3,6 +3,7 @@ namespace Paysera\Bundle\ApiBundle\Tests\Functional; +use Paysera\Bundle\ApiBundle\Tests\Functional\Fixtures\FixtureTestBundle\Service\TestHelper; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -18,15 +19,64 @@ protected function setUp(): void * @dataProvider restRequestsConfigurationProvider * @param Response $expectedResponse * @param Request $request + * @param Response|null $extraResponseVersion */ - public function testRestRequestConfiguration(Response $expectedResponse, Request $request) - { - $response = $this->handleRequest($request); - $this->assertEquals( - $expectedResponse->getContent(), - $response->getContent(), - 'expected correct content' + public function testAnnotatedRestRequestConfiguration( + Response $expectedResponse, + Request $request, + Response $extraResponseVersion = null + ) { + $this->makeTest('annotated', $expectedResponse, $request, $extraResponseVersion); + } + + /** + * @dataProvider restRequestsConfigurationProvider + * @param Response $expectedResponse + * @param Request $request + * @param Response|null $extraResponseVersion + */ + public function testAttributedRestRequestConfiguration( + Response $expectedResponse, + Request $request, + Response $extraResponseVersion = null + ) { + $this->makeTest('attributed', $expectedResponse, $request, $extraResponseVersion); + } + + private function makeTest( + string $pathPrefix, + Response $expectedResponse, + Request $request, + Response $extraResponseVersion = null + ): void { + if ($pathPrefix === 'attributed' && !TestHelper::phpAttributeSupportExists()) { + $this->markTestSkipped('Unsupported environment'); + } + + $request->server->set( + 'REQUEST_URI', + sprintf('/%s%s', $pathPrefix, $request->server->get('REQUEST_URI')) ); + $response = $this->handleRequest($request); + $assertionMessage = 'expected correct content'; + + if ($extraResponseVersion === null) { + $this->assertEquals( + $expectedResponse->getContent(), + $response->getContent(), + $assertionMessage + ); + } else { + $this->assertThat( + $response->getContent(), + $this->logicalOr( + $this->equalTo($expectedResponse->getContent()), + $this->equalTo($extraResponseVersion->getContent()) + ), + $assertionMessage + ); + } + $this->assertEquals( $expectedResponse->getStatusCode(), $response->getStatusCode(), @@ -34,14 +84,14 @@ public function testRestRequestConfiguration(Response $expectedResponse, Request ); } - public function restRequestsConfigurationProvider() + public function restRequestsConfigurationProvider(): array { return [ 'testBodyNormalizationWithExtractedKeyValue' => [ new Response('this_should_be_extracted'), $this->createJsonRequest( 'POST', - '/annotated/testBodyNormalizationWithExtractedKeyValue', + '/testBodyNormalizationWithExtractedKeyValue', [ 'something' => 'unimportant', 'key' => 'this_should_be_extracted', @@ -52,14 +102,14 @@ public function restRequestsConfigurationProvider() new Response('default'), $this->createRequest( 'POST', - '/annotated/testBodyNormalizationWithExtractedKeyValue' + '/testBodyNormalizationWithExtractedKeyValue' ), ], 'testBodyNormalizationWithDenormalizationGroup' => [ new Response('custom'), $this->createJsonRequest( 'POST', - '/annotated/testBodyNormalizationWithDenormalizationGroup', + '/testBodyNormalizationWithDenormalizationGroup', [ 'key_custom' => 'custom', 'key' => 'wrong_key', @@ -73,7 +123,7 @@ public function restRequestsConfigurationProvider() ), $this->createRequest( 'POST', - '/annotated/testBodyNormalizationWithExtractedKeyValue', + '/testBodyNormalizationWithExtractedKeyValue', 'something', ['Content-Type' => 'text/plain'] ), @@ -85,14 +135,14 @@ public function restRequestsConfigurationProvider() ), $this->createRequest( 'POST', - '/annotated/testBodyNormalizationWithRequiredBody' + '/testBodyNormalizationWithRequiredBody' ), ], 'testBodyAndResponseNormalization' => [ new Response('{"field1":"value1"}'), $this->createJsonRequest( 'POST', - '/annotated/testBodyAndResponseNormalization', + '/testBodyAndResponseNormalization', ['field1' => 'value1'] ), ], @@ -103,7 +153,7 @@ public function restRequestsConfigurationProvider() ), $this->createJsonRequest( 'POST', - '/annotated/testBodyAndResponseNormalization', + '/testBodyAndResponseNormalization', ['internal' => ['field1' => 1]] ), ], @@ -114,7 +164,7 @@ public function restRequestsConfigurationProvider() ), $this->createJsonRequest( 'POST', - '/annotated/testBodyNormalizationWithCustomContentType', + '/testBodyNormalizationWithCustomContentType', ['key' => 'value'] ), ], @@ -122,7 +172,7 @@ public function restRequestsConfigurationProvider() new Response('prefixed_my_text'), $this->createRequest( 'POST', - '/annotated/testBodyNormalizationWithCustomContentType', + '/testBodyNormalizationWithCustomContentType', 'my_text', ['Content-Type' => 'text/plain'] ), @@ -134,7 +184,7 @@ public function restRequestsConfigurationProvider() ), $this->createJsonRequest( 'POST', - '/annotated/testBodyNormalizationWithCustomContentTypeAndJsonDecode', + '/testBodyNormalizationWithCustomContentTypeAndJsonDecode', ['key' => 'value'] ), ], @@ -142,7 +192,7 @@ public function restRequestsConfigurationProvider() new Response('value'), $this->createRequest( 'POST', - '/annotated/testBodyNormalizationWithCustomContentTypeAndJsonDecode', + '/testBodyNormalizationWithCustomContentTypeAndJsonDecode', json_encode(['key' => 'value']), ['Content-Type' => 'text/plain'] ), @@ -151,7 +201,7 @@ public function restRequestsConfigurationProvider() new Response('prefixed_by_body'), $this->createRequest( 'POST', - '/annotated/testBodyNormalizationWithSemiContentTypeRestriction', + '/testBodyNormalizationWithSemiContentTypeRestriction', 'by_body', ['Content-Type' => 'text/something'] ), @@ -163,7 +213,7 @@ public function restRequestsConfigurationProvider() ), $this->createRequest( 'POST', - '/annotated/testBodyNormalizationWithSemiContentTypeRestriction', + '/testBodyNormalizationWithSemiContentTypeRestriction', 'by_body', ['Content-Type' => 'image/gif'] ), @@ -185,9 +235,23 @@ public function restRequestsConfigurationProvider() ), $this->createJsonRequest( 'POST', - '/annotated/testBodyNormalizationWithValidation', + '/testBodyNormalizationWithValidation', ['field1' => 'not an email'] ), + new Response( + json_encode([ + 'error' => 'invalid_parameters', + 'error_description' => 'Some required parameter is missing or it\'s format is invalid', + 'errors' => [ + [ + 'code' => 'invalid_format', + 'message' => 'This value is not a valid email address.', + 'field' => 'my_mapped_key', + ], + ], + ]), + 400 + ), ], 'testBodyNormalizationWithInnerTypeValidation - should convert to snake case' => [ new Response( @@ -206,15 +270,29 @@ public function restRequestsConfigurationProvider() ), $this->createJsonRequest( 'POST', - '/annotated/testBodyNormalizationWithInnerTypeValidation', + '/testBodyNormalizationWithInnerTypeValidation', ['field1' => 'blah', 'internal' => ['field1' => 'not an email']] ), + new Response( + json_encode([ + 'error' => 'invalid_parameters', + 'error_description' => 'Some required parameter is missing or it\'s format is invalid', + 'errors' => [ + [ + 'code' => 'invalid_format', + 'message' => 'Custom message', + 'field' => 'internal_field1', // <-- this is converted from internalField1 + ], + ], + ]), + 400 + ), ], 'testBodyValidationCanBeTurnedOff' => [ new Response('OK', 200), $this->createJsonRequest( 'POST', - '/annotated/testBodyValidationCanBeTurnedOff', + '/testBodyValidationCanBeTurnedOff', ['field1' => ''] ), ], @@ -222,7 +300,7 @@ public function restRequestsConfigurationProvider() new Response('OK', 200), $this->createJsonRequest( 'POST', - '/annotated/testBodyValidationCanBeTurnedOffWithEmptyGroups', + '/testBodyValidationCanBeTurnedOffWithEmptyGroups', ['field1' => ''] ), ], @@ -230,35 +308,35 @@ public function restRequestsConfigurationProvider() new Response('prefixed_123'), $this->createRequest( 'GET', - '/annotated/testPathAttribute/123' + '/testPathAttribute/123' ), ], 'testPathAttribute with optional resolution' => [ new Response('default'), $this->createRequest( 'GET', - '/annotated/testPathAttribute' + '/testPathAttribute' ), ], 'testPathAttributeWithFindingObject' => [ new Response('123'), $this->createRequest( 'GET', - '/annotated/testPathAttributeWithFindingObject/123' + '/testPathAttributeWithFindingObject/123' ), ], 'testPathAttributeWithFailedResolution' => [ new Response('{"error":"not_found","error_description":"Resource was not found"}', 404), $this->createRequest( 'GET', - '/annotated/testPathAttributeWithFailedResolution/{id}' + '/testPathAttributeWithFailedResolution/{id}' ), ], 'testQueryResolver' => [ new Response('my_param'), $this->createRequest( 'GET', - '/annotated/testQueryResolver?parameter=my_param' + '/testQueryResolver?parameter=my_param' ), ], 'testQueryResolver is always mandatory' => [ @@ -268,21 +346,21 @@ public function restRequestsConfigurationProvider() ), $this->createRequest( 'GET', - '/annotated/testQueryResolver?other_parameter=my_param' + '/testQueryResolver?other_parameter=my_param' ), ], 'testQueryResolverWithDenormalizationGroup' => [ new Response('custom'), $this->createRequest( 'GET', - '/annotated/testQueryResolverWithDenormalizationGroup?parameter=wrong_key¶meter_custom=custom' + '/testQueryResolverWithDenormalizationGroup?parameter=wrong_key¶meter_custom=custom' ), ], 'testQueryResolverPagerLimitIs42' => [ new Response('OK'), $this->createRequest( 'GET', - '/annotated/testQueryResolverPagerLimitIs42?limit=42' + '/testQueryResolverPagerLimitIs42?limit=42' ), ], 'testQueryResolverHasDefaultValidation' => [ @@ -292,7 +370,7 @@ public function restRequestsConfigurationProvider() ), $this->createRequest( 'GET', - '/annotated/testQueryResolverHasDefaultValidation?field1=' + '/testQueryResolverHasDefaultValidation?field1=' ), ], 'testQueryResolverCanTurnOffValidation' => [ @@ -302,7 +380,7 @@ public function restRequestsConfigurationProvider() ), $this->createRequest( 'GET', - '/annotated/testQueryResolverCanTurnOffValidation?field1=' + '/testQueryResolverCanTurnOffValidation?field1=' ), ], 'testQueryResolverCanTurnOffValidationWithEmptyGroups' => [ @@ -312,7 +390,7 @@ public function restRequestsConfigurationProvider() ), $this->createRequest( 'GET', - '/annotated/testQueryResolverCanTurnOffValidationWithEmptyGroups?field1=' + '/testQueryResolverCanTurnOffValidationWithEmptyGroups?field1=' ), ], 'testQueryResolverValidationWithInvalidData - normalizer errors do not map fields' => [ @@ -322,7 +400,7 @@ public function restRequestsConfigurationProvider() ), $this->createRequest( 'GET', - '/annotated/testQueryResolverValidationWithInvalidData' + '/testQueryResolverValidationWithInvalidData' ), ], 'testQueryResolverValidationWithInvalidData' => [ @@ -342,7 +420,21 @@ public function restRequestsConfigurationProvider() ), $this->createRequest( 'GET', - '/annotated/testQueryResolverValidationWithInvalidData?field1=not_an_email' + '/testQueryResolverValidationWithInvalidData?field1=not_an_email' + ), + new Response( + json_encode([ + 'error' => 'invalid_parameters', + 'error_description' => 'Some required parameter is missing or it\'s format is invalid', + 'errors' => [ + [ + 'code' => 'invalid_format', + 'message' => 'This value is not a valid email address.', + 'field' => 'mapped_key', + ], + ], + ]), + 400 ), ], 'testRequiredPermissions without auth' => [ @@ -352,7 +444,7 @@ public function restRequestsConfigurationProvider() ), $this->createRequest( 'GET', - '/annotated/testRequiredPermissions' + '/testRequiredPermissions' ), ], 'testRequiredPermissions without not enough permissions' => [ @@ -362,7 +454,7 @@ public function restRequestsConfigurationProvider() ), $this->createRequest( 'GET', - '/annotated/testRequiredPermissions', + '/testRequiredPermissions', null, [], 'user' @@ -372,7 +464,7 @@ public function restRequestsConfigurationProvider() new Response('OK'), $this->createRequest( 'GET', - '/annotated/testRequiredPermissions', + '/testRequiredPermissions', null, [], 'admin' @@ -385,7 +477,7 @@ public function restRequestsConfigurationProvider() ), $this->createRequest( 'GET', - '/annotated/class/testRequiredPermissions' + '/class/testRequiredPermissions' ), ], 'testRequiredPermissions with class annotation and with not enough permissions' => [ @@ -395,7 +487,7 @@ public function restRequestsConfigurationProvider() ), $this->createRequest( 'GET', - '/annotated/class/testRequiredPermissions', + '/class/testRequiredPermissions', null, [], 'user' @@ -405,7 +497,7 @@ public function restRequestsConfigurationProvider() new Response('OK'), $this->createRequest( 'GET', - '/annotated/class/testRequiredPermissions', + '/class/testRequiredPermissions', null, [], 'admin' @@ -418,14 +510,14 @@ public function restRequestsConfigurationProvider() ), $this->createRequest( 'GET', - '/annotated/class/simpleAction' + '/class/simpleAction' ), ], 'testRequiredPermissions with class annotation and REST-specific method annotations: OK' => [ new Response('OK'), $this->createRequest( 'GET', - '/annotated/class/simpleAction', + '/class/simpleAction', null, [], 'user' @@ -448,9 +540,23 @@ public function restRequestsConfigurationProvider() ), $this->createJsonRequest( 'POST', - '/annotated/class/testValidation', + '/class/testValidation', ['field1' => 'not an email', 'internal' => ['field1' => 'also not an email']] ), + new Response( + json_encode([ + 'error' => 'invalid_parameters', + 'error_description' => 'Some required parameter is missing or it\'s format is invalid', + 'errors' => [ + [ + 'code' => 'invalid_format', + 'message' => 'This value is not a valid email address.', + 'field' => 'my_mapped_key', + ], + ], + ]), + 400 + ), ], 'testValidation with class annotation' => [ new Response( @@ -469,29 +575,43 @@ public function restRequestsConfigurationProvider() ), $this->createJsonRequest( 'POST', - '/annotated/class/testValidationFromClass', + '/class/testValidationFromClass', ['field1' => 'not an email', 'internal' => ['field1' => 'also not an email']] ), + new Response( + json_encode([ + 'error' => 'invalid_parameters', + 'error_description' => 'Some required parameter is missing or it\'s format is invalid', + 'errors' => [ + [ + 'code' => 'invalid_format', + 'message' => 'Custom message', + 'field' => 'internal.field1', + ], + ], + ]), + 400 + ), ], 'testResponseNormalization' => [ new Response('{"field1_custom":"hi"}'), $this->createRequest( 'GET', - '/annotated/testResponseNormalization' + '/testResponseNormalization' ), ], 'testResponseNormalizationWithNormalizationGroup' => [ new Response('{"field1_custom":"hi"}'), $this->createRequest( 'GET', - '/annotated/testResponseNormalizationWithNormalizationGroup' + '/testResponseNormalizationWithNormalizationGroup' ), ], 'testResponseNormalizationWithGuessedNormalizer' => [ new Response('{"field1":"hi"}'), $this->createRequest( 'GET', - '/annotated/testResponseNormalizationWithGuessedNormalizer' + '/testResponseNormalizationWithGuessedNormalizer' ), ], ]; diff --git a/tests/Functional/FunctionalTestCase.php b/tests/Functional/FunctionalTestCase.php index 199f425..0a1f80e 100644 --- a/tests/Functional/FunctionalTestCase.php +++ b/tests/Functional/FunctionalTestCase.php @@ -7,6 +7,7 @@ use Doctrine\ORM\Tools\SchemaTool; use Paysera\Bundle\ApiBundle\Tests\Functional\Fixtures\TestKernel; use PHPUnit\Framework\TestCase; +use Symfony\Bundle\FrameworkBundle\Routing\AttributeRouteControllerLoader; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\ResettableContainerInterface; use Symfony\Component\HttpKernel\Kernel; diff --git a/tests/Unit/Service/Annotation/ReflectionMethodWrapperTest.php b/tests/Unit/Service/RoutingLoader/ReflectionMethodWrapperTest.php similarity index 79% rename from tests/Unit/Service/Annotation/ReflectionMethodWrapperTest.php rename to tests/Unit/Service/RoutingLoader/ReflectionMethodWrapperTest.php index a271b98..030e782 100644 --- a/tests/Unit/Service/Annotation/ReflectionMethodWrapperTest.php +++ b/tests/Unit/Service/RoutingLoader/ReflectionMethodWrapperTest.php @@ -1,24 +1,24 @@ getParameterByName('param2'); $this->assertSame('param2', $reflectionParameter->getName()); } - public function testGetParameterByNameWithNoSuchParameter() + public function testGetParameterByNameWithNoSuchParameter(): void { $wrapper = new ReflectionMethodWrapper(new ReflectionMethod(self::class, 'fixtureMethod')); @@ -26,14 +26,14 @@ public function testGetParameterByNameWithNoSuchParameter() $wrapper->getParameterByName('nonExisting'); } - public function testGetNonBuiltInTypeForParameter() + public function testGetNonBuiltInTypeForParameter(): void { $wrapper = new ReflectionMethodWrapper(new ReflectionMethod(self::class, 'fixtureMethod')); $this->assertSame('DateTime', $wrapper->getNonBuiltInTypeForParameter('param2')); } - public function testGetNonBuiltInTypeForParameterWithBuiltInType() + public function testGetNonBuiltInTypeForParameterWithBuiltInType(): void { $wrapper = new ReflectionMethodWrapper(new ReflectionMethod(self::class, 'fixtureMethod')); @@ -41,7 +41,7 @@ public function testGetNonBuiltInTypeForParameterWithBuiltInType() $wrapper->getNonBuiltInTypeForParameter('param1'); } - public function testGetNonBuiltInTypeForParameterWithNoType() + public function testGetNonBuiltInTypeForParameterWithNoType(): void { $wrapper = new ReflectionMethodWrapper(new ReflectionMethod(self::class, 'fixtureMethod')); @@ -49,11 +49,11 @@ public function testGetNonBuiltInTypeForParameterWithNoType() $wrapper->getNonBuiltInTypeForParameter('param3'); } - public function testGetFriendlyName() + public function testGetFriendlyName(): void { $wrapper = new ReflectionMethodWrapper(new ReflectionMethod(self::class, 'fixtureMethod')); $this->assertSame( - 'Paysera\Bundle\ApiBundle\Tests\Unit\Service\Annotation\ReflectionMethodWrapperTest::fixtureMethod', + sprintf('%s::%s', get_class($this), 'fixtureMethod'), $wrapper->getFriendlyName() ); } diff --git a/tests/Unit/Service/Annotation/RestRequestOptionsBuilderTest.php b/tests/Unit/Service/RoutingLoader/RestRequestAnnotationOptionsBuilderTest.php similarity index 78% rename from tests/Unit/Service/Annotation/RestRequestOptionsBuilderTest.php rename to tests/Unit/Service/RoutingLoader/RestRequestAnnotationOptionsBuilderTest.php index d5b93f8..a36d451 100644 --- a/tests/Unit/Service/Annotation/RestRequestOptionsBuilderTest.php +++ b/tests/Unit/Service/RoutingLoader/RestRequestAnnotationOptionsBuilderTest.php @@ -1,7 +1,7 @@ shouldReceive('validateRestRequestOptions') - ->andReturnUsing(function (RestRequestOptions $options, string $fieldlyName) use ($expectedOptions) { + ->andReturnUsing(function (RestRequestOptions $options, string $friendlyName) use ($expectedOptions) { $this->assertEquals($expectedOptions, $options); $this->assertEquals( - 'Paysera\Bundle\ApiBundle\Tests\Unit\Service\Annotation\RestRequestOptionsBuilderTest::fixtureMethod', - $fieldlyName + sprintf('%s::%s', get_class($this), 'fixtureMethod'), + $friendlyName ); }) ; @@ -61,11 +61,11 @@ public function testBuildOptions() $this->assertEquals($expectedOptions, $options); } - public function testBuildOptionsWithSeveralUnsupportedAnnotations() + public function testBuildOptionsWithSeveralUnsupportedAnnotations(): void { $optionsValidator = Mockery::mock(RestRequestOptionsValidator::class); - $builder = new RestRequestOptionsBuilder($optionsValidator); + $builder = new RestRequestAnnotationOptionsBuilder($optionsValidator); $this->expectException(ConfigurationException::class); @@ -75,7 +75,7 @@ public function testBuildOptionsWithSeveralUnsupportedAnnotations() ], new ReflectionMethod(self::class, 'fixtureMethod')); } - public function fixtureMethod() + public function fixtureMethod(): void { // do nothing } diff --git a/tests/Unit/Service/RoutingLoader/RestRequestAttributeOptionsBuilderTest.php b/tests/Unit/Service/RoutingLoader/RestRequestAttributeOptionsBuilderTest.php new file mode 100644 index 0000000..4e74811 --- /dev/null +++ b/tests/Unit/Service/RoutingLoader/RestRequestAttributeOptionsBuilderTest.php @@ -0,0 +1,68 @@ +shouldReceive('apply')->andReturnUsing(function (RestRequestOptions $options) { + $options->setRequiredPermissions(['modified1']); + }); + + $annotationMock2 = Mockery::mock(RestAttributeInterface::class); + $annotationMock2->shouldReceive('apply')->andReturnUsing(function (RestRequestOptions $options) { + $options->setResponseNormalizationType('modified2'); + }); + + $expectedOptions = (new RestRequestOptions()) + ->setRequiredPermissions(['modified1']) + ->setResponseNormalizationType('modified2') + ; + + $optionsValidator + ->shouldReceive('validateRestRequestOptions') + ->andReturnUsing(function (RestRequestOptions $options, string $friendlyName) use ($expectedOptions) { + $this->assertEquals($expectedOptions, $options); + $this->assertEquals( + sprintf('%s::%s', get_class($this), 'fixtureMethod'), + $friendlyName + ); + }) + ; + + $annotations = [ + $annotationMock1, + $annotationMock2, + ]; + + $options = $builder->buildOptions($annotations, $reflectionMethod); + + $this->assertEquals($expectedOptions, $options); + } + + public function fixtureMethod(): void + { + // do nothing + } +}