在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
开源软件名称(OpenSource Name):Ecodev/graphql-doctrine开源软件地址(OpenSource Url):https://github.com/Ecodev/graphql-doctrine开源编程语言(OpenSource Language):PHP 99.6%开源软件介绍(OpenSource Introduction):GraphQL DoctrineA library to declare GraphQL types from Doctrine entities, PHP type hinting, and annotations, and to be used with webonyx/graphql-php. It reads most information from type hints, complete some things from existing
Doctrine annotations and allow further customizations with specialized annotations.
It will then create It will not build the entire schema. It is up to the user to use automated types, and other custom types, to define root queries. Quick startInstall the library via composer: composer require ecodev/graphql-doctrine And start using it: <?php
use GraphQLTests\Doctrine\Blog\Model\Post;
use GraphQLTests\Doctrine\Blog\Types\DateTimeType;
use GraphQLTests\Doctrine\Blog\Types\PostStatusType;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Schema;
use GraphQL\Doctrine\DefaultFieldResolver;
use GraphQL\Doctrine\Types;
use Laminas\ServiceManager\ServiceManager;
// Define custom types with a PSR-11 container
$customTypes = new ServiceManager([
'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 = new Types($entityManager, $customTypes);
// Configure default field resolver to be able to use getters
GraphQL::setDefaultFieldResolver(new DefaultFieldResolver());
// Build your Schema
$schema = new Schema([
'query' => new ObjectType([
'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' => new ObjectType([
'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...
},
],
],
]),
]); UsageThe public API is limited to the public methods on Here is a quick overview of
Information priorityTo avoid code duplication as much as possible, information are gathered from several places, where available. And each of those might be overridden. The order of priority, from the least to most important is:
That means it is always possible to override everything with annotations. But existing type hints and dock blocks should cover the majority of cases. Exclude sensitive thingsAll 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 use GraphQL\Doctrine\Annotation as API;
/**
* Returns the hashed password
*
* @API\Exclude
*
* @return string
*/
public function getPassword(): string
{
return $this->password;
} And to exclude a property from being exposed as a filter: use GraphQL\Doctrine\Annotation as API;
/**
* @var string
*
* @API\Exclude
*
* @ORM\Column(type="string", length=255)
*/
private $password = ''; Override output typesEven if a getter returns a PHP scalar type, such as use GraphQL\Doctrine\Annotation as API;
/**
* Get status
*
* @API\Field(type="GraphQLTests\Doctrine\Blog\Types\PostStatusType")
*
* @return string
*/
public function getStatus(): string
{
return $this->status;
} The type must be the PHP class implementing the GraphQL type (see limitations). The declaration can be defined as nullable and/or as an array with one the following syntaxes (PHP style or GraphQL style):
This annotation can be used to override other things, such as Override argumentsSimilarly to use GraphQL\Doctrine\Annotation as API;
/**
* Returns all posts of the specified status
*
* @API\Field(args={@API\Argument(name="status", type="?GraphQLTests\Doctrine\Blog\Types\PostStatusType")})
*
* @param string $status the status of posts as defined in \GraphQLTests\Doctrine\Blog\Model\Post
*
* @return Collection
*/
public function getPosts(?string $status = Post::STATUS_PUBLIC): Collection
{
// ...
} Once again, it also allows to override other things such as Override input types
use GraphQL\Doctrine\Annotation as API;
/**
* Set status
*
* @API\Input(type="GraphQLTests\Doctrine\Blog\Types\PostStatusType")
*
* @param string $status
*/
public function setStatus(string $status = self::STATUS_PUBLIC): void
{
$this->status = $status;
} This annotation also supports Override filter types
use GraphQL\Doctrine\Annotation as API;
/**
* @var string
*
* @API\FilterGroupCondition(type="?GraphQLTests\Doctrine\Blog\Types\PostStatusType")
* @ORM\Column(type="string", options={"default" = Post::STATUS_PRIVATE})
*/
private $status = self::STATUS_PRIVATE; An important thing to note is that the value of the type specified will be directly used in DQL. That means
that if the value is not a PHP scalar, then it must be convertible to string via Custom typesBy default all PHP scalar types and Doctrine collection are automatically detected
and mapped to a GraphQL type. However if some getter return custom types, such
as The configuration is done with a PSR-11 container implementation configured according to your needs. In the following example, we use laminas/laminas-servicemanager, because it offers useful concepts such as: invokables, aliases, factories and abstract factories. 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 as $customTypes = new ServiceManager([
'invokables' => [
DateTimeImmutable::class => DateTimeType::class,
'PostStatus' => PostStatusType::class,
],
]);
$types = new Types($entityManager, $customTypes);
// Build schema... That way it is not necessary to annotate every single getter returning one of the configured type. It will be mapped automatically. Entities as input argumentsIf a getter takes an entity as parameter, then a specialized public function isAllowedEditing(User $user): bool
{
return $this->getUser() === $user;
} You may also get an input type for an entity by using [
// ...
'args' => [
'id' => $types->getId(Post::class),
],
'resolve' => function ($root, array $args) {
$post = $args['id']->getEntity();
// ...
},
] Partial inputsIn addition to normal input types, it is possible to get a partial input type via
This potentially reduces network traffic, because the client does not need to fetch all fields just to be able re-submit them when he wants to modify only one field. And it also enable to easily design mass editing mutations where the client would submit 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 valuesDefault values are automatically detected from arguments for getters, as seen in
For setters, the default value will be looked up on the mapped property, if there is one matching the setter name. But if the setter itself has an argument with a default value, it will take precedence. So the following will make an input type with an optional field /**
* @ORM\Column(type="string")
*/
private $name = 'jane';
public function setName(string $name = 'john'): void
{
$this->name = $name;
}
public function setFoo(string $foo = 'defaultFoo'): void
{
// do something
}
public function setBar(string $bar): void
{
// do something
} Filtering and sortingIt is possible to expose generic filtering for entity fields and their types to let users easily create and apply generic filters. This expose basic SQL-like syntax that should cover most simple cases. Filters are structured in an ordered list of groups. Each group contains an unordered set of joins
and 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 with In the case of the For concrete examples of possibilities and variables syntax, refer to the test cases. For security and complexity reasons, it is not meant to solve advanced use cases. For those it is possible to write custom filters and sorting. Custom filtersA custom filer must extend This would also allow to filter on joined relations by carefully adding joins when necessary. Then a custom filter might be used like so: use GraphQL\Doctrine\Annotation as API;
/**
* A blog post with title and body
*
* @ORM\Entity
* @API\Filters({
* @API\Filter(field="custom", operator="GraphQLTests\Doctrine\Blog\Filtering\SearchOperatorType", type="string")
* })
*/
final class Post extends AbstractModel Custom sortingA custom sorting option must implement Similarly to custom filters, it may be possible to carefully add joins if necessary. Then a custom sorting might be used like so: use GraphQL\Doctrine\Annotation as API;
/**
* A blog post with title and body
*
* @ORM\Entity
* @API\Sorting({"GraphQLTests\Doctrine\Blog\Sorting\UserName"})
*/
final class Post extends AbstractModel LimitationsNamespacesThe Composite identifiersEntities with composite identifiers are not supported for automatic creation of input types. Possible workarounds are to change input argument to be something else than an entity, write custom input types and use them via annotations, or adapt the database schema. Logical operators in filteringLogical operators support only two levels, and second level cannot mix logic operators. In SQL that would means only one level of parentheses. So you can generate SQL that would look like: -- mixed top level
WHERE cond1 AND cond2 OR cond3 AND ...
-- mixed top level and non-mixed sublevels
WHERE cond1 OR (cond2 OR cond3 OR ...) AND (cond4 AND cond5 AND ...) OR ... But you cannot generate SQL that would like that: -- mixed sublevels does NOT work
WHERE cond1 AND (cond2 OR cond3 AND cond4) AND ...
-- more than two levels will NOT work
WHERE cond1 OR (cond2 AND (cond3 OR cond4)) OR ... Those cases would probably end up being too complex to handle on the client-side. And we recommend instead to implement them as a custom filter on the server side, in order to hide complexity from the client and benefit from Doctrine's QueryBuilder full flexibility. Sorting on joinOut 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 workDoctrine GraphQL Mapper has been an inspiration to write this package. While the goals are similar, the way it works is different. In Doctrine GraphQL Mapper, annotations are spread between properties 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 conventions and widespread use of PHP type hinting to have an easier out-of-the-box experience. |
2023-10-27
2022-08-15
2022-08-17
2022-09-23
2022-08-13
请发表评论