diff --git a/Controller/Annotations/FileParam.php b/Controller/Annotations/FileParam.php index afae0673c..33b7fe8c2 100644 --- a/Controller/Annotations/FileParam.php +++ b/Controller/Annotations/FileParam.php @@ -21,10 +21,12 @@ * Represents a file that must be present. * * @Annotation + * @NamedArgumentConstructor * @Target("METHOD") * * @author Ener-Getick */ +#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_METHOD)] class FileParam extends AbstractParam { /** @var bool */ @@ -39,6 +41,32 @@ class FileParam extends AbstractParam /** @var bool */ public $map = false; + /** + * @param mixed $requirements + * @param mixed $default + */ + public function __construct( + string $name = '', + bool $strict = true, + $requirements = null, + bool $image = false, + bool $map = false, + ?string $key = null, + $default = null, + string $description = '', + bool $nullable = false + ) { + $this->strict = $strict; + $this->requirements = $requirements; + $this->image = $image; + $this->map = $map; + $this->name = $name; + $this->key = $key; + $this->default = $default; + $this->description = $description; + $this->nullable = $nullable; + } + /** * {@inheritdoc} */ diff --git a/Controller/Annotations/QueryParam.php b/Controller/Annotations/QueryParam.php index e0d34ed3f..ba272eac7 100644 --- a/Controller/Annotations/QueryParam.php +++ b/Controller/Annotations/QueryParam.php @@ -17,12 +17,42 @@ * Represents a parameter that must be present in GET data. * * @Annotation + * @NamedArgumentConstructor * @Target({"CLASS", "METHOD"}) * * @author Alexander */ +#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)] class QueryParam extends AbstractScalarParam { + /** + * @param mixed $requirements + * @param mixed $default + */ + public function __construct( + string $name = '', + ?string $key = null, + $requirements = null, + $default = null, + array $incompatibles = [], + string $description = '', + bool $strict = false, + bool $map = false, + bool $nullable = false, + bool $allowBlank = true + ) { + $this->name = $name; + $this->key = $key; + $this->requirements = $requirements; + $this->default = $default; + $this->incompatibles = $incompatibles; + $this->description = $description; + $this->strict = $strict; + $this->map = $map; + $this->nullable = $nullable; + $this->allowBlank = $allowBlank; + } + /** * {@inheritdoc} */ diff --git a/Controller/Annotations/RequestParam.php b/Controller/Annotations/RequestParam.php index 9c89a8646..da8b9a9ef 100644 --- a/Controller/Annotations/RequestParam.php +++ b/Controller/Annotations/RequestParam.php @@ -17,16 +17,46 @@ * Represents a parameter that must be present in POST data. * * @Annotation + * @NamedArgumentConstructor * @Target("METHOD") * * @author Jordi Boggiano * @author Boris Guéry */ +#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_METHOD)] class RequestParam extends AbstractScalarParam { /** @var bool */ public $strict = true; + /** + * @param mixed $requirements + * @param mixed $default + */ + public function __construct( + string $name = '', + ?string $key = null, + $requirements = null, + $default = null, + string $description = '', + array $incompatibles = [], + bool $strict = false, + bool $map = false, + bool $nullable = false, + bool $allowBlank = true + ) { + $this->name = $name; + $this->key = $key; + $this->requirements = $requirements; + $this->default = $default; + $this->description = $description; + $this->incompatibles = $incompatibles; + $this->strict = $strict; + $this->map = $map; + $this->nullable = $nullable; + $this->allowBlank = $allowBlank; + } + /** * {@inheritdoc} */ diff --git a/Request/ParamReader.php b/Request/ParamReader.php index da903d9d4..0c1062985 100644 --- a/Request/ParamReader.php +++ b/Request/ParamReader.php @@ -23,9 +23,12 @@ */ final class ParamReader implements ParamReaderInterface { + /** + * @var Reader|null + */ private $annotationReader; - public function __construct(Reader $annotationReader) + public function __construct(?Reader $annotationReader = null) { $this->annotationReader = $annotationReader; } @@ -50,7 +53,17 @@ public function read(\ReflectionClass $reflection, string $method): array */ public function getParamsFromMethod(\ReflectionMethod $method): array { - $annotations = $this->annotationReader->getMethodAnnotations($method); + $annotations = []; + if (\PHP_VERSION_ID >= 80000) { + $annotations = $this->getParamsFromAttributes($method); + } + + if (null !== $this->annotationReader) { + $annotations = array_merge( + $annotations, + $this->annotationReader->getMethodAnnotations($method) ?? [] + ); + } return $this->getParamsFromAnnotationArray($annotations); } @@ -60,7 +73,17 @@ public function getParamsFromMethod(\ReflectionMethod $method): array */ public function getParamsFromClass(\ReflectionClass $class): array { - $annotations = $this->annotationReader->getClassAnnotations($class); + $annotations = []; + if (\PHP_VERSION_ID >= 80000) { + $annotations = $this->getParamsFromAttributes($class); + } + + if (null !== $this->annotationReader) { + $annotations = array_merge( + $annotations, + $this->annotationReader->getClassAnnotations($class) ?? [] + ); + } return $this->getParamsFromAnnotationArray($annotations); } @@ -79,4 +102,20 @@ private function getParamsFromAnnotationArray(array $annotations): array return $params; } + + /** + * @param \ReflectionClass|\ReflectionMethod $reflection + * + * @return ParamInterface[] + */ + private function getParamsFromAttributes($reflection): array + { + $params = []; + foreach ($reflection->getAttributes(ParamInterface::class, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) { + $param = $attribute->newInstance(); + $params[$param->getName()] = $param; + } + + return $params; + } } diff --git a/Resources/config/request.xml b/Resources/config/request.xml index 627695803..d55542764 100644 --- a/Resources/config/request.xml +++ b/Resources/config/request.xml @@ -16,7 +16,7 @@ - + diff --git a/Resources/doc/annotations-reference.rst b/Resources/doc/annotations-reference.rst index d26589f64..16cbfcc6a 100644 --- a/Resources/doc/annotations-reference.rst +++ b/Resources/doc/annotations-reference.rst @@ -7,63 +7,126 @@ Param fetcher QueryParam ~~~~~~~~~~ -.. code-block:: php - - use FOS\RestBundle\Controller\Annotations\QueryParam; - - /** - * @QueryParam( - * name="", - * key=null, - * requirements="", - * incompatibles={}, - * default=null, - * description="", - * strict=false, - * map=false, - * nullable=false - * ) - */ +.. tabs:: + + .. tab:: Annotations + + .. code-block:: php + + use FOS\RestBundle\Controller\Annotations\QueryParam; + + /** + * @QueryParam( + * name="", + * key=null, + * requirements="", + * incompatibles={}, + * default=null, + * description="", + * strict=false, + * map=false, + * nullable=false + * ) + */ + + .. tab:: Attributes + + .. code-block:: php + + use FOS\RestBundle\Controller\Annotations\QueryParam; + + #[QueryParam( + name: '', + key: null, + requirements: '', + incompatibles: [], + default: null, + description: '', + strict: false, + map: false, + nullable: false + )] RequestParam ~~~~~~~~~~~~ -.. code-block:: php +.. tabs:: - use FOS\RestBundle\Controller\Annotations\RequestParam; + .. tab:: Annotations - /** - * @RequestParam( - * name="", - * key=null, - * requirements="", - * default=null, - * description="", - * strict=true, - * map=false, - * nullable=false - * ) - */ + .. code-block:: php + + use FOS\RestBundle\Controller\Annotations\RequestParam; + + /** + * @RequestParam( + * name="", + * key=null, + * requirements="", + * default=null, + * description="", + * strict=true, + * map=false, + * nullable=false + * ) + */ + + .. tab:: Attributes + + .. code-block:: php + + use FOS\RestBundle\Controller\Annotations\RequestParam; + + #[RequestParam( + name: '', + key: null, + requirements: '', + default: null, + description: '', + strict: true, + map: false, + nullable: false + )] FileParam ~~~~~~~~~ -.. code-block:: php - - use FOS\RestBundle\Controller\Annotations\FileParam; - - /** - * @FileParam( - * name="", - * key=null, - * requirements={}, - * default=null, - * description="", - * strict=true, - * nullable=false, - * image=false - * ) - */ +.. tabs:: + + .. tab:: Annotations + + .. code-block:: php + + use FOS\RestBundle\Controller\Annotations\FileParam; + + /** + * @FileParam( + * name="", + * key=null, + * requirements={}, + * default=null, + * description="", + * strict=true, + * nullable=false, + * image=false + * ) + */ + .. tab:: Attributes + + .. code-block:: php + + use FOS\RestBundle\Controller\Annotations\FileParam; + + #[FileParam( + name: '', + key: null, + requirements: [], + default: null, + description: '', + strict: true, + nullable: false, + image: false + )] View ---- diff --git a/Tests/Fixtures/Controller/ParamsAnnotatedController.php b/Tests/Fixtures/Controller/ParamsAnnotatedController.php index 454e58ba3..7aa40e5ac 100644 --- a/Tests/Fixtures/Controller/ParamsAnnotatedController.php +++ b/Tests/Fixtures/Controller/ParamsAnnotatedController.php @@ -35,4 +35,14 @@ class ParamsAnnotatedController public function getArticlesAction(ParamFetcher $paramFetcher) { } + + #[QueryParam(name: 'page', requirements: '\d+', default: '1', description: 'Page of the overview')] + #[RequestParam(name: 'byauthor', requirements: '[a-z]+', description: 'by author', incompatibles: ['search'], strict: true)] + #[QueryParam(name: 'filters', requirements: '\d+', default: '1', description: 'Page of the overview')] + #[FileParam(name: 'avatar', requirements: ['mimeTypes' => 'application/json'], image: true)] + #[FileParam(name: 'foo', strict: false)] + #[FileParam(name: 'bar', map: true)] + public function getArticlesAttributesAction(ParamFetcher $paramFetcher) + { + } } diff --git a/Tests/Request/ParamReaderTest.php b/Tests/Request/ParamReaderTest.php index 0c1e9e407..70e2408b7 100644 --- a/Tests/Request/ParamReaderTest.php +++ b/Tests/Request/ParamReaderTest.php @@ -93,6 +93,53 @@ public function testReadsOnlyParamAnnotations() } } + /** + * @requires PHP 8 + */ + public function testReadsAttributes() + { + $paramReader = new ParamReader(); + $params = $paramReader->read(new \ReflectionClass(ParamsAnnotatedController::class), 'getArticlesAttributesAction'); + + $this->assertCount(6, $params); + + // Param 1 (query) + $this->assertArrayHasKey('page', $params); + $this->assertEquals('page', $params['page']->name); + $this->assertEquals('\\d+', $params['page']->requirements); + $this->assertEquals('1', $params['page']->default); + $this->assertEquals('Page of the overview', $params['page']->description); + $this->assertFalse($params['page']->map); + $this->assertFalse($params['page']->strict); + + // Param 2 (request) + $this->assertArrayHasKey('byauthor', $params); + $this->assertEquals('byauthor', $params['byauthor']->name); + $this->assertEquals('[a-z]+', $params['byauthor']->requirements); + $this->assertEquals('by author', $params['byauthor']->description); + $this->assertEquals(['search'], $params['byauthor']->incompatibles); + $this->assertFalse($params['byauthor']->map); + $this->assertTrue($params['byauthor']->strict); + + // Param 3 (query) + $this->assertArrayHasKey('filters', $params); + $this->assertEquals('filters', $params['filters']->name); + $this->assertFalse($params['filters']->map); + + // Param 4 (file) + $this->assertArrayHasKey('avatar', $params); + $this->assertEquals('avatar', $params['avatar']->name); + $this->assertEquals(['mimeTypes' => 'application/json'], $params['avatar']->requirements); + $this->assertTrue($params['avatar']->image); + $this->assertTrue($params['avatar']->strict); + + // Param 5 (file) + $this->assertArrayHasKey('foo', $params); + $this->assertEquals('foo', $params['foo']->name); + $this->assertFalse($params['foo']->image); + $this->assertFalse($params['foo']->strict); + } + public function testExceptionOnNonExistingMethod() { $this->expectException(\InvalidArgumentException::class);