From ca403c72af08e8567523e6ff5258965dff44a085 Mon Sep 17 00:00:00 2001 From: Jack Bentley Date: Tue, 21 May 2024 13:25:19 +0100 Subject: [PATCH 1/4] Add support for attributes --- Controller/Annotations/NamePrefix.php | 1 + Controller/Annotations/NoRoute.php | 107 +++++++++++++++++- Controller/Annotations/Prefix.php | 1 + Controller/Annotations/RouteResource.php | 1 + Controller/Annotations/Version.php | 1 + Routing/Loader/Reader/RestActionReader.php | 71 +++++++++--- .../Loader/Reader/RestControllerReader.php | 34 ++++-- 7 files changed, 188 insertions(+), 28 deletions(-) diff --git a/Controller/Annotations/NamePrefix.php b/Controller/Annotations/NamePrefix.php index 115c43d..7d8abbf 100644 --- a/Controller/Annotations/NamePrefix.php +++ b/Controller/Annotations/NamePrefix.php @@ -21,6 +21,7 @@ * @Annotation * @Target("CLASS") */ +#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_CLASS)] class NamePrefix extends Annotation { } diff --git a/Controller/Annotations/NoRoute.php b/Controller/Annotations/NoRoute.php index 7104431..27d9ae1 100644 --- a/Controller/Annotations/NoRoute.php +++ b/Controller/Annotations/NoRoute.php @@ -13,7 +13,28 @@ namespace HandcraftedInTheAlps\RestRoutingBundle\Controller\Annotations; use FOS\RestBundle\Controller\Annotations\NoRoute as OldNoRoute; -use Symfony\Component\Routing\Annotation\Route as BaseRoute; +use Symfony\Component\Routing\Annotation\Route as BaseAnnotationRoute; +use Symfony\Component\Routing\Attribute\Route as BaseAttributeRoute; + +if (class_exists(BaseAttributeRoute::class)) { + /** + * Compatibility layer for Symfony 6.4 and later. + * + * @internal + */ + class CompatRoute extends BaseAttributeRoute + { + } +} else { + /** + * Compatibility layer for Symfony 6.3 and earlier. + * + * @internal + */ + class CompatRoute extends BaseAnnotationRoute + { + } +} /** * No Route annotation class. @@ -21,11 +42,87 @@ * @Annotation * @Target({"METHOD","CLASS"}) */ -class NoRoute extends BaseRoute +#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)] +class NoRoute extends CompatRoute { - public function __construct(array $data) - { - parent::__construct($data); + /** + * @param array|string $data + * @param array|string|null $path + * @param array $requirements + * @param string[]|string $methods + * @param string[]|string $schemes + * + * @throws \TypeError if the $data argument is an unsupported type + */ + public function __construct( + $data = [], + $path = null, + string $name = null, + array $requirements = [], + array $options = [], + array $defaults = [], + string $host = null, + $methods = [], + $schemes = [], + string $condition = null, + int $priority = null, + string $locale = null, + string $format = null, + bool $utf8 = null, + bool $stateless = null, + string $env = null + ) { + // Use Reflection to get the constructor from the parent class two levels up (accounting for our compat definition) + $method = (new \ReflectionClass($this))->getParentClass()->getParentClass()->getMethod('__construct'); + + // The $data constructor parameter was removed in Symfony 6.0 in favor of named arguments + if ('data' === $method->getParameters()[0]->getName()) { + parent::__construct( + $data, + $path, + $name, + $requirements, + $options, + $defaults, + $host, + $methods, + $schemes, + $condition, + $priority, + $locale, + $format, + $utf8, + $stateless, + $env + ); + } else { + if (\is_string($data)) { + $data = ['path' => $data]; + } elseif (!\is_array($data)) { + throw new \TypeError(sprintf('"%s": Argument $data is expected to be a string or array, got "%s".', __METHOD__, get_debug_type($data))); + } elseif (0 !== \count($data) && [] === array_intersect(array_keys($data), ['path', 'name', 'requirements', 'options', 'defaults', 'host', 'methods', 'schemes', 'condition', 'priority', 'locale', 'format', 'utf8', 'stateless', 'env'])) { + $localizedPaths = $data; + $data = ['path' => $localizedPaths]; + } + + parent::__construct( + $data['path'] ?? $path, + $data['name'] ?? $name, + $data['requirements'] ?? $requirements, + $data['options'] ?? $options, + $data['defaults'] ?? $defaults, + $data['host'] ?? $host, + $data['methods'] ?? $methods, + $data['schemes'] ?? $schemes, + $data['condition'] ?? $condition, + $data['priority'] ?? $priority, + $data['locale'] ?? $locale, + $data['format'] ?? $format, + $data['utf8'] ?? $utf8, + $data['stateless'] ?? $stateless, + $data['env'] ?? $env + ); + } if (!$this->getMethods()) { $this->setMethods((array) $this->getMethod()); diff --git a/Controller/Annotations/Prefix.php b/Controller/Annotations/Prefix.php index e23c358..d4731b0 100644 --- a/Controller/Annotations/Prefix.php +++ b/Controller/Annotations/Prefix.php @@ -21,6 +21,7 @@ * @Annotation * @Target("CLASS") */ +#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_CLASS)] class Prefix extends Annotation { } diff --git a/Controller/Annotations/RouteResource.php b/Controller/Annotations/RouteResource.php index d07064b..1e4398b 100644 --- a/Controller/Annotations/RouteResource.php +++ b/Controller/Annotations/RouteResource.php @@ -20,6 +20,7 @@ * @Annotation * @Target("CLASS") */ +#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_CLASS)] class RouteResource { /** diff --git a/Controller/Annotations/Version.php b/Controller/Annotations/Version.php index 0709aa8..2a6dd49 100644 --- a/Controller/Annotations/Version.php +++ b/Controller/Annotations/Version.php @@ -21,6 +21,7 @@ * @Annotation * @Target("CLASS") */ +#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_CLASS)] class Version extends Annotation { } diff --git a/Routing/Loader/Reader/RestActionReader.php b/Routing/Loader/Reader/RestActionReader.php index 0d7bfcf..3afde14 100644 --- a/Routing/Loader/Reader/RestActionReader.php +++ b/Routing/Loader/Reader/RestActionReader.php @@ -433,12 +433,22 @@ private function getMethodArguments(\ReflectionMethod $method): array // check if a parameter is coming from the request body $ignoreParameters = []; if (class_exists(ParamConverter::class)) { - $ignoreParameters = array_map(function ($annotation) { - return - $annotation instanceof ParamConverter && - 'fos_rest.request_body' === $annotation->getConverter() - ? $annotation->getName() : null; - }, $this->annotationReader->getMethodAnnotations($method)); + $ignoreParameters = array_merge( + array_map(function ($annotation) { + return + $annotation instanceof ParamConverter && + 'fos_rest.request_body' === $annotation->getConverter() + ? $annotation->getName() : null; + }, $this->annotationReader->getMethodAnnotations($method)), + \PHP_VERSION_ID > 80000 ? array_map(function (ParamConverter $reflectionAttribute) { + $attribute = $reflectionAttribute->newInstance(); + + return + $attribute instanceof ParamConverter && + 'fos_rest.request_body' === $attribute->getConverter() + ? $attribute->getName() : null; + }, $method->getAttributes(ParamConverter::class)) : [], + ); } // ignore several type hinted arguments @@ -586,15 +596,26 @@ private function readRouteAnnotation(\ReflectionMethod $reflectionMethod): array private function readClassAnnotation(\ReflectionClass $reflectionClass, string $annotationName): ?RouteAnnotation { $annotationClass = "HandcraftedInTheAlps\\RestRoutingBundle\\Controller\\Annotations\\$annotationName"; + $oldAnnotationClass = "FOS\\RestBundle\\Controller\\Annotations\\$annotationName"; + + if (class_exists($annotationClass)) { + if (\PHP_VERSION_ID > 80000 && $attribute = $reflectionClass->getAttributes($annotationClass, \ReflectionAttribute::IS_INSTANCEOF)) { + return $attribute[0]->newInstance(); + } - if ($annotation = $this->annotationReader->getClassAnnotation($reflectionClass, $annotationClass)) { - return $annotation; + if ($annotation = $this->annotationReader->getClassAnnotation($reflectionClass, $annotationClass)) { + return $annotation; + } } - $oldAnnotationClass = "FOS\\RestBundle\\Controller\\Annotations\\$annotationName"; + if (class_exists($oldAnnotationClass)) { + if (\PHP_VERSION_ID > 80000 && $oldAttribute = $reflectionClass->getAttributes($oldAnnotationClass, \ReflectionAttribute::IS_INSTANCEOF)) { + return $oldAttribute[0]->newInstance(); + } - if ($oldAnnotation = $this->annotationReader->getClassAnnotation($reflectionClass, $oldAnnotationClass)) { - return $oldAnnotation; + if ($oldAnnotation = $this->annotationReader->getClassAnnotation($reflectionClass, $oldAnnotationClass)) { + return $oldAnnotation; + } } return null; @@ -606,15 +627,23 @@ private function readMethodAnnotation(\ReflectionMethod $reflectionMethod, strin $oldAnnotationClass = "FOS\\RestBundle\\Controller\\Annotations\\$annotationName"; if (class_exists($annotationClass)) { + if (\PHP_VERSION_ID > 80000 && $attribute = $reflectionMethod->getAttributes($annotationClass, \ReflectionAttribute::IS_INSTANCEOF)) { + return $attribute[0]->newInstance(); + } + if ($annotation = $this->annotationReader->getMethodAnnotation($reflectionMethod, $annotationClass)) { return $annotation; } } - if (class_exists($oldAnnotationClass) - && $oldAnnotation = $this->annotationReader->getMethodAnnotation($reflectionMethod, $oldAnnotationClass) - ) { - return $oldAnnotation; + if (class_exists($oldAnnotationClass)) { + if (\PHP_VERSION_ID > 80000 && $oldAttribute = $reflectionMethod->getAttributes($oldAnnotationClass, \ReflectionAttribute::IS_INSTANCEOF)) { + return $oldAttribute[0]->newInstance(); + } + + if ($oldAnnotation = $this->annotationReader->getMethodAnnotation($reflectionMethod, $oldAnnotationClass)) { + return $oldAnnotation; + } } return null; @@ -637,6 +666,18 @@ private function readMethodAnnotations(\ReflectionMethod $reflectionMethod, stri } } + if (\PHP_VERSION_ID > 80000) { + /** @var \ReflectionAttribute[] $reflectionAttributes */ + $reflectionAttributes = [ + ...(class_exists($annotationClass) ? $reflectionMethod->getAttributes($annotationClass, \ReflectionAttribute::IS_INSTANCEOF) : []), + ...(class_exists($oldAnnotationClass) ? $reflectionMethod->getAttributes($oldAnnotationClass, \ReflectionAttribute::IS_INSTANCEOF) : []), + ]; + + foreach ($reflectionAttributes as $reflectionAttribute) { + $annotations[] = $reflectionAttribute->newInstance(); + } + } + return $annotations; } diff --git a/Routing/Loader/Reader/RestControllerReader.php b/Routing/Loader/Reader/RestControllerReader.php index cf6e6ec..e4c84b5 100644 --- a/Routing/Loader/Reader/RestControllerReader.php +++ b/Routing/Loader/Reader/RestControllerReader.php @@ -49,38 +49,56 @@ public function getActionReader(): RestActionReader return $this->actionReader; } + /** + * @param \ReflectionClass $reflectionClass the ReflectionClass of the class from which + * the class annotations should be read + * @param class-string $annotationName the name of the annotation + * + * @return T|null the Annotation or NULL, if the requested annotation does not exist + * + * @template T + */ + private function readClassAnnotation(\ReflectionClass $reflectionClass, string $annotationName): ?object + { + if (\PHP_VERSION_ID > 80000 && $attributes = $reflectionClass->getAttributes($annotationName, \ReflectionAttribute::IS_INSTANCEOF)) { + return $attributes[0]->newInstance(); + } + + return $this->annotationReader->getClassAnnotation($reflectionClass, Prefix::class); + } + public function read(\ReflectionClass $reflectionClass): RestRouteCollection { $collection = new RestRouteCollection(); $collection->addResource(new FileResource($reflectionClass->getFileName())); // read prefix annotation - if ($annotation = $this->annotationReader->getClassAnnotation($reflectionClass, Prefix::class)) { + if ($annotation = $this->readClassAnnotation($reflectionClass, Prefix::class)) { $this->actionReader->setRoutePrefix($annotation->value); - } elseif ($annotation = $this->annotationReader->getClassAnnotation($reflectionClass, OldPrefix::class)) { + } elseif ($annotation = $this->readClassAnnotation($reflectionClass, OldPrefix::class)) { $this->actionReader->setRoutePrefix($annotation->value); } // read name-prefix annotation - if ($annotation = $this->annotationReader->getClassAnnotation($reflectionClass, NamePrefix::class)) { + if ($annotation = $this->readClassAnnotation($reflectionClass, NamePrefix::class)) { $this->actionReader->setNamePrefix($annotation->value); - } elseif ($annotation = $this->annotationReader->getClassAnnotation($reflectionClass, OldNamePrefix::class)) { + } elseif ($annotation = $this->readClassAnnotation($reflectionClass, OldNamePrefix::class)) { $this->actionReader->setNamePrefix($annotation->value); } // read version annotation - if ($annotation = $this->annotationReader->getClassAnnotation($reflectionClass, Version::class)) { + if ($annotation = $this->readClassAnnotation($reflectionClass, Version::class)) { $this->actionReader->setVersions($annotation->value); - } elseif ($annotation = $this->annotationReader->getClassAnnotation($reflectionClass, OldVersion::class)) { + } elseif ($annotation = $this->readClassAnnotation($reflectionClass, OldVersion::class)) { $this->actionReader->setVersions($annotation->value); } $resource = []; // read route-resource annotation - if ($annotation = $this->annotationReader->getClassAnnotation($reflectionClass, RouteResource::class)) { + if ($annotation = $this->readClassAnnotation($reflectionClass, RouteResource::class)) { $resource = explode('_', $annotation->resource); $this->actionReader->setPluralize($annotation->pluralize); - } elseif ($annotation = $this->annotationReader->getClassAnnotation($reflectionClass, OldRouteResource::class)) { + } elseif ($annotation = $this->readClassAnnotation($reflectionClass, OldRouteResource::class)) { $resource = explode('_', $annotation->resource); $this->actionReader->setPluralize($annotation->pluralize); } elseif ($reflectionClass->implementsInterface(ClassResourceInterface::class) From 2a41b716353c525dbce32dece90db037382cade8 Mon Sep 17 00:00:00 2001 From: Jack Bentley Date: Tue, 21 May 2024 13:45:36 +0100 Subject: [PATCH 2/4] Fix tests --- Routing/Loader/Reader/RestControllerReader.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Routing/Loader/Reader/RestControllerReader.php b/Routing/Loader/Reader/RestControllerReader.php index e4c84b5..84290c6 100644 --- a/Routing/Loader/Reader/RestControllerReader.php +++ b/Routing/Loader/Reader/RestControllerReader.php @@ -64,7 +64,7 @@ private function readClassAnnotation(\ReflectionClass $reflectionClass, string $ return $attributes[0]->newInstance(); } - return $this->annotationReader->getClassAnnotation($reflectionClass, Prefix::class); + return $this->annotationReader->getClassAnnotation($reflectionClass, $annotationName); } public function read(\ReflectionClass $reflectionClass): RestRouteCollection From 5e23a7e2ea81b71f8b362cd5c81d7f7a5b9a4b8e Mon Sep 17 00:00:00 2001 From: Jack Bentley Date: Tue, 21 May 2024 13:47:35 +0100 Subject: [PATCH 3/4] Fix tests --- Routing/Loader/Reader/RestActionReader.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Routing/Loader/Reader/RestActionReader.php b/Routing/Loader/Reader/RestActionReader.php index 3afde14..d938657 100644 --- a/Routing/Loader/Reader/RestActionReader.php +++ b/Routing/Loader/Reader/RestActionReader.php @@ -447,7 +447,7 @@ private function getMethodArguments(\ReflectionMethod $method): array $attribute instanceof ParamConverter && 'fos_rest.request_body' === $attribute->getConverter() ? $attribute->getName() : null; - }, $method->getAttributes(ParamConverter::class)) : [], + }, $method->getAttributes(ParamConverter::class)) : [] ); } From bde60e3f4dfefe1161b5eacf3d5b9a2cf80f5a9b Mon Sep 17 00:00:00 2001 From: Jack Bentley Date: Tue, 21 May 2024 13:51:11 +0100 Subject: [PATCH 4/4] Fix tests --- Routing/Loader/Reader/RestActionReader.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Routing/Loader/Reader/RestActionReader.php b/Routing/Loader/Reader/RestActionReader.php index d938657..0699a88 100644 --- a/Routing/Loader/Reader/RestActionReader.php +++ b/Routing/Loader/Reader/RestActionReader.php @@ -668,10 +668,10 @@ private function readMethodAnnotations(\ReflectionMethod $reflectionMethod, stri if (\PHP_VERSION_ID > 80000) { /** @var \ReflectionAttribute[] $reflectionAttributes */ - $reflectionAttributes = [ - ...(class_exists($annotationClass) ? $reflectionMethod->getAttributes($annotationClass, \ReflectionAttribute::IS_INSTANCEOF) : []), - ...(class_exists($oldAnnotationClass) ? $reflectionMethod->getAttributes($oldAnnotationClass, \ReflectionAttribute::IS_INSTANCEOF) : []), - ]; + $reflectionAttributes = array_merge( + class_exists($annotationClass) ? $reflectionMethod->getAttributes($annotationClass, \ReflectionAttribute::IS_INSTANCEOF) : [], + class_exists($oldAnnotationClass) ? $reflectionMethod->getAttributes($oldAnnotationClass, \ReflectionAttribute::IS_INSTANCEOF) : [] + ); foreach ($reflectionAttributes as $reflectionAttribute) { $annotations[] = $reflectionAttribute->newInstance();