Skip to content

Commit 8f8f6be

Browse files
authored
Merge pull request #269 from devmaslov/annotated-input-type
Annotated input type
2 parents c20aa26 + 1860b20 commit 8f8f6be

File tree

72 files changed

+2748
-251
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

72 files changed

+2748
-251
lines changed

.github/workflows/continuous_integration.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ jobs:
3131
- name: "Install PHP with extensions"
3232
uses: "shivammathur/setup-php@v2"
3333
with:
34-
coverage: "pcov"
34+
coverage: "xdebug"
3535
php-version: "${{ matrix.php-version }}"
3636
tools: composer:v2
3737

composer.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"phpunit/phpunit": "^8.2.4||^9.4",
3535
"php-coveralls/php-coveralls": "^2.1",
3636
"mouf/picotainer": "^1.1",
37-
"phpstan/phpstan": "^0.12.25",
37+
"phpstan/phpstan": "^0.12.82",
3838
"beberlei/porpaginas": "^1.2",
3939
"myclabs/php-enum": "^1.6.6",
4040
"doctrine/coding-standard": "^8.2",
@@ -66,7 +66,7 @@
6666
},
6767
"extra": {
6868
"branch-alias": {
69-
"dev-master": "4.1.x-dev"
69+
"dev-master": "4.2.x-dev"
7070
}
7171
}
7272
}

docs/annotations_reference.md

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -55,16 +55,35 @@ name | see below | string | The targeted GraphQL output type.
5555

5656
One and only one of "class" and "name" parameter can be passed at the same time.
5757

58+
## @Input annotation
59+
60+
The `@Input` annotation is used to declare a GraphQL input type.
61+
62+
**Applies on**: classes.
63+
64+
Attribute | Compulsory | Type | Definition
65+
---------------|------------|--------|--------
66+
name | *no* | string | The name of the GraphQL input type generated. If not passed, the name of the class with suffix "Input" is used. If the class ends with "Input", the "Input" suffix is not added.
67+
description | *no* | string | Description of the input type in the documentation. If not passed, PHP doc comment is used.
68+
default | *no* | bool | Defaults to *true* if name is not specified. Whether the annotated PHP class should be mapped by default to this type.
69+
update | *no* | bool | Determines if the the input represents a partial update. When set to *true* all input fields will become optional and won't have default values thus won't be set on resolve if they are not specified in the query/mutation.
70+
71+
5872
## @Field annotation
5973

6074
The `@Field` annotation is used to declare a GraphQL field.
6175

62-
**Applies on**: methods of classes annotated with `@Type` or `@ExtendType`.
76+
**Applies on**: methods or properties of classes annotated with `@Type`, `@ExtendType` or `@Input`.
77+
When it's applied on private or protected property, public getter or/and setter method is expected in the class accordingly
78+
whether it's used for output type or input type. For example if property name is `foo` then getter should be `getFoo()` or setter should be `setFoo($foo)`. Setter can be omitted if property related to the field is present in the constructor with the same name.
6379

64-
Attribute | Compulsory | Type | Definition
65-
---------------|------------|------|--------
66-
name | *no* | string | The name of the field. If skipped, the name of the method is used instead.
67-
[outputType](custom_types.md) | *no* | string | Forces the GraphQL output type of a query.
80+
Attribute | Compulsory | Type | Definition
81+
------------------------------|------------|---------------|--------
82+
name | *no* | string | The name of the field. If skipped, the name of the method is used instead.
83+
for | *no* | string, array | Forces the field to be used only for specific output or input type(s). By default field is used for all possible declared types.
84+
description | *no* | string | Field description displayed in the GraphQL docs. If it's empty PHP doc comment is used instead.
85+
[outputType](custom_types.md) | *no* | string | Forces the GraphQL output type of a query.
86+
[inputType](input_types.md) | *no* | string | Forces the GraphQL input type of a query.
6887

6988
## @SourceField annotation
7089

@@ -100,15 +119,15 @@ annotations | *no* | array<Annotations> | A set of annotations that ap
100119

101120
The `@Logged` annotation is used to declare a Query/Mutation/Field is only visible to logged users.
102121

103-
**Applies on**: methods annotated with `@Query`, `@Mutation` or `@Field`.
122+
**Applies on**: methods or properties annotated with `@Query`, `@Mutation` or `@Field`.
104123

105124
This annotation allows no attributes.
106125

107126
## @Right annotation
108127

109128
The `@Right` annotation is used to declare a Query/Mutation/Field is only visible to users with a specific right.
110129

111-
**Applies on**: methods annotated with `@Query`, `@Mutation` or `@Field`.
130+
**Applies on**: methods or properties annotated with `@Query`, `@Mutation` or `@Field`.
112131

113132
Attribute | Compulsory | Type | Definition
114133
---------------|------------|------|--------
@@ -119,7 +138,7 @@ name | *yes* | string | The name of the right.
119138
The `@FailWith` annotation is used to declare a default value to return in the user is not authorized to see a specific
120139
query / mutation / field (according to the `@Logged` and `@Right` annotations).
121140

122-
**Applies on**: methods annotated with `@Query`, `@Mutation` or `@Field` and one of `@Logged` or `@Right` annotations.
141+
**Applies on**: methods or properties annotated with `@Query`, `@Mutation` or `@Field` and one of `@Logged` or `@Right` annotations.
123142

124143
Attribute | Compulsory | Type | Definition
125144
---------------|------------|------|--------
@@ -130,7 +149,7 @@ value | *yes* | mixed | The value to return if the user is not au
130149
The `@HideIfUnauthorized` annotation is used to completely hide the query / mutation / field if the user is not authorized
131150
to access it (according to the `@Logged` and `@Right` annotations).
132151

133-
**Applies on**: methods annotated with `@Query`, `@Mutation` or `@Field` and one of `@Logged` or `@Right` annotations.
152+
**Applies on**: methods or properties annotated with `@Query`, `@Mutation` or `@Field` and one of `@Logged` or `@Right` annotations.
134153

135154
`@HideIfUnauthorized` and `@FailWith` are mutually exclusive.
136155

@@ -152,7 +171,7 @@ It is very flexible: it allows you to pass an expression that can contains custo
152171

153172
See [the fine grained security page](fine-grained-security.md) for more details.
154173

155-
**Applies on**: methods annotated with `@Query`, `@Mutation` or `@Field`.
174+
**Applies on**: methods or properties annotated with `@Query`, `@Mutation` or `@Field`.
156175

157176
Attribute | Compulsory | Type | Definition
158177
---------------|------------|--------|--------

docs/input_types.md

Lines changed: 166 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,9 @@ You are running into this error because GraphQLite does not know how to handle t
9595

9696
In GraphQL, an object passed in parameter of a query or mutation (or any field) is called an **Input Type**.
9797

98-
In order to declare that type, in GraphQLite, we will declare a **Factory**.
98+
There are two ways for declaring that type, in GraphQLite: using **Factory** or annotating the class with `@Input`.
99+
100+
## Factory
99101

100102
A **Factory** is a method that takes in parameter all the fields of the input type and return an object.
101103

@@ -136,7 +138,7 @@ class MyFactory
136138
and now, you can run query like this:
137139

138140
```
139-
mutation {
141+
query {
140142
getCities(location: {
141143
latitude: 45.0,
142144
longitude: 0.0,
@@ -387,3 +389,165 @@ public function getProductById(string $id, bool $lazyLoad = true): Product
387389
With the `@HideParameter` annotation, you can choose to remove from the GraphQL schema any argument.
388390

389391
To be able to hide an argument, the argument must have a default value.
392+
393+
## @Input Annotation
394+
395+
Let's transform `Location` class into an input type by adding `@Input` annotation to it and `@Field` annotation to corresponding properties:
396+
397+
<!--DOCUSAURUS_CODE_TABS-->
398+
<!--PHP 8+-->
399+
```php
400+
#[Input]
401+
class Location
402+
{
403+
404+
#[Field]
405+
private float $latitude;
406+
407+
#[Field]
408+
private float $longitude;
409+
410+
public function __construct(float $latitude, float $longitude)
411+
{
412+
$this->latitude = $latitude;
413+
$this->longitude = $longitude;
414+
}
415+
416+
public function getLatitude(): float
417+
{
418+
return $this->latitude;
419+
}
420+
421+
public function getLongitude(): float
422+
{
423+
return $this->longitude;
424+
}
425+
}
426+
```
427+
<!--PHP 7+-->
428+
```php
429+
/**
430+
* @Input
431+
*/
432+
class Location
433+
{
434+
435+
/**
436+
* @Field
437+
* @var float
438+
*/
439+
private $latitude;
440+
441+
/**
442+
* @Field
443+
* @var float
444+
*/
445+
private $longitude;
446+
447+
public function __construct(float $latitude, float $longitude)
448+
{
449+
$this->latitude = $latitude;
450+
$this->longitude = $longitude;
451+
}
452+
453+
public function getLatitude(): float
454+
{
455+
return $this->latitude;
456+
}
457+
458+
public function getLongitude(): float
459+
{
460+
return $this->longitude;
461+
}
462+
}
463+
```
464+
<!--END_DOCUSAURUS_CODE_TABS-->
465+
466+
Now if you call `getCities()` query you can pass the location input in the same way as with factories.
467+
The `Location` object will be automatically instantiated with provided `latitude` / `longitude` and passed to the controller as a parameter.
468+
469+
There are some important things to notice:
470+
471+
- `@Field` annotation is recognized only on properties for Input Type.
472+
- There are 3 ways for fields to be resolved:
473+
- Via constructor if corresponding properties are mentioned as parameters with the same names - exactly as in the example above.
474+
- If properties are public, they will be just set without any additional effort.
475+
- For private or protected properties implemented public setter is required (if they are not set via constructor). For example `setLatitude(float $latitude)`.
476+
477+
### Multiple input types per one class
478+
479+
Simple usage of `@Input` annotation on a class creates an GraphQl input named by class name + "Input" suffix if a class name does not end with it already.
480+
You can add multiple `@Input` annotations to the same class, give them different names and link different fields.
481+
Consider the following example:
482+
483+
<!--DOCUSAURUS_CODE_TABS-->
484+
<!--PHP 8+-->
485+
```php
486+
#[Input(name: 'CreateUserInput', default: true)]
487+
#[Input(name: 'UpdateUserInput', update: true)]
488+
class UserInput
489+
{
490+
491+
#[Field]
492+
public string $username;
493+
494+
#[Field(for: 'CreateUserInput')]
495+
public string $email;
496+
497+
#[Field(for: 'CreateUserInput', inputType: 'String!')]
498+
#[Field(for: 'UpdateUserInput', inputType: 'String')]
499+
public string $password;
500+
501+
#[Field]
502+
public ?int $age;
503+
}
504+
```
505+
<!--PHP 7+-->
506+
```php
507+
/**
508+
* @Input(name="CreateUserInput", default=true)
509+
* @Input(name="UpdateUserInput", update=true)
510+
*/
511+
class UserInput
512+
{
513+
514+
/**
515+
* @Field()
516+
* @var string
517+
*/
518+
public $username;
519+
520+
/**
521+
* @Field(for="CreateUserInput")
522+
* @var string
523+
*/
524+
public string $email;
525+
526+
/**
527+
* @Field(for="CreateUserInput", inputType="String!")
528+
* @Field(for="UpdateUserInput", inputType="String")
529+
* @var string|null
530+
*/
531+
public $password;
532+
533+
/**
534+
* @Field()
535+
* @var int|null
536+
*/
537+
public $age;
538+
}
539+
```
540+
<!--END_DOCUSAURUS_CODE_TABS-->
541+
542+
There are 2 input types created for just one class: `CreateUserInput` and `UpdateUserInput`. A few notes:
543+
- `CreateUserInput` input will be used by default for this class.
544+
- Field `username` is created for both input types, and it is required because the property type is not nullable.
545+
- Field `email` will appear only for `CreateUserInput` input.
546+
- Field `password` will appear for both. For `CreateUserInput` it'll be the required field and for `UpdateUserInput` optional.
547+
- Field `age` is optional for both input types.
548+
549+
Note that `update: true` argument for `UpdateUserInput`. It should be used when input type is used for a partial update,
550+
It makes all fields optional and removes all default values from thus prevents setting default values via setters or directly to public properties.
551+
In example above if you use the class as `UpdateUserInput` and set only `username` the other ones will be ignored.
552+
In PHP 7 they will be set to `null`, while in PHP 8 they will be in not initialized state - this can be used as a trick
553+
to check if user actually passed a value for a certain field.

phpstan.neon

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@ parameters:
33
ignoreErrors:
44
- "#PHPDoc tag \\@throws with type Psr\\\\Container\\\\ContainerExceptionInterface is not subtype of Throwable#"
55
#- "#Property TheCodingMachine\\\\GraphQLite\\\\Types\\\\ResolvableInputObjectType::\\$resolve \\(array<int, object\\|string>&callable\\) does not accept array<int,object\\|string>#"
6-
- "#Variable \\$prefetchRefMethod might not be defined.#"
76
#- "#Parameter \\#2 \\$type of class TheCodingMachine\\\\GraphQLite\\\\Parameters\\\\InputTypeParameter constructor expects GraphQL\\\\Type\\\\Definition\\\\InputType&GraphQL\\\\Type\\\\Definition\\\\Type, GraphQL\\\\Type\\\\Definition\\\\InputType\\|GraphQL\\\\Type\\\\Definition\\\\Type given.#"
8-
- "#Parameter .* of class ReflectionMethod constructor expects string, object\\|string given.#"
7+
- "#Parameter .* of class ReflectionMethod constructor expects string(\\|null)?, object\\|string given.#"
98
-
109
message: '#Method TheCodingMachine\\GraphQLite\\Types\\Mutable(Interface|Object)Type::getFields\(\) should return array<GraphQL\\Type\\Definition\\FieldDefinition> but returns array\|float\|int#'
1110
path: src/Types/MutableTrait.php
@@ -34,9 +33,14 @@ parameters:
3433
message: '#Property TheCodingMachine\\GraphQLite\\Annotations\\Type::\$class \(class-string<object>\\|null\) does not accept string.#'
3534
path: src/Annotations/Type.php
3635
-
37-
message: '#Method TheCodingMachine\\GraphQLite\\AnnotationReader::getMethodAnnotations\(\) should return array<int, T of object> but returns array<object>.#'
36+
message: '#Method TheCodingMachine\\GraphQLite\\AnnotationReader::(getMethodAnnotations|getPropertyAnnotations)\(\) should return array<int, T of object> but returns array<object>.#'
37+
path: src/AnnotationReader.php
38+
-
39+
message: '#Method TheCodingMachine\\GraphQLite\\AnnotationReader::getClassAnnotations\(\) should return array<A of object> but returns array<object>.#'
40+
path: src/AnnotationReader.php
41+
-
42+
message: '#Parameter \#1 \$annotations of class TheCodingMachine\\GraphQLite\\Annotations\\ParameterAnnotations constructor expects array<int, TheCodingMachine\\GraphQLite\\Annotations\\ParameterAnnotationInterface>, array<object> given.#'
3843
path: src/AnnotationReader.php
39-
- '#Call to an undefined method GraphQL\\Error\\ClientAware::getMessage\(\)#'
4044

4145

4246
#-

phpunit.xml.dist

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22

3-
<phpunit backupGlobals="false"
3+
<phpunit
4+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5+
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/8.5/phpunit.xsd"
6+
backupGlobals="false"
47
backupStaticAttributes="false"
58
colors="true"
69
convertErrorsToExceptions="true"
@@ -10,21 +13,21 @@
1013
stopOnFailure="false"
1114
bootstrap="tests/Bootstrap.php"
1215
>
13-
<testsuites>
14-
<testsuite name="GraphQLite Test Suite">
15-
<directory>./tests/</directory>
16-
<exclude>./tests/dependencies/</exclude>
17-
<exclude>./tests/Bootstrap.php</exclude>
18-
</testsuite>
19-
</testsuites>
16+
<testsuites>
17+
<testsuite name="GraphQLite Test Suite">
18+
<directory>./tests/</directory>
19+
<exclude>./tests/dependencies/</exclude>
20+
<exclude>./tests/Bootstrap.php</exclude>
21+
</testsuite>
22+
</testsuites>
2023

21-
<filter>
22-
<whitelist processUncoveredFilesFromWhitelist="true">
23-
<directory suffix=".php">src/</directory>
24-
</whitelist>
25-
</filter>
26-
<logging>
27-
<log type="coverage-html" target="build/coverage"/>
28-
<log type="coverage-clover" target="build/logs/clover.xml"/>
29-
</logging>
24+
<filter>
25+
<whitelist processUncoveredFilesFromWhitelist="true">
26+
<directory suffix=".php">src/</directory>
27+
</whitelist>
28+
</filter>
29+
<logging>
30+
<log type="coverage-html" target="build/coverage"/>
31+
<log type="coverage-clover" target="build/logs/clover.xml"/>
32+
</logging>
3033
</phpunit>

0 commit comments

Comments
 (0)