ecodev / graphql-doctrine
从 Doctrine 实体和属性声明 GraphQL 类型
Requires
- php: ^8.2
- doctrine/orm: ^2.15
- psr/container: ^1.1 || ^2.0
- webonyx/graphql-php: ^15.7
Requires (Dev)
- friendsofphp/php-cs-fixer: @stable
- laminas/laminas-servicemanager: @stable
- phpstan/phpstan: @stable
- phpunit/phpunit: @stable
- symfony/cache: @stable
- dev-master
- 10.0.1
- 10.0.0
- 9.0.0
- 8.1.2
- 8.1.1
- 8.1.0
- 8.0.1
- 8.0.0
- 7.3.0
- 7.2.4
- 7.2.3
- 7.2.2
- 7.2.1
- 7.2.0
- 7.1.4
- 7.1.3
- 7.1.2
- 7.1.1
- 7.1.0
- 7.0.0
- 6.4.4
- 6.4.3
- 6.4.2
- 6.4.1
- 6.4.0
- 6.3.0
- 6.2.0
- 6.1.0
- 6.0.0
- 5.0.1
- 5.0.0
- 4.0.0
- 3.0.0
- 2.0.1
- 2.0.0
- 1.1.0
- 1.0.0
- dev-doctrine3
- dev-dependabot/composer/doctrine/persistence-3.1.4
- dev-dependabot/composer/webonyx/graphql-php-15.2.1
- dev-persistence-v3
- dev-dependabot/add-v2-config-file
- dev-dependabot/composer/friendsofphp/php-cs-fixer-2.18.2
This package is auto-updated.
Last update: 2024-09-06 21:17:04 UTC
README
一个库,用于从 Doctrine 实体、PHP 类型提示和属性中声明 GraphQL 类型,并与 webonyx/graphql-php 一起使用。
它从类型提示中读取大部分信息,从现有的 Doctrine 属性中完成一些事情,并允许通过特定的属性进行进一步的定制。然后,它会创建具有所有在 Doctrine 实体上找到的 getter 和 setter 字段的 ObjectType
和 InputObjectType
实例。
它不会构建整个模式。用户需要使用自动类型和其他自定义类型来定义根查询。
快速开始
通过 composer 安装库
composer require ecodev/graphql-doctrine
并开始使用它
<?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... }, ], ], ]), ]);
用法
公共 API 限制在 TypesInterface
的公共方法、Types
的构造函数和属性上。
以下是对 TypesInterface
的简要概述
$types->get()
获取自定义类型$types->getOutput()
获取用于查询的ObjectType
$types->getFilter()
获取用于查询的InputObjectType
$types->getSorting()
获取用于查询的InputObjectType
$types->getInput()
获取用于变更(通常是创建)的InputObjectType
$types->getPartialInput()
获取用于变更(通常是更新)的InputObjectType
$types->getId()
获取可能用于从数据库接收对象的EntityIDType
而不是标量$types->has()
检查是否存在类型$types->createFilteredQueryBuilder()
用于查询解析器
信息优先级
为了最大限度地减少代码重复,信息来自几个地方,其中可用。并且这些都可以被覆盖。从最不重要的是到最重要的事物的优先级顺序是
- 类型提示
- 文档块
- 属性
这意味着始终可以使用属性覆盖一切。但现有的类型提示和文档块应该涵盖了大多数情况。
排除敏感内容
默认情况下,类型包含所有获取器和设置器,并且所有属性都包含在过滤器中。但对于每个方法和属性都可以指定其他内容。
要排除从 API 中公开的敏感字段,请使用 #[API\Exclude]
use GraphQL\Doctrine\Attribute as API; /** * Returns the hashed password * * @return string */ #[API\Exclude] public function getPassword(): string { return $this->password; }
要排除属性作为过滤器公开
use GraphQL\Doctrine\Attribute as API; #[ORM\Column(name: 'password', type: 'string', length: 255)] #[API\Exclude] private string $password = '';
覆盖输出类型
即使获取器返回 PHP 标量类型,如 string
,也可能更喜欢使用自定义 GraphQL 类型来覆盖类型。这对于枚举或其他验证目的(如电子邮件地址)通常很有用。这可以通过指定通过 #[API\Field]
属性的 GraphQL 类型 FQCN 来完成
use GraphQL\Doctrine\Attribute as API; use GraphQLTests\Doctrine\Blog\Types\PostStatusType; /** * Get status * * @return string */ #[API\Field(type: PostStatusType::class)] public function getStatus(): string { return $this->status; }
类型语法
在大多数情况下,类型必须使用 ::class
符号来指定 PHP 类,该类可以是实现 GraphQL 类型的类或实体本身(参见 限制)。只有当必须将其定义为可空且/或为数组时才使用字符串字面量。永远不要使用实体的短名(仅适用于用户定义的自定义类型)。
支持的语法(PHP 风格或 GraphQL 风格)包括
MyType::class
'?Application\MyType'
'null|Application\MyType'
'Application\MyType|null'
'Application\MyType[]'
'?Application\MyType[]'
'null|Application\MyType[]'
'Application\MyType[]|null'
'Collection
'
此属性可用于覆盖其他内容,例如 name
、description
和 args
。
覆盖参数
类似于 #[API\Field]
,#[API\Argument]
允许在 PHP 类型提示不足的情况下覆盖参数的类型。
use GraphQL\Doctrine\Attribute as API; /** * Returns all posts of the specified status * * @param string $status the status of posts as defined in \GraphQLTests\Doctrine\Blog\Model\Post * * @return Collection */ public function getPosts( #[API\Argument(type: '?GraphQLTests\Doctrine\Blog\Types\PostStatusType')] ?string $status = Post::STATUS_PUBLIC ): Collection { // ... }
它还可以覆盖其他内容,例如 name
、description
和 defaultValue
。
覆盖输入类型
#[API\Input]
是 #[API\Field]
的对立面,可用于覆盖输入类型(设置器)的内容,通常用于验证目的。这看起来是这样的
use GraphQL\Doctrine\Attribute as API; use GraphQLTests\Doctrine\Blog\Types\PostStatusType; /** * Set status * * @param string $status */ #[API\Input(type: PostStatusType::class)] public function setStatus(string $status = self::STATUS_PUBLIC): void { $this->status = $status; }
此属性也支持 description
和 defaultValue
。
覆盖过滤器类型
#[API\FilterGroupCondition]
是从属性生成的过滤器的等效项。因此,用法如下
use GraphQL\Doctrine\Attribute as API; #[API\FilterGroupCondition(type: '?GraphQLTests\Doctrine\Blog\Types\PostStatusType')] #[ORM\Column(type: 'string', options: ['default' => self::STATUS_PRIVATE])] private string $status = self::STATUS_PRIVATE;
需要注意的是,指定类型的值将直接用于 DQL。这意味着如果该值不是 PHP 标量,则它必须可以通过 __toString()
转换为字符串,或者您必须在将筛选器值传递给 Types::createFilteredQueryBuilder()
之前自行进行转换。
自定义类型
默认情况下,所有 PHP 标量类型和 Doctrine 集合都会自动检测并映射到 GraphQL 类型。但是,如果某些 getter 返回自定义类型,例如 DateTimeImmutable
或自定义类,则必须在之前进行配置。
配置是通过根据您的需求配置的 PSR-11 容器 实现完成的。在以下示例中,我们使用 laminas/laminas-servicemanager,因为它提供了有用的概念,例如:可调用对象、别名、工厂和抽象工厂。但也可以使用任何其他 PSR-11 容器实现。
键应该是您在模型中用来引用类型的任何内容。通常,这将是对 PHP 类 "原生" 类型(如 DateTimeImmutable
)的 FQCN、实现 GraphQL 类型的 PHP 类的 FQCN,或直接是 GraphQL 类型名
$customTypes = new ServiceManager([ 'invokables' => [ DateTimeImmutable::class => DateTimeType::class, 'PostStatus' => PostStatusType::class, ], ]); $types = new Types($entityManager, $customTypes); // Build schema...
这样,就无需为返回配置类型之一的每个 getter 进行注释。它将自动映射。
实体作为输入参数
如果 getter 使用实体作为参数,则会自动创建一个特殊的 InputType
以接受 ID
。然后,实体将自动从数据库中获取并转发给 getter。因此,这可以无缝工作
public function isAllowedEditing(User $user): bool { return $this->getUser() === $user; }
您还可以通过使用 Types::getId()
来获取实体的输入类型,如下所示
[ // ... 'args' => [ 'id' => $types->getId(Post::class), ], 'resolve' => function ($root, array $args) { $post = $args['id']->getEntity(); // ... }, ]
部分输入
除了正常输入类型之外,还可以通过 getPartialInput()
获取部分输入类型。这对于更新现有实体的突变特别有用,当我们不想重新提交所有字段时。通过使用部分输入,API 客户端可以仅提交需要更新的字段,而无需提交更多内容。
这可能会减少网络流量,因为客户端不需要获取所有字段,只是为了在他想修改一个字段时再次提交。
它还允许轻松设计批量编辑突变,客户端只需提交少量需要更新的字段即可一次性更新多个实体。这可以看起来像
<?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... } ], ];
默认值
默认值是从getters的参数自动检测到的,如上面getPosts()
示例所示。
对于setter,将在映射的属性中查找默认值,如果存在匹配setter名称的属性。但如果setter本身有一个具有默认值的参数,它将具有优先级。
因此,以下将创建一个具有可选字段name
(默认值为john
)、可选字段foo
(默认值为defaultFoo
)和必填字段bar
(没有默认值)的输入类型。
#[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 }
过滤和排序
可以公开对实体字段及其类型的通用过滤,以便用户轻松创建和应用通用过滤器。这公开了类似于SQL的基本语法,应该涵盖大多数简单情况。
过滤器是按顺序排列的组列表。每个组包含字段上的连接和条件的不有序集合。对于简单情况,一个包含少量条件的单个组可能就足够了。但是,有序的组列表允许使用一组条件之间的OR
逻辑进行更高级的过滤。
在Post
类的例子中,它会生成用于过滤的该GraphQL模式,用于排序的将是该更简单的模式。
有关具体示例和变量语法,请参阅测试用例。
出于安全和复杂性的原因,它不是为了解决高级用例。对于这些情况,可以编写自定义过滤和排序。
自定义过滤
自定义过滤器必须扩展AbstractOperator
。这将允许为API定义自定义参数,然后定义一个方法来构建与参数相对应的DQL条件。
这还允许通过在必要时谨慎添加连接来过滤连接关系。
然后,自定义过滤器可以这样使用
use Doctrine\ORM\Mapping as ORM; use GraphQL\Doctrine\Attribute as API; use GraphQLTests\Doctrine\Blog\Filtering\SearchOperatorType; /** * A blog post with title and body */ #[ORM\Entity] #[API\Filter(field: 'custom', operator: SearchOperatorType::class, type: 'string')] final class Post extends AbstractModel
自定义排序
自定义排序选项必须实现SortingInterface
。构造函数没有参数,并且__invoke()
必须定义如何应用排序。
与自定义过滤器类似,如果需要,可能可以谨慎添加连接。
然后,自定义排序可以这样使用
use Doctrine\ORM\Mapping as ORM; use GraphQL\Doctrine\Attribute as API; use GraphQLTests\Doctrine\Blog\Sorting\UserName; /** * A blog post with title and body */ #[ORM\Entity] #[API\Sorting([UserName::class])] final class Post extends AbstractModel
限制
命名空间
不支持use
语句。因此,属性或doc块中的类型必须是FQCN,或者用户定义的自定义类型的名称(但永远不是实体的简称)。
复合标识符
具有复合标识符的实体不支持自动创建输入类型。可能的解决方案是将输入参数更改为不是实体,编写自定义输入类型并通过属性使用它们,或者调整数据库模式。
过滤中的逻辑运算符
逻辑运算符仅支持两个级别,第二级不能混合逻辑运算符。在SQL中,这意味着只有一个括号级别。因此,可以生成看起来像这样的SQL
-- 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 ...
但是,不能生成看起来像这样的SQL
-- 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 ...
这些情况可能过于复杂,无法在客户端处理。我们建议在服务器端实现它们作为自定义过滤器,以隐藏客户端的复杂性,并从Doctrine的QueryBuilder的完整灵活性中受益。
连接排序
默认情况下,无法通过连接关系的字段进行排序。这应通过自定义排序来实现,以确保连接执行正确。
前期工作
Doctrine GraphQL Mapper 为编写此包提供了灵感。尽管目标相似,但工作方式不同。在 Doctrine GraphQL Mapper 中,属性分散在属性、方法和(用于过滤的)类中,但我们只关注方法。设置似乎稍微复杂一些,但可能更灵活。我们基于约定和 PHP 类型提示的广泛使用,以提供更易于使用的默认体验。