Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Automatic GraphQL types from Doctrine entities

License

NotificationsYou must be signed in to change notification settings

Ecodev/graphql-doctrine

Repository files navigation

Build StatusCode QualityCode CoverageTotal DownloadsLatest Stable VersionLicense

A library to declare GraphQL types from Doctrine entities, PHP type hinting,and attributes, and to be used withwebonyx/graphql-php.

It reads most information from type hints, complete some things from existingDoctrine attributes and allow further customizations with specialized attributes.It will then createObjectType andInputObjectTypeinstances with fields for all getter and setter respectively found on Doctrine entities.

It willnot build the entire schema. It is up to the user to use automatedtypes, and other custom types, to define root queries.

Quick start

Install the library via composer:

composer require ecodev/graphql-doctrine

And start using it:

<?phpuseGraphQLTests\Doctrine\Blog\Model\Post;useGraphQLTests\Doctrine\Blog\Types\DateTimeType;useGraphQLTests\Doctrine\Blog\Types\PostStatusType;useGraphQL\Type\Definition\ObjectType;useGraphQL\Type\Definition\Type;useGraphQL\Type\Schema;useGraphQL\Doctrine\DefaultFieldResolver;useGraphQL\Doctrine\Types;useLaminas\ServiceManager\ServiceManager;// Define custom types with a PSR-11 container$customTypes =newServiceManager(['invokables' => [        DateTimeImmutable::class => DateTimeType::class,'PostStatus' => PostStatusType::class,    ],'aliases' => ['datetime_immutable' => DateTimeImmutable::class,// Declare alias for Doctrine type to be used for filters    ],]);// Configure the type registry$types =newTypes($entityManager,$customTypes);// Configure default field resolver to be able to use gettersGraphQL::setDefaultFieldResolver(newDefaultFieldResolver());// Build your Schema$schema =newSchema(['query' =>newObjectType(['name' =>'query','fields' => ['posts' => ['type' => Type::listOf($types->getOutput(Post::class)),// Use automated ObjectType for output'args' => [                    ['name' =>'filter','type' =>$types->getFilter(Post::class),// Use automated filtering options                    ],                    ['name' =>'sorting','type' =>$types->getSorting(Post::class),// Use automated sorting options                    ],                ],'resolve' =>function ($root,$args)use ($types):void {$queryBuilder =$types->createFilteredQueryBuilder(Post::class,$args['filter'] ?? [],$args['sorting'] ?? []);// execute query...                },            ],        ],    ]),'mutation' =>newObjectType(['name' =>'mutation','fields' => ['createPost' => ['type' => Type::nonNull($types->getOutput(Post::class)),'args' => ['input' => Type::nonNull($types->getInput(Post::class)),// Use automated InputObjectType for input                ],'resolve' =>function ($root,$args):void {// create new post and flush...                },            ],'updatePost' => ['type' => Type::nonNull($types->getOutput(Post::class)),'args' => ['id' => Type::nonNull(Type::id()),// Use standard API when needed'input' =>$types->getPartialInput(Post::class),// Use automated InputObjectType for partial input for updates                ],'resolve' =>function ($root,$args):void {// update existing post and flush...                },            ],        ],    ]),]);

Usage

The public API is limited to the public methods onTypesInterface,Types's constructor, and the attributes.

Here is a quick overview ofTypesInterface:

  • $types->get() to get custom types
  • $types->getOutput() to get anObjectType to be used in queries
  • $types->getFilter() to get anInputObjectType to be used in queries
  • $types->getSorting() to get anInputObjectType to be used in queries
  • $types->getInput() to get anInputObjectType to be used in mutations (typically for creation)
  • $types->getPartialInput() to get anInputObjectType to be used in mutations (typically for update)
  • $types->getId() to get anEntityIDType which may be used to receive anobject from database instead of a scalar
  • $types->has() to check whether a type exists
  • $types->createFilteredQueryBuilder() to be used in query resolvers

Information priority

To avoid code duplication as much as possible, information are gathered fromseveral places, where available. And each of those might be overridden. The orderof priority, from the least to most important is:

  1. Type hinting
  2. Doc blocks
  3. Attributes

That means it is always possible to override everything with attributes. Butexisting type hints and dock blocks should cover the majority of cases.

Exclude sensitive things

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

To exclude a sensitive field from ever being exposed through the API, use#[API\Exclude]:

useGraphQL\Doctrine\AttributeasAPI;/** * Returns the hashed password * * @return string */#[API\Exclude]publicfunctiongetPassword():string{return$this->password;}

And to exclude a property from being exposed as a filter:

useGraphQL\Doctrine\AttributeasAPI;#[ORM\Column(name:'password', type:'string', length:255)]#[API\Exclude]private string$password ='';

Override output types

Even if a getter returns a PHP scalar type, such asstring, it might be preferableto override the type with a custom GraphQL type. This is typically useful for enumor other validation purposes, such as email address. This is done by specifying theGraphQL type FQCN via#[API\Field] attribute:

useGraphQL\Doctrine\AttributeasAPI;useGraphQLTests\Doctrine\Blog\Types\PostStatusType;/** * Get status * * @return string */#[API\Field(type: PostStatusType::class)]publicfunctiongetStatus():string{return$this->status;}

Type syntax

In most cases, the type must use the::class notation to specify the PHP class that is either implementing the GraphQLtype or the entity itself (seelimitations). Use string literals only if you must define it as nullableand/or as an array. Never use the short name of an entity (it is only possible for user-defined custom types).

Supported syntaxes (PHP style or GraphQL style) are:

  • MyType::class
  • '?Application\MyType'
  • 'null|Application\MyType'
  • 'Application\MyType|null'
  • 'Application\MyType[]'
  • '?Application\MyType[]'
  • 'null|Application\MyType[]'
  • 'Application\MyType[]|null'
  • 'Collection<int, Application\MyType>'

This attribute can be used to override other things, such asname,descriptionandargs.

Override arguments

Similarly to#[API\Field],#[API\Argument] allows to override the type of argumentif the PHP type hint is not enough:

useGraphQL\Doctrine\AttributeasAPI;/** * Returns all posts of the specified status * * @param string $status the status of posts as defined in \GraphQLTests\Doctrine\Blog\Model\Post * * @return Collection<int, Post> */publicfunctiongetPosts(     #[API\Argument(type:'?GraphQLTests\Doctrine\Blog\Types\PostStatusType')]    ?string$status = Post::STATUS_PUBLIC):Collection{// ...}

Once again, it also allows to override other things such asname,descriptionanddefaultValue.

Override input types

#[API\Input] is the opposite of#[API\Field] and can be used to override things forinput types (setters), typically for validations purpose. This would look like:

useGraphQL\Doctrine\AttributeasAPI;useGraphQLTests\Doctrine\Blog\Types\PostStatusType;/** * Set status * * @param string $status */#[API\Input(type: PostStatusType::class)]publicfunctionsetStatus(string$status =self::STATUS_PUBLIC):void{$this->status =$status;}

This attribute also supportsdescription, anddefaultValue.

Override filter types

#[API\FilterGroupCondition] is the equivalent for filters that are generated from properties.So usage would be like:

useGraphQL\Doctrine\AttributeasAPI;#[API\FilterGroupCondition(type:'?GraphQLTests\Doctrine\Blog\Types\PostStatusType')]#[ORM\Column(type:'string', options: ['default' =>self::STATUS_PRIVATE])]private string$status =self::STATUS_PRIVATE;

An important thing to note is that the value of the type specified will be directly used in DQL. That meansthat if the value is not a PHP scalar, then it must be convertible to string via__toString(), or you have todo the conversion yourself before passing the filter values toTypes::createFilteredQueryBuilder().

Custom types

By default, all PHP scalar types and Doctrine collection are automatically detectedand mapped to a GraphQL type. However, if some getter return custom types, suchasDateTimeImmutable, or a custom class, then it will have to be configured beforehand.

The configuration is done with aPSR-11 containerimplementation configured according to your needs. In the following example, we uselaminas/laminas-servicemanager,because it offers useful concepts such as: invokables, aliases, factories and abstractfactories. But any other PSR-11 container implementation could be used instead.

The keys should be the whatever you use to refer to the type in your model. Typically,that would be either the FQCN of a PHP class "native" type such asDateTimeImmutable, or theFQCN of a PHP class implementing the GraphQL type, or directly the GraphQL type name:

$customTypes =newServiceManager(['invokables' => [        DateTimeImmutable::class => DateTimeType::class,'PostStatus' => PostStatusType::class,    ],]);$types =newTypes($entityManager,$customTypes);// Build schema...

That way it is not necessary to annotate every single getter returning one of theconfigured type. It will be mapped automatically.

Entities as input arguments

If a getter takes an entity as parameter, then a specializedInputType willbe created automatically to accept anID. The entity will then be automaticallyfetched from the database and forwarded to the getter. So this will work out ofthe box:

publicfunctionisAllowedEditing(User$user):bool{return$this->getUser() ===$user;}

You may also get an input type for an entity by usingTypes::getId() to writethings like:

[// ...'args' => ['id' =>$types->getId(Post::class),    ],'resolve' =>function ($root,array$args) {$post =$args['id']->getEntity();// ...    },]

Partial inputs

In addition to normal input types, it is possible to get a partial input type viagetPartialInput(). This is especially useful for mutations that update existingentities, when we do not want to have to re-submit all fields. By using a partialinput, the API client is able to submit only the fields that need to be updatedand nothing more.

This potentially reduces network traffic, because the client does not needto fetch all fields just to be able re-submit them when he wants to modify onlyone field.

And it also enables to easily design mass editing mutations where the client wouldsubmit only a few fields to be updated for many entities at once. This could look like:

<?php$mutations = ['updatePosts' => ['type' => Type::nonNull(Type::listOf(Type::nonNull($types->get(Post::class)))),'args' => ['ids' => Type::nonNull(Type::listOf(Type::nonNull(Type::id()))),'input' =>$types->getPartialInput(Post::class),// Use automated InputObjectType for partial input for updates        ],'resolve' =>function ($root,$args) {// update existing posts and flush...        }    ],];

Default values

Default values are automatically detected from arguments for getters, as seen ingetPosts() example above.

For setters, the default value will be looked up on the mapped property, if there isone matching the setter name. But if the setter itself has an argument with a defaultvalue, it will take precedence.

So the following will make an input type with an optional fieldname with adefault valuejohn, an optional fieldfoo with a default valuedefaultFoo anda mandatory fieldbar without any default value:

#[ORM\Column(type:'string']private$name ='jane';publicfunctionsetName(string$name ='john'):void{$this->name =$name;}publicfunctionsetFoo(string$foo ='defaultFoo'):void{// do something}publicfunctionsetBar(string$bar):void{// do something}

Filtering and sorting

It is possible to expose generic filtering for entity fields and their types to let users easilycreate and apply generic filters. This expose basic SQL-like syntax that should cover most simplecases.

Filters are structured in an ordered list of groups. Each group contains an unordered set of joinsand conditions on fields. For simple cases a single group of a few conditions would probably be enough.But the ordered list of group allow more advanced filtering withOR logic between a set of conditions.

In the case of thePost class, it would generatethat GraphQL schemafor filtering, and for sorting it would bethat simpler schema.

For concrete examples of possibilities and variables syntax, refer to thetest cases.

For security and complexity reasons, it is not meant to solve advanced use cases. For those it ispossible to write custom filters and sorting.

Custom filters

A custom filer must extendAbstractOperator. This will allow to define custom arguments forthe API, and then a method to build the DQL condition corresponding to the argument.

This would also allow to filter on joined relations by carefully adding joins when necessary.

Then a custom filter might be used like so:

useDoctrine\ORM\MappingasORM;useGraphQL\Doctrine\AttributeasAPI;useGraphQLTests\Doctrine\Blog\Filtering\SearchOperatorType;/** * A blog post with title and body */#[ORM\Entity]#[API\Filter(field:'custom', operator: SearchOperatorType::class, type:'string')]finalclass Postextends AbstractModel

Custom sorting

A custom sorting option must implementSortingInterface. The constructor has no arguments andthe__invoke() must define how to apply the sorting.

Similarly to custom filters, it may be possible to carefully add joins if necessary.

Then a custom sorting might be used like so:

useDoctrine\ORM\MappingasORM;useGraphQL\Doctrine\AttributeasAPI;useGraphQLTests\Doctrine\Blog\Sorting\UserName;/** * A blog post with title and body */#[ORM\Entity]#[API\Sorting([UserName::class])]finalclass Postextends AbstractModel

Limitations

Namespaces

Theuse statement is not supported. So types in attributes or doc blocks mustbe the FQCN, or the name of a user-defined custom types (but never the short name of an entity).

Composite identifiers

Entities with composite identifiers are not supported for automatic creation ofinput types. Possible workarounds are to change input argument to be somethingelse than an entity, write custom input types and use them via attributes, oradapt the database schema.

Logical operators in filtering

Logical operators support only two levels, and second level cannot mix logic operators. In SQLthat would mean only one level of parentheses. So you can generate SQL that would look like:

-- mixed top levelWHERE cond1AND cond2OR cond3AND ...-- mixed top level and non-mixed sublevelsWHERE cond1OR (cond2OR cond3OR ...)AND (cond4AND cond5AND ...)OR ...

But youcannot generate SQL that would like that:

-- mixed sublevels does NOT workWHERE cond1AND (cond2OR cond3AND cond4)AND ...-- more than two levels will NOT workWHERE cond1OR (cond2AND (cond3OR cond4))OR ...

Those cases would probably end up being too complex to handle on the client-side. And we recommendinstead to implement them as a custom filter on the server side, in order to hide complexityfrom the client and benefit from Doctrine's QueryBuilder full flexibility.

Sorting on join

Out of the box, it is not possible to sort by a field from a joined relation.This should be done via a custom sorting to ensure that joins are done properly.

Prior work

Doctrine GraphQL Mapper hasbeen an inspiration to write this package. While the goals are similar, the wayit works is different. In Doctrine GraphQL Mapper, attributes are spread betweenproperties and methods (and classes for filtering), but we work only on methods.Setup seems slightly more complex, but might be more flexible. We built on conventionsand widespread use of PHP type hinting to have an easier out-of-the-box experience.

About

Automatic GraphQL types from Doctrine entities

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors6


[8]ページ先頭

©2009-2025 Movatter.jp