├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── README.md
├── composer.json
├── docs
└── README-zh.md
├── phpunit.xml.dist
├── src
├── ErrorFormatter.php
├── GraphQL.php
├── GraphQLAction.php
├── GraphQLModuleTrait.php
├── TypeResolution.php
├── base
│ ├── GraphQLField.php
│ ├── GraphQLInterfaceType.php
│ ├── GraphQLModel.php
│ ├── GraphQLMutation.php
│ ├── GraphQLQuery.php
│ ├── GraphQLType.php
│ └── GraphQLUnionType.php
├── exceptions
│ ├── SchemaNotFound.php
│ ├── TypeNotFound.php
│ └── ValidatorException.php
├── filters
│ └── auth
│ │ └── CompositeAuth.php
├── traits
│ ├── GlobalIdTrait.php
│ └── ShouldValidate.php
└── types
│ ├── EmailType.php
│ ├── PageInfoType.php
│ ├── PaginationType.php
│ ├── SimpleExpressionType.php
│ └── UrlType.php
└── tests
├── DefaultController.php
├── GraphQLActionTest.php
├── GraphQLTest.php
├── GraphqlMutationTest.php
├── GraphqlQueryTest.php
├── Module.php
├── TestCase.php
├── bootstrap.php
├── data
├── Comment.php
├── DataSource.php
├── Image.php
├── Story.php
└── User.php
├── objects
├── mutation
│ └── UpdateUserPwdMutation.php
├── queries.php
├── query
│ ├── HelloQuery.php
│ ├── LastStoryPostedQuery.php
│ ├── NodeQuery.php
│ ├── SearchQuery.php
│ ├── StoryListQuery.php
│ ├── UserQuery.php
│ └── ViewerQuery.php
└── types
│ ├── CommentType.php
│ ├── ContentFormatEnumType.php
│ ├── ExampleType.php
│ ├── HtmlField.php
│ ├── ImageSizeEnumType.php
│ ├── ImageType.php
│ ├── NodeType.php
│ ├── ResultItemConnectionType.php
│ ├── ResultItemType.php
│ ├── StoryType.php
│ └── UserType.php
└── types
└── SimpleExpressionTypeTest.php
/.gitignore:
--------------------------------------------------------------------------------
1 | .DStore
2 | .idea
3 | vendor
4 | tests/runtime
5 | composer.lock
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | #
2 | # Travis Setup
3 | #
4 |
5 | # faster builds on new travis setup not using sudo
6 | sudo: false
7 |
8 | # build only on master branches
9 | # commented as this prevents people from running builds on their forks:
10 | # https://github.com/yiisoft/yii2/commit/bd87be990fa238c6d5e326d0a171f38d02dc253a
11 | #branches:
12 | # only:
13 | # - master
14 | # - 2.1
15 |
16 |
17 | #
18 | # Test Matrix
19 | #
20 |
21 | language: php
22 |
23 | env:
24 | global:
25 | - DEFAULT_COMPOSER_FLAGS="--prefer-dist --no-interaction --no-progress --optimize-autoloader"
26 | - TASK_TESTS_PHP=1
27 | - TASK_TESTS_JS=0
28 | - TASK_TESTS_COVERAGE=0
29 |
30 | # cache vendor dirs
31 | cache:
32 | directories:
33 | - vendor
34 | - $HOME/.composer/cache
35 | - $HOME/.npm
36 |
37 | matrix:
38 | fast_finish: true
39 | include:
40 | # run tests coverage on PHP 7.1
41 | - php: 7.1
42 | env: TASK_TESTS_COVERAGE=1
43 |
44 | - php: 7.0
45 | allow_failures:
46 | - php: 7.1
47 |
48 | install:
49 | - |
50 | if [[ $TASK_TESTS_COVERAGE != 1 && $TRAVIS_PHP_VERSION != hhv* ]]; then
51 | # disable xdebug for performance reasons when code coverage is not needed. note: xdebug on hhvm is disabled by default
52 | phpenv config-rm xdebug.ini || echo "xdebug is not installed"
53 | fi
54 |
55 | # install composer dependencies
56 | - travis_retry composer self-update
57 | - export PATH="$HOME/.composer/vendor/bin:$PATH"
58 | - travis_retry composer global require "fxp/composer-asset-plugin:^1.3.1"
59 | - travis_retry composer install $DEFAULT_COMPOSER_FLAGS
60 |
61 | before_script:
62 | # show some versions and env information
63 | - php --version
64 | - composer --version
65 | - |
66 | if [ $TASK_TESTS_JS == 1 ]; then
67 | node --version
68 | npm --version
69 | fi
70 | # enable code coverage
71 | - |
72 | if [ $TASK_TESTS_COVERAGE == 1 ]; then
73 | PHPUNIT_FLAGS="--coverage-clover=coverage.clover"
74 | fi
75 |
76 |
77 | script:
78 | # PHP tests
79 | - |
80 | if [ $TASK_TESTS_PHP == 1 ]; then
81 | vendor/bin/phpunit --verbose $PHPUNIT_FLAGS --configuration phpunit.xml.dist
82 | fi
83 |
84 | after_script:
85 | - |
86 | if [ $TASK_TESTS_COVERAGE == 1 ]; then
87 | travis_retry wget https://scrutinizer-ci.com/ocular.phar
88 | php ocular.phar code-coverage:upload --format=php-clover coverage.clover
89 | fi
90 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | Yii2 Graphql Changelog
2 | =======================
3 | # 0.13
4 | - update for graphql-php(v0.13)
5 | - add graphql-upload(v4.0.0)
6 | # 0.11
7 | - update for graphql-php(v0.11)
8 | - Enh: default ErrorFormatter log the error from graphql-php
9 | # 0.9.1
10 | - Enh: add the default errorFormat to format the model validator error,add "code" field to response errors segment.
11 | # 0.9
12 | - New support facebook graphql server
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | yii-graphql
2 | ==========
3 | Using Facebook [GraphQL](http://facebook.github.io/graphql/) PHP server implementation. Extends [graphql-php](https://github.com/webonyx/graphql-php) to apply to YII2.
4 |
5 | [](https://packagist.org/packages/tsingsun/yii2-graphql)
6 | [](https://travis-ci.org/tsingsun/yii2-graphql)
7 | [](https://packagist.org/packages/tsingsun/yii2-graphql)
8 |
9 | --------
10 |
11 | [Chinese document](/docs/README-zh.md)
12 |
13 | -------
14 |
15 | Features
16 |
17 | * Configuration includes simplifying the definition of standard graphql protocols.
18 | * Based on the full name defined by the type, implementing on-demand loading and lazy loading, and no need to define all type definitions into the system at load.
19 | * Mutation input validation support.
20 | * Provide controller integration and authorization support.
21 |
22 | ### Install
23 |
24 | Using [composer](https://getcomposer.org/)
25 | ```
26 | composer require tsingsun/yii2-graphql
27 | ```
28 |
29 | ### Type
30 | The type system is the core of GraphQL, which is embodied in `GraphQLType`. By deconstructing the GraphQL protocol and using the [graph-php](https://github.com/webonyx/graphql-php) library to achieve fine-grained control of all elements, it is convenient to extend the class according to its own needs
31 |
32 |
33 | #### The main elements of `GraphQLType`
34 |
35 | The following elements can be declared in the `$attributes` property of the class, or as a method, unless stated otherwise. This also applies to all elements after this.
36 |
37 | Element | Type | Description
38 | ----- | ----- | -----
39 | `name` | string | **Required** Each type needs to be named, with unique names preferred to resolve potential conflicts. The property needs to be defined in the `$attributes` property.
40 | `description` | string | A description of the type and its use. The property needs to be defined in the `$attributes` property.
41 | `fields` | array | **Required** The included field content is represented by the fields () method.
42 | `resolveField` | callback | **function($value, $args, $context, GraphQL\Type\Definition\ResolveInfo $info)** For the interpretation of a field. For example: the fields definition of the user property, the corresponding method is `resolveUserField()`, and `$value` is the passed type instance defined by `type`.
43 |
44 | ### Query
45 |
46 | `GraphQLQuery` and `GraphQLMutation` inherit `GraphQLField`. The element structure is consistent, and if you would like a reusable `Field`, you can inherit it.
47 | Each query of `Graphql` needs to correspond to a `GraphQLQuery` object
48 |
49 | #### The main elements of `GraphQLField`
50 |
51 | Element | Type | Description
52 | ----- | ----- | -----
53 | `type` | ObjectType | For the corresponding query type. The single type is specified by `GraphQL::type`, and a list by `Type::listOf(GraphQL::type)`.
54 | `args` | array | The available query parameters, each of which is defined by `Field`.
55 | `resolve` | callback | **function($value, $args, $context, GraphQL\Type\Definition\ResolveInfo $info)** `$value` is the root data, `$args` is the query parameters, `$context` is the `yii\web\Application` object, and `$info` resolves the object for the query. The root object is handled in this method.
56 |
57 | ### Mutation
58 |
59 | Definition is similar to `GraphQLQuery`, please refer to the above.
60 |
61 | ### Simplified Field Definition
62 |
63 | Simplifies the declarations of `Field`, removing the need to defined as an array with the type key.
64 |
65 | #### Standard Definition
66 |
67 | ```php
68 | //...
69 | 'id' => [
70 | 'type' => Type::id(),
71 | ],
72 | //...
73 | ```
74 |
75 | #### Simplified Definition
76 |
77 | ```php
78 | //...
79 | 'id' => Type::id(),
80 | //...
81 | ```
82 |
83 | ### Yii Implementation
84 |
85 | ### General configuration
86 |
87 | JsonParser configuration required
88 |
89 | ```php
90 | 'components' => [
91 | 'request' => [
92 | 'parsers' => [
93 | 'application/json' => 'yii\web\JsonParser',
94 | ],
95 | ],
96 | ];
97 | ```
98 |
99 | #### Module support
100 |
101 | Can easily be implemented with `yii\graphql\GraphQLModuleTrait`. The trait is responsible for initialization.
102 |
103 | ```php
104 | class MyModule extends \yii\base\Module
105 | {
106 | use \yii\graphql\GraphQLModuleTrait;
107 | }
108 | ```
109 |
110 | In your application configuration file:
111 |
112 | ```php
113 | 'modules'=>[
114 | 'moduleName ' => [
115 | 'class' => 'path\to\module'
116 | //graphql config
117 | 'schema' => [
118 | 'query' => [
119 | 'user' => 'app\graphql\query\UsersQuery'
120 | ],
121 | 'mutation' => [
122 | 'login'
123 | ],
124 | // you do not need to set the types if your query contains interfaces or fragments
125 | // the key must same as your defined class
126 | 'types' => [
127 | 'Story' => 'yiiunit\extensions\graphql\objects\types\StoryType'
128 | ],
129 | ],
130 | ],
131 | ];
132 | ```
133 |
134 | Use the controller to receive requests by using `yii\graphql\GraphQLAction`
135 |
136 | ```php
137 | class MyController extends Controller
138 | {
139 | function actions() {
140 | return [
141 | 'index'=>[
142 | 'class'=>'yii\graphql\GraphQLAction'
143 | ],
144 | ];
145 | }
146 | }
147 | ```
148 |
149 | #### Component Support
150 | also you can include the trait with your own components,then initialization yourself.
151 | ```php
152 | 'components'=>[
153 | 'componentsName' => [
154 | 'class' => 'path\to\components'
155 | //graphql config
156 | 'schema' => [
157 | 'query' => [
158 | 'user' => 'app\graphql\query\UsersQuery'
159 | ],
160 | 'mutation' => [
161 | 'login'
162 | ],
163 | // you do not need to set the types if your query contains interfaces or fragments
164 | // the key must same as your defined class
165 | 'types'=>[
166 | 'Story'=>'yiiunit\extensions\graphql\objects\types\StoryType'
167 | ],
168 | ],
169 | ],
170 | ];
171 | ```
172 |
173 |
174 | ### Input validation
175 |
176 | Validation rules are supported.
177 | In addition to graphql based validation, you can also use Yii Model validation, which is currently used for the validation of input parameters. The rules method is added directly to the mutation definition.
178 |
179 | ```php
180 | public function rules() {
181 | return [
182 | ['password','boolean']
183 | ];
184 | }
185 | ```
186 |
187 | ### Authorization verification
188 |
189 | Since graphql queries can be combined, such as when a query merges two query, and the two query have different authorization constraints, custom authentication is required.
190 | I refer to this query as "graphql actions"; when all graphql actions conditions are configured, it passes the authorization check.
191 |
192 | #### Authenticate
193 |
194 | In the behavior method of controller, the authorization method is set as follows
195 |
196 | ```php
197 | function behaviors() {
198 | return [
199 | 'authenticator'=>[
200 | 'class' => 'yii\graphql\filter\auth\CompositeAuth',
201 | 'authMethods' => [
202 | \yii\filters\auth\QueryParamAuth::className(),
203 | ],
204 | 'except' => ['hello']
205 | ],
206 | ];
207 | }
208 | ```
209 | If you want to support IntrospectionQuery authorization, the corresponding graphql action is `__schema`
210 |
211 | #### Authorization
212 |
213 | If the user has passed authentication, you may want to check the access for the resource. You can use `GraphqlAction`'s `checkAccess` method
214 | in the controller. It will check all graphql actions.
215 |
216 | ```php
217 | class GraphqlController extends Controller
218 | {
219 | public function actions() {
220 | return [
221 | 'index' => [
222 | 'class' => 'yii\graphql\GraphQLAction',
223 | 'checkAccess'=> [$this,'checkAccess'],
224 | ]
225 | ];
226 | }
227 |
228 | /**
229 | * authorization
230 | * @param $actionName
231 | * @throws yii\web\ForbiddenHttpException
232 | */
233 | public function checkAccess($actionName) {
234 | $permissionName = $this->module->id . '/' . $actionName;
235 | $pass = Yii::$app->getAuthManager()->checkAccess(Yii::$app->user->id,$permissionName);
236 | if (!$pass){
237 | throw new yii\web\ForbiddenHttpException('Access Denied');
238 | }
239 | }
240 | }
241 | ```
242 |
243 | ### Demo
244 |
245 | #### Creating queries based on graphql protocols
246 |
247 | Each query corresponds to a GraphQLQuery file.
248 |
249 | ```php
250 | class UserQuery extends GraphQLQuery
251 | {
252 | public function type() {
253 | return GraphQL::type(UserType::class);
254 | }
255 |
256 | public function args() {
257 | return [
258 | 'id'=>[
259 | 'type' => Type::nonNull(Type::id())
260 | ],
261 | ];
262 | }
263 |
264 | public function resolve($value, $args, $context, ResolveInfo $info) {
265 | return DataSource::findUser($args['id']);
266 | }
267 |
268 |
269 | }
270 | ```
271 |
272 | Define type files based on query protocols
273 |
274 | ```php
275 |
276 | class UserType extends GraphQLType
277 | {
278 | protected $attributes = [
279 | 'name'=>'user',
280 | 'description'=>'user is user'
281 | ];
282 |
283 | public function fields()
284 | {
285 | $result = [
286 | 'id' => ['type'=>Type::id()],
287 | 'email' => Types::email(),
288 | 'email2' => Types::email(),
289 | 'photo' => [
290 | 'type' => GraphQL::type(ImageType::class),
291 | 'description' => 'User photo URL',
292 | 'args' => [
293 | 'size' => Type::nonNull(GraphQL::type(ImageSizeEnumType::class)),
294 | ]
295 | ],
296 | 'firstName' => [
297 | 'type' => Type::string(),
298 | ],
299 | 'lastName' => [
300 | 'type' => Type::string(),
301 | ],
302 | 'lastStoryPosted' => GraphQL::type(StoryType::class),
303 | 'fieldWithError' => [
304 | 'type' => Type::string(),
305 | 'resolve' => function() {
306 | throw new \Exception("This is error field");
307 | }
308 | ]
309 | ];
310 | return $result;
311 | }
312 |
313 | public function resolvePhotoField(User $user,$args){
314 | return DataSource::getUserPhoto($user->id, $args['size']);
315 | }
316 |
317 | public function resolveIdField(User $user, $args)
318 | {
319 | return $user->id.'test';
320 | }
321 |
322 | public function resolveEmail2Field(User $user, $args)
323 | {
324 | return $user->email2.'test';
325 | }
326 |
327 |
328 | }
329 | ```
330 |
331 | #### Query instance
332 |
333 | ```php
334 | 'hello' => "
335 | query hello{hello}
336 | ",
337 |
338 | 'singleObject' => "
339 | query user {
340 | user(id:\"2\") {
341 | id
342 | email
343 | email2
344 | photo(size:ICON){
345 | id
346 | url
347 | }
348 | firstName
349 | lastName
350 |
351 | }
352 | }
353 | ",
354 | 'multiObject' => "
355 | query multiObject {
356 | user(id: \"2\") {
357 | id
358 | email
359 | photo(size:ICON){
360 | id
361 | url
362 | }
363 | }
364 | stories(after: \"1\") {
365 | id
366 | author{
367 | id
368 | }
369 | body
370 | }
371 | }
372 | ",
373 | 'updateObject' => "
374 | mutation updateUserPwd{
375 | updateUserPwd(id: \"1001\", password: \"123456\") {
376 | id,
377 | username
378 | }
379 | }
380 | "
381 | ```
382 |
383 | ### Exception Handling
384 |
385 | You can config the error formater for graph. The default handle uses `yii\graphql\ErrorFormatter`,
386 | which optimizes the processing of Model validation results.
387 |
388 | ```php
389 | 'modules'=>[
390 | 'moduleName' => [
391 | 'class' => 'path\to\module'
392 | 'errorFormatter' => ['yii\graphql\ErrorFormatter', 'formatError'],
393 | ],
394 | ];
395 | ```
396 |
397 | ### Future
398 |
399 | * `ActiveRecord` tool for generating query and mutation class.
400 | * Some of the special syntax for graphql, such as `@Directives`, has not been tested
401 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name":"tsingsun/yii2-graphql",
3 | "description":"facebook graphql server side for yii2 php framework",
4 | "keyword":["yii2","graphql"],
5 | "type":"yii2-extension",
6 | "license": "BSD-3-Clause",
7 | "authors":[
8 | {
9 | "name":"QingShan li",
10 | "email":"21997272@qq.com"
11 | }
12 | ],
13 | "minimum-stability":"dev",
14 | "require":{
15 | "php": ">=5.6.0",
16 | "yiisoft/yii2":"~2.0.10",
17 | "webonyx/graphql-php": "^0.13.0",
18 | "ecodev/graphql-upload": "^4.0.0",
19 | "zendframework/zend-diactoros": "^2.1"
20 | },
21 | "require-dev":{
22 | "phpunit/phpunit": "4.8.*"
23 | },
24 | "autoload":{
25 | "psr-4":{
26 | "yii\\graphql\\":"src"
27 | }
28 | },
29 | "archive": {
30 | "exclude": [
31 | "/tests"
32 | ]
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/docs/README-zh.md:
--------------------------------------------------------------------------------
1 | # yii-graphql #
2 |
3 | 使用 Facebook [GraphQL](http://facebook.github.io/graphql/) 的PHP服务端实现. 扩展 [graphql-php](https://github.com/webonyx/graphql-php) 以适用于 YII2.
4 |
5 | [](https://packagist.org/packages/tsingsun/yii2-graphql)
6 | [](https://travis-ci.org/tsingsun/yii2-graphql)
7 | [](https://packagist.org/packages/tsingsun/yii2-graphql)
8 |
9 | --------
10 |
11 | yii-graphql特点
12 |
13 | * 配置简化,包括简化标准graphql协议的定义.
14 | * 按需要\懒加载,根据类型定义的全限定名,实现按需加载与懒,不需要在系统初始时将全部类型定义加载进入.
15 | * mutation输入验证支持
16 | * 提供控制器集成与授权支持
17 |
18 | ### 安装 ###
19 |
20 | 本库位于私有库中,需要在项目composer.json添加库地址
21 | ```php
22 | "require": {
23 | "tsingsun/yii2-graphql": "^0.9"
24 | }
25 | ```
26 |
27 | ### Type ###
28 | 类型系统是GraphQL的核心,体现在GraphQLType中,通过解构graphql协议,并利用graph-php库达到细粒度的对所有元素的控制,方便根据自身需要进行类扩展.
29 |
30 | GraphQLType的主要元素,** 注意元素并不对应到属性或方法中(下同) **
31 |
32 | 元素 | 类型 | 说明
33 | ----- | ----- | -----
34 | name | string | **Required** 每一个类型都需要为其命名,如果能唯一是比较安全,但并不强制,该属性需要定义于attribute中
35 | fields | array | **Required** 包含的字段内容,以fields()方法体现.
36 | resolveField | callback | **function($value, $args, $context, GraphQL\Type\Definition\ResolveInfo $info)** 对于字段的解释,比如fields定义user属性,则对应的解释方法为resolveUserField() ,$value指定为type定义的类型实例
37 |
38 | ### Query ###
39 |
40 | GraphQLQuery,GraphQLMutation继承了GraphQLField,元素结构是一致的,想做对于一些复用性的Field,可以继承它.
41 | Graphql的每次查询都需要对应到一个GraphQLQuery对象
42 |
43 | GraphQLField的主要元素
44 |
45 | 元素 | 类型 | 说明
46 | ----- | ----- | -----
47 | type | ObjectType | 对应的查询类型,单一类型用GraphQL::type指定,列表用Type::listOf(GraphQL::type)
48 | args | array | 查询需要使用的参数,其中每个参数按照Field定义
49 | resolve | callback | **function($value, $args, $context, GraphQL\Type\Definition\ResolveInfo $info)**,$value为root数据,$args即查询参数,$context上下文,为Yii的yii\web\Application对象,$info为查询解析对象,一般在这个方法中处理根对象
50 |
51 | ### Mutation ###
52 |
53 | 与GraphQLQuery是非常相像,参考说明.
54 |
55 | ### 简化处理 ###
56 |
57 | 简化了Field的声明,字段可直接使用type
58 |
59 | ```php
60 | 标准方式
61 | 'id'=>[
62 | 'type'=>type::id(),
63 | ],
64 | 简化写法
65 | 'id'=>type::id()
66 | ```
67 |
68 | ### 在YII使用 ###
69 |
70 | 本组件采用trait的方式在Component组件中被引入,组件宿主建议的方式是Module
71 | ```php
72 | class Module extends Module{
73 | use GraphQLModuleTrait;
74 | }
75 | ```
76 | Yii config file:
77 | ```php
78 | 'components'=>[
79 | 'graphql'=>[
80 | 'class'=>'xxx\xxxx\module'
81 | //主graphql协议配置
82 | 'schema' => [
83 | 'query' => [
84 | 'user' => 'app\graphql\query\UsersQuery'
85 | ],
86 | 'mutation' => [
87 | 'login'
88 | ],
89 | //if you use sample query except query contain interface,fragment,not need set
90 | //the key must same as your class definded
91 | 'types'=>[
92 | 'Story'=>'yiiunit\extensions\graphql\objects\types\StoryType'
93 | ],
94 | ]
95 | ],
96 | ];
97 | ```
98 | 采用的是actions的方法进行集成
99 | ```php
100 | class xxxController extends Controller{
101 | function actions()
102 | {
103 | return [
104 | 'index'=>[
105 | 'class'=>'yii\graphql\GraphQLAction'
106 | ]
107 | ];
108 | }
109 | }
110 | ```
111 |
112 | 在采用动态解析的情况下,如果不想定义types时,schema的写法有讲究.可采用Type::class,避免采用Key方式,也方便直接通过IDE导航到对应的类下
113 | ```php
114 | 'type'=>GraphQL::type(UserType::class)
115 | ```
116 |
117 | ### 输入验证
118 |
119 | 针对mutation的数据提交,提供了验证支持.
120 | 除了graphql基于的验证外,还可以使用yii的验证,目前为针对输入参数验证.直接在mutation定义中增加rules方法,
121 | 与Yii Model的使用方式是一致的.
122 | ```php
123 | public function rules()
124 | {
125 | return [
126 | ['password','boolean']
127 | ];
128 | }
129 |
130 | ```
131 |
132 | ### 授权验证
133 |
134 | 由于graphql查询是可以采用组合方式,如一次查询合并了两个query,而这两个query具有不同的授权约束,因此在graph中需要采用自定义的验证方式。
135 | 我把这多次查询查询称为graphql actions;当所有的graphql actions条件都满足配置时,才通过授权检查。
136 |
137 | #### 授权
138 | 在controller的行为方法中设置采用的授权方法,例子如下,
139 | ```php
140 | function behaviors()
141 | {
142 | return [
143 | 'authenticator'=>[
144 | 'class'=>'yii\graphql\filter\auth\CompositeAuth',
145 | 'authMethods'=>[
146 | \yii\filters\auth\QueryParamAuth::className(),
147 | ],
148 | 'except'=>['hello']
149 | ],
150 | ];
151 | }
152 | ```
153 | 如果要支持IntrospectionQueryr的授权,相应的graphql action为"__schema"
154 |
155 | ### Demo ###
156 |
157 | #### 创建基于graphql协议的查询 ####
158 |
159 | 每次查询对应一个GraphQLQuery文件,
160 | ```php
161 |
162 | class UserQuery extends GraphQLQuery
163 | {
164 | public function type()
165 | {
166 | return GraphQL::type(UserType::class);
167 | }
168 |
169 | public function args()
170 | {
171 | return [
172 | 'id'=>[
173 | 'type'=>Type::nonNull(Type::id())
174 | ],
175 | ];
176 | }
177 |
178 | public function resolve($value, $args, $context, ResolveInfo $info)
179 | {
180 | return DataSource::findUser($args['id']);
181 | }
182 |
183 |
184 | }
185 | ```
186 |
187 | 根据查询协议定义类型文件
188 | ```php
189 |
190 | class UserType extends GraphQLType
191 | {
192 | protected $attributes = [
193 | 'name'=>'user',
194 | 'description'=>'user is user'
195 | ];
196 |
197 | public function fields()
198 | {
199 | $result = [
200 | 'id' => ['type'=>Type::id()],
201 | 'email' => Types::email(),
202 | 'email2' => Types::email(),
203 | 'photo' => [
204 | 'type' => GraphQL::type(ImageType::class),
205 | 'description' => 'User photo URL',
206 | 'args' => [
207 | 'size' => Type::nonNull(GraphQL::type(ImageSizeEnumType::class)),
208 | ]
209 | ],
210 | 'firstName' => [
211 | 'type' => Type::string(),
212 | ],
213 | 'lastName' => [
214 | 'type' => Type::string(),
215 | ],
216 | 'lastStoryPosted' => GraphQL::type(StoryType::class),
217 | 'fieldWithError' => [
218 | 'type' => Type::string(),
219 | 'resolve' => function() {
220 | throw new \Exception("This is error field");
221 | }
222 | ]
223 | ];
224 | return $result;
225 | }
226 |
227 | public function resolvePhotoField(User $user,$args){
228 | return DataSource::getUserPhoto($user->id, $args['size']);
229 | }
230 |
231 | public function resolveIdField(User $user, $args)
232 | {
233 | return $user->id.'test';
234 | }
235 |
236 | public function resolveEmail2Field(User $user, $args)
237 | {
238 | return $user->email2.'test';
239 | }
240 |
241 |
242 | }
243 | ```
244 |
245 | #### 查询实例 ####
246 |
247 | ```php
248 | 'hello' => "
249 | query hello{hello}
250 | ",
251 |
252 | 'singleObject' => "
253 | query user {
254 | user(id:\"2\") {
255 | id
256 | email
257 | email2
258 | photo(size:ICON){
259 | id
260 | url
261 | }
262 | firstName
263 | lastName
264 |
265 | }
266 | }
267 | ",
268 | 'multiObject' => "
269 | query multiObject {
270 | user(id: \"2\") {
271 | id
272 | email
273 | photo(size:ICON){
274 | id
275 | url
276 | }
277 | }
278 | stories(after: \"1\") {
279 | id
280 | author{
281 | id
282 | }
283 | body
284 | }
285 | }
286 | ",
287 | 'updateObject' => "
288 | mutation updateUserPwd{
289 | updateUserPwd(id: \"1001\", password: \"123456\") {
290 | id,
291 | username
292 | }
293 | }
294 | "
295 | ```
296 |
297 | ### 深入了解 ###
298 |
299 | 有必要了解一些graphql-php的相关知识,这部分git上的文档相对还少些,需要对源码的阅读.下面列出重点
300 |
301 | #### DocumentNode (语法解构) ####
302 |
303 | ```
304 | array definitions
305 | array OperationDefinitionNode
306 | string kind
307 | array NameNode
308 | string kind
309 | string value
310 | ```
311 |
312 | ### Future
313 | * ActiveRecord generate tool for generating query and mutation class.
314 | * 对于graphql的一些特殊语法,像参数语法,内置指令语法还未进行测试
315 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 | ./tests
12 |
13 |
14 |
15 |
16 | framework/
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/ErrorFormatter.php:
--------------------------------------------------------------------------------
1 | getPrevious();
20 | if ($previous) {
21 | Yii::$app->getErrorHandler()->logException($previous);
22 | if ($previous instanceof ValidatorException) {
23 | return $previous->formatErrors;
24 | }
25 | if ($previous instanceof HttpException) {
26 | return ['code' => $previous->statusCode, 'message' => $previous->getMessage()];
27 | } else {
28 | return ['code' => $previous->getCode(), 'message' => $previous->getMessage()];
29 | }
30 | } else {
31 | Yii::error($e->getMessage(), get_class($e));
32 | }
33 |
34 | return FormattedError::createFromException($e, YII_DEBUG);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/GraphQL.php:
--------------------------------------------------------------------------------
1 | typeResolution) {
69 | $this->typeResolution = new TypeResolution();
70 | }
71 | return $this->typeResolution;
72 | }
73 |
74 | /**
75 | * Receive schema data and incorporate configuration information
76 | *
77 | * array format:
78 | * $schema = new [
79 | * 'query'=>[
80 | * //the key is alias,mutation,types and so on
81 | * 'hello'=>HelloQuery::class
82 | * ],
83 | * 'mutation'=>[],
84 | * 'types'=>[],
85 | * ];
86 | * @param null|array $schema 配置数组,该数组会导入对象自身的配置持久化下来
87 | */
88 | public function schema($schema = null)
89 | {
90 | if (is_array($schema)) {
91 | $schemaQuery = ArrayHelper::getValue($schema, 'query', []);
92 | $schemaMutation = ArrayHelper::getValue($schema, 'mutation', []);
93 | $schemaTypes = ArrayHelper::getValue($schema, 'types', []);
94 | $this->queries += $schemaQuery;
95 | $this->mutations += $schemaMutation;
96 | $this->types += $schemaTypes;
97 | $this->getTypeResolution()->setAlias($schemaTypes);
98 | }
99 | }
100 |
101 | /**
102 | * GraphQl Schema is built according to input. Especially,
103 | * due to the need of Module and Controller in the process of building ObjectType,
104 | * the execution position of the method is restricted to a certain extent.
105 | * @param Schema|array $schema schema data
106 | * @return Schema
107 | */
108 | public function buildSchema($schema = null)
109 | {
110 | if ($schema instanceof Schema) {
111 | return $schema;
112 | }
113 | if ($schema === null) {
114 | list($schemaQuery, $schemaMutation, $schemaTypes) = [$this->queries, $this->mutations, $this->types];
115 | } else {
116 | list($schemaQuery, $schemaMutation, $schemaTypes) = $schema;
117 | }
118 | $types = [];
119 | if (sizeof($schemaTypes)) {
120 | foreach ($schemaTypes as $name => $type) {
121 | $types[] = $this->getTypeResolution()->parseType($name, true);
122 | }
123 | }
124 | //graqhql的validator要求query必须有
125 | $query = $this->getTypeResolution()->objectType($schemaQuery, [
126 | 'name' => 'Query'
127 | ]);
128 |
129 | $mutation = null;
130 | if (!empty($schemaMutation)) {
131 | $mutation = $this->getTypeResolution()->objectType($schemaMutation, [
132 | 'name' => 'Mutation'
133 | ]);
134 | }
135 |
136 | $this->getTypeResolution()->initTypes([$query, $mutation], $schema == null);
137 |
138 | $result = new Schema([
139 | 'query' => $query,
140 | 'mutation' => $mutation,
141 | 'types' => $types,
142 | 'typeLoader' => function ($name) {
143 | return $this->getTypeResolution()->parseType($name, true);
144 | }
145 | ]);
146 | return $result;
147 | }
148 |
149 |
150 | /**
151 | * query access
152 | * @param $requestString
153 | * @param null $rootValue
154 | * @param null $contextValue
155 | * @param null $variableValues
156 | * @param string $operationName
157 | * @return array|Error\InvariantViolation
158 | */
159 | public function query($requestString, $rootValue = null, $contextValue = null, $variableValues = null, $operationName = '')
160 | {
161 | $sl = $this->parseRequestQuery($requestString);
162 | if ($sl === true) {
163 | $sl = [$this->queries, $this->mutations, $this->types];
164 | }
165 | $schema = $this->buildSchema($sl);
166 |
167 | $val = $this->execute($schema, $rootValue, $contextValue, $variableValues, $operationName);
168 | return $this->getResult($val);
169 | }
170 |
171 | /**
172 | * @param $executeResult
173 | * @return array|Promise
174 | */
175 | public function getResult($executeResult)
176 | {
177 | if ($executeResult instanceof ExecutionResult) {
178 | if ($this->errorFormatter) {
179 | $executeResult->setErrorFormatter($this->errorFormatter);
180 | }
181 | return $this->parseExecutionResult($executeResult);
182 | } elseif ($executeResult instanceof Promise) {
183 | return $executeResult->then(function (ExecutionResult $executionResult) {
184 | if ($this->errorFormatter) {
185 | $executionResult->setErrorFormatter($this->errorFormatter);
186 | }
187 | return $this->parseExecutionResult($executionResult);
188 | });
189 | } else {
190 | throw new Error\InvariantViolation("Unexpected execution result");
191 | }
192 | }
193 |
194 | private function parseExecutionResult(ExecutionResult $executeResult)
195 | {
196 | if (empty($executeResult->errors) || empty($this->errorFormatter)) {
197 | return $executeResult->toArray();
198 | }
199 | $result = [];
200 |
201 | if (null !== $executeResult->data) {
202 | $result['data'] = $executeResult->data;
203 | }
204 |
205 | if (!empty($executeResult->errors)) {
206 | $result['errors'] = [];
207 | foreach ($executeResult->errors as $er) {
208 | $fn = $this->errorFormatter;
209 | $fr = $fn($er);
210 | if (isset($fr['message'])) {
211 | $result['errors'][] = $fr;
212 | } else {
213 | $result['errors'] += $fr;
214 | }
215 | }
216 | // $result['errors'] = array_map($executeResult->errorFormatter, $executeResult->errors);
217 | }
218 |
219 | if (!empty($executeResult->extensions)) {
220 | $result['extensions'] = (array)$executeResult->extensions;
221 | }
222 |
223 | return $result;
224 | }
225 |
226 | /**
227 | * Executing the query according to schema, this method needs to be executed after the schema is generated
228 | * @param $schema
229 | * @param $rootValue
230 | * @param $contextValue
231 | * @param $variableValues
232 | * @param $operationName
233 | * @return ExecutionResult|Promise
234 | */
235 | public function execute($schema, $rootValue, $contextValue, $variableValues, $operationName)
236 | {
237 | try {
238 | /** @var QueryComplexity $queryComplexity */
239 | $queryComplexity = DocumentValidator::getRule('QueryComplexity');
240 | $queryComplexity->setRawVariableValues($variableValues);
241 |
242 | $validationErrors = DocumentValidator::validate($schema, $this->currentDocument);
243 |
244 | if (!empty($validationErrors)) {
245 | return new ExecutionResult(null, $validationErrors);
246 | }
247 | return Executor::execute($schema, $this->currentDocument, $rootValue, $contextValue, $variableValues, $operationName);
248 | } catch (Error\Error $e) {
249 | return new ExecutionResult(null, [$e]);
250 | } finally {
251 | $this->currentDocument = null;
252 | }
253 | }
254 |
255 | /**
256 | * 将查询请求转换为可以转换为schema方法的数组
257 | * @param $requestString
258 | * @return array|bool 数组元素为0:query,1:mutation,2:types,当返回true时,表示为IntrospectionQuery
259 | */
260 | public function parseRequestQuery($requestString)
261 | {
262 | $source = new Source($requestString ?: '', 'GraphQL request');
263 | $this->currentDocument = Parser::parse($source);
264 | $queryTypes = [];
265 | $mutation = [];
266 | $types = [];
267 | $isAll = false;
268 | foreach ($this->currentDocument->definitions as $definition) {
269 | if ($definition instanceof OperationDefinitionNode) {
270 | $selections = $definition->selectionSet->selections;
271 | foreach ($selections as $selection) {
272 | $node = $selection->name;
273 | if ($node instanceof NameNode) {
274 | if ($definition->operation == 'query') {
275 | if ($definition->name && $definition->name->value == 'IntrospectionQuery') {
276 | $isAll = true;
277 | break 2;
278 | }
279 | if (isset($this->queries[$node->value])) {
280 | $queryTypes[$node->value] = $this->queries[$node->value];
281 | }
282 | if (isset($this->types[$node->value])) {
283 | $types[$node->value] = $this->types[$node->value];
284 | }
285 | } elseif ($definition->operation == 'mutation') {
286 | if (isset($this->mutations[$node->value])) {
287 | $mutation[$node->value] = $this->mutations[$node->value];
288 | }
289 | }
290 | }
291 | }
292 | }
293 | }
294 | return $isAll ?: [$queryTypes, $mutation, $types];
295 | }
296 |
297 | /**
298 | * Type manager access
299 | * @param string|Type $name
300 | * @param bool $byAlias if use alias
301 | * @return mixed
302 | */
303 | public static function type($name, $byAlias = false)
304 | {
305 | /** @var GraphQLModuleTrait $module */
306 | $module = Yii::$app->controller ? Yii::$app->controller->module : Yii::$app->getModule('graphql');
307 | $gql = $module->getGraphQL();
308 |
309 | return $gql->getTypeResolution()->parseType($name, $byAlias);
310 | }
311 |
312 | /**
313 | * @param $class
314 | * @param null $name
315 | */
316 | public function addType($class, $name = null)
317 | {
318 | $name = $this->getTypeName($class, $name);
319 | $this->types[$name] = $class;
320 | }
321 |
322 | /**
323 | *
324 | * @param $class
325 | * @param null $name
326 | * @return null
327 | * @throws InvalidConfigException
328 | */
329 | protected function getTypeName($class, $name = null)
330 | {
331 | if ($name) {
332 | return $name;
333 | }
334 |
335 | $type = is_object($class) ? $class : Yii::createObject($class);
336 | return $type->name;
337 | }
338 |
339 | /**
340 | * set error formatter
341 | * @param Callable $errorFormatter
342 | */
343 | public function setErrorFormatter(Callable $errorFormatter)
344 | {
345 | $this->errorFormatter = $errorFormatter;
346 | }
347 |
348 | /**
349 | * validate the schema.
350 | *
351 | * when initial the schema,the types parameter must not passed.
352 | *
353 | * @param Schema $schema
354 | */
355 | public function assertValid($schema)
356 | {
357 | //the type come from the TypeResolution.
358 | foreach ($this->types as $name => $type) {
359 | $schema->getType($name);
360 | }
361 | $schema->assertValid();
362 | }
363 | }
--------------------------------------------------------------------------------
/src/GraphQLAction.php:
--------------------------------------------------------------------------------
1 | ['class'=>'yii\graphql\GraphQLAction']
26 | * ]
27 | * }
28 | * ```
29 | * @package yii\graphql
30 | */
31 | class GraphQLAction extends Action
32 | {
33 | const INTROSPECTIONQUERY = '__schema';
34 | /**
35 | * @var GraphQL
36 | */
37 | private $graphQL;
38 | private $schemaArray;
39 | private $query;
40 | private $variables;
41 | private $operationName;
42 | /**
43 | * @var array child graphql actions
44 | */
45 | private $authActions = [];
46 | /**
47 | * @var callable a PHP callable that will be called when running an action to determine
48 | * if the current user has the permission to execute the action. If not set, the access
49 | * check will not be performed. The signature of the callable should be as follows,
50 | *
51 | * ```php
52 | * function ($actionName) {
53 | *
54 | * // If null, it means no specific model (e.g. IndexAction)
55 | * }
56 | * ```
57 | */
58 | public $checkAccess;
59 | /**
60 | * @var bool whether use Schema validation , and it is recommended only in the development environment
61 | */
62 | public $enableSchemaAssertValid = YII_ENV_DEV;
63 |
64 | public function init()
65 | {
66 | parent::init();
67 |
68 | $request = Yii::$app->getRequest();
69 | if ($request->isGet) {
70 | $this->query = $request->get('query');
71 | $this->variables = $request->get('variables');
72 | $this->operationName = $request->get('operationName');
73 | } else {
74 | $body = $request->getBodyParams();
75 | if (empty($body)) {
76 | //取原始文件当查询,这时只支持如其他方式下的query的节点的查询
77 | $this->query = $request->getRawBody();
78 | } else {
79 | if (!empty($body['operations'])) {
80 | $serverRequest = ServerRequestFactory::fromGlobals();
81 | $uploadMiddleware = new UploadMiddleware();
82 | $serverRequest = $uploadMiddleware->processRequest($serverRequest);
83 | $parsedBody = $serverRequest->getParsedBody();
84 |
85 | $this->query = $parsedBody['query'] ?? $parsedBody;
86 | $this->variables = $parsedBody['variables'] ?? [];
87 | $this->operationName = $parsedBody['operationName'] ?? null;
88 | } else {
89 | $this->query = $body['query'] ?? $body;
90 | $this->variables = $body['variables'] ?? [];
91 | $this->operationName = $body['operationName'] ?? null;
92 | }
93 | }
94 | }
95 | if (empty($this->query)) {
96 | throw new InvalidParamException('invalid query,query document not found');
97 | }
98 | if (is_string($this->variables)) {
99 | $this->variables = json_decode($this->variables, true);
100 | }
101 |
102 | /** @var GraphQLModuleTrait $module */
103 | $module = $this->controller->module;
104 | $this->graphQL = $module->getGraphQL();
105 |
106 | $this->schemaArray = $this->graphQL->parseRequestQuery($this->query);
107 | }
108 |
109 | /**
110 | * 返回本次查询的所有graphql action,如果本次查询为introspection时,则为查询的
111 | * @return array
112 | */
113 | public function getGraphQLActions()
114 | {
115 | if ($this->schemaArray === true) {
116 | return [self::INTROSPECTIONQUERY => 'true'];
117 | }
118 | $ret = array_merge($this->schemaArray[0], $this->schemaArray[1]);
119 | if (!$this->authActions) {
120 | //init
121 | $this->authActions = array_merge($this->schemaArray[0], $this->schemaArray[1]);
122 | }
123 | return $ret;
124 | }
125 |
126 | /**
127 | * remove action that no need check access
128 | * @param $key
129 | */
130 | public function removeGraphQlAction($key)
131 | {
132 | unset($this->authActions[$key]);
133 | }
134 |
135 | /**
136 | * @return array
137 | */
138 | public function run()
139 | {
140 | Yii::$app->response->format = Response::FORMAT_JSON;
141 | if ($this->authActions && $this->checkAccess) {
142 | foreach ($this->authActions as $childAction => $class) {
143 | $fn = $this->checkAccess;
144 | $fn($childAction);
145 | }
146 | }
147 | $schema = $this->graphQL->buildSchema($this->schemaArray === true ? null : $this->schemaArray);
148 | //TODO the graphql-php's valid too strict,the lazy load has can't pass when execute mutation(must has query node)
149 | // if ($this->enableSchemaAssertValid) {
150 | // $this->graphQL->assertValid($schema);
151 | // }
152 | $val = $this->graphQL->execute($schema, null, Yii::$app, $this->variables, $this->operationName);
153 | $result = $this->graphQL->getResult($val);
154 | return $result;
155 | }
156 | }
--------------------------------------------------------------------------------
/src/GraphQLModuleTrait.php:
--------------------------------------------------------------------------------
1 | [
27 | * 'query' => [
28 | * 'user' => 'App\GraphQL\Query\UsersQuery'
29 | * ],
30 | * 'mutation' => [
31 | *
32 | * ],
33 | * 'types'=>[
34 | * 'user'=>'app\modules\graph\type\UserType'
35 | * ],
36 | * ]
37 | *
38 | * @var array
39 | */
40 | public $schema = [];
41 |
42 | /**
43 | * @var GraphQL the Graph handle
44 | */
45 | private $graphQL;
46 |
47 | /**
48 | * @var callable if don't set error formatter,it will use php-graphql default
49 | * @see \GraphQL\Executor\ExecutionResult
50 | */
51 | public $errorFormatter;
52 |
53 | /**
54 | * get graphql handler
55 | * @return GraphQL
56 | */
57 | public function getGraphQL()
58 | {
59 | if ($this->graphQL == null) {
60 | $this->graphQL = new GraphQL();
61 | $this->graphQL->schema($this->schema);
62 | if ($this->errorFormatter) {
63 | $this->graphQL->setErrorFormatter($this->errorFormatter);
64 | } else {
65 | $this->graphQL->setErrorFormatter(['yii\graphql\ErrorFormatter', 'formatError']);
66 | }
67 | }
68 | return $this->graphQL;
69 | }
70 | }
--------------------------------------------------------------------------------
/src/TypeResolution.php:
--------------------------------------------------------------------------------
1 | alias for className to map the type
29 | */
30 | private $alias = [];
31 | /**
32 | * @var Type[]
33 | */
34 | private $typeMap = [];
35 |
36 | /**
37 | * @var array
38 | */
39 | private $implementations = [];
40 |
41 | /**
42 | * EagerResolution constructor.
43 | */
44 | public function __construct()
45 | {
46 |
47 | }
48 |
49 | /**
50 | * set type config
51 | * @param $config
52 | */
53 | public function setAlias($config)
54 | {
55 |
56 | $this->alias = $config;
57 | }
58 |
59 | /**
60 | * @param Type[] $graphTypes
61 | * @param bool $needIntrospection if need IntrospectionQuery set true
62 | */
63 | public function initTypes($graphTypes, $needIntrospection = false)
64 | {
65 | $typeMap = [];
66 | if ($needIntrospection) {
67 | $graphTypes[] = Introspection::_schema();
68 | }
69 | foreach ($graphTypes as $type) {
70 | $typeMap = Utils\TypeInfo::extractTypes($type, $typeMap);
71 | }
72 | $this->typeMap = $typeMap + Type::getStandardTypes();
73 |
74 | // Keep track of all possible types for abstract types
75 | foreach ($this->typeMap as $typeName => $type) {
76 | if ($type instanceof ObjectType) {
77 | foreach ($type->getInterfaces() as $iface) {
78 | $this->implementations[$iface->name][] = $type;
79 | }
80 | }
81 | }
82 | }
83 |
84 | /**
85 | * transform single type
86 | * @param Type $type
87 | */
88 | protected function transformType($type)
89 | {
90 | if ($type instanceof ObjectType) {
91 | foreach ($type->getInterfaces() as $iface) {
92 | $this->implementations[$iface->name][] = $type;
93 | }
94 | }
95 | }
96 |
97 | /**
98 | * @inheritdoc
99 | */
100 | public function resolveType($name)
101 | {
102 | return isset($this->typeMap[$name]) ? $this->typeMap[$name] : $this->parseType($name, true);
103 | }
104 |
105 | /**
106 | * @inheritdoc
107 | */
108 | public function resolvePossibleTypes(AbstractType $abstractType)
109 | {
110 | if (!isset($this->typeMap[$abstractType->name])) {
111 | return [];
112 | }
113 |
114 | if ($abstractType instanceof UnionType) {
115 | return $abstractType->getTypes();
116 | }
117 |
118 | /** @var InterfaceType $abstractType */
119 | Utils::invariant($abstractType instanceof InterfaceType);
120 | return isset($this->implementations[$abstractType->name]) ? $this->implementations[$abstractType->name] : [];
121 | }
122 |
123 | /**
124 | * @return Type[]
125 | */
126 | public function getTypeMap()
127 | {
128 | return $this->typeMap;
129 | }
130 |
131 | /**
132 | * Returns serializable schema representation suitable for GraphQL\Type\LazyResolution
133 | *
134 | * @return array
135 | */
136 | public function getDescriptor()
137 | {
138 | $typeMap = [];
139 | $possibleTypesMap = [];
140 | foreach ($this->getTypeMap() as $type) {
141 | if ($type instanceof UnionType) {
142 | foreach ($type->getTypes() as $innerType) {
143 | $possibleTypesMap[$type->name][$innerType->name] = 1;
144 | }
145 | } else if ($type instanceof InterfaceType) {
146 | foreach ($this->implementations[$type->name] as $obj) {
147 | $possibleTypesMap[$type->name][$obj->name] = 1;
148 | }
149 | }
150 | $typeMap[$type->name] = 1;
151 | }
152 | return [
153 | 'version' => '1.0',
154 | 'typeMap' => $typeMap,
155 | 'possibleTypeMap' => $possibleTypesMap
156 | ];
157 | }
158 |
159 | /**
160 | * convert type declare to ObjectType instance
161 | * @param $type
162 | * @param array $opts
163 | * @return ObjectType|null
164 | */
165 | public function objectType($type, $opts = [])
166 | {
167 | // If it's already an ObjectType, just update properties and return it.
168 | // If it's an array, assume it's an array of fields and build ObjectType
169 | // from it. Otherwise, build it from a string or an instance.
170 | $objectType = null;
171 | if ($type instanceof ObjectType) {
172 | $objectType = $type;
173 | foreach ($opts as $key => $value) {
174 | if (property_exists($objectType, $key)) {
175 | $objectType->{$key} = $value;
176 | }
177 | if (isset($objectType->config[$key])) {
178 | $objectType->config[$key] = $value;
179 | }
180 | }
181 | } elseif (is_array($type)) {
182 | $objectType = $this->buildObjectTypeFromFields($type, $opts);
183 | } else {
184 | $objectType = $this->buildObjectTypeFromClass($type, $opts);
185 | }
186 |
187 | return $objectType;
188 | }
189 |
190 | /**
191 | * build ObjectType from classname config
192 | * @param Object|array $type 能够转换为ObjectType的类实例或者类配置
193 | * @param array $opts
194 | * @return object
195 | * @throws InvalidConfigException
196 | */
197 | protected function buildObjectTypeFromClass($type, $opts = [])
198 | {
199 | if (!is_object($type)) {
200 | $type = Yii::createObject($type);
201 | }
202 |
203 | foreach ($opts as $key => $value) {
204 | $type->{$key} = $value;
205 | }
206 |
207 | return $type->toType();
208 | }
209 |
210 | /**
211 | * Configuring GraphQL ObjectType through the graphql declaration configuration
212 | * @param array $fields use standard graphql declare.
213 | * @param array $opts
214 | * @return ObjectType
215 | * @throws InvalidConfigException
216 | */
217 | protected function buildObjectTypeFromFields($fields, $opts = [])
218 | {
219 | $typeFields = [];
220 | foreach ($fields as $name => $field) {
221 | if (is_string($field)) {
222 | $field = Yii::createObject($field);
223 | $name = is_numeric($name) ? $field->name : $name;
224 | $field['name'] = $name;
225 | $field = $field->toArray();
226 | } else {
227 | $name = is_numeric($name) ? $field['name'] : $name;
228 | $field['name'] = $name;
229 | }
230 | $typeFields[$name] = $field;
231 | }
232 |
233 | return new ObjectType(array_merge([
234 | 'fields' => $typeFields
235 | ], $opts));
236 | }
237 |
238 | /**
239 | * get type by name,this method is use in Type definition class for TypeSystem
240 | * @param $name
241 | * @param bool $byAlias if use alias;
242 | * @return Type|null
243 | * @throws TypeNotFound | NotSupportedException
244 | */
245 | public function parseType($name, $byAlias = false)
246 | {
247 | $class = $name;
248 | if (is_object($class)) {
249 | $name = get_class($class);
250 | }
251 |
252 | if ($byAlias && isset($this->alias[$name])) {
253 | $class = $this->alias[$name];
254 | } elseif (!$byAlias && isset($this->alias[$name])) {
255 | $name = $this->alias[$name];
256 | }
257 |
258 | if (isset($this->typeMap[$name])) {
259 | return $this->typeMap[$name];
260 | }
261 |
262 | //class is string or not found;
263 | if (is_string($class)) {
264 | if (strpos($class, '\\') !== false && !class_exists($class)) {
265 | throw new TypeNotFound('Type ' . $name . ' not found.');
266 | }
267 |
268 | } elseif (!is_object($class)) {
269 | throw new TypeNotFound('Type ' . $name . ' not found.');
270 | }
271 | $type = $this->buildType($class);
272 | $this->alias[$type->name] = $class;
273 | $this->alias[$class] = $type->name;
274 | $this->typeMap[$type->name] = $type;
275 | $this->transformType($type);
276 | return $type;
277 | }
278 |
279 | /**
280 | * @param string $type type name
281 | * @param array $opts return Type's attribute set
282 | * @return ObjectType|Type|GraphQLField
283 | * @throws InvalidConfigException
284 | * @throws NotSupportedException
285 | */
286 | protected function buildType($type)
287 | {
288 | if (!is_object($type)) {
289 | $type = Yii::createObject($type);
290 | }
291 | if ($type instanceof Type) {
292 | return $type;
293 | } elseif ($type instanceof GraphQLType) {
294 | //transfer ObjectType
295 | return $type->toType();
296 | } elseif ($type instanceof GraphQLField) {
297 | //field is not need transfer to ObjectType,it just need config array
298 | return $type;
299 | }
300 |
301 | throw new NotSupportedException("Type:{$type} is not support translate to Graph Type");
302 | }
303 | }
--------------------------------------------------------------------------------
/src/base/GraphQLField.php:
--------------------------------------------------------------------------------
1 | attributes;
62 | $args = $this->args();
63 |
64 | $attributes = array_merge([
65 | 'args' => $args
66 | ], $attributes);
67 |
68 | $type = $this->type();
69 | if (isset($type)) {
70 | if(!is_object($type)){
71 | $type = GraphQL::type($type);
72 | }
73 | $attributes['type'] = $type;
74 | }
75 |
76 | $resolver = $this->getResolver();
77 | if (isset($resolver)) {
78 | $attributes['resolve'] = $resolver;
79 | }
80 |
81 | return $attributes;
82 | }
83 | }
--------------------------------------------------------------------------------
/src/base/GraphQLInterfaceType.php:
--------------------------------------------------------------------------------
1 | getTypeResolver();
38 | if (isset($resolver)) {
39 | $attributes['resolveType'] = $resolver;
40 | }
41 |
42 | return $attributes;
43 | }
44 |
45 | public function toType()
46 | {
47 | return new InterfaceType($this->toArray());
48 | }
49 | }
--------------------------------------------------------------------------------
/src/base/GraphQLModel.php:
--------------------------------------------------------------------------------
1 | attributes);
32 | }
33 |
34 | public function fields()
35 | {
36 | return $this->getAttributes();
37 | }
38 |
39 | /**
40 | * Converts the object into an array.
41 | *
42 | * @param array $fields the fields that the output array should contain. Fields not specified
43 | * in [[fields()]] will be ignored. If this parameter is empty, all fields as specified in [[fields()]] will be returned.
44 | * @param array $expand the additional fields that the output array should contain.
45 | * Fields not specified in [[extraFields()]] will be ignored. If this parameter is empty, no extra fields
46 | * will be returned.
47 | * @param boolean $recursive whether to recursively return array representation of embedded objects.
48 | * @return array the array representation of the object
49 | */
50 | public function toArray(array $fields = [], array $expand = [], $recursive = true)
51 | {
52 | return $this->getAttributes();
53 | }
54 |
55 | public function __get($name)
56 | {
57 | $attributes = $this->getAttributes();
58 | return isset($attributes[$name]) ? $attributes[$name] : null;
59 | }
60 |
61 | public function __isset($name)
62 | {
63 | $attributes = $this->getAttributes();
64 | return isset($attributes[$name]);
65 | }
66 |
67 |
68 | public function __set($name, $value)
69 | {
70 | $this->attributes[$name] = $value;
71 | }
72 |
73 | public function __unset($name)
74 | {
75 | unset($this->attributes[$name]);
76 | }
77 |
78 |
79 | }
--------------------------------------------------------------------------------
/src/base/GraphQLMutation.php:
--------------------------------------------------------------------------------
1 | getAttributes();
68 | }
69 |
70 | /**
71 | * the the field parsed,field could defined like:
72 | * //GraphQlType Node
73 | * 'fieldByType'=> GraphQl::types([typeName = 'className'|'ConfigName']);
74 | * 'field1ByTypeClassName'=> UserType::class
75 | * //GraphQLField Node
76 | * 'field2'=> HtmlField::class
77 | *
78 | * @return array
79 | */
80 | public function getFields()
81 | {
82 | $fields = $this->fields();
83 | $allFields = [];
84 | foreach ($fields as $name => $field) {
85 | //the field is a GraphQlType or GraphQLField
86 | if (is_string($field)) {
87 | $type = GraphQL::type($field);
88 | if ($type instanceof GraphQLField) {
89 | $field = $type->toArray();
90 | $field['name'] = $name;
91 | } else {
92 | $field = [
93 | 'name' => $name,
94 | 'type' => $type,
95 | ];
96 | }
97 | } elseif ($field instanceof Type) {
98 | $field = [
99 | 'name' => $name,
100 | 'type' => $field
101 | ];
102 |
103 | }
104 | $resolver = $this->getFieldResolver($name, $field);
105 | if ($resolver) {
106 | $field['resolve'] = $resolver;
107 | }
108 | $allFields[$name] = $field;
109 | }
110 |
111 | return $allFields;
112 | }
113 |
114 | /**
115 | * Get the graphql office's description format,that will be used for create GraphQL Object Type.
116 | * @param $name
117 | * @param $except
118 | * @return array
119 | */
120 | public function getAttributes($name = null, $except = null)
121 | {
122 | $attributes = array_merge($this->attributes, [
123 | 'fields' => function () {
124 | return $this->getFields();
125 | }
126 | ]);
127 |
128 | $interfaces = $this->interfaces();
129 | if (sizeof($interfaces)) {
130 | $attributes['interfaces'] = $interfaces;
131 | }
132 |
133 | return $attributes;
134 | }
135 |
136 | /**
137 | * Convert this class to its ObjectType.
138 | *
139 | * @return ObjectType |InputObjectType
140 | */
141 | public function toType()
142 | {
143 | if ($this->inputObject) {
144 | return new InputObjectType($this->toArray());
145 | }
146 | return new ObjectType($this->toArray());
147 | }
148 | }
--------------------------------------------------------------------------------
/src/base/GraphQLUnionType.php:
--------------------------------------------------------------------------------
1 | attributes;
52 |
53 | $resolver = $this->getTypeResolver();
54 | if (isset($resolver)) {
55 | $attributes['resolveType'] = $resolver;
56 | }
57 | $types = array_map(function ($item) {
58 | if (is_string($item)) {
59 | return GraphQL::type($item);
60 | } else {
61 | return $item;
62 | }
63 | }, static::types());
64 |
65 | $attributes['types'] = $types;
66 | //TODO support $name and $except??
67 | return $attributes;
68 | }
69 |
70 | public function toType()
71 | {
72 | return new UnionType($this->toArray());
73 | }
74 | }
--------------------------------------------------------------------------------
/src/exceptions/SchemaNotFound.php:
--------------------------------------------------------------------------------
1 | formName()} validate false", $code, $previous);
29 | $this->formatModelErrors($model);
30 | }
31 |
32 | /**
33 | * @param Model $model
34 | */
35 | private function formatModelErrors($model)
36 | {
37 | foreach ($model->getErrors() as $field => $fielsErrors) {
38 | foreach ($fielsErrors as $error) {
39 | $this->formatErrors[] = ['code' => $field, 'message' => $error];
40 | }
41 | }
42 | }
43 | }
--------------------------------------------------------------------------------
/src/filters/auth/CompositeAuth.php:
--------------------------------------------------------------------------------
1 | authMethods) ? true : parent::beforeAction($action);
40 | }
41 |
42 | /**
43 | * @inheritdoc
44 | */
45 | public function authenticate($user, $request, $response)
46 | {
47 | foreach ($this->authMethods as $i => $auth) {
48 | if (!$auth instanceof AuthInterface) {
49 | $this->authMethods[$i] = $auth = Yii::createObject($auth);
50 | if (!$auth instanceof AuthInterface) {
51 | throw new InvalidConfigException(get_class($auth) . ' must implement yii\filters\auth\AuthInterface');
52 | }
53 | }
54 |
55 | $identity = $auth->authenticate($user, $request, $response);
56 | if ($identity !== null) {
57 | return $identity;
58 | }
59 | }
60 |
61 | return null;
62 | }
63 |
64 | /**
65 | * @inheritdoc
66 | */
67 | public function challenge($response)
68 | {
69 | foreach ($this->authMethods as $method) {
70 | /** @var $method AuthInterface */
71 | $method->challenge($response);
72 | }
73 | }
74 |
75 | protected function isActive($action)
76 | {
77 | if ($action instanceof GraphQLAction) {
78 | $maps = $action->getGraphQLActions();
79 |
80 | if (empty($this->only)) {
81 | $onlyMatch = true;
82 | } else {
83 | $onlyMatch = true;
84 | foreach ($maps as $key => $value) {
85 | foreach ($this->only as $pattern) {
86 | if (fnmatch($pattern, $key)) {
87 | continue 2;
88 | }
89 | }
90 | $onlyMatch = false;
91 | break;
92 | }
93 | }
94 |
95 | $exceptMatch = true;
96 | foreach ($maps as $key => $value) {
97 | foreach ($this->except as $pattern) {
98 | if (fnmatch($pattern, $key)) {
99 | $action->removeGraphQlAction($key);
100 | continue 2;
101 | }
102 | }
103 | $exceptMatch = false;
104 | break;
105 | }
106 | return !$exceptMatch && $onlyMatch;
107 | } else {
108 | return parent::isActive($action);
109 | }
110 | }
111 |
112 |
113 | }
--------------------------------------------------------------------------------
/src/traits/GlobalIdTrait.php:
--------------------------------------------------------------------------------
1 | decodeGlobalId($id);
39 |
40 | return $id;
41 | }
42 |
43 | /**
44 | * Get the decoded GraphQL Type.
45 | *
46 | * @param string $id
47 | * @return string
48 | */
49 | public function decodeRelayType($id)
50 | {
51 | list($type, $id) = $this->decodeGlobalId($id);
52 |
53 | return $type;
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/traits/ShouldValidate.php:
--------------------------------------------------------------------------------
1 | rules();
29 | if (sizeof($rules)) {
30 | //索引1的为args参数.
31 | $args = ArrayHelper::getValue($arguments, 1, []);
32 | $val = DynamicModel::validateData($args, $rules);
33 | if ($error = $val->getFirstErrors()) {
34 | $msg = 'input argument(' . key($error) . ') has validate error:' . reset($error);
35 | throw new InvalidParamException($msg);
36 | }
37 | }
38 |
39 | return $resolver(...$arguments);
40 | };
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/types/EmailType.php:
--------------------------------------------------------------------------------
1 | 'Email',
16 | 'serialize' => [$this, 'serialize'],
17 | 'parseValue' => [$this, 'parseValue'],
18 | 'parseLiteral' => [$this, 'parseLiteral'],
19 | ];
20 | parent::__construct($config);
21 | }
22 |
23 | /**
24 | * Serializes an internal value to include in a response.
25 | *
26 | * @param string $value
27 | * @return string
28 | */
29 | public function serialize($value)
30 | {
31 | // Assuming internal representation of email is always correct:
32 | return $value;
33 |
34 | // If it might be incorrect and you want to make sure that only correct values are included in response -
35 | // use following line instead:
36 | // return $this->parseValue($value);
37 | }
38 |
39 | /**
40 | * Parses an externally provided value (query variable) to use as an input
41 | *
42 | * @param mixed $value
43 | * @return mixed
44 | */
45 | public function parseValue($value)
46 | {
47 | if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
48 | throw new \UnexpectedValueException("Cannot represent value as email: " . Utils::printSafe($value));
49 | }
50 | return $value;
51 | }
52 |
53 | /**
54 | * Parses an externally provided literal value (hardcoded in GraphQL query) to use as an input
55 | *
56 | * @param \GraphQL\Language\AST\Node $valueAST
57 | * @return string
58 | * @throws Error
59 | */
60 | public function parseLiteral($valueAST)
61 | {
62 | // Note: throwing GraphQL\Error\Error vs \UnexpectedValueException to benefit from GraphQL
63 | // error location in query:
64 | if (!$valueAST instanceof StringValueNode) {
65 | throw new Error('Query error: Can only parse strings got: ' . $valueAST->kind, [$valueAST]);
66 | }
67 | if (!filter_var($valueAST->value, FILTER_VALIDATE_EMAIL)) {
68 | throw new Error("Not a valid email", [$valueAST]);
69 | }
70 | return $valueAST->value;
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/types/PageInfoType.php:
--------------------------------------------------------------------------------
1 | 'PageInfo',
21 | 'description' => 'Information about pagination in a connection',
22 | 'fields' => [
23 | 'endCursor' => [
24 | 'type' => Type::string(),
25 | 'description' => 'When paginating forwards, the cursor to continue.'
26 | ],
27 | 'hasNextPage' => [
28 | 'type' => Type::boolean(),
29 | 'description' => 'When paginating forwards, are there more items?',
30 | ],
31 | 'hasPreviousPage' => [
32 | 'type' => Type::boolean(),
33 | 'description' => 'When paginating backwards, are there more items?',
34 | ],
35 | 'startCursor' => [
36 | 'type' => Type::string(),
37 | 'description' => 'When paginating backwards, the cursor to continue.',
38 | ],
39 | ],
40 | ];
41 | parent::__construct($config);
42 | }
43 | }
--------------------------------------------------------------------------------
/src/types/PaginationType.php:
--------------------------------------------------------------------------------
1 | 'Pagination',
21 | 'description' => '',
22 | 'fields' => [
23 | 'first' => [
24 | 'type' => Type::int(),
25 | 'description' => 'Returns the first n elements from the list.'
26 | ],
27 | 'after' => [
28 | 'type' => Type::string(),
29 | 'description' => 'Returns the elements in the list that come after the specified global ID.'
30 | ],
31 | 'last' => [
32 | 'type' => Type::int(),
33 | 'description' => 'Returns the last n elements from the list..'
34 | ],
35 | 'before' => [
36 | 'type' => Type::string(),
37 | 'description' => 'Returns the elements in the list that come before the specified global ID.'
38 | ],
39 | ],
40 | ];
41 |
42 | parent::__construct($config);
43 | }
44 |
45 | }
--------------------------------------------------------------------------------
/src/types/SimpleExpressionType.php:
--------------------------------------------------------------------------------
1 | '=',
24 | 'gt' => '>',
25 | 'lt' => '<',
26 | 'gte' => '>=',
27 | 'lte' => '<=',
28 | ];
29 |
30 | protected $attributes = [
31 | 'name' => 'FieldCondition',
32 | 'description' => 'simple query expression,backend parse it to prepare to query data source',
33 | ];
34 |
35 | public function fields()
36 | {
37 | return [
38 | 'gt' => [
39 | 'type' => Type::int(),
40 | 'description' => 'great than',
41 | ],
42 | 'gte' => [
43 | 'type' => Type::int(),
44 | 'description' => 'great than or equals',
45 | ],
46 | 'lt' => [
47 | 'type' => Type::int(),
48 | 'description' => 'less than',
49 | ],
50 | 'lte' => [
51 | 'type' => Type::int(),
52 | 'description' => 'less than or equals',
53 | ],
54 | 'eq' => [
55 | 'type' => Type::string(),
56 | 'description' => 'equals',
57 | ],
58 | 'in' => [
59 | 'type' => Type::listOf(Type::int()),
60 | 'description' => 'value in list',
61 | ],
62 | ];
63 | }
64 |
65 | public static function toQueryCondition($source)
66 | {
67 | $ret = [];
68 | foreach ($source as $key => $value) {
69 | if (is_scalar($value)) {
70 | $ret[$key] = $value;
71 | } elseif (is_array($value)) {
72 | $opExp = key($value);
73 | $op = self::$operatorMap[$opExp]??$opExp;
74 | $ret[] = [$op, $key, $value[$opExp]];
75 | }
76 | }
77 | return $ret;
78 | }
79 | }
--------------------------------------------------------------------------------
/src/types/UrlType.php:
--------------------------------------------------------------------------------
1 | parseValue($value);
33 | }
34 |
35 | /**
36 | * Parses an externally provided value (query variable) to use as an input
37 | *
38 | * @param mixed $value
39 | * @return mixed
40 | */
41 | public function parseValue($value)
42 | {
43 | if (!is_string($value) || !filter_var($value, FILTER_VALIDATE_URL)) { // quite naive, but after all this is example
44 | throw new \UnexpectedValueException("Cannot represent value as URL: " . Utils::printSafe($value));
45 | }
46 | return $value;
47 | }
48 |
49 | /**
50 | * Parses an externally provided literal value to use as an input (e.g. in Query AST)
51 | *
52 | * @param $ast Node
53 | * @return null|string
54 | * @throws Error
55 | */
56 | public function parseLiteral($ast)
57 | {
58 | // Note: throwing GraphQL\Error\Error vs \UnexpectedValueException to benefit from GraphQL
59 | // error location in query:
60 | if (!($ast instanceof StringValueNode)) {
61 | throw new Error('Query error: Can only parse strings got: ' . $ast->kind, [$ast]);
62 | }
63 | if (!is_string($ast->value) || !filter_var($ast->value, FILTER_VALIDATE_URL)) {
64 | throw new Error('Query error: Not a valid URL', [$ast]);
65 | }
66 | return $ast->value;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/tests/DefaultController.php:
--------------------------------------------------------------------------------
1 | [
25 | 'class' => 'yii\graphql\GraphQLAction',
26 | ]
27 | ];
28 | }
29 |
30 | }
--------------------------------------------------------------------------------
/tests/GraphQLActionTest.php:
--------------------------------------------------------------------------------
1 | mockWebApplication();
27 | $this->controller = new DefaultController('default', \Yii::$app->getModule('graphql'));
28 | }
29 |
30 |
31 | function testAction()
32 | {
33 | $_GET = [
34 | 'query' => $this->queries['hello'],
35 | ];
36 | $controller = $this->controller;
37 | $ret = $controller->runAction('index');
38 | $this->assertNotEmpty($ret);
39 | }
40 |
41 | function testRunError()
42 | {
43 | $_GET = [
44 | 'query' => 'query error{error}',
45 | ];
46 | $controller = $this->controller;
47 | $action = $controller->createAction('index');
48 | $action->enableSchemaAssertValid = false;
49 | $ret = $action->runWithParams([]);
50 | $this->assertNotEmpty($ret);
51 | $this->assertArrayHasKey('errors', $ret);
52 | }
53 |
54 | function testAuthBehavior()
55 | {
56 | $_GET = [
57 | 'query' => $this->queries['hello'],
58 | 'access-token' => 'testtoken',
59 | ];
60 | $controller = $this->controller;
61 | $controller->attachBehavior('authenticator', [
62 | 'class' => QueryParamAuth::className()
63 | ]);
64 | $ret = $controller->runAction('index');
65 | $this->assertNotEmpty($ret);
66 | }
67 |
68 | function testAuthBehaviorExcept()
69 | {
70 | $_GET = [
71 | 'query' => $this->queries['hello'],
72 | ];
73 | $controller = $this->controller;
74 | $controller->attachBehavior('authenticator', [
75 | 'class' => CompositeAuth::className(),
76 | 'authMethods' => [
77 | \yii\filters\auth\QueryParamAuth::className(),
78 | ],
79 | 'except' => ['hello'],
80 | ]);
81 | $ret = $controller->runAction('index');
82 | $this->assertNotEmpty($ret);
83 | }
84 |
85 | function testIntrospectionQuery()
86 | {
87 | $_GET = [
88 | 'query' => $this->queries['introspectionQuery'],
89 | ];
90 | $controller = $this->controller;
91 | $controller->attachBehavior('authenticator', [
92 | 'class' => CompositeAuth::className(),
93 | 'authMethods' => [
94 | \yii\filters\auth\QueryParamAuth::className(),
95 | ],
96 | 'except' => ['__schema'],
97 | ]);
98 | $ret = $controller->runAction('index');
99 | $this->assertNotEmpty($ret);
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/tests/GraphQLTest.php:
--------------------------------------------------------------------------------
1 | mockWebApplication();
29 | $this->graphql = \Yii::$app->getModule('graphql')->getGraphQL();
30 | }
31 |
32 |
33 | /**
34 | * Test schema default
35 | *
36 | * @test
37 | */
38 | public function testSchema()
39 | {
40 | $schema = $this->graphql->buildSchema();
41 |
42 | $this->assertGraphQLSchema($schema);
43 | $this->assertGraphQLSchemaHasQuery($schema, 'stories');
44 | $this->assertGraphQLSchemaHasMutation($schema, 'updateUserPwd');
45 | $this->assertArrayHasKey('user', $schema->getTypeMap());
46 | }
47 |
48 | /**
49 | * Test schema with object
50 | *
51 | * @test
52 | */
53 | public function testSchemaWithSchemaObject()
54 | {
55 | $schemaObject = new Schema([
56 | 'query' => new ObjectType([
57 | 'name' => 'Query'
58 | ]),
59 | 'mutation' => new ObjectType([
60 | 'name' => 'Mutation'
61 | ]),
62 | 'types' => []
63 | ]);
64 | $schema = $this->graphql->buildSchema($schemaObject);
65 |
66 | $this->assertGraphQLSchema($schema);
67 | $this->assertEquals($schemaObject, $schema);
68 | }
69 |
70 | /**
71 | * Test type
72 | *
73 | * @test
74 | */
75 | public function testType()
76 | {
77 | $type = GraphQL::type(ExampleType::class);
78 | $this->assertInstanceOf(\GraphQL\Type\Definition\ObjectType::class, $type);
79 |
80 | $typeOther = GraphQL::type('example', true);
81 | $this->assertTrue($type === $typeOther);
82 |
83 | }
84 |
85 | public function testUnionType()
86 | {
87 | $type = GraphQL::type(ResultItemType::className());
88 | $this->assertInstanceOf(\GraphQL\Type\Definition\UnionType::class, $type);
89 | }
90 |
91 | public function testParseRequestQuery()
92 | {
93 | $query = $this->queries['multiQuery'];
94 | $ret = $this->graphql->parseRequestQuery($query);
95 | $this->assertNotEmpty($ret);
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/tests/GraphqlMutationTest.php:
--------------------------------------------------------------------------------
1 | mockWebApplication();
24 | $this->graphQL = \Yii::$app->getModule('graphql')->getGraphQL();
25 | }
26 |
27 | public function testUpdateUserPwd(){
28 | $query = "mutation updateUserPwd{
29 | updateUserPwd(id: \"qsli@google.com\", password: \"123456\") {
30 | id,
31 | firstName
32 | }
33 | }";
34 | $expect = [
35 | 'updateUserPwd' => ['id' => "1", 'firstName' => "John"]
36 | ];
37 | $result = $this->graphQL->query($query, null, \Yii::$app);
38 | $this->assertArrayHasKey('data', $result);
39 | $this->assertEquals($expect, $result['data']);
40 | }
41 |
42 | public function testValidator()
43 | {
44 | $query = "
45 | mutation updateUserPwd{
46 | updateUserPwd(id: \"1001\",password: \"123456\") {
47 | id,
48 | email
49 | }
50 | }
51 | ";
52 | $result = $this->graphQL->query($query, null, \Yii::$app);
53 | $this->assertArrayHasKey('errors', $result);
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/tests/GraphqlQueryTest.php:
--------------------------------------------------------------------------------
1 | mockWebApplication();
21 | $this->graphQL = \Yii::$app->getModule('graphql')->getGraphQL();
22 | }
23 |
24 | /**
25 | * test if work
26 | */
27 | public function testQueryValid()
28 | {
29 | $result = $this->graphQL->query($this->queries['hello']);
30 | $this->assertArrayHasKey('data', $result);
31 | $this->assertArrayNotHasKey('errors', $result);
32 | }
33 |
34 | /**
35 | * test sample object query
36 | */
37 | public function testQueryWithSingleObject()
38 | {
39 | $result = $this->graphQL->query($this->queries['singleObject'], null, \Yii::$app);
40 | $this->assertArrayHasKey('data', $result);
41 | $this->assertArrayNotHasKey('errors', $result);
42 | }
43 |
44 | /**
45 | * test multi object in a query
46 | */
47 | public function testQueryWithMultiObject()
48 | {
49 | $result = $this->graphQL->query($this->queries['multiObject'], null, \Yii::$app);
50 | $this->assertArrayHasKey('data', $result);
51 | $this->assertArrayNotHasKey('errors', $result);
52 | }
53 |
54 | public function testQueryWithUnion()
55 | {
56 | $query = '
57 | query search
58 | {
59 | search(query:"a",limit:2,after:1,type:story){
60 | nodes{
61 | ... on story{
62 | id
63 | }
64 | }
65 | }
66 | }
67 | ';
68 | $result = $this->graphQL->query($query, null, \Yii::$app);
69 | $this->assertArrayHasKey('data', $result);
70 | $this->assertArrayNotHasKey('errors', $result);
71 | }
72 |
73 | public function testQueryWithInterface()
74 | {
75 | $query = '
76 | query {
77 | node(id:"1",type:"story"){
78 | id,
79 | ... on story{
80 | author{
81 | id,
82 | email
83 | }
84 | }
85 | }
86 | }
87 | ';
88 | $result = $this->graphQL->query($query, null, \Yii::$app);
89 | $this->assertArrayHasKey('data', $result);
90 | $this->assertArrayNotHasKey('errors', $result);
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/tests/Module.php:
--------------------------------------------------------------------------------
1 | destroyApplication();
36 | }
37 |
38 | /**
39 | * Populates Yii::$app with a new application
40 | * The application will be destroyed on tearDown() automatically.
41 | * @param array $config The application configuration, if needed
42 | * @param string $appClass name of the application class to create
43 | */
44 | protected function mockApplication($config = [], $appClass = '\yii\console\Application')
45 | {
46 | new $appClass(ArrayHelper::merge([
47 | 'id' => 'testapp',
48 | 'basePath' => __DIR__,
49 | 'vendorPath' => dirname(__DIR__) . '/vendor',
50 | ], $config));
51 | }
52 |
53 | protected function mockWebApplication($config = [], $appClass = '\yii\web\Application')
54 | {
55 | new $appClass(ArrayHelper::merge([
56 | 'id' => 'testapp',
57 | 'basePath' => __DIR__,
58 | 'vendorPath' => dirname(__DIR__) . '/vendor',
59 | 'components' => [
60 | 'request' => [
61 | 'cookieValidationKey' => 'wefJDF8sfdsfSDefwqdxj9oq',
62 | 'scriptFile' => __DIR__ . '/index.php',
63 | 'scriptUrl' => '/index.php',
64 | ],
65 | 'graphQLCache' => [
66 | 'class' => 'yii\caching\FileCache',
67 | 'cachePath' => '@runtime/graphql',
68 | 'directoryLevel' => 0,
69 | ],
70 | 'db'=>[
71 | 'class' => 'yii\db\Connection',
72 | 'dsn' => 'mysql:host=localhost;dbname=test',
73 | 'username' => 'root',
74 | 'password' => '',
75 | 'charset' => 'utf8',
76 | ],
77 | 'user' => [
78 | 'class' => 'yii\web\User',
79 | 'identityClass' => 'yiiunit\extensions\graphql\data\User'
80 | ],
81 |
82 | ],
83 | 'modules' => [
84 | 'graphql' => [
85 | 'class' => Module::class,
86 | 'schema' => [
87 | 'query' => [
88 | 'hello' => HelloQuery::class,
89 | 'user' => UserQuery::class,
90 | 'viewer' => ViewerQuery::class,
91 | 'stories' => StoryListQuery::class,
92 | 'lastStoryPosted' => LastStoryPostedQuery::class,
93 | 'search' => SearchQuery::className(),
94 | 'node' => NodeQuery::className(),
95 | ],
96 | 'mutation' => [
97 | 'updateUserPwd' => UpdateUserPwdMutation::class
98 | ],
99 | 'types' => [
100 | 'example' => ExampleType::class,
101 | 'story' => StoryType::class,
102 | // 'comment' => CommentType::class,
103 | // 'image' => ImageType::class,
104 | // 'imageSizeEnum' => ImageSizeEnumType::class,
105 | // 'ContentFormatEnum' => ContentFormatEnumType::class,
106 | ],
107 | ],
108 | ]
109 | ],
110 | 'bootstrap' => [
111 | 'graphql'
112 | ],
113 | ], $config));
114 | }
115 |
116 | /**
117 | * Destroys application in Yii::$app by setting it to null.
118 | */
119 | protected function destroyApplication()
120 | {
121 | Yii::$app = null;
122 | Yii::$container = new Container();
123 | }
124 |
125 | /**
126 | * Invokes object method, even if it is private or protected.
127 | * @param object $object object.
128 | * @param string $method method name.
129 | * @param array $args method arguments
130 | * @return mixed method result
131 | */
132 | protected function invoke($object, $method, array $args = [])
133 | {
134 | $classReflection = new \ReflectionClass(get_class($object));
135 | $methodReflection = $classReflection->getMethod($method);
136 | $methodReflection->setAccessible(true);
137 | $result = $methodReflection->invokeArgs($object, $args);
138 | $methodReflection->setAccessible(false);
139 | return $result;
140 | }
141 |
142 | protected function setUp()
143 | {
144 | parent::setUp();
145 | $this->queries = include(__DIR__ . '/objects/queries.php');
146 | DataSource::init();
147 | }
148 |
149 |
150 | protected function assertGraphQLSchema($schema)
151 | {
152 | $this->assertInstanceOf('GraphQL\Type\Schema', $schema);
153 | }
154 |
155 | /**
156 | * @param Schema $schema
157 | * @param $key
158 | */
159 | protected function assertGraphQLSchemaHasQuery($schema, $key)
160 | {
161 | //Query
162 | $query = $schema->getQueryType();
163 | $queryFields = $query->getFields();
164 | $this->assertArrayHasKey($key, $queryFields);
165 |
166 | $queryField = $queryFields[$key];
167 | $queryListType = $queryField->getType();
168 | $queryType = $queryListType->getWrappedType();
169 | $this->assertInstanceOf('GraphQL\Type\Definition\FieldDefinition', $queryField);
170 | $this->assertInstanceOf('GraphQL\Type\Definition\ListOfType', $queryListType);
171 | $this->assertInstanceOf('GraphQL\Type\Definition\ObjectType', $queryType);
172 | }
173 |
174 | /**
175 | * @param Schema $schema
176 | * @param $key
177 | */
178 | protected function assertGraphQLSchemaHasMutation($schema, $key)
179 | {
180 | //Mutation
181 | $mutation = $schema->getMutationType();
182 | $mutationFields = $mutation->getFields();
183 | $this->assertArrayHasKey($key, $mutationFields);
184 |
185 | $mutationField = $mutationFields[$key];
186 | $mutationType = $mutationField->getType();
187 | $this->assertInstanceOf('GraphQL\Type\Definition\FieldDefinition', $mutationField);
188 | $this->assertInstanceOf('GraphQL\Type\Definition\ObjectType', $mutationType);
189 | }
190 | }
191 |
--------------------------------------------------------------------------------
/tests/bootstrap.php:
--------------------------------------------------------------------------------
1 | new User([
26 | 'id' => '1',
27 | 'email' => 'john@example.com',
28 | 'email2' => 'john2@example.com',
29 | 'firstName' => 'John',
30 | 'lastName' => 'Doe',
31 | 'password' => '123456',
32 | ]),
33 | '2' => new User([
34 | 'id' => '2',
35 | 'email' => 'jane@example.com',
36 | 'email2' => 'john2@example.com',
37 | 'firstName' => 'Jane',
38 | 'lastName' => 'Doe',
39 | 'password' => '123456',
40 | ]),
41 | '3' => new User([
42 | 'id' => '3',
43 | 'email' => 'john@example.com',
44 | 'email2' => 'john2@example.com',
45 | 'firstName' => 'John',
46 | 'lastName' => 'Doe',
47 | 'password' => '123456',
48 | ]),
49 | ];
50 |
51 | self::$stories = [
52 | '1' => new Story(['id' => '1', 'authorId' => '1', 'body' => 'GraphQL is awesome!
']),
53 | '2' => new Story(['id' => '2', 'authorId' => '1', 'body' => 'Test this']),
54 | '3' => new Story(['id' => '3', 'authorId' => '3', 'body' => "This\n
story\n
spans\n
newlines"]),
55 | ];
56 |
57 | self::$storyLikes = [
58 | '1' => ['1', '2', '3'],
59 | '2' => [],
60 | '3' => ['1']
61 | ];
62 |
63 | self::$comments = [
64 | // thread #1:
65 | '100' => new Comment(['id' => '100', 'authorId' => '3', 'storyId' => '1', 'body' => 'Likes']),
66 | '110' => new Comment(['id' =>'110', 'authorId' =>'2', 'storyId' => '1', 'body' => 'Reply #1', 'parentId' => '100']),
67 | '111' => new Comment(['id' => '111', 'authorId' => '1', 'storyId' => '1', 'body' => 'Reply #1-1', 'parentId' => '110']),
68 | '112' => new Comment(['id' => '112', 'authorId' => '3', 'storyId' => '1', 'body' => 'Reply #1-2', 'parentId' => '110']),
69 | '113' => new Comment(['id' => '113', 'authorId' => '2', 'storyId' => '1', 'body' => 'Reply #1-3', 'parentId' => '110']),
70 | '114' => new Comment(['id' => '114', 'authorId' => '1', 'storyId' => '1', 'body' => 'Reply #1-4', 'parentId' => '110']),
71 | '115' => new Comment(['id' => '115', 'authorId' => '3', 'storyId' => '1', 'body' => 'Reply #1-5', 'parentId' => '110']),
72 | '116' => new Comment(['id' => '116', 'authorId' => '1', 'storyId' => '1', 'body' => 'Reply #1-6', 'parentId' => '110']),
73 | '117' => new Comment(['id' => '117', 'authorId' => '2', 'storyId' => '1', 'body' => 'Reply #1-7', 'parentId' => '110']),
74 | '120' => new Comment(['id' => '120', 'authorId' => '3', 'storyId' => '1', 'body' => 'Reply #2', 'parentId' => '100']),
75 | '130' => new Comment(['id' => '130', 'authorId' => '3', 'storyId' => '1', 'body' => 'Reply #3', 'parentId' => '100']),
76 | '200' => new Comment(['id' => '200', 'authorId' => '2', 'storyId' => '1', 'body' => 'Me2']),
77 | '300' => new Comment(['id' => '300', 'authorId' => '3', 'storyId' => '1', 'body' => 'U2']),
78 |
79 | # thread #2:
80 | '400' => new Comment(['id' => '400', 'authorId' => '2', 'storyId' => '2', 'body' => 'Me too']),
81 | '500' => new Comment(['id' => '500', 'authorId' => '2', 'storyId' => '2', 'body' => 'Nice!']),
82 | ];
83 |
84 | self::$storyComments = [
85 | '1' => ['100', '200', '300'],
86 | '2' => ['400', '500']
87 | ];
88 |
89 | self::$commentReplies = [
90 | '100' => ['110', '120', '130'],
91 | '110' => ['111', '112', '113', '114', '115', '116', '117'],
92 | ];
93 |
94 | self::$storyMentions = [
95 | '1' => [
96 | self::$users['2']
97 | ],
98 | '2' => [
99 | self::$stories['1'],
100 | self::$users['3']
101 | ]
102 | ];
103 | }
104 |
105 | public static function findUser($id)
106 | {
107 | return isset(self::$users[$id]) ? self::$users[$id] : null;
108 | }
109 |
110 | public static function findStory($id)
111 | {
112 | return isset(self::$stories[$id]) ? self::$stories[$id] : null;
113 | }
114 |
115 | public static function findComment($id)
116 | {
117 | return isset(self::$comments[$id]) ? self::$comments[$id] : null;
118 | }
119 |
120 | public static function findLastStoryFor($authorId)
121 | {
122 | $storiesFound = array_filter(self::$stories, function(Story $story) use ($authorId) {
123 | return $story->authorId == $authorId;
124 | });
125 | return !empty($storiesFound) ? $storiesFound[count($storiesFound) - 1] : null;
126 | }
127 |
128 | public static function findLikes($storyId, $limit)
129 | {
130 | $likes = isset(self::$storyLikes[$storyId]) ? self::$storyLikes[$storyId] : [];
131 | $result = array_map(
132 | function($userId) {
133 | return self::$users[$userId];
134 | },
135 | $likes
136 | );
137 | return array_slice($result, 0, $limit);
138 | }
139 |
140 | public static function isLikedBy($storyId, $userId)
141 | {
142 | $subscribers = isset(self::$storyLikes[$storyId]) ? self::$storyLikes[$storyId] : [];
143 | return in_array($userId, $subscribers);
144 | }
145 |
146 | public static function getUserPhoto($userId, $size)
147 | {
148 | return new Image([
149 | 'id' => $userId,
150 | 'type' => Image::TYPE_USERPIC,
151 | 'size' => $size,
152 | 'width' => rand(100, 200),
153 | 'height' => rand(100, 200)
154 | ]);
155 | }
156 |
157 | public static function findLatestStory()
158 | {
159 | return array_pop(self::$stories);
160 | }
161 |
162 | public static function findStories($limit, $afterId = null)
163 | {
164 | $start = $afterId ? (int) array_search($afterId, array_keys(self::$stories)) + 1 : 0;
165 | return array_slice(array_values(self::$stories), $start, $limit);
166 | }
167 |
168 | public static function findComments($storyId, $limit = 5, $afterId = null)
169 | {
170 | $storyComments = isset(self::$storyComments[$storyId]) ? self::$storyComments[$storyId] : [];
171 |
172 | $start = isset($after) ? (int) array_search($afterId, $storyComments) + 1 : 0;
173 | $storyComments = array_slice($storyComments, $start, $limit);
174 |
175 | return array_map(
176 | function($commentId) {
177 | return self::$comments[$commentId];
178 | },
179 | $storyComments
180 | );
181 | }
182 |
183 | public static function findReplies($commentId, $limit = 5, $afterId = null)
184 | {
185 | $commentReplies = isset(self::$commentReplies[$commentId]) ? self::$commentReplies[$commentId] : [];
186 |
187 | $start = isset($after) ? (int) array_search($afterId, $commentReplies) + 1: 0;
188 | $commentReplies = array_slice($commentReplies, $start, $limit);
189 |
190 | return array_map(
191 | function($replyId) {
192 | return self::$comments[$replyId];
193 | },
194 | $commentReplies
195 | );
196 | }
197 |
198 | public static function countComments($storyId)
199 | {
200 | return isset(self::$storyComments[$storyId]) ? count(self::$storyComments[$storyId]) : 0;
201 | }
202 |
203 | public static function countReplies($commentId)
204 | {
205 | return isset(self::$commentReplies[$commentId]) ? count(self::$commentReplies[$commentId]) : 0;
206 | }
207 |
208 | public static function findStoryMentions($storyId)
209 | {
210 | return isset(self::$storyMentions[$storyId]) ? self::$storyMentions[$storyId] :[];
211 | }
212 | }
213 |
--------------------------------------------------------------------------------
/tests/data/Image.php:
--------------------------------------------------------------------------------
1 | id;
41 | }
42 |
43 | public function getAuthKey()
44 | {
45 | // TODO: Implement getAuthKey() method.
46 | }
47 |
48 | public function validateAuthKey($authKey)
49 | {
50 | // TODO: Implement validateAuthKey() method.
51 | }
52 |
53 |
54 | }
55 |
--------------------------------------------------------------------------------
/tests/objects/mutation/UpdateUserPwdMutation.php:
--------------------------------------------------------------------------------
1 | 'updateUserPwd'
23 | ];
24 |
25 | public function type()
26 | {
27 | return GraphQL::type(UserType::class);
28 | }
29 |
30 | public function args()
31 | {
32 | return [
33 | 'id' => [
34 | 'name' => 'id',
35 | 'type' => Type::nonNull(Type::string())
36 | ],
37 | 'password' => [
38 | 'name' => 'password',
39 | 'type' => Type::nonNull(Type::string())
40 | ]
41 | ];
42 | }
43 |
44 | public function resolve($root, $args)
45 | {
46 | if ($args['id'] == 'qsli@google.com') {
47 | $args['id'] = 1;
48 | }
49 | $user = DataSource::findUser($args['id']);
50 |
51 | if(!$user)
52 | {
53 | return null;
54 | }
55 |
56 | $user->password = md5($args['password']);
57 | return $user;
58 | }
59 |
60 | public function rules()
61 | {
62 | return [
63 | ['id', 'email']
64 | ];
65 | }
66 |
67 | }
--------------------------------------------------------------------------------
/tests/objects/queries.php:
--------------------------------------------------------------------------------
1 | "
6 | query IntrospectionQuery {
7 | __schema {
8 | queryType { name }
9 | mutationType { name }
10 | types {
11 | ...FullType
12 | }
13 | directives {
14 | name
15 | description
16 | args {
17 | ...InputValue
18 | }
19 | onOperation
20 | onFragment
21 | onField
22 | }
23 | }
24 | }
25 |
26 | fragment FullType on __Type {
27 | kind
28 | name
29 | description
30 | fields {
31 | name
32 | description
33 | args {
34 | ...InputValue
35 | }
36 | type {
37 | ...TypeRef
38 | }
39 | isDeprecated
40 | deprecationReason
41 | }
42 | inputFields {
43 | ...InputValue
44 | }
45 | interfaces {
46 | ...TypeRef
47 | }
48 | enumValues {
49 | name
50 | description
51 | isDeprecated
52 | deprecationReason
53 | }
54 | possibleTypes {
55 | ...TypeRef
56 | }
57 | }
58 |
59 | fragment InputValue on __InputValue {
60 | name
61 | description
62 | type { ...TypeRef }
63 | defaultValue
64 | }
65 |
66 | fragment TypeRef on __Type {
67 | kind
68 | name
69 | ofType {
70 | kind
71 | name
72 | ofType {
73 | kind
74 | name
75 | ofType {
76 | kind
77 | name
78 | }
79 | }
80 | }
81 | }
82 | ",
83 | 'hello' => "
84 | query hello{hello}
85 | ",
86 |
87 | 'singleObject' => "
88 | query user {
89 | user(id:\"2\") {
90 | id
91 | email
92 | email2
93 | photo(size:ICON){
94 | id
95 | url
96 | }
97 | firstName
98 | lastName
99 |
100 | }
101 | }
102 | ",
103 | 'multiObject' => "
104 | query multiObject {
105 | user(id: \"2\") {
106 | id
107 | email
108 | photo(size:ICON){
109 | id
110 | url
111 | }
112 | }
113 | stories(after: \"1\") {
114 | id
115 | author{
116 | id
117 | }
118 | body
119 | }
120 | }
121 | ",
122 | 'repeatObject' => "
123 | query repeatObject {
124 | user(id: \"2\") {
125 | id
126 | email
127 | }
128 | stories(after: \"1\") {
129 | id
130 | author
131 | body
132 | }
133 | }
134 | ",
135 | 'userModel'=>"
136 | query userModel{
137 | userModel(id: \"1001\") {
138 | id
139 | email
140 | }
141 | }
142 | ",
143 | 'updateObject' => "
144 | mutation updateUserPwd{
145 | updateUserPwd(id: \"1001\", password: \"123456\") {
146 | id,
147 | username
148 | }
149 | }
150 | ",
151 | 'mutationValidate' => "
152 | mutation updateUserPwd{
153 | updateUserPwd(id: \"1001\",password: \"123456\") {
154 | id,
155 | email
156 | }
157 | }
158 | ",
159 | 'multiQuery' => "
160 | query hello{hello}
161 | query userModel{
162 | userModel(id: \"1001\") {
163 | id
164 | email
165 | }
166 | }
167 | ",
168 | ];
169 |
--------------------------------------------------------------------------------
/tests/objects/query/HelloQuery.php:
--------------------------------------------------------------------------------
1 | 'hello',
20 | ];
21 |
22 | public function type()
23 | {
24 | return Type::string();
25 | }
26 |
27 |
28 | protected function resolve($value, $args, $context, ResolveInfo $info)
29 | {
30 | return 'Your graphql-php endpoint is ready! Use GraphiQL to browse API';
31 | }
32 |
33 |
34 | }
--------------------------------------------------------------------------------
/tests/objects/query/LastStoryPostedQuery.php:
--------------------------------------------------------------------------------
1 | 'Returns last story posted for this blog',
22 | ];
23 |
24 | public function type()
25 | {
26 | return GraphQL::type(StoryType::class);
27 | }
28 |
29 | protected function resolve($value, $args, $context, ResolveInfo $info)
30 | {
31 | return DataSource::findLatestStory();
32 | }
33 |
34 |
35 | }
--------------------------------------------------------------------------------
/tests/objects/query/NodeQuery.php:
--------------------------------------------------------------------------------
1 | Type::nonNull(Type::id()),
33 | 'type' => Type::string()
34 | ];
35 | }
36 |
37 | public function resolve($value, $args, $context, ResolveInfo $info)
38 | {
39 | if ($args['type'] == 'user') {
40 | return DataSource::findUser($args['id']);
41 | } elseif ($args['type'] == 'story') {
42 | return DataSource::findStory($args['id']);
43 | }
44 |
45 | }
46 |
47 |
48 | }
--------------------------------------------------------------------------------
/tests/objects/query/SearchQuery.php:
--------------------------------------------------------------------------------
1 | 'search',
27 | 'description' => 'search user or story',
28 | ];
29 |
30 | public function args()
31 | {
32 | return [
33 | 'limit' => Type::int(),
34 | 'after' => Type::int(),
35 | 'query' => Type::string(),
36 | 'type' => new EnumType(['name' => 't', 'values' => ['user' => ['value' => 'user'], 'story' => ['value' => 'story']]]),
37 | ];
38 | }
39 |
40 | public function type()
41 | {
42 | return GraphQL::type(ResultItemConnectionType::className());
43 | }
44 |
45 | protected function resolve($value, $args, Application $context, ResolveInfo $info)
46 | {
47 | $result = [];
48 | if ($args['type'] == 'user') {
49 | $result = [DataSource::findUser(1)];
50 | } elseif ($args['type'] == 'story') {
51 | $result = DataSource::findStories($args['limit'], $args['after']);
52 | }
53 | return ['nodes' => $result];
54 | }
55 | }
--------------------------------------------------------------------------------
/tests/objects/query/StoryListQuery.php:
--------------------------------------------------------------------------------
1 | 'stories',
23 | 'description'=>'Returns subset of stories posted for this blog',
24 | ];
25 |
26 | public function type()
27 | {
28 | return Type::listOf(GraphQL::type(StoryType::class));
29 | }
30 |
31 | public function args()
32 | {
33 | return [
34 | 'after'=>[
35 | 'type'=>Type::id(),
36 | 'description'=>'Fetch stories listed after the story with this ID'
37 | ],
38 | 'limit' => [
39 | 'type' => Type::int(),
40 | 'description' => 'Number of stories to be returned',
41 | 'defaultValue' => 10
42 | ]
43 | ];
44 | }
45 |
46 | protected function resolve($value, $args, $context, ResolveInfo $info)
47 | {
48 | $args += ['after' => null];
49 | return DataSource::findStories($args['limit'], $args['after']);
50 | }
51 |
52 |
53 | }
--------------------------------------------------------------------------------
/tests/objects/query/UserQuery.php:
--------------------------------------------------------------------------------
1 | [
32 | 'type'=>Type::nonNull(Type::id())
33 | ],
34 | ];
35 | }
36 |
37 | public function resolve($value, $args, $context, ResolveInfo $info)
38 | {
39 | return DataSource::findUser($args['id']);
40 | }
41 |
42 |
43 | }
--------------------------------------------------------------------------------
/tests/objects/query/ViewerQuery.php:
--------------------------------------------------------------------------------
1 | 'Represents currently logged-in user (for the sake of example - simply returns user with id == 1)',
23 | ];
24 |
25 | public function type()
26 | {
27 | return GraphQL::type(UserType::class);
28 | }
29 |
30 | protected function resolve($value, $args, Application $context, ResolveInfo $info)
31 | {
32 | return $context->user->getIdentity();
33 | }
34 | }
--------------------------------------------------------------------------------
/tests/objects/types/CommentType.php:
--------------------------------------------------------------------------------
1 | 'comment',
15 | 'description'=>'user make a view for story',
16 | ];
17 |
18 | public function fields()
19 | {
20 | return [
21 | 'id'=>Type::id(),
22 | 'author'=>GraphQL::type(UserType::class),
23 | 'parent'=>GraphQL::type(CommentType::class),
24 | 'isAnonymous'=>Type::boolean(),
25 | 'replies' => [
26 | 'type' => Type::listOf(GraphQL::type(CommentType::class)),
27 | 'args' => [
28 | 'after' => Type::int(),
29 | 'limit' => [
30 | 'type' => Type::int(),
31 | 'defaultValue' => 5
32 | ]
33 | ]
34 | ],
35 | 'totalReplyCount' => Type::int(),
36 | ];
37 | }
38 |
39 | public function resolveAuthorField(Comment $comment)
40 | {
41 | if ($comment->isAnonymous) {
42 | return null;
43 | }
44 | return DataSource::findUser($comment->authorId);
45 | }
46 |
47 | public function resolveParentField(Comment $comment)
48 | {
49 | if ($comment->parentId) {
50 | return DataSource::findComment($comment->parentId);
51 | }
52 | return null;
53 | }
54 |
55 | public function resolveRepliesField(Comment $comment, $args)
56 | {
57 | $args += ['after' => null];
58 | return DataSource::findReplies($comment->id, $args['limit'], $args['after']);
59 | }
60 |
61 | public function resolveTotalReplyCountField(Comment $comment)
62 | {
63 | return DataSource::countReplies($comment->id);
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/tests/objects/types/ContentFormatEnumType.php:
--------------------------------------------------------------------------------
1 | 'ContentFormatEnum',
16 | 'values' => [self::FORMAT_TEXT, self::FORMAT_HTML]
17 | ];
18 | parent::__construct($config);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/tests/objects/types/ExampleType.php:
--------------------------------------------------------------------------------
1 | 'example',
23 | 'description'=>'user is user'
24 | ];
25 |
26 | public function fields()
27 | {
28 | $result = [
29 | 'id' => ['type'=>Type::id()],
30 | 'email' => GraphQL::type(EmailType::class),
31 | 'email2' => GraphQL::type(EmailType::class),
32 | 'photo' => [
33 | 'type' => GraphQL::type(ImageType::class),
34 | 'description' => 'User photo URL',
35 | 'args' => [
36 | 'size' => Type::nonNull(GraphQL::type(ImageSizeEnumType::class)),
37 | ]
38 | ],
39 | 'firstName' => [
40 | 'type' => Type::string(),
41 | ],
42 | 'lastName' => [
43 | 'type' => Type::string(),
44 | ],
45 | 'lastStoryPosted' => GraphQL::type(StoryType::class),
46 | 'fieldWithError' => [
47 | 'type' => Type::string(),
48 | 'resolve' => function() {
49 | throw new \Exception("This is error field");
50 | }
51 | ]
52 | ];
53 | return $result;
54 | }
55 |
56 | public function resolvePhotoField(User $user,$args){
57 | return DataSource::getUserPhoto($user->id, $args['size']);
58 | }
59 |
60 | public function resolveIdField(User $user, $args)
61 | {
62 | return $user->id.'test';
63 | }
64 |
65 | public function resolveEmail2Field(User $user, $args)
66 | {
67 | return $user->email2.'test';
68 | }
69 |
70 |
71 | }
--------------------------------------------------------------------------------
/tests/objects/types/HtmlField.php:
--------------------------------------------------------------------------------
1 | 'a html tag',
22 | ];
23 |
24 | public function type()
25 | {
26 | return Type::string();
27 | }
28 |
29 | public function args()
30 | {
31 | return [
32 | 'format' => [
33 | 'type' => GraphQL::type(ContentFormatEnumType::class),
34 | 'defaultValue' => ContentFormatEnumType::FORMAT_HTML,
35 | ],
36 | 'maxLength' => Type::int(),
37 | ];
38 | }
39 |
40 | public function resolve($root, $args,$context,ResolveInfo $info)
41 | {
42 | // $fields = $info->getFieldSelection($depth = 3);
43 | $html = $root->{$info->fieldName};
44 | $text = strip_tags($html);
45 |
46 | if (!empty($args['maxLength'])) {
47 | $safeText = mb_substr($text, 0, $args['maxLength']);
48 | } else {
49 | $safeText = $text;
50 | }
51 |
52 | switch ($args['format']) {
53 | case ContentFormatEnumType::FORMAT_HTML:
54 | if ($safeText !== $text) {
55 | // Text was truncated, so just show what's safe:
56 | return nl2br($safeText);
57 | } else {
58 | return $html;
59 | }
60 |
61 | case ContentFormatEnumType::FORMAT_TEXT:
62 | default:
63 | return $safeText;
64 | }
65 | }
66 |
67 | }
--------------------------------------------------------------------------------
/tests/objects/types/ImageSizeEnumType.php:
--------------------------------------------------------------------------------
1 | 'imageSizeEnum',
14 | // Note: 'name' option is not needed in this form - it will be inferred from className
15 | 'values' => [
16 | 'ICON' => [
17 | 'value'=>Image::SIZE_ICON
18 | ],
19 | 'SMALL' => [
20 | 'value'=> Image::SIZE_SMALL,
21 | ],
22 | 'MEDIUM' => [
23 | 'value'=> Image::SIZE_MEDIUM,
24 | ],
25 | 'ORIGINAL' => [
26 | 'value'=> Image::SIZE_ORIGINAL
27 | ],
28 | ]
29 | ];
30 |
31 | parent::__construct($config);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/tests/objects/types/ImageType.php:
--------------------------------------------------------------------------------
1 | 'image',
18 | 'description'=>'a common image type'
19 | ];
20 |
21 | public function interfaces()
22 | {
23 | return [GraphQL::type(NodeType::className())];
24 | }
25 |
26 | public function fields()
27 | {
28 | $result = [
29 | 'id' => Type::id(),
30 | 'type' => new EnumType([
31 | 'name' => 'ImageTypeEnum',
32 | 'values' => [
33 | 'USERPIC' => 'userpic'
34 | ]
35 | ]),
36 | 'size' => ImageSizeEnumType::class,
37 | 'width' => Type::int(),
38 | 'height' => Type::int(),
39 | 'url' => [
40 | 'type' => GraphQL::Type(UrlType::class),
41 | 'resolve' => [$this, 'resolveUrl']
42 | ],
43 |
44 | // Just for the sake of example
45 | 'fieldWithError' => [
46 | 'type' => Type::string(),
47 | 'resolve' => function() {
48 | throw new \Exception("Field with exception");
49 | }
50 | ],
51 | 'nonNullFieldWithError' => [
52 | 'type' => Type::nonNull(Type::string()),
53 | 'resolve' => function() {
54 | throw new \Exception("Non-null field with exception");
55 | }
56 | ]
57 | ];
58 | return $result;
59 | }
60 |
61 | public function resolveUrl(Image $value, $args, Application $context)
62 | {
63 | switch ($value->type) {
64 | case Image::TYPE_USERPIC:
65 | $path = "/images/user/{$value->id}-{$value->size}.jpg";
66 | break;
67 | default:
68 | throw new \UnexpectedValueException("Unexpected image type: " . $value->type);
69 | }
70 | return $context->getHomeUrl() . $path;
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/tests/objects/types/NodeType.php:
--------------------------------------------------------------------------------
1 | 'node'
14 | ];
15 |
16 | public function fields()
17 | {
18 | return [
19 | 'id'=>Type::id()
20 | ];
21 | }
22 |
23 | public function resolveType($object, $types)
24 | {
25 | if ($object instanceof UserType) {
26 | return GraphQL::type('user', true);
27 | } else if ($object instanceof ImageType) {
28 | return GraphQL::type('image', true);
29 | } else if ($object instanceof Story) {
30 | return GraphQL::type('story', true);
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/tests/objects/types/ResultItemConnectionType.php:
--------------------------------------------------------------------------------
1 | 'ResultItemConnection'
20 | ];
21 |
22 | public function fields()
23 | {
24 | return [
25 | 'nodes' => Type::listOf(GraphQL::type(ResultItemType::className())),
26 | ];
27 | }
28 | }
--------------------------------------------------------------------------------
/tests/objects/types/ResultItemType.php:
--------------------------------------------------------------------------------
1 | 'ResultItem',
21 | 'description' => 'result type'
22 | ];
23 |
24 | public function types()
25 | {
26 | return [
27 | StoryType::className(),
28 | UserType::className()
29 | ];
30 | }
31 |
32 | protected function resolveType($value)
33 | {
34 | if ($value instanceof Story) {
35 | return GraphQL::type(StoryType::className());
36 | } elseif ($value instanceof User) {
37 | return GraphQL::type(UserType::className());
38 | }
39 | }
40 | }
--------------------------------------------------------------------------------
/tests/objects/types/StoryType.php:
--------------------------------------------------------------------------------
1 | 'story',
29 | 'description'=>'it is a story'
30 | ];
31 |
32 | public function interfaces()
33 | {
34 | return [GraphQL::type(NodeType::className())];
35 | }
36 |
37 | public function fields()
38 | {
39 | return [
40 | 'id' => ['type'=>Type::id()],
41 | 'author' => GraphQL::type(UserType::class),
42 | // 'mentions' => Type::listOf(Types::mention()),
43 | 'totalCommentCount' => ['type'=>Type::int()],
44 | 'comments' => [
45 | 'type' => Type::listOf(GraphQL::type(CommentType::class)),
46 | 'args' => [
47 | 'after' => [
48 | 'type' => Type::id(),
49 | 'description' => 'Load all comments listed after given comment ID'
50 | ],
51 | 'limit' => [
52 | 'type' => Type::int(),
53 | 'defaultValue' => 5
54 | ]
55 | ]
56 | ],
57 | 'likes' => [
58 | 'type' => Type::listOf(GraphQL::type(UserType::class)),
59 | 'args' => [
60 | 'limit' => [
61 | 'type' => Type::int(),
62 | 'description' => 'Limit the number of recent likes returned',
63 | 'defaultValue' => 5
64 | ]
65 | ]
66 | ],
67 | 'likedBy' => [
68 | 'type' => Type::listOf(GraphQL::type(UserType::class)),
69 | ],
70 | 'affordances' => ['type'=>Type::listOf(new EnumType([
71 | 'name' => 'StoryAffordancesEnum',
72 | 'values' => [
73 | self::EDIT,
74 | self::DELETE,
75 | self::LIKE,
76 | self::UNLIKE,
77 | self::REPLY
78 | ]
79 | ]))],
80 | 'hasViewerLiked' => ['type'=>Type::boolean()],
81 |
82 | 'body'=>HtmlField::class,
83 | ];
84 | }
85 |
86 | public function resolveAuthorField(Story $story)
87 | {
88 | return DataSource::findUser($story->authorId);
89 | }
90 |
91 | public function resolveAffordancesField(Story $story, $args, Application $context)
92 | {
93 | $viewer = $context->user->getIdentity();
94 | $isViewer = $viewer === DataSource::findUser($story->authorId);
95 | $isLiked = DataSource::isLikedBy($story->id, $viewer->getId());
96 |
97 | if ($isViewer) {
98 | $affordances[] = self::EDIT;
99 | $affordances[] = self::DELETE;
100 | }
101 | if ($isLiked) {
102 | $affordances[] = self::UNLIKE;
103 | } else {
104 | $affordances[] = self::LIKE;
105 | }
106 | return $affordances;
107 | }
108 |
109 | public function resolveHasViewerLikedField(Story $story, $args, Application $context)
110 | {
111 | return DataSource::isLikedBy($story->id, $context->getUser()->getId());
112 | }
113 |
114 | public function resolveTotalCommentCountField(Story $story)
115 | {
116 | return DataSource::countComments($story->id);
117 | }
118 |
119 | public function resolveCommentsField(Story $story, $args)
120 | {
121 | $args += ['after' => null];
122 | return DataSource::findComments($story->id, $args['limit'], $args['after']);
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/tests/objects/types/UserType.php:
--------------------------------------------------------------------------------
1 | 'user',
23 | 'description'=>'user is user'
24 | ];
25 |
26 | public function interfaces()
27 | {
28 | return [GraphQL::type(NodeType::className())];
29 | }
30 |
31 | public function fields()
32 | {
33 | $result = [
34 | 'id' => ['type'=>Type::id()],
35 | 'email' => GraphQL::type(EmailType::class),
36 | 'email2' => GraphQL::type(EmailType::class),
37 | 'photo' => [
38 | 'type' => GraphQL::type(ImageType::class),
39 | 'description' => 'User photo URL',
40 | 'args' => [
41 | 'size' => Type::nonNull(GraphQL::type(ImageSizeEnumType::class)),
42 | ]
43 | ],
44 | 'firstName' => [
45 | 'type' => Type::string(),
46 | ],
47 | 'lastName' => [
48 | 'type' => Type::string(),
49 | ],
50 | 'lastStoryPosted' => GraphQL::type(StoryType::class),
51 | 'fieldWithError' => [
52 | 'type' => Type::string(),
53 | 'resolve' => function() {
54 | throw new \Exception("This is error field");
55 | }
56 | ]
57 | ];
58 | return $result;
59 | }
60 |
61 | public function resolvePhotoField(User $user,$args){
62 | return DataSource::getUserPhoto($user->id, $args['size']);
63 | }
64 |
65 | }
--------------------------------------------------------------------------------
/tests/types/SimpleExpressionTypeTest.php:
--------------------------------------------------------------------------------
1 | 1,
20 | 'name' => [
21 | 'eq' => 'abc'
22 | ],
23 | 'count' => [
24 | 'lt' => 1
25 | ],
26 | 'age' => [
27 | 'gt' => 20
28 | ],
29 | ];
30 | $expect = [
31 | 'id' => 1,
32 | ['=', 'name', 'abc'],
33 | ['<', 'count', 1],
34 | ['>', 'age', 20],
35 | ];
36 |
37 | $val = SimpleExpressionType::toQueryCondition($express);
38 | $this->assertEquals($expect, $val);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------