Skip to content

Commit 7847db6

Browse files
committed
Adding support for PHP 8 attributes!
Bonus, parameter related attributes can now be added as parameter attributes (instead of method anotations)
1 parent 802fb3a commit 7847db6

29 files changed

+376
-83
lines changed

docs/fine-grained-security.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Using the `@Security` annotation, you can write an *expression* that can contain
1717

1818
The `@Security` annotation is very flexible: it allows you to pass an expression that can contains custom logic:
1919

20+
**PHP 7**
2021
```php
2122
use TheCodingMachine\GraphQLite\Annotations\Security;
2223

@@ -32,6 +33,20 @@ public function getPost(Post $post): array
3233
}
3334
```
3435

36+
**PHP 8+**
37+
```php
38+
use TheCodingMachine\GraphQLite\Annotations\Security;
39+
40+
// ...
41+
42+
#[Query]
43+
#[Security("is_granted('ROLE_ADMIN') or is_granted('POST_SHOW', post)")]
44+
public function getPost(Post $post): array
45+
{
46+
// ...
47+
}
48+
```
49+
3550
The *expression* defined in the `@Security` annotation must conform to [Symfony's Expression Language syntax](https://symfony.com/doc/4.4/components/expression_language/syntax.html)
3651

3752
<div class="alert alert-info">
@@ -75,6 +90,7 @@ In the example above, the `getPost` method can be called only if the logged user
7590

7691
All parameters passed to the method can be accessed in the `@Security` expression.
7792

93+
**PHP 7**
7894
```php
7995
/**
8096
* @Query
@@ -86,6 +102,16 @@ public function getPosts(DateTimeImmutable $startDate, DateTimeImmutable $endDat
86102
}
87103
```
88104

105+
**PHP 8+**
106+
```php
107+
#[Query]
108+
#[Security(expression: "startDate < endDate", statusCode: 400, message: "End date must be after start date")]
109+
public function getPosts(DateTimeImmutable $startDate, DateTimeImmutable $endDate): array
110+
{
111+
// ...
112+
}
113+
```
114+
89115
In the example above, we tweak a bit the Security annotation purpose to do simple input validation.
90116

91117
## Setting HTTP code and error message

src/AnnotationReader.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,12 @@
3535
use function assert;
3636
use function get_class;
3737
use function in_array;
38+
use function is_a;
3839
use function reset;
3940
use function strpos;
4041
use function strrpos;
4142
use function substr;
43+
use const PHP_MAJOR_VERSION;
4244

4345
class AnnotationReader
4446
{
@@ -217,6 +219,21 @@ public function getParameterAnnotationsPerParameter(array $refParameters): array
217219
}
218220
}
219221

222+
// Now, let's add PHP 8 parameter attributes
223+
if (PHP_MAJOR_VERSION >= 8) {
224+
foreach ($refParameters as $refParameter) {
225+
$attributes = $refParameter->getAttributes();
226+
$parameterAnnotationsPerParameter[$refParameter->getName()] = array_merge($parameterAnnotationsPerParameter[$refParameter->getName()] ?? [], array_map(
227+
static function ($attribute) {
228+
return $attribute->newInstance();
229+
},
230+
array_filter($attributes, static function ($annotation): bool {
231+
return is_a($annotation->getName(), ParameterAnnotationInterface::class, true);
232+
})
233+
));
234+
}
235+
}
236+
220237
return array_map(static function (array $parameterAnnotations) {
221238
return new ParameterAnnotations($parameterAnnotations);
222239
}, $parameterAnnotationsPerParameter);
@@ -246,6 +263,16 @@ private function getClassAnnotation(ReflectionClass $refClass, string $annotatio
246263
{
247264
$type = null;
248265
try {
266+
// If attribute & annotation, let's prefer the PHP 8 attribute
267+
if (PHP_MAJOR_VERSION >= 8) {
268+
$attribute = $refClass->getAttributes($annotationClass)[0] ?? null;
269+
if ($attribute) {
270+
/** @var T $instance */
271+
$instance = $attribute->newInstance();
272+
return $instance;
273+
}
274+
}
275+
249276
$type = $this->reader->getClassAnnotation($refClass, $annotationClass);
250277
assert($type === null || $type instanceof $annotationClass);
251278
} catch (AnnotationException $e) {
@@ -282,6 +309,14 @@ private function getMethodAnnotation(ReflectionMethod $refMethod, string $annota
282309
}
283310

284311
try {
312+
// If attribute & annotation, let's prefer the PHP 8 attribute
313+
if (PHP_MAJOR_VERSION >= 8) {
314+
$attribute = $refMethod->getAttributes($annotationClass)[0] ?? null;
315+
if ($attribute) {
316+
return $this->methodAnnotationCache[$cacheKey] = $attribute->newInstance();
317+
}
318+
}
319+
285320
return $this->methodAnnotationCache[$cacheKey] = $this->reader->getMethodAnnotation($refMethod, $annotationClass);
286321
} catch (AnnotationException $e) {
287322
switch ($this->mode) {
@@ -336,6 +371,17 @@ public function getClassAnnotations(ReflectionClass $refClass, string $annotatio
336371
$toAddAnnotations[] = array_filter($allAnnotations, static function ($annotation) use ($annotationClass): bool {
337372
return $annotation instanceof $annotationClass;
338373
});
374+
if (PHP_MAJOR_VERSION >= 8) {
375+
$attributes = $refClass->getAttributes();
376+
$toAddAnnotations[] = array_map(
377+
static function ($attribute) {
378+
return $attribute->newInstance();
379+
},
380+
array_filter($attributes, static function ($annotation) use ($annotationClass): bool {
381+
return is_a($annotation->getName(), $annotationClass, true);
382+
})
383+
);
384+
}
339385
} catch (AnnotationException $e) {
340386
if ($this->mode === self::STRICT_MODE) {
341387
throw $e;
@@ -384,6 +430,17 @@ public function getMethodAnnotations(ReflectionMethod $refMethod, string $annota
384430
$toAddAnnotations = array_filter($allAnnotations, static function ($annotation) use ($annotationClass): bool {
385431
return $annotation instanceof $annotationClass;
386432
});
433+
if (PHP_MAJOR_VERSION >= 8) {
434+
$attributes = $refMethod->getAttributes();
435+
$toAddAnnotations = array_merge($toAddAnnotations, array_map(
436+
static function ($attribute) {
437+
return $attribute->newInstance();
438+
},
439+
array_filter($attributes, static function ($annotation) use ($annotationClass): bool {
440+
return is_a($annotation->getName(), $annotationClass, true);
441+
})
442+
));
443+
}
387444
} catch (AnnotationException $e) {
388445
if ($this->mode === self::STRICT_MODE) {
389446
throw $e;

src/Annotations/AbstractRequest.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ abstract class AbstractRequest
1515
/**
1616
* @param mixed[] $attributes
1717
*/
18-
public function __construct(array $attributes = [])
18+
public function __construct(array $attributes = [], string $name = null, string $outputType = null)
1919
{
20-
$this->outputType = $attributes['outputType'] ?? null;
21-
$this->name = $attributes['name'] ?? null;
20+
$this->outputType = $outputType ?? $attributes['outputType'] ?? null;
21+
$this->name = $name ?? $attributes['name'] ?? null;
2222
}
2323

2424
/**

src/Annotations/Autowire.php

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44

55
namespace TheCodingMachine\GraphQLite\Annotations;
66

7+
use Attribute;
78
use BadMethodCallException;
9+
use function is_string;
810
use function ltrim;
911

1012
/**
@@ -17,27 +19,35 @@
1719
* @Attribute("identifier", type = "string")
1820
* })
1921
*/
22+
#[Attribute(Attribute::TARGET_PARAMETER)]
2023
class Autowire implements ParameterAnnotationInterface
2124
{
22-
/** @var string */
25+
/** @var string|null */
2326
private $for;
2427
/** @var string|null */
2528
private $identifier;
2629

2730
/**
28-
* @param array<string, mixed> $values
31+
* @param array<string, mixed>|string $identifier
2932
*/
30-
public function __construct(array $values)
33+
public function __construct($identifier = [])
3134
{
32-
if (! isset($values['for'])) {
33-
throw new BadMethodCallException('The @Autowire annotation must be passed a target. For instance: "@Autowire(for="$myService")"');
35+
$values = $identifier;
36+
if (is_string($values)) {
37+
$this->identifier = $values;
38+
} else {
39+
$this->identifier = $values['identifier'] ?? $values['value'] ?? null;
40+
if (isset($values['for'])) {
41+
$this->for = ltrim($values['for'], '$');
42+
}
3443
}
35-
$this->for = ltrim($values['for'], '$');
36-
$this->identifier = $values['identifier'] ?? $values['value'] ?? null;
3744
}
3845

3946
public function getTarget(): string
4047
{
48+
if ($this->for === null) {
49+
throw new BadMethodCallException('The @Autowire annotation must be passed a target. For instance: "@Autowire(for="$myService")"');
50+
}
4151
return $this->for;
4252
}
4353

src/Annotations/Decorate.php

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44

55
namespace TheCodingMachine\GraphQLite\Annotations;
66

7+
use Attribute;
78
use BadMethodCallException;
9+
use function is_string;
810

911
/**
1012
* Methods with this annotation are decorating an input type when the input type is resolved.
@@ -16,22 +18,27 @@
1618
* @Attribute("inputTypeName", type = "string"),
1719
* })
1820
*/
21+
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
1922
class Decorate
2023
{
2124
/** @var string */
2225
private $inputTypeName;
2326

2427
/**
25-
* @param array<string, mixed> $values
28+
* @param array<string, mixed>|string $inputTypeName
2629
*
2730
* @throws BadMethodCallException
2831
*/
29-
public function __construct(array $values)
32+
public function __construct($inputTypeName = [])
3033
{
31-
if (! isset($values['value']) && ! isset($values['inputTypeName'])) {
34+
$values = $inputTypeName;
35+
if (is_string($values)) {
36+
$this->inputTypeName = $values;
37+
} elseif (! isset($values['value']) && ! isset($values['inputTypeName'])) {
3238
throw new BadMethodCallException('The @Decorate annotation must be passed an input type. For instance: "@Decorate("MyInputType")"');
39+
} else {
40+
$this->inputTypeName = $values['value'] ?? $values['inputTypeName'];
3341
}
34-
$this->inputTypeName = $values['value'] ?? $values['inputTypeName'];
3542
}
3643

3744
public function getInputTypeName(): string

src/Annotations/EnumType.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
namespace TheCodingMachine\GraphQLite\Annotations;
66

7+
use Attribute;
8+
79
/**
810
* The EnumType annotation is useful to change the name of the generated "enum" type.
911
*
@@ -13,6 +15,7 @@
1315
* @Attribute("name", type = "string"),
1416
* })
1517
*/
18+
#[Attribute(Attribute::TARGET_CLASS)]
1619
class EnumType
1720
{
1821
/** @var string|null */
@@ -21,9 +24,9 @@ class EnumType
2124
/**
2225
* @param mixed[] $attributes
2326
*/
24-
public function __construct(array $attributes = [])
27+
public function __construct(array $attributes = [], string $name = null)
2528
{
26-
$this->name = $attributes['name'] ?? null;
29+
$this->name = $name ?? $attributes['name'] ?? null;
2730
}
2831

2932
/**

src/Annotations/ExtendType.php

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace TheCodingMachine\GraphQLite\Annotations;
66

7+
use Attribute;
78
use BadMethodCallException;
89
use TheCodingMachine\GraphQLite\Annotations\Exceptions\ClassNotFoundException;
910
use function class_exists;
@@ -20,6 +21,7 @@
2021
* @Attribute("name", type = "string"),
2122
* })
2223
*/
24+
#[Attribute(Attribute::TARGET_CLASS)]
2325
class ExtendType
2426
{
2527
/** @var class-string<object>|null */
@@ -30,17 +32,18 @@ class ExtendType
3032
/**
3133
* @param mixed[] $attributes
3234
*/
33-
public function __construct(array $attributes = [])
35+
public function __construct(array $attributes = [], string $class = null, string $name = null)
3436
{
35-
if (! isset($attributes['class']) && ! isset($attributes['name'])) {
36-
throw new BadMethodCallException('In annotation @ExtendType, missing one of the compulsory parameter "class" or "name".');
37+
$className = isset($attributes['class']) ? ltrim($attributes['class'], '\\') : null;
38+
$className = $className ?? $class;
39+
if ($className !== null && ! class_exists($className) && ! interface_exists($className)) {
40+
throw ClassNotFoundException::couldNotFindClass($className);
3741
}
38-
$class = isset($attributes['class']) ? ltrim($attributes['class'], '\\') : null;
39-
$this->name = $attributes['name'] ?? null;
40-
if ($class !== null && ! class_exists($class) && ! interface_exists($class)) {
41-
throw ClassNotFoundException::couldNotFindClass($class);
42+
$this->name = $name ?? $attributes['name'] ?? null;
43+
$this->class = $className;
44+
if (! $this->class && ! $this->name) {
45+
throw new BadMethodCallException('In annotation @ExtendType, missing one of the compulsory parameter "class" or "name".');
4246
}
43-
$this->class = $class;
4447
}
4548

4649
/**

src/Annotations/Factory.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace TheCodingMachine\GraphQLite\Annotations;
66

7+
use Attribute;
78
use TheCodingMachine\GraphQLite\GraphQLRuntimeException;
89

910
/**
@@ -16,6 +17,7 @@
1617
* @Attribute("default", type = "bool")
1718
* })
1819
*/
20+
#[Attribute(Attribute::TARGET_METHOD)]
1921
class Factory
2022
{
2123
/** @var string|null */
@@ -26,11 +28,11 @@ class Factory
2628
/**
2729
* @param mixed[] $attributes
2830
*/
29-
public function __construct(array $attributes = [])
31+
public function __construct(array $attributes = [], ?string $name = null, bool $default = null)
3032
{
31-
$this->name = $attributes['name'] ?? null;
33+
$this->name = $name ?? $attributes['name'] ?? null;
3234
// This IS the default if no name is set and no "default" attribute is passed.
33-
$this->default = $attributes['default'] ?? ! isset($attributes['name']);
35+
$this->default = $default ?? $attributes['default'] ?? ! isset($attributes['name']);
3436

3537
if ($this->name === null && $this->default === false) {
3638
throw new GraphQLRuntimeException('A @Factory that has "default=false" attribute must be given a name (i.e. add a name="FooBarInput" attribute).');

0 commit comments

Comments
 (0)