Skip to content

Commit 9642833

Browse files
themaschPowerKiKi
authored andcommitted
BREAKING native PHP attributes replace annotations
All previously existing annotations are now PHP native `Attribute`. This is a major breaking change and all annotations must be migrated to the new attributes. Notable changes: - `API\Argument` is now declared on the parameter itself - `API\Filter` is used directly, possibly multiple times, without wrapping in `API\Filters` - `API\Filters` is not necessary anymore and was dropped - `API\Sorting` requires a single class name, but can be used multiple times # Partially automated migration The following is a configuration file for [Rector](https://getrector.com) that will migrate both Doctrine annotations and also GraphQL Doctrine annotations to attributes. The migration for GraphQL Doctrine is imperfect, so `Sorting` and `Argument` will have to be adjusted manually afterward. ```php <?php declare(strict_types=1); use Rector\Config\RectorConfig; use Rector\Php80\Rector\Class_\AnnotationToAttributeRector; use Rector\Php80\Rector\Property\NestedAnnotationToAttributeRector; use Rector\Php80\ValueObject\AnnotationPropertyToAttributeClass; use Rector\Php80\ValueObject\AnnotationToAttribute; use Rector\Php80\ValueObject\NestedAnnotationToAttribute; return static function (RectorConfig $rectorConfig): void { $rectorConfig->parallel(); $rectorConfig->sets([ \Rector\Doctrine\Set\DoctrineSetList::ANNOTATIONS_TO_ATTRIBUTES, ]); $rectorConfig->ruleWithConfiguration(NestedAnnotationToAttributeRector::class, [ new NestedAnnotationToAttribute('GraphQL\\Doctrine\\Annotation\\Filters', [new AnnotationPropertyToAttributeClass('GraphQL\\Doctrine\\Attribute\\Filter')], \true), new NestedAnnotationToAttribute('GraphQL\\Doctrine\\Annotation\\Sorting', [new AnnotationPropertyToAttributeClass('GraphQL\\Doctrine\\Attribute\\Sorting', 'classes', \true)]), new NestedAnnotationToAttribute('API\Filters', [new AnnotationPropertyToAttributeClass('GraphQL\\Doctrine\\Attribute\\Filter')], \true), new NestedAnnotationToAttribute('API\Sorting', [new AnnotationPropertyToAttributeClass('GraphQL\\Doctrine\\Attribute\\Sorting', 'classes', \true)]), ]); $rectorConfig->ruleWithConfiguration(AnnotationToAttributeRector::class, [ new AnnotationToAttribute('GraphQL\\Doctrine\\Annotation\\Argument', 'GraphQL\\Doctrine\\Attribute\\Argument'), new AnnotationToAttribute('GraphQL\\Doctrine\\Annotation\\Exclude', 'GraphQL\\Doctrine\\Attribute\\Exclude'), new AnnotationToAttribute('GraphQL\\Doctrine\\Annotation\\Field', 'GraphQL\\Doctrine\\Attribute\\Field'), new AnnotationToAttribute('GraphQL\\Doctrine\\Annotation\\FilterGroupCondition', 'GraphQL\\Doctrine\\Attribute\\FilterGroupCondition'), new AnnotationToAttribute('GraphQL\\Doctrine\\Annotation\\Filter', 'GraphQL\\Doctrine\\Attribute\\Filter'), new AnnotationToAttribute('GraphQL\\Doctrine\\Annotation\\Input', 'GraphQL\\Doctrine\\Attribute\\Input'), new AnnotationToAttribute('API\Argument', 'GraphQL\\Doctrine\\Attribute\\Argument'), new AnnotationToAttribute('API\Exclude', 'GraphQL\\Doctrine\\Attribute\\Exclude'), new AnnotationToAttribute('API\Field', 'GraphQL\\Doctrine\\Attribute\\Field'), new AnnotationToAttribute('API\FilterGroupCondition', 'GraphQL\\Doctrine\\Attribute\\FilterGroupCondition'), new AnnotationToAttribute('API\Filter', 'GraphQL\\Doctrine\\Attribute\\Filter'), new AnnotationToAttribute('API\Input', 'GraphQL\\Doctrine\\Attribute\\Input'), ]); }; ``` Closes #63
1 parent 05fc618 commit 9642833

Some content is hidden

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

65 files changed

+658
-893
lines changed

README.md

Lines changed: 52 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@
99
[![Join the chat at https://gitter.im/Ecodev/graphql-doctrine](https://badges.gitter.im/Ecodev/graphql-doctrine.svg)](https://gitter.im/Ecodev/graphql-doctrine)
1010

1111
A library to declare GraphQL types from Doctrine entities, PHP type hinting,
12-
and annotations, and to be used with [webonyx/graphql-php](https://github.com/webonyx/graphql-php).
12+
and attributes, and to be used with [webonyx/graphql-php](https://github.com/webonyx/graphql-php).
1313

1414
It reads most information from type hints, complete some things from existing
15-
Doctrine annotations and allow further customizations with specialized annotations.
15+
Doctrine attributes and allow further customizations with specialized attributes.
1616
It will then create [`ObjectType`](https://webonyx.github.io/graphql-php/type-system/object-types/#object-type-definition) and
1717
[`InputObjectType`](https://webonyx.github.io/graphql-php/type-system/input-types/#input-object-type)
1818
instances with fields for all getter and setter respectively found on Doctrine entities.
@@ -114,7 +114,7 @@ $schema = new Schema([
114114

115115
## Usage
116116

117-
The public API is limited to the public methods on `TypesInterface`, `Types`'s constructor, and the annotations.
117+
The public API is limited to the public methods on `TypesInterface`, `Types`'s constructor, and the attributes.
118118

119119
Here is a quick overview of `TypesInterface`:
120120

@@ -137,28 +137,27 @@ of priority, from the least to most important is:
137137

138138
1. Type hinting
139139
2. Doc blocks
140-
3. Annotations
140+
3. Attributes
141141

142-
That means it is always possible to override everything with annotations. But
142+
That means it is always possible to override everything with attributes. But
143143
existing type hints and dock blocks should cover the majority of cases.
144144

145145
### Exclude sensitive things
146146

147147
All getters, and setters, are included by default in the type. And all properties are included in the filters.
148148
But it can be specified otherwise for each method and property.
149149

150-
To exclude a sensitive field from ever being exposed through the API, use `@API\Exclude`:
150+
To exclude a sensitive field from ever being exposed through the API, use `#[API\Exclude]`:
151151

152152
```php
153-
use GraphQL\Doctrine\Annotation as API;
153+
use GraphQL\Doctrine\Attribute as API;
154154

155155
/**
156156
* Returns the hashed password
157157
*
158-
* @API\Exclude
159-
*
160158
* @return string
161159
*/
160+
#[API\Exclude]
162161
public function getPassword(): string
163162
{
164163
return $this->password;
@@ -168,13 +167,10 @@ public function getPassword(): string
168167
And to exclude a property from being exposed as a filter:
169168

170169
```php
171-
use GraphQL\Doctrine\Annotation as API;
170+
use GraphQL\Doctrine\Attribute as API;
172171

173-
/**
174-
* @API\Exclude
175-
*
176-
* @ORM\Column(type="string", length=255)
177-
*/
172+
#[ORM\Column(name: 'password', type: 'string', length: 255)]
173+
#[API\Exclude]
178174
private string $password = '';
179175
```
180176

@@ -183,18 +179,18 @@ private string $password = '';
183179
Even if a getter returns a PHP scalar type, such as `string`, it might be preferable
184180
to override the type with a custom GraphQL type. This is typically useful for enum
185181
or other validation purposes, such as email address. This is done by specifying the
186-
GraphQL type FQCN via `@API\Field` annotation:
182+
GraphQL type FQCN via `#[API\Field]` attribute:
187183

188184
```php
189-
use GraphQL\Doctrine\Annotation as API;
185+
use GraphQL\Doctrine\Attribute as API;
186+
use GraphQLTests\Doctrine\Blog\Types\PostStatusType;
190187

191188
/**
192189
* Get status
193190
*
194-
* @API\Field(type="GraphQLTests\Doctrine\Blog\Types\PostStatusType")
195-
*
196191
* @return string
197192
*/
193+
#[API\Field(type: PostStatusType::class)]
198194
public function getStatus(): string
199195
{
200196
return $this->status;
@@ -203,7 +199,7 @@ public function getStatus(): string
203199

204200
The type must be the PHP class implementing the GraphQL type (see
205201
[limitations](#limitations)). The declaration can be defined as nullable and/or as
206-
an array with one the following syntaxes (PHP style or GraphQL style):~~~~
202+
an array with one the following syntaxes (PHP style or GraphQL style):
207203

208204
- `?MyType`
209205
- `null|MyType`
@@ -214,28 +210,29 @@ an array with one the following syntaxes (PHP style or GraphQL style):~~~~
214210
- `MyType[]|null`
215211
- `Collection<MyType>`
216212

217-
This annotation can be used to override other things, such as `name`, `description`
213+
This attribute can be used to override other things, such as `name`, `description`
218214
and `args`.
219215

220216

221217
#### Override arguments
222218

223-
Similarly to `@API\Field`, `@API\Argument` allows to override the type of argument
219+
Similarly to `#[API\Field]`, `#[API\Argument]` allows to override the type of argument
224220
if the PHP type hint is not enough:
225221

226222
```php
227-
use GraphQL\Doctrine\Annotation as API;
223+
use GraphQL\Doctrine\Attribute as API;
228224

229225
/**
230226
* Returns all posts of the specified status
231227
*
232-
* @API\Field(args={@API\Argument(name="status", type="?GraphQLTests\Doctrine\Blog\Types\PostStatusType")})
233-
*
234228
* @param string $status the status of posts as defined in \GraphQLTests\Doctrine\Blog\Model\Post
235229
*
236230
* @return Collection
237231
*/
238-
public function getPosts(?string $status = Post::STATUS_PUBLIC): Collection
232+
public function getPosts(
233+
#[API\Argument(type: '?GraphQLTests\Doctrine\Blog\Types\PostStatusType')]
234+
?string $status = Post::STATUS_PUBLIC
235+
): Collection
239236
{
240237
// ...
241238
}
@@ -246,39 +243,37 @@ and `defaultValue`.
246243

247244
### Override input types
248245

249-
`@API\Input` is the opposite of `@API\Field` and can be used to override things for
246+
`#[API\Input]` is the opposite of `#[API\Field]` and can be used to override things for
250247
input types (setters), typically for validations purpose. This would look like:
251248

252249
```php
253-
use GraphQL\Doctrine\Annotation as API;
250+
use GraphQL\Doctrine\Attribute as API;
251+
use GraphQLTests\Doctrine\Blog\Types\PostStatusType;
254252

255253
/**
256254
* Set status
257255
*
258-
* @API\Input(type="GraphQLTests\Doctrine\Blog\Types\PostStatusType")
259-
*
260256
* @param string $status
261257
*/
258+
#[API\Input(type: PostStatusType::class)]
262259
public function setStatus(string $status = self::STATUS_PUBLIC): void
263260
{
264261
$this->status = $status;
265262
}
266263
```
267264

268-
This annotation also supports `name`, `description`, and `defaultValue`.
265+
This attribute also supports `description`, and `defaultValue`.
269266

270267
### Override filter types
271268

272-
`@API\FilterGroupCondition` is the equivalent for filters that are generated from properties.
269+
`#[API\FilterGroupCondition]` is the equivalent for filters that are generated from properties.
273270
So usage would be like:
274271

275272
```php
276-
use GraphQL\Doctrine\Annotation as API;
273+
use GraphQL\Doctrine\Attribute as API;
277274

278-
/**
279-
* @API\FilterGroupCondition(type="?GraphQLTests\Doctrine\Blog\Types\PostStatusType")
280-
* @ORM\Column(type="string", options={"default" = Post::STATUS_PRIVATE})
281-
*/
275+
#[API\FilterGroupCondition(type: '?GraphQLTests\Doctrine\Blog\Types\PostStatusType')]
276+
#[ORM\Column(type: 'string', options: ['default' => self::STATUS_PRIVATE])]
282277
private string $status = self::STATUS_PRIVATE;
283278
```
284279

@@ -288,8 +283,8 @@ do the conversion yourself before passing the filter values to `Types::createFil
288283

289284
### Custom types
290285

291-
By default all PHP scalar types and Doctrine collection are automatically detected
292-
and mapped to a GraphQL type. However if some getter return custom types, such
286+
By default, all PHP scalar types and Doctrine collection are automatically detected
287+
and mapped to a GraphQL type. However, if some getter return custom types, such
293288
as `DateTimeImmutable`, or a custom class, then it will have to be configured beforehand.
294289

295290
The configuration is done with a [PSR-11 container](https://www.php-fig.org/psr/psr-11/)
@@ -299,7 +294,7 @@ because it offers useful concepts such as: invokables, aliases, factories and ab
299294
factories. But any other PSR-11 container implementation could be used instead.
300295

301296

302-
The keys should be the whatever you use to refer to the type in your model. Typically
297+
The keys should be the whatever you use to refer to the type in your model. Typically,
303298
that would be either the FQCN of a PHP class "native" type such as `DateTimeImmutable`, or the
304299
FQCN of a PHP class implementing the GraphQL type, or directly the GraphQL type name:
305300

@@ -354,15 +349,15 @@ things like:
354349

355350
In addition to normal input types, it is possible to get a partial input type via
356351
`getPartialInput()`. This is especially useful for mutations that update existing
357-
entities and we do not want to have to re-submit all fields. By using a partial
352+
entities, when we do not want to have to re-submit all fields. By using a partial
358353
input, the API client is able to submit only the fields that need to be updated
359354
and nothing more.
360355

361356
This potentially reduces network traffic, because the client does not need
362357
to fetch all fields just to be able re-submit them when he wants to modify only
363358
one field.
364359

365-
And it also enable to easily design mass editing mutations where the client would
360+
And it also enables to easily design mass editing mutations where the client would
366361
submit only a few fields to be updated for many entities at once. This could look like:
367362

368363
```php
@@ -396,9 +391,7 @@ default value `john`, an optional field `foo` with a default value `defaultFoo`
396391
a mandatory field `bar` without any default value:
397392

398393
```php
399-
/**
400-
* @ORM\Column(type="string")
401-
*/
394+
#[ORM\Column(type: 'string']
402395
private $name = 'jane';
403396

404397
public function setName(string $name = 'john'): void
@@ -446,16 +439,15 @@ This would also allow to filter on joined relations by carefully adding joins wh
446439
Then a custom filter might be used like so:
447440

448441
```php
449-
use GraphQL\Doctrine\Annotation as API;
442+
use Doctrine\ORM\Mapping as ORM;
443+
use GraphQL\Doctrine\Attribute as API;
444+
use GraphQLTests\Doctrine\Blog\Filtering\SearchOperatorType;
450445

451446
/**
452447
* A blog post with title and body
453-
*
454-
* @ORM\Entity
455-
* @API\Filters({
456-
* @API\Filter(field="custom", operator="GraphQLTests\Doctrine\Blog\Filtering\SearchOperatorType", type="string")
457-
* })
458448
*/
449+
#[ORM\Entity]
450+
#[API\Filter(field: 'custom', operator: SearchOperatorType::class, type: 'string')]
459451
final class Post extends AbstractModel
460452
```
461453

@@ -469,34 +461,35 @@ Similarly to custom filters, it may be possible to carefully add joins if necess
469461
Then a custom sorting might be used like so:
470462

471463
```php
472-
use GraphQL\Doctrine\Annotation as API;
464+
use Doctrine\ORM\Mapping as ORM;
465+
use GraphQL\Doctrine\Attribute as API;
466+
use GraphQLTests\Doctrine\Blog\Sorting\UserName;
473467

474468
/**
475469
* A blog post with title and body
476-
*
477-
* @ORM\Entity
478-
* @API\Sorting({"GraphQLTests\Doctrine\Blog\Sorting\UserName"})
479470
*/
471+
#[ORM\Entity]
472+
#[API\Sorting([UserName::class])]
480473
final class Post extends AbstractModel
481474
```
482475
## Limitations
483476

484477
### Namespaces
485478

486-
The `use` statement is not supported. So types in annotation or doc blocks should
479+
The `use` statement is not supported. So types in attributes or doc blocks should
487480
either be the FQCN or in the same namespace as the getter.
488481

489482
### Composite identifiers
490483

491484
Entities with composite identifiers are not supported for automatic creation of
492485
input types. Possible workarounds are to change input argument to be something
493-
else than an entity, write custom input types and use them via annotations, or
486+
else than an entity, write custom input types and use them via attributes, or
494487
adapt the database schema.
495488

496489
### Logical operators in filtering
497490

498491
Logical operators support only two levels, and second level cannot mix logic operators. In SQL
499-
that would means only one level of parentheses. So you can generate SQL that would look like:
492+
that would mean only one level of parentheses. So you can generate SQL that would look like:
500493

501494
```sql
502495
-- mixed top level
@@ -530,7 +523,7 @@ This should be done via a custom sorting to ensure that joins are done properly.
530523

531524
[Doctrine GraphQL Mapper](https://github.com/rahuljayaraman/doctrine-graphql) has
532525
been an inspiration to write this package. While the goals are similar, the way
533-
it works is different. In Doctrine GraphQL Mapper, annotations are spread between
526+
it works is different. In Doctrine GraphQL Mapper, attributes are spread between
534527
properties and methods (and classes for filtering), but we work only on methods.
535528
Setup seems slightly more complex, but might be more flexible. We built on conventions
536529
and widespread use of PHP type hinting to have an easier out-of-the-box experience.

composer.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ecodev/graphql-doctrine",
3-
"description": "Declare GraphQL types from Doctrine entities and annotations",
3+
"description": "Declare GraphQL types from Doctrine entities and attributes",
44
"type": "library",
55
"license": "MIT",
66
"config": {
@@ -37,7 +37,7 @@
3737
},
3838
"require": {
3939
"php": "^8.1",
40-
"doctrine/orm": "^2.13",
40+
"doctrine/orm": "^2.14",
4141
"doctrine/persistence": "^2.0",
4242
"psr/container": "^1.1 || ^2.0",
4343
"webonyx/graphql-php": "^14.11"

0 commit comments

Comments
 (0)