Total Complexity | 73 |
Total Lines | 1674 |
Duplicated Lines | 0 % |
Changes | 1 | ||
Bugs | 0 | Features | 0 |
Complex classes like DefinitionTest often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use DefinitionTest, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
33 | class DefinitionTest extends TestCase |
||
34 | { |
||
35 | /** @var ObjectType */ |
||
36 | public $blogImage; |
||
37 | |||
38 | /** @var ObjectType */ |
||
39 | public $blogArticle; |
||
40 | |||
41 | /** @var ObjectType */ |
||
42 | public $blogAuthor; |
||
43 | |||
44 | /** @var ObjectType */ |
||
45 | public $blogMutation; |
||
46 | |||
47 | /** @var ObjectType */ |
||
48 | public $blogQuery; |
||
49 | |||
50 | /** @var ObjectType */ |
||
51 | public $blogSubscription; |
||
52 | |||
53 | /** @var ObjectType */ |
||
54 | public $objectType; |
||
55 | |||
56 | /** @var ObjectType */ |
||
57 | public $objectWithIsTypeOf; |
||
58 | |||
59 | /** @var InterfaceType */ |
||
60 | public $interfaceType; |
||
61 | |||
62 | /** @var UnionType */ |
||
63 | public $unionType; |
||
64 | |||
65 | /** @var EnumType */ |
||
66 | public $enumType; |
||
67 | |||
68 | /** @var InputObjectType */ |
||
69 | public $inputObjectType; |
||
70 | |||
71 | /** @var CustomScalarType */ |
||
72 | public $scalarType; |
||
73 | |||
74 | public function setUp() |
||
75 | { |
||
76 | $this->objectType = new ObjectType(['name' => 'Object', 'fields' => ['tmp' => Type::string()]]); |
||
77 | $this->interfaceType = new InterfaceType(['name' => 'Interface']); |
||
78 | $this->unionType = new UnionType(['name' => 'Union', 'types' => [$this->objectType]]); |
||
79 | $this->enumType = new EnumType(['name' => 'Enum']); |
||
80 | $this->inputObjectType = new InputObjectType(['name' => 'InputObject']); |
||
81 | |||
82 | $this->objectWithIsTypeOf = new ObjectType([ |
||
83 | 'name' => 'ObjectWithIsTypeOf', |
||
84 | 'fields' => ['f' => ['type' => Type::string()]], |
||
85 | ]); |
||
86 | |||
87 | $this->scalarType = new CustomScalarType([ |
||
88 | 'name' => 'Scalar', |
||
89 | 'serialize' => static function () { |
||
90 | }, |
||
91 | 'parseValue' => static function () { |
||
92 | }, |
||
93 | 'parseLiteral' => static function () { |
||
94 | }, |
||
95 | ]); |
||
96 | |||
97 | $this->blogImage = new ObjectType([ |
||
98 | 'name' => 'Image', |
||
99 | 'fields' => [ |
||
100 | 'url' => ['type' => Type::string()], |
||
101 | 'width' => ['type' => Type::int()], |
||
102 | 'height' => ['type' => Type::int()], |
||
103 | ], |
||
104 | ]); |
||
105 | |||
106 | $this->blogAuthor = new ObjectType([ |
||
107 | 'name' => 'Author', |
||
108 | 'fields' => function () { |
||
109 | return [ |
||
110 | 'id' => ['type' => Type::string()], |
||
111 | 'name' => ['type' => Type::string()], |
||
112 | 'pic' => [ |
||
113 | 'type' => $this->blogImage, |
||
114 | 'args' => [ |
||
115 | 'width' => ['type' => Type::int()], |
||
116 | 'height' => ['type' => Type::int()], |
||
117 | ], |
||
118 | ], |
||
119 | 'recentArticle' => $this->blogArticle, |
||
120 | ]; |
||
121 | }, |
||
122 | ]); |
||
123 | |||
124 | $this->blogArticle = new ObjectType([ |
||
125 | 'name' => 'Article', |
||
126 | 'fields' => [ |
||
127 | 'id' => ['type' => Type::string()], |
||
128 | 'isPublished' => ['type' => Type::boolean()], |
||
129 | 'author' => ['type' => $this->blogAuthor], |
||
130 | 'title' => ['type' => Type::string()], |
||
131 | 'body' => ['type' => Type::string()], |
||
132 | ], |
||
133 | ]); |
||
134 | |||
135 | $this->blogQuery = new ObjectType([ |
||
136 | 'name' => 'Query', |
||
137 | 'fields' => [ |
||
138 | 'article' => [ |
||
139 | 'type' => $this->blogArticle, |
||
140 | 'args' => [ |
||
141 | 'id' => ['type' => Type::string()], |
||
142 | ], |
||
143 | ], |
||
144 | 'feed' => ['type' => new ListOfType($this->blogArticle)], |
||
145 | ], |
||
146 | ]); |
||
147 | |||
148 | $this->blogMutation = new ObjectType([ |
||
149 | 'name' => 'Mutation', |
||
150 | 'fields' => [ |
||
151 | 'writeArticle' => ['type' => $this->blogArticle], |
||
152 | ], |
||
153 | ]); |
||
154 | |||
155 | $this->blogSubscription = new ObjectType([ |
||
156 | 'name' => 'Subscription', |
||
157 | 'fields' => [ |
||
158 | 'articleSubscribe' => [ |
||
159 | 'args' => ['id' => ['type' => Type::string()]], |
||
160 | 'type' => $this->blogArticle, |
||
161 | ], |
||
162 | ], |
||
163 | ]); |
||
164 | } |
||
165 | |||
166 | // Type System: Example |
||
167 | |||
168 | /** |
||
169 | * @see it('defines a query only schema') |
||
170 | */ |
||
171 | public function testDefinesAQueryOnlySchema() : void |
||
172 | { |
||
173 | $blogSchema = new Schema([ |
||
174 | 'query' => $this->blogQuery, |
||
175 | ]); |
||
176 | |||
177 | self::assertSame($blogSchema->getQueryType(), $this->blogQuery); |
||
178 | |||
179 | $articleField = $this->blogQuery->getField('article'); |
||
180 | self::assertSame($articleField->getType(), $this->blogArticle); |
||
181 | self::assertSame($articleField->getType()->name, 'Article'); |
||
182 | self::assertSame($articleField->name, 'article'); |
||
183 | |||
184 | /** @var ObjectType $articleFieldType */ |
||
185 | $articleFieldType = $articleField->getType(); |
||
186 | $titleField = $articleFieldType->getField('title'); |
||
187 | |||
188 | self::assertInstanceOf('GraphQL\Type\Definition\FieldDefinition', $titleField); |
||
189 | self::assertSame('title', $titleField->name); |
||
190 | self::assertSame(Type::string(), $titleField->getType()); |
||
191 | |||
192 | $authorField = $articleFieldType->getField('author'); |
||
193 | self::assertInstanceOf('GraphQL\Type\Definition\FieldDefinition', $authorField); |
||
194 | |||
195 | /** @var ObjectType $authorFieldType */ |
||
196 | $authorFieldType = $authorField->getType(); |
||
197 | self::assertSame($this->blogAuthor, $authorFieldType); |
||
198 | |||
199 | $recentArticleField = $authorFieldType->getField('recentArticle'); |
||
200 | self::assertInstanceOf('GraphQL\Type\Definition\FieldDefinition', $recentArticleField); |
||
201 | self::assertSame($this->blogArticle, $recentArticleField->getType()); |
||
202 | |||
203 | $feedField = $this->blogQuery->getField('feed'); |
||
204 | self::assertInstanceOf('GraphQL\Type\Definition\FieldDefinition', $feedField); |
||
205 | |||
206 | /** @var ListOfType $feedFieldType */ |
||
207 | $feedFieldType = $feedField->getType(); |
||
208 | self::assertInstanceOf('GraphQL\Type\Definition\ListOfType', $feedFieldType); |
||
209 | self::assertSame($this->blogArticle, $feedFieldType->getWrappedType()); |
||
210 | } |
||
211 | |||
212 | /** |
||
213 | * @see it('defines a mutation schema') |
||
214 | */ |
||
215 | public function testDefinesAMutationSchema() : void |
||
216 | { |
||
217 | $schema = new Schema([ |
||
218 | 'query' => $this->blogQuery, |
||
219 | 'mutation' => $this->blogMutation, |
||
220 | ]); |
||
221 | |||
222 | self::assertSame($this->blogMutation, $schema->getMutationType()); |
||
223 | $writeMutation = $this->blogMutation->getField('writeArticle'); |
||
224 | |||
225 | self::assertInstanceOf('GraphQL\Type\Definition\FieldDefinition', $writeMutation); |
||
226 | self::assertSame($this->blogArticle, $writeMutation->getType()); |
||
227 | self::assertSame('Article', $writeMutation->getType()->name); |
||
228 | self::assertSame('writeArticle', $writeMutation->name); |
||
229 | } |
||
230 | |||
231 | /** |
||
232 | * @see it('defines a subscription schema') |
||
233 | */ |
||
234 | public function testDefinesSubscriptionSchema() : void |
||
235 | { |
||
236 | $schema = new Schema([ |
||
237 | 'query' => $this->blogQuery, |
||
238 | 'subscription' => $this->blogSubscription, |
||
239 | ]); |
||
240 | |||
241 | self::assertEquals($this->blogSubscription, $schema->getSubscriptionType()); |
||
242 | |||
243 | $sub = $this->blogSubscription->getField('articleSubscribe'); |
||
244 | self::assertEquals($sub->getType(), $this->blogArticle); |
||
245 | self::assertEquals($sub->getType()->name, 'Article'); |
||
246 | self::assertEquals($sub->name, 'articleSubscribe'); |
||
247 | } |
||
248 | |||
249 | /** |
||
250 | * @see it('defines an enum type with deprecated value') |
||
251 | */ |
||
252 | public function testDefinesEnumTypeWithDeprecatedValue() : void |
||
253 | { |
||
254 | $enumTypeWithDeprecatedValue = new EnumType([ |
||
255 | 'name' => 'EnumWithDeprecatedValue', |
||
256 | 'values' => [ |
||
257 | 'foo' => ['deprecationReason' => 'Just because'], |
||
258 | ], |
||
259 | ]); |
||
260 | |||
261 | $value = $enumTypeWithDeprecatedValue->getValues()[0]; |
||
262 | |||
263 | self::assertArraySubset( |
||
264 | [ |
||
265 | 'name' => 'foo', |
||
266 | 'description' => null, |
||
267 | 'deprecationReason' => 'Just because', |
||
268 | 'value' => 'foo', |
||
269 | 'astNode' => null, |
||
270 | ], |
||
271 | (array) $value |
||
272 | ); |
||
273 | |||
274 | self::assertEquals(true, $value->isDeprecated()); |
||
275 | } |
||
276 | |||
277 | /** |
||
278 | * @see it('defines an enum type with a value of `null` and `undefined`') |
||
279 | */ |
||
280 | public function testDefinesAnEnumTypeWithAValueOfNullAndUndefined() : void |
||
281 | { |
||
282 | $EnumTypeWithNullishValue = new EnumType([ |
||
283 | 'name' => 'EnumWithNullishValue', |
||
284 | 'values' => [ |
||
285 | 'NULL' => ['value' => null], |
||
286 | 'UNDEFINED' => ['value' => null], |
||
287 | ], |
||
288 | ]); |
||
289 | |||
290 | $expected = [ |
||
291 | [ |
||
292 | 'name' => 'NULL', |
||
293 | 'description' => null, |
||
294 | 'deprecationReason' => null, |
||
295 | 'value' => null, |
||
296 | 'astNode' => null, |
||
297 | ], |
||
298 | [ |
||
299 | 'name' => 'UNDEFINED', |
||
300 | 'description' => null, |
||
301 | 'deprecationReason' => null, |
||
302 | 'value' => null, |
||
303 | 'astNode' => null, |
||
304 | ], |
||
305 | ]; |
||
306 | |||
307 | $actual = $EnumTypeWithNullishValue->getValues(); |
||
308 | |||
309 | self::assertEquals(count($expected), count($actual)); |
||
310 | self::assertArraySubset($expected[0], (array) $actual[0]); |
||
311 | self::assertArraySubset($expected[1], (array) $actual[1]); |
||
312 | } |
||
313 | |||
314 | /** |
||
315 | * @see it('defines an object type with deprecated field') |
||
316 | */ |
||
317 | public function testDefinesAnObjectTypeWithDeprecatedField() : void |
||
318 | { |
||
319 | $TypeWithDeprecatedField = new ObjectType([ |
||
320 | 'name' => 'foo', |
||
321 | 'fields' => [ |
||
322 | 'bar' => [ |
||
323 | 'type' => Type::string(), |
||
324 | 'deprecationReason' => 'A terrible reason', |
||
325 | ], |
||
326 | ], |
||
327 | ]); |
||
328 | |||
329 | $field = $TypeWithDeprecatedField->getField('bar'); |
||
330 | |||
331 | self::assertEquals(Type::string(), $field->getType()); |
||
332 | self::assertEquals(true, $field->isDeprecated()); |
||
333 | self::assertEquals('A terrible reason', $field->deprecationReason); |
||
334 | self::assertEquals('bar', $field->name); |
||
335 | self::assertEquals([], $field->args); |
||
336 | } |
||
337 | |||
338 | /** |
||
339 | * @see it('includes nested input objects in the map') |
||
340 | */ |
||
341 | public function testIncludesNestedInputObjectInTheMap() : void |
||
342 | { |
||
343 | $nestedInputObject = new InputObjectType([ |
||
344 | 'name' => 'NestedInputObject', |
||
345 | 'fields' => ['value' => ['type' => Type::string()]], |
||
346 | ]); |
||
347 | $someInputObject = new InputObjectType([ |
||
348 | 'name' => 'SomeInputObject', |
||
349 | 'fields' => ['nested' => ['type' => $nestedInputObject]], |
||
350 | ]); |
||
351 | $someMutation = new ObjectType([ |
||
352 | 'name' => 'SomeMutation', |
||
353 | 'fields' => [ |
||
354 | 'mutateSomething' => [ |
||
355 | 'type' => $this->blogArticle, |
||
356 | 'args' => ['input' => ['type' => $someInputObject]], |
||
357 | ], |
||
358 | ], |
||
359 | ]); |
||
360 | |||
361 | $schema = new Schema([ |
||
362 | 'query' => $this->blogQuery, |
||
363 | 'mutation' => $someMutation, |
||
364 | ]); |
||
365 | self::assertSame($nestedInputObject, $schema->getType('NestedInputObject')); |
||
366 | } |
||
367 | |||
368 | /** |
||
369 | * @see it('includes interface possible types in the type map') |
||
370 | */ |
||
371 | public function testIncludesInterfaceSubtypesInTheTypeMap() : void |
||
372 | { |
||
373 | $someInterface = new InterfaceType([ |
||
374 | 'name' => 'SomeInterface', |
||
375 | 'fields' => [ |
||
376 | 'f' => ['type' => Type::int()], |
||
377 | ], |
||
378 | ]); |
||
379 | |||
380 | $someSubtype = new ObjectType([ |
||
381 | 'name' => 'SomeSubtype', |
||
382 | 'fields' => [ |
||
383 | 'f' => ['type' => Type::int()], |
||
384 | ], |
||
385 | 'interfaces' => [$someInterface], |
||
386 | ]); |
||
387 | |||
388 | $schema = new Schema([ |
||
389 | 'query' => new ObjectType([ |
||
390 | 'name' => 'Query', |
||
391 | 'fields' => [ |
||
392 | 'iface' => ['type' => $someInterface], |
||
393 | ], |
||
394 | ]), |
||
395 | 'types' => [$someSubtype], |
||
396 | ]); |
||
397 | self::assertSame($someSubtype, $schema->getType('SomeSubtype')); |
||
398 | } |
||
399 | |||
400 | /** |
||
401 | * @see it('includes interfaces' thunk subtypes in the type map') |
||
402 | */ |
||
403 | public function testIncludesInterfacesThunkSubtypesInTheTypeMap() : void |
||
404 | { |
||
405 | $someInterface = null; |
||
406 | |||
407 | $someSubtype = new ObjectType([ |
||
408 | 'name' => 'SomeSubtype', |
||
409 | 'fields' => [ |
||
410 | 'f' => ['type' => Type::int()], |
||
411 | ], |
||
412 | 'interfaces' => static function () use (&$someInterface) { |
||
413 | return [$someInterface]; |
||
414 | }, |
||
415 | ]); |
||
416 | |||
417 | $someInterface = new InterfaceType([ |
||
418 | 'name' => 'SomeInterface', |
||
419 | 'fields' => [ |
||
420 | 'f' => ['type' => Type::int()], |
||
421 | ], |
||
422 | ]); |
||
423 | |||
424 | $schema = new Schema([ |
||
425 | 'query' => new ObjectType([ |
||
426 | 'name' => 'Query', |
||
427 | 'fields' => [ |
||
428 | 'iface' => ['type' => $someInterface], |
||
429 | ], |
||
430 | ]), |
||
431 | 'types' => [$someSubtype], |
||
432 | ]); |
||
433 | |||
434 | self::assertSame($someSubtype, $schema->getType('SomeSubtype')); |
||
435 | } |
||
436 | |||
437 | /** |
||
438 | * @see it('stringifies simple types') |
||
439 | */ |
||
440 | public function testStringifiesSimpleTypes() : void |
||
441 | { |
||
442 | self::assertSame('Int', (string) Type::int()); |
||
443 | self::assertSame('Article', (string) $this->blogArticle); |
||
444 | |||
445 | self::assertSame('Interface', (string) $this->interfaceType); |
||
446 | self::assertSame('Union', (string) $this->unionType); |
||
447 | self::assertSame('Enum', (string) $this->enumType); |
||
448 | self::assertSame('InputObject', (string) $this->inputObjectType); |
||
449 | self::assertSame('Object', (string) $this->objectType); |
||
450 | |||
451 | self::assertSame('Int!', (string) new NonNull(Type::int())); |
||
452 | self::assertSame('[Int]', (string) new ListOfType(Type::int())); |
||
453 | self::assertSame('[Int]!', (string) new NonNull(new ListOfType(Type::int()))); |
||
454 | self::assertSame('[Int!]', (string) new ListOfType(new NonNull(Type::int()))); |
||
455 | self::assertSame('[[Int]]', (string) new ListOfType(new ListOfType(Type::int()))); |
||
456 | } |
||
457 | |||
458 | /** |
||
459 | * @see it('JSON stringifies simple types') |
||
460 | */ |
||
461 | public function testJSONStringifiesSimpleTypes() : void |
||
462 | { |
||
463 | self::assertEquals('"Int"', json_encode(Type::int())); |
||
464 | self::assertEquals('"Article"', json_encode($this->blogArticle)); |
||
465 | self::assertEquals('"Interface"', json_encode($this->interfaceType)); |
||
466 | self::assertEquals('"Union"', json_encode($this->unionType)); |
||
467 | self::assertEquals('"Enum"', json_encode($this->enumType)); |
||
468 | self::assertEquals('"InputObject"', json_encode($this->inputObjectType)); |
||
469 | self::assertEquals('"Int!"', json_encode(Type::nonNull(Type::int()))); |
||
470 | self::assertEquals('"[Int]"', json_encode(Type::listOf(Type::int()))); |
||
471 | self::assertEquals('"[Int]!"', json_encode(Type::nonNull(Type::listOf(Type::int())))); |
||
472 | self::assertEquals('"[Int!]"', json_encode(Type::listOf(Type::nonNull(Type::int())))); |
||
473 | self::assertEquals('"[[Int]]"', json_encode(Type::listOf(Type::listOf(Type::int())))); |
||
474 | } |
||
475 | |||
476 | /** |
||
477 | * @see it('identifies input types') |
||
478 | */ |
||
479 | public function testIdentifiesInputTypes() : void |
||
480 | { |
||
481 | $expected = [ |
||
482 | [Type::int(), true], |
||
483 | [$this->objectType, false], |
||
484 | [$this->interfaceType, false], |
||
485 | [$this->unionType, false], |
||
486 | [$this->enumType, true], |
||
487 | [$this->inputObjectType, true], |
||
488 | |||
489 | [Type::boolean(), true], |
||
490 | [Type::float(),true ], |
||
491 | [Type::id(), true], |
||
492 | [Type::int(), true], |
||
493 | [Type::listOf(Type::string()), true], |
||
494 | [Type::listOf($this->objectType), false], |
||
495 | [Type::nonNull(Type::string()), true], |
||
496 | [Type::nonNull($this->objectType), false], |
||
497 | [Type::string(), true], |
||
498 | ]; |
||
499 | |||
500 | foreach ($expected as $index => $entry) { |
||
501 | self::assertSame( |
||
502 | $entry[1], |
||
503 | Type::isInputType($entry[0]), |
||
504 | sprintf('Type %s was detected incorrectly', $entry[0]) |
||
505 | ); |
||
506 | } |
||
507 | } |
||
508 | |||
509 | /** |
||
510 | * @see it('identifies output types') |
||
511 | */ |
||
512 | public function testIdentifiesOutputTypes() : void |
||
513 | { |
||
514 | $expected = [ |
||
515 | [Type::int(), true], |
||
516 | [$this->objectType, true], |
||
517 | [$this->interfaceType, true], |
||
518 | [$this->unionType, true], |
||
519 | [$this->enumType, true], |
||
520 | [$this->inputObjectType, false], |
||
521 | |||
522 | [Type::boolean(), true], |
||
523 | [Type::float(),true ], |
||
524 | [Type::id(), true], |
||
525 | [Type::int(), true], |
||
526 | [Type::listOf(Type::string()), true], |
||
527 | [Type::listOf($this->objectType), true], |
||
528 | [Type::nonNull(Type::string()), true], |
||
529 | [Type::nonNull($this->objectType), true], |
||
530 | [Type::string(), true], |
||
531 | ]; |
||
532 | |||
533 | foreach ($expected as $index => $entry) { |
||
534 | self::assertSame( |
||
535 | $entry[1], |
||
536 | Type::isOutputType($entry[0]), |
||
537 | sprintf('Type %s was detected incorrectly', $entry[0]) |
||
538 | ); |
||
539 | } |
||
540 | } |
||
541 | |||
542 | /** |
||
543 | * @see it('allows a thunk for Union member types') |
||
544 | */ |
||
545 | public function testAllowsThunkForUnionTypes() : void |
||
546 | { |
||
547 | $union = new UnionType([ |
||
548 | 'name' => 'ThunkUnion', |
||
549 | 'types' => function () { |
||
550 | return [$this->objectType]; |
||
551 | }, |
||
552 | ]); |
||
553 | |||
554 | $types = $union->getTypes(); |
||
555 | self::assertEquals(1, count($types)); |
||
556 | self::assertSame($this->objectType, $types[0]); |
||
557 | } |
||
558 | |||
559 | public function testAllowsRecursiveDefinitions() : void |
||
560 | { |
||
561 | // See https://github.com/webonyx/graphql-php/issues/16 |
||
562 | $node = new InterfaceType([ |
||
563 | 'name' => 'Node', |
||
564 | 'fields' => [ |
||
565 | 'id' => ['type' => Type::nonNull(Type::id())], |
||
566 | ], |
||
567 | ]); |
||
568 | |||
569 | $blog = null; |
||
570 | $called = false; |
||
571 | |||
572 | $user = new ObjectType([ |
||
573 | 'name' => 'User', |
||
574 | 'fields' => static function () use (&$blog, &$called) { |
||
575 | self::assertNotNull($blog, 'Blog type is expected to be defined at this point, but it is null'); |
||
576 | $called = true; |
||
577 | |||
578 | return [ |
||
579 | 'id' => ['type' => Type::nonNull(Type::id())], |
||
580 | 'blogs' => ['type' => Type::nonNull(Type::listOf(Type::nonNull($blog)))], |
||
581 | ]; |
||
582 | }, |
||
583 | 'interfaces' => static function () use ($node) { |
||
584 | return [$node]; |
||
585 | }, |
||
586 | ]); |
||
587 | |||
588 | $blog = new ObjectType([ |
||
589 | 'name' => 'Blog', |
||
590 | 'fields' => static function () use ($user) { |
||
591 | return [ |
||
592 | 'id' => ['type' => Type::nonNull(Type::id())], |
||
593 | 'owner' => ['type' => Type::nonNull($user)], |
||
594 | ]; |
||
595 | }, |
||
596 | 'interfaces' => static function () use ($node) { |
||
597 | return [$node]; |
||
598 | }, |
||
599 | ]); |
||
600 | |||
601 | $schema = new Schema([ |
||
602 | 'query' => new ObjectType([ |
||
603 | 'name' => 'Query', |
||
604 | 'fields' => [ |
||
605 | 'node' => ['type' => $node], |
||
606 | ], |
||
607 | ]), |
||
608 | 'types' => [$user, $blog], |
||
609 | ]); |
||
610 | |||
611 | self::assertTrue($called); |
||
612 | $schema->getType('Blog'); |
||
613 | |||
614 | self::assertEquals([$node], $blog->getInterfaces()); |
||
615 | self::assertEquals([$node], $user->getInterfaces()); |
||
616 | |||
617 | self::assertNotNull($user->getField('blogs')); |
||
618 | /** @var NonNull $blogFieldReturnType */ |
||
619 | $blogFieldReturnType = $user->getField('blogs')->getType(); |
||
620 | self::assertSame($blog, $blogFieldReturnType->getWrappedType(true)); |
||
621 | |||
622 | self::assertNotNull($blog->getField('owner')); |
||
623 | /** @var NonNull $ownerFieldReturnType */ |
||
624 | $ownerFieldReturnType = $blog->getField('owner')->getType(); |
||
625 | self::assertSame($user, $ownerFieldReturnType->getWrappedType(true)); |
||
626 | } |
||
627 | |||
628 | public function testInputObjectTypeAllowsRecursiveDefinitions() : void |
||
629 | { |
||
630 | $called = false; |
||
631 | $inputObject = new InputObjectType([ |
||
632 | 'name' => 'InputObject', |
||
633 | 'fields' => static function () use (&$inputObject, &$called) { |
||
634 | $called = true; |
||
635 | |||
636 | return [ |
||
637 | 'value' => ['type' => Type::string()], |
||
638 | 'nested' => ['type' => $inputObject], |
||
639 | ]; |
||
640 | }, |
||
641 | ]); |
||
642 | |||
643 | $someMutation = new ObjectType([ |
||
644 | 'name' => 'SomeMutation', |
||
645 | 'fields' => [ |
||
646 | 'mutateSomething' => [ |
||
647 | 'type' => $this->blogArticle, |
||
648 | 'args' => ['input' => ['type' => $inputObject]], |
||
649 | ], |
||
650 | ], |
||
651 | ]); |
||
652 | |||
653 | $schema = new Schema([ |
||
654 | 'query' => $this->blogQuery, |
||
655 | 'mutation' => $someMutation, |
||
656 | ]); |
||
657 | |||
658 | self::assertSame($inputObject, $schema->getType('InputObject')); |
||
659 | self::assertTrue($called); |
||
660 | self::assertEquals(count($inputObject->getFields()), 2); |
||
661 | self::assertSame($inputObject->getField('nested')->getType(), $inputObject); |
||
662 | self::assertSame($someMutation->getField('mutateSomething')->getArg('input')->getType(), $inputObject); |
||
663 | } |
||
664 | |||
665 | public function testInterfaceTypeAllowsRecursiveDefinitions() : void |
||
666 | { |
||
667 | $called = false; |
||
668 | $interface = new InterfaceType([ |
||
669 | 'name' => 'SomeInterface', |
||
670 | 'fields' => static function () use (&$interface, &$called) { |
||
671 | $called = true; |
||
672 | |||
673 | return [ |
||
674 | 'value' => ['type' => Type::string()], |
||
675 | 'nested' => ['type' => $interface], |
||
676 | ]; |
||
677 | }, |
||
678 | ]); |
||
679 | |||
680 | $query = new ObjectType([ |
||
681 | 'name' => 'Query', |
||
682 | 'fields' => [ |
||
683 | 'test' => ['type' => $interface], |
||
684 | ], |
||
685 | ]); |
||
686 | |||
687 | $schema = new Schema(['query' => $query]); |
||
688 | |||
689 | self::assertSame($interface, $schema->getType('SomeInterface')); |
||
690 | self::assertTrue($called); |
||
691 | self::assertEquals(count($interface->getFields()), 2); |
||
692 | self::assertSame($interface->getField('nested')->getType(), $interface); |
||
693 | self::assertSame($interface->getField('value')->getType(), Type::string()); |
||
694 | } |
||
695 | |||
696 | public function testAllowsShorthandFieldDefinition() : void |
||
697 | { |
||
698 | $interface = new InterfaceType([ |
||
699 | 'name' => 'SomeInterface', |
||
700 | 'fields' => static function () use (&$interface) { |
||
701 | return [ |
||
702 | 'value' => Type::string(), |
||
703 | 'nested' => $interface, |
||
704 | 'withArg' => [ |
||
705 | 'type' => Type::string(), |
||
706 | 'args' => [ |
||
707 | 'arg1' => Type::int(), |
||
708 | ], |
||
709 | ], |
||
710 | ]; |
||
711 | }, |
||
712 | ]); |
||
713 | |||
714 | $query = new ObjectType([ |
||
715 | 'name' => 'Query', |
||
716 | 'fields' => ['test' => $interface], |
||
717 | ]); |
||
718 | |||
719 | $schema = new Schema(['query' => $query]); |
||
720 | |||
721 | /** @var InterfaceType $SomeInterface */ |
||
722 | $SomeInterface = $schema->getType('SomeInterface'); |
||
723 | |||
724 | $valueField = $SomeInterface->getField('value'); |
||
725 | self::assertEquals(Type::string(), $valueField->getType()); |
||
726 | |||
727 | $nestedField = $SomeInterface->getField('nested'); |
||
728 | self::assertEquals($interface, $nestedField->getType()); |
||
729 | |||
730 | $withArg = $SomeInterface->getField('withArg'); |
||
731 | self::assertEquals(Type::string(), $withArg->getType()); |
||
732 | |||
733 | self::assertEquals('arg1', $withArg->args[0]->name); |
||
734 | self::assertEquals(Type::int(), $withArg->args[0]->getType()); |
||
735 | |||
736 | /** @var ObjectType $Query */ |
||
737 | $Query = $schema->getType('Query'); |
||
738 | $testField = $Query->getField('test'); |
||
739 | self::assertEquals($interface, $testField->getType()); |
||
740 | self::assertEquals('test', $testField->name); |
||
741 | } |
||
742 | |||
743 | public function testInfersNameFromClassname() : void |
||
744 | { |
||
745 | $myObj = new MyCustomType(); |
||
746 | self::assertEquals('MyCustom', $myObj->name); |
||
747 | |||
748 | $otherCustom = new OtherCustom(); |
||
749 | self::assertEquals('OtherCustom', $otherCustom->name); |
||
750 | } |
||
751 | |||
752 | public function testAllowsOverridingInternalTypes() : void |
||
753 | { |
||
754 | $idType = new CustomScalarType([ |
||
755 | 'name' => 'ID', |
||
756 | 'serialize' => static function () { |
||
757 | }, |
||
758 | 'parseValue' => static function () { |
||
759 | }, |
||
760 | 'parseLiteral' => static function () { |
||
761 | }, |
||
762 | ]); |
||
763 | |||
764 | $schema = new Schema([ |
||
765 | 'query' => new ObjectType(['name' => 'Query', 'fields' => []]), |
||
766 | 'types' => [$idType], |
||
767 | ]); |
||
768 | |||
769 | self::assertSame($idType, $schema->getType('ID')); |
||
770 | } |
||
771 | |||
772 | // Field config must be object |
||
773 | |||
774 | /** |
||
775 | * @see it('accepts an Object type with a field function') |
||
776 | */ |
||
777 | public function testAcceptsAnObjectTypeWithAFieldFunction() : void |
||
778 | { |
||
779 | $objType = new ObjectType([ |
||
780 | 'name' => 'SomeObject', |
||
781 | 'fields' => static function () { |
||
782 | return [ |
||
783 | 'f' => ['type' => Type::string()], |
||
784 | ]; |
||
785 | }, |
||
786 | ]); |
||
787 | $objType->assertValid(); |
||
788 | self::assertSame(Type::string(), $objType->getField('f')->getType()); |
||
789 | } |
||
790 | |||
791 | /** |
||
792 | * @see it('rejects an Object type field with undefined config') |
||
793 | */ |
||
794 | public function testRejectsAnObjectTypeFieldWithUndefinedConfig() : void |
||
795 | { |
||
796 | $objType = new ObjectType([ |
||
797 | 'name' => 'SomeObject', |
||
798 | 'fields' => ['f' => null], |
||
799 | ]); |
||
800 | $this->expectException(InvariantViolation::class); |
||
801 | $this->expectExceptionMessage( |
||
802 | 'SomeObject.f field config must be an array, but got: null' |
||
803 | ); |
||
804 | $objType->getFields(); |
||
805 | } |
||
806 | |||
807 | /** |
||
808 | * @see it('rejects an Object type with incorrectly typed fields') |
||
809 | */ |
||
810 | public function testRejectsAnObjectTypeWithIncorrectlyTypedFields() : void |
||
811 | { |
||
812 | $objType = new ObjectType([ |
||
813 | 'name' => 'SomeObject', |
||
814 | 'fields' => [['field' => Type::string()]], |
||
815 | ]); |
||
816 | $this->expectException(InvariantViolation::class); |
||
817 | $this->expectExceptionMessage( |
||
818 | 'SomeObject fields must be an associative array with field names as keys or a ' . |
||
819 | 'function which returns such an array.' |
||
820 | ); |
||
821 | $objType->getFields(); |
||
822 | } |
||
823 | |||
824 | /** |
||
825 | * @see it('rejects an Object type with a field function that returns incorrect type') |
||
826 | */ |
||
827 | public function testRejectsAnObjectTypeWithAFieldFunctionThatReturnsIncorrectType() : void |
||
828 | { |
||
829 | $objType = new ObjectType([ |
||
830 | 'name' => 'SomeObject', |
||
831 | 'fields' => static function () { |
||
832 | return [['field' => Type::string()]]; |
||
833 | }, |
||
834 | ]); |
||
835 | $this->expectException(InvariantViolation::class); |
||
836 | $this->expectExceptionMessage( |
||
837 | 'SomeObject fields must be an associative array with field names as keys or a ' . |
||
838 | 'function which returns such an array.' |
||
839 | ); |
||
840 | $objType->getFields(); |
||
841 | } |
||
842 | |||
843 | // Field arg config must be object |
||
844 | |||
845 | /** |
||
846 | * @see it('accepts an Object type with field args') |
||
847 | */ |
||
848 | public function testAcceptsAnObjectTypeWithFieldArgs() : void |
||
849 | { |
||
850 | $this->expectNotToPerformAssertions(); |
||
851 | $objType = new ObjectType([ |
||
852 | 'name' => 'SomeObject', |
||
853 | 'fields' => [ |
||
854 | 'goodField' => [ |
||
855 | 'type' => Type::string(), |
||
856 | 'args' => [ |
||
857 | 'goodArg' => ['type' => Type::string()], |
||
858 | ], |
||
859 | ], |
||
860 | ], |
||
861 | ]); |
||
862 | // Should not throw: |
||
863 | $objType->assertValid(); |
||
864 | } |
||
865 | |||
866 | // rejects an Object type with incorrectly typed field args |
||
867 | |||
868 | /** |
||
869 | * @see it('does not allow isDeprecated without deprecationReason on field') |
||
870 | */ |
||
871 | public function testDoesNotAllowIsDeprecatedWithoutDeprecationReasonOnField() : void |
||
872 | { |
||
873 | $OldObject = new ObjectType([ |
||
874 | 'name' => 'OldObject', |
||
875 | 'fields' => [ |
||
876 | 'field' => [ |
||
877 | 'type' => Type::string(), |
||
878 | 'isDeprecated' => true, |
||
879 | ], |
||
880 | ], |
||
881 | ]); |
||
882 | |||
883 | $this->expectException(InvariantViolation::class); |
||
884 | $this->expectExceptionMessage( |
||
885 | 'OldObject.field should provide "deprecationReason" instead of "isDeprecated".' |
||
886 | ); |
||
887 | $OldObject->assertValid(); |
||
888 | } |
||
889 | |||
890 | // Object interfaces must be array |
||
891 | |||
892 | /** |
||
893 | * @see it('accepts an Object type with array interfaces') |
||
894 | */ |
||
895 | public function testAcceptsAnObjectTypeWithArrayInterfaces() : void |
||
896 | { |
||
897 | $objType = new ObjectType([ |
||
898 | 'name' => 'SomeObject', |
||
899 | 'interfaces' => [$this->interfaceType], |
||
900 | 'fields' => ['f' => ['type' => Type::string()]], |
||
901 | ]); |
||
902 | self::assertSame($this->interfaceType, $objType->getInterfaces()[0]); |
||
903 | } |
||
904 | |||
905 | /** |
||
906 | * @see it('accepts an Object type with interfaces as a function returning an array') |
||
907 | */ |
||
908 | public function testAcceptsAnObjectTypeWithInterfacesAsAFunctionReturningAnArray() : void |
||
909 | { |
||
910 | $objType = new ObjectType([ |
||
911 | 'name' => 'SomeObject', |
||
912 | 'interfaces' => function () { |
||
913 | return [$this->interfaceType]; |
||
914 | }, |
||
915 | 'fields' => ['f' => ['type' => Type::string()]], |
||
916 | ]); |
||
917 | self::assertSame($this->interfaceType, $objType->getInterfaces()[0]); |
||
918 | } |
||
919 | |||
920 | /** |
||
921 | * @see it('rejects an Object type with incorrectly typed interfaces') |
||
922 | */ |
||
923 | public function testRejectsAnObjectTypeWithIncorrectlyTypedInterfaces() : void |
||
924 | { |
||
925 | $objType = new ObjectType([ |
||
926 | 'name' => 'SomeObject', |
||
927 | 'interfaces' => new stdClass(), |
||
928 | 'fields' => ['f' => ['type' => Type::string()]], |
||
929 | ]); |
||
930 | $this->expectException(InvariantViolation::class); |
||
931 | $this->expectExceptionMessage( |
||
932 | 'SomeObject interfaces must be an Array or a callable which returns an Array.' |
||
933 | ); |
||
934 | $objType->getInterfaces(); |
||
935 | } |
||
936 | |||
937 | /** |
||
938 | * @see it('rejects an Object type with interfaces as a function returning an incorrect type') |
||
939 | */ |
||
940 | public function testRejectsAnObjectTypeWithInterfacesAsAFunctionReturningAnIncorrectType() : void |
||
941 | { |
||
942 | $objType = new ObjectType([ |
||
943 | 'name' => 'SomeObject', |
||
944 | 'interfaces' => static function () { |
||
945 | return new stdClass(); |
||
946 | }, |
||
947 | 'fields' => ['f' => ['type' => Type::string()]], |
||
948 | ]); |
||
949 | $this->expectException(InvariantViolation::class); |
||
950 | $this->expectExceptionMessage( |
||
951 | 'SomeObject interfaces must be an Array or a callable which returns an Array.' |
||
952 | ); |
||
953 | $objType->getInterfaces(); |
||
954 | } |
||
955 | |||
956 | // Type System: Object fields must have valid resolve values |
||
957 | |||
958 | /** |
||
959 | * @see it('accepts a lambda as an Object field resolver') |
||
960 | */ |
||
961 | public function testAcceptsALambdaAsAnObjectFieldResolver() : void |
||
962 | { |
||
963 | $this->expectNotToPerformAssertions(); |
||
964 | // should not throw: |
||
965 | $this->schemaWithObjectWithFieldResolver(static function () { |
||
966 | }); |
||
967 | } |
||
968 | |||
969 | private function schemaWithObjectWithFieldResolver($resolveValue) |
||
970 | { |
||
971 | $BadResolverType = new ObjectType([ |
||
972 | 'name' => 'BadResolver', |
||
973 | 'fields' => [ |
||
974 | 'badField' => [ |
||
975 | 'type' => Type::string(), |
||
976 | 'resolve' => $resolveValue, |
||
977 | ], |
||
978 | ], |
||
979 | ]); |
||
980 | |||
981 | $schema = new Schema([ |
||
982 | 'query' => new ObjectType([ |
||
983 | 'name' => 'Query', |
||
984 | 'fields' => [ |
||
985 | 'f' => ['type' => $BadResolverType], |
||
986 | ], |
||
987 | ]), |
||
988 | ]); |
||
989 | $schema->assertValid(); |
||
990 | |||
991 | return $schema; |
||
992 | } |
||
993 | |||
994 | /** |
||
995 | * @see it('rejects an empty Object field resolver') |
||
996 | */ |
||
997 | public function testRejectsAnEmptyObjectFieldResolver() : void |
||
998 | { |
||
999 | $this->expectException(InvariantViolation::class); |
||
1000 | $this->expectExceptionMessage( |
||
1001 | 'BadResolver.badField field resolver must be a function if provided, but got: []' |
||
1002 | ); |
||
1003 | $this->schemaWithObjectWithFieldResolver([]); |
||
1004 | } |
||
1005 | |||
1006 | /** |
||
1007 | * @see it('rejects a constant scalar value resolver') |
||
1008 | */ |
||
1009 | public function testRejectsAConstantScalarValueResolver() : void |
||
1010 | { |
||
1011 | $this->expectException(InvariantViolation::class); |
||
1012 | $this->expectExceptionMessage( |
||
1013 | 'BadResolver.badField field resolver must be a function if provided, but got: 0' |
||
1014 | ); |
||
1015 | $this->schemaWithObjectWithFieldResolver(0); |
||
1016 | } |
||
1017 | |||
1018 | // Type System: Interface types must be resolvable |
||
1019 | |||
1020 | /** |
||
1021 | * @see it('accepts an Interface type defining resolveType') |
||
1022 | */ |
||
1023 | public function testAcceptsAnInterfaceTypeDefiningResolveType() : void |
||
1024 | { |
||
1025 | $this->expectNotToPerformAssertions(); |
||
1026 | $AnotherInterfaceType = new InterfaceType([ |
||
1027 | 'name' => 'AnotherInterface', |
||
1028 | 'fields' => ['f' => ['type' => Type::string()]], |
||
1029 | ]); |
||
1030 | |||
1031 | // Should not throw: |
||
1032 | $this->schemaWithFieldType( |
||
1033 | new ObjectType([ |
||
1034 | 'name' => 'SomeObject', |
||
1035 | 'interfaces' => [$AnotherInterfaceType], |
||
1036 | 'fields' => ['f' => ['type' => Type::string()]], |
||
1037 | ]) |
||
1038 | ); |
||
1039 | } |
||
1040 | |||
1041 | private function schemaWithFieldType($type) |
||
1042 | { |
||
1043 | $schema = new Schema([ |
||
1044 | 'query' => new ObjectType([ |
||
1045 | 'name' => 'Query', |
||
1046 | 'fields' => ['field' => ['type' => $type]], |
||
1047 | ]), |
||
1048 | 'types' => [$type], |
||
1049 | ]); |
||
1050 | $schema->assertValid(); |
||
1051 | |||
1052 | return $schema; |
||
1053 | } |
||
1054 | |||
1055 | /** |
||
1056 | * @see it('accepts an Interface with implementing type defining isTypeOf') |
||
1057 | */ |
||
1058 | public function testAcceptsAnInterfaceWithImplementingTypeDefiningIsTypeOf() : void |
||
1059 | { |
||
1060 | $this->expectNotToPerformAssertions(); |
||
1061 | $InterfaceTypeWithoutResolveType = new InterfaceType([ |
||
1062 | 'name' => 'InterfaceTypeWithoutResolveType', |
||
1063 | 'fields' => ['f' => ['type' => Type::string()]], |
||
1064 | ]); |
||
1065 | |||
1066 | // Should not throw: |
||
1067 | $this->schemaWithFieldType( |
||
1068 | new ObjectType([ |
||
1069 | 'name' => 'SomeObject', |
||
1070 | 'interfaces' => [$InterfaceTypeWithoutResolveType], |
||
1071 | 'fields' => ['f' => ['type' => Type::string()]], |
||
1072 | ]) |
||
1073 | ); |
||
1074 | } |
||
1075 | |||
1076 | /** |
||
1077 | * @see it('accepts an Interface type defining resolveType with implementing type defining isTypeOf') |
||
1078 | */ |
||
1079 | public function testAcceptsAnInterfaceTypeDefiningResolveTypeWithImplementingTypeDefiningIsTypeOf() : void |
||
1080 | { |
||
1081 | $this->expectNotToPerformAssertions(); |
||
1082 | $AnotherInterfaceType = new InterfaceType([ |
||
1083 | 'name' => 'AnotherInterface', |
||
1084 | 'fields' => ['f' => ['type' => Type::string()]], |
||
1085 | ]); |
||
1086 | |||
1087 | // Should not throw: |
||
1088 | $this->schemaWithFieldType( |
||
1089 | new ObjectType([ |
||
1090 | 'name' => 'SomeObject', |
||
1091 | 'interfaces' => [$AnotherInterfaceType], |
||
1092 | 'fields' => ['f' => ['type' => Type::string()]], |
||
1093 | ]) |
||
1094 | ); |
||
1095 | } |
||
1096 | |||
1097 | /** |
||
1098 | * @see it('rejects an Interface type with an incorrect type for resolveType') |
||
1099 | */ |
||
1100 | public function testRejectsAnInterfaceTypeWithAnIncorrectTypeForResolveType() : void |
||
1101 | { |
||
1102 | $this->expectException(InvariantViolation::class); |
||
1103 | $this->expectExceptionMessage( |
||
1104 | 'AnotherInterface must provide "resolveType" as a function, but got: instance of stdClass' |
||
1105 | ); |
||
1106 | |||
1107 | $type = new InterfaceType([ |
||
1108 | 'name' => 'AnotherInterface', |
||
1109 | 'resolveType' => new stdClass(), |
||
1110 | 'fields' => ['f' => ['type' => Type::string()]], |
||
1111 | ]); |
||
1112 | $type->assertValid(); |
||
1113 | } |
||
1114 | |||
1115 | // Type System: Union types must be resolvable |
||
1116 | |||
1117 | /** |
||
1118 | * @see it('accepts a Union type defining resolveType') |
||
1119 | */ |
||
1120 | public function testAcceptsAUnionTypeDefiningResolveType() : void |
||
1121 | { |
||
1122 | $this->expectNotToPerformAssertions(); |
||
1123 | // Should not throw: |
||
1124 | $this->schemaWithFieldType( |
||
1125 | new UnionType([ |
||
1126 | 'name' => 'SomeUnion', |
||
1127 | 'types' => [$this->objectType], |
||
1128 | ]) |
||
1129 | ); |
||
1130 | } |
||
1131 | |||
1132 | /** |
||
1133 | * @see it('accepts a Union of Object types defining isTypeOf') |
||
1134 | */ |
||
1135 | public function testAcceptsAUnionOfObjectTypesDefiningIsTypeOf() : void |
||
1136 | { |
||
1137 | $this->expectNotToPerformAssertions(); |
||
1138 | // Should not throw: |
||
1139 | $this->schemaWithFieldType( |
||
1140 | new UnionType([ |
||
1141 | 'name' => 'SomeUnion', |
||
1142 | 'types' => [$this->objectWithIsTypeOf], |
||
1143 | ]) |
||
1144 | ); |
||
1145 | } |
||
1146 | |||
1147 | /** |
||
1148 | * @see it('accepts a Union type defining resolveType of Object types defining isTypeOf') |
||
1149 | */ |
||
1150 | public function testAcceptsAUnionTypeDefiningResolveTypeOfObjectTypesDefiningIsTypeOf() : void |
||
1151 | { |
||
1152 | $this->expectNotToPerformAssertions(); |
||
1153 | // Should not throw: |
||
1154 | $this->schemaWithFieldType( |
||
1155 | new UnionType([ |
||
1156 | 'name' => 'SomeUnion', |
||
1157 | 'types' => [$this->objectWithIsTypeOf], |
||
1158 | ]) |
||
1159 | ); |
||
1160 | } |
||
1161 | |||
1162 | /** |
||
1163 | * @see it('rejects an Union type with an incorrect type for resolveType') |
||
1164 | */ |
||
1165 | public function testRejectsAnUnionTypeWithAnIncorrectTypeForResolveType() : void |
||
1166 | { |
||
1167 | $this->expectException(InvariantViolation::class); |
||
1168 | $this->expectExceptionMessage( |
||
1169 | 'SomeUnion must provide "resolveType" as a function, but got: instance of stdClass' |
||
1170 | ); |
||
1171 | $this->schemaWithFieldType( |
||
1172 | new UnionType([ |
||
1173 | 'name' => 'SomeUnion', |
||
1174 | 'resolveType' => new stdClass(), |
||
1175 | 'types' => [$this->objectWithIsTypeOf], |
||
1176 | ]) |
||
1177 | ); |
||
1178 | } |
||
1179 | |||
1180 | /** |
||
1181 | * @see it('accepts a Scalar type defining serialize') |
||
1182 | */ |
||
1183 | public function testAcceptsAScalarTypeDefiningSerialize() : void |
||
1184 | { |
||
1185 | $this->expectNotToPerformAssertions(); |
||
1186 | // Should not throw |
||
1187 | $this->schemaWithFieldType( |
||
1188 | new CustomScalarType([ |
||
1189 | 'name' => 'SomeScalar', |
||
1190 | 'serialize' => static function () { |
||
1191 | return null; |
||
1192 | }, |
||
1193 | ]) |
||
1194 | ); |
||
1195 | } |
||
1196 | |||
1197 | // Type System: Scalar types must be serializable |
||
1198 | |||
1199 | /** |
||
1200 | * @see it('rejects a Scalar type not defining serialize') |
||
1201 | */ |
||
1202 | public function testRejectsAScalarTypeNotDefiningSerialize() : void |
||
1203 | { |
||
1204 | $this->expectException(InvariantViolation::class); |
||
1205 | $this->expectExceptionMessage( |
||
1206 | 'SomeScalar must provide "serialize" function. If this custom Scalar ' . |
||
1207 | 'is also used as an input type, ensure "parseValue" and "parseLiteral" ' . |
||
1208 | 'functions are also provided.' |
||
1209 | ); |
||
1210 | $this->schemaWithFieldType( |
||
1211 | new CustomScalarType(['name' => 'SomeScalar']) |
||
1212 | ); |
||
1213 | } |
||
1214 | |||
1215 | /** |
||
1216 | * @see it('rejects a Scalar type defining serialize with an incorrect type') |
||
1217 | */ |
||
1218 | public function testRejectsAScalarTypeDefiningSerializeWithAnIncorrectType() : void |
||
1219 | { |
||
1220 | $this->expectException(InvariantViolation::class); |
||
1221 | $this->expectExceptionMessage( |
||
1222 | 'SomeScalar must provide "serialize" function. If this custom Scalar ' . |
||
1223 | 'is also used as an input type, ensure "parseValue" and "parseLiteral" ' . |
||
1224 | 'functions are also provided.' |
||
1225 | ); |
||
1226 | $this->schemaWithFieldType( |
||
1227 | new CustomScalarType([ |
||
1228 | 'name' => 'SomeScalar', |
||
1229 | 'serialize' => new stdClass(), |
||
1230 | ]) |
||
1231 | ); |
||
1232 | } |
||
1233 | |||
1234 | /** |
||
1235 | * @see it('accepts a Scalar type defining parseValue and parseLiteral') |
||
1236 | */ |
||
1237 | public function testAcceptsAScalarTypeDefiningParseValueAndParseLiteral() : void |
||
1238 | { |
||
1239 | $this->expectNotToPerformAssertions(); |
||
1240 | // Should not throw: |
||
1241 | $this->schemaWithFieldType( |
||
1242 | new CustomScalarType([ |
||
1243 | 'name' => 'SomeScalar', |
||
1244 | 'serialize' => static function () { |
||
1245 | }, |
||
1246 | 'parseValue' => static function () { |
||
1247 | }, |
||
1248 | 'parseLiteral' => static function () { |
||
1249 | }, |
||
1250 | ]) |
||
1251 | ); |
||
1252 | } |
||
1253 | |||
1254 | /** |
||
1255 | * @see it('rejects a Scalar type defining parseValue but not parseLiteral') |
||
1256 | */ |
||
1257 | public function testRejectsAScalarTypeDefiningParseValueButNotParseLiteral() : void |
||
1258 | { |
||
1259 | $this->expectException(InvariantViolation::class); |
||
1260 | $this->expectExceptionMessage( |
||
1261 | 'SomeScalar must provide both "parseValue" and "parseLiteral" functions.' |
||
1262 | ); |
||
1263 | $this->schemaWithFieldType( |
||
1264 | new CustomScalarType([ |
||
1265 | 'name' => 'SomeScalar', |
||
1266 | 'serialize' => static function () { |
||
1267 | }, |
||
1268 | 'parseValue' => static function () { |
||
1269 | }, |
||
1270 | ]) |
||
1271 | ); |
||
1272 | } |
||
1273 | |||
1274 | /** |
||
1275 | * @see it('rejects a Scalar type defining parseLiteral but not parseValue') |
||
1276 | */ |
||
1277 | public function testRejectsAScalarTypeDefiningParseLiteralButNotParseValue() : void |
||
1278 | { |
||
1279 | $this->expectException(InvariantViolation::class); |
||
1280 | $this->expectExceptionMessage( |
||
1281 | 'SomeScalar must provide both "parseValue" and "parseLiteral" functions.' |
||
1282 | ); |
||
1283 | $this->schemaWithFieldType( |
||
1284 | new CustomScalarType([ |
||
1285 | 'name' => 'SomeScalar', |
||
1286 | 'serialize' => static function () { |
||
1287 | }, |
||
1288 | 'parseLiteral' => static function () { |
||
1289 | }, |
||
1290 | ]) |
||
1291 | ); |
||
1292 | } |
||
1293 | |||
1294 | /** |
||
1295 | * @see it('rejects a Scalar type defining parseValue and parseLiteral with an incorrect type') |
||
1296 | */ |
||
1297 | public function testRejectsAScalarTypeDefiningParseValueAndParseLiteralWithAnIncorrectType() : void |
||
1298 | { |
||
1299 | $this->expectException(InvariantViolation::class); |
||
1300 | $this->expectExceptionMessage( |
||
1301 | 'SomeScalar must provide both "parseValue" and "parseLiteral" functions.' |
||
1302 | ); |
||
1303 | $this->schemaWithFieldType( |
||
1304 | new CustomScalarType([ |
||
1305 | 'name' => 'SomeScalar', |
||
1306 | 'serialize' => static function () { |
||
1307 | }, |
||
1308 | 'parseValue' => new stdClass(), |
||
1309 | 'parseLiteral' => new stdClass(), |
||
1310 | ]) |
||
1311 | ); |
||
1312 | } |
||
1313 | |||
1314 | /** |
||
1315 | * @see it('accepts an Object type with an isTypeOf function') |
||
1316 | */ |
||
1317 | public function testAcceptsAnObjectTypeWithAnIsTypeOfFunction() : void |
||
1318 | { |
||
1319 | $this->expectNotToPerformAssertions(); |
||
1320 | // Should not throw |
||
1321 | $this->schemaWithFieldType( |
||
1322 | new ObjectType([ |
||
1323 | 'name' => 'AnotherObject', |
||
1324 | 'fields' => ['f' => ['type' => Type::string()]], |
||
1325 | ]) |
||
1326 | ); |
||
1327 | } |
||
1328 | |||
1329 | // Type System: Object types must be assertable |
||
1330 | |||
1331 | /** |
||
1332 | * @see it('rejects an Object type with an incorrect type for isTypeOf') |
||
1333 | */ |
||
1334 | public function testRejectsAnObjectTypeWithAnIncorrectTypeForIsTypeOf() : void |
||
1335 | { |
||
1336 | $this->expectException(InvariantViolation::class); |
||
1337 | $this->expectExceptionMessage( |
||
1338 | 'AnotherObject must provide "isTypeOf" as a function, but got: instance of stdClass' |
||
1339 | ); |
||
1340 | $this->schemaWithFieldType( |
||
1341 | new ObjectType([ |
||
1342 | 'name' => 'AnotherObject', |
||
1343 | 'isTypeOf' => new stdClass(), |
||
1344 | 'fields' => ['f' => ['type' => Type::string()]], |
||
1345 | ]) |
||
1346 | ); |
||
1347 | } |
||
1348 | |||
1349 | /** |
||
1350 | * @see it('accepts a Union type with array types') |
||
1351 | */ |
||
1352 | public function testAcceptsAUnionTypeWithArrayTypes() : void |
||
1353 | { |
||
1354 | $this->expectNotToPerformAssertions(); |
||
1355 | // Should not throw: |
||
1356 | $this->schemaWithFieldType( |
||
1357 | new UnionType([ |
||
1358 | 'name' => 'SomeUnion', |
||
1359 | 'types' => [$this->objectType], |
||
1360 | ]) |
||
1361 | ); |
||
1362 | } |
||
1363 | |||
1364 | // Type System: Union types must be array |
||
1365 | |||
1366 | /** |
||
1367 | * @see it('accepts a Union type with function returning an array of types') |
||
1368 | */ |
||
1369 | public function testAcceptsAUnionTypeWithFunctionReturningAnArrayOfTypes() : void |
||
1370 | { |
||
1371 | $this->expectNotToPerformAssertions(); |
||
1372 | $this->schemaWithFieldType( |
||
1373 | new UnionType([ |
||
1374 | 'name' => 'SomeUnion', |
||
1375 | 'types' => function () { |
||
1376 | return [$this->objectType]; |
||
1377 | }, |
||
1378 | ]) |
||
1379 | ); |
||
1380 | } |
||
1381 | |||
1382 | /** |
||
1383 | * @see it('rejects a Union type without types') |
||
1384 | */ |
||
1385 | public function testRejectsAUnionTypeWithoutTypes() : void |
||
1386 | { |
||
1387 | $this->expectException(InvariantViolation::class); |
||
1388 | $this->expectExceptionMessage( |
||
1389 | 'Must provide Array of types or a callable which returns such an array for Union SomeUnion' |
||
1390 | ); |
||
1391 | $this->schemaWithFieldType( |
||
1392 | new UnionType(['name' => 'SomeUnion']) |
||
1393 | ); |
||
1394 | } |
||
1395 | |||
1396 | /** |
||
1397 | * @see it('rejects a Union type with incorrectly typed types') |
||
1398 | */ |
||
1399 | public function testRejectsAUnionTypeWithIncorrectlyTypedTypes() : void |
||
1400 | { |
||
1401 | $this->expectException(InvariantViolation::class); |
||
1402 | $this->expectExceptionMessage( |
||
1403 | 'Must provide Array of types or a callable which returns such an array for Union SomeUnion' |
||
1404 | ); |
||
1405 | $this->schemaWithFieldType( |
||
1406 | new UnionType([ |
||
1407 | 'name' => 'SomeUnion', |
||
1408 | 'types' => (object) ['test' => $this->objectType], |
||
1409 | ]) |
||
1410 | ); |
||
1411 | } |
||
1412 | |||
1413 | /** |
||
1414 | * @see it('accepts an Input Object type with fields') |
||
1415 | */ |
||
1416 | public function testAcceptsAnInputObjectTypeWithFields() : void |
||
1417 | { |
||
1418 | $inputObjType = new InputObjectType([ |
||
1419 | 'name' => 'SomeInputObject', |
||
1420 | 'fields' => [ |
||
1421 | 'f' => ['type' => Type::string()], |
||
1422 | ], |
||
1423 | ]); |
||
1424 | $inputObjType->assertValid(); |
||
1425 | self::assertSame(Type::string(), $inputObjType->getField('f')->getType()); |
||
1426 | } |
||
1427 | |||
1428 | // Type System: Input Objects must have fields |
||
1429 | |||
1430 | /** |
||
1431 | * @see it('accepts an Input Object type with a field function') |
||
1432 | */ |
||
1433 | public function testAcceptsAnInputObjectTypeWithAFieldFunction() : void |
||
1434 | { |
||
1435 | $inputObjType = new InputObjectType([ |
||
1436 | 'name' => 'SomeInputObject', |
||
1437 | 'fields' => static function () { |
||
1438 | return [ |
||
1439 | 'f' => ['type' => Type::string()], |
||
1440 | ]; |
||
1441 | }, |
||
1442 | ]); |
||
1443 | $inputObjType->assertValid(); |
||
1444 | self::assertSame(Type::string(), $inputObjType->getField('f')->getType()); |
||
1445 | } |
||
1446 | |||
1447 | /** |
||
1448 | * @see it('rejects an Input Object type with incorrect fields') |
||
1449 | */ |
||
1450 | public function testRejectsAnInputObjectTypeWithIncorrectFields() : void |
||
1451 | { |
||
1452 | $inputObjType = new InputObjectType([ |
||
1453 | 'name' => 'SomeInputObject', |
||
1454 | 'fields' => [], |
||
1455 | ]); |
||
1456 | $this->expectException(InvariantViolation::class); |
||
1457 | $this->expectExceptionMessage( |
||
1458 | 'SomeInputObject fields must be an associative array with field names as keys or a callable ' . |
||
1459 | 'which returns such an array.' |
||
1460 | ); |
||
1461 | $inputObjType->assertValid(); |
||
1462 | } |
||
1463 | |||
1464 | /** |
||
1465 | * @see it('rejects an Input Object type with fields function that returns incorrect type') |
||
1466 | */ |
||
1467 | public function testRejectsAnInputObjectTypeWithFieldsFunctionThatReturnsIncorrectType() : void |
||
1468 | { |
||
1469 | $inputObjType = new InputObjectType([ |
||
1470 | 'name' => 'SomeInputObject', |
||
1471 | 'fields' => static function () { |
||
1472 | return []; |
||
1473 | }, |
||
1474 | ]); |
||
1475 | $this->expectException(InvariantViolation::class); |
||
1476 | $this->expectExceptionMessage( |
||
1477 | 'SomeInputObject fields must be an associative array with field names as keys or a ' . |
||
1478 | 'callable which returns such an array.' |
||
1479 | ); |
||
1480 | $inputObjType->assertValid(); |
||
1481 | } |
||
1482 | |||
1483 | /** |
||
1484 | * @see it('rejects an Input Object type with resolvers') |
||
1485 | */ |
||
1486 | public function testRejectsAnInputObjectTypeWithResolvers() : void |
||
1487 | { |
||
1488 | $inputObjType = new InputObjectType([ |
||
1489 | 'name' => 'SomeInputObject', |
||
1490 | 'fields' => [ |
||
1491 | 'f' => [ |
||
1492 | 'type' => Type::string(), |
||
1493 | 'resolve' => static function () { |
||
1494 | return 0; |
||
1495 | }, |
||
1496 | ], |
||
1497 | ], |
||
1498 | ]); |
||
1499 | $this->expectException(InvariantViolation::class); |
||
1500 | $this->expectExceptionMessage( |
||
1501 | 'SomeInputObject.f field type has a resolve property, ' . |
||
1502 | 'but Input Types cannot define resolvers.' |
||
1503 | ); |
||
1504 | $inputObjType->assertValid(); |
||
1505 | } |
||
1506 | |||
1507 | // Type System: Input Object fields must not have resolvers |
||
1508 | |||
1509 | /** |
||
1510 | * @see it('rejects an Input Object type with resolver constant') |
||
1511 | */ |
||
1512 | public function testRejectsAnInputObjectTypeWithResolverConstant() : void |
||
1513 | { |
||
1514 | $inputObjType = new InputObjectType([ |
||
1515 | 'name' => 'SomeInputObject', |
||
1516 | 'fields' => [ |
||
1517 | 'f' => [ |
||
1518 | 'type' => Type::string(), |
||
1519 | 'resolve' => new stdClass(), |
||
1520 | ], |
||
1521 | ], |
||
1522 | ]); |
||
1523 | $this->expectException(InvariantViolation::class); |
||
1524 | $this->expectExceptionMessage( |
||
1525 | 'SomeInputObject.f field type has a resolve property, ' . |
||
1526 | 'but Input Types cannot define resolvers.' |
||
1527 | ); |
||
1528 | $inputObjType->assertValid(); |
||
1529 | } |
||
1530 | |||
1531 | /** |
||
1532 | * @see it('accepts a well defined Enum type with empty value definition') |
||
1533 | */ |
||
1534 | public function testAcceptsAWellDefinedEnumTypeWithEmptyValueDefinition() : void |
||
1535 | { |
||
1536 | $enumType = new EnumType([ |
||
1537 | 'name' => 'SomeEnum', |
||
1538 | 'values' => [ |
||
1539 | 'FOO' => [], |
||
1540 | 'BAR' => [], |
||
1541 | ], |
||
1542 | ]); |
||
1543 | self::assertEquals('FOO', $enumType->getValue('FOO')->value); |
||
1544 | self::assertEquals('BAR', $enumType->getValue('BAR')->value); |
||
1545 | } |
||
1546 | |||
1547 | // Type System: Enum types must be well defined |
||
1548 | |||
1549 | /** |
||
1550 | * @see it('accepts a well defined Enum type with internal value definition') |
||
1551 | */ |
||
1552 | public function testAcceptsAWellDefinedEnumTypeWithInternalValueDefinition() : void |
||
1553 | { |
||
1554 | $enumType = new EnumType([ |
||
1555 | 'name' => 'SomeEnum', |
||
1556 | 'values' => [ |
||
1557 | 'FOO' => ['value' => 10], |
||
1558 | 'BAR' => ['value' => 20], |
||
1559 | ], |
||
1560 | ]); |
||
1561 | self::assertEquals(10, $enumType->getValue('FOO')->value); |
||
1562 | self::assertEquals(20, $enumType->getValue('BAR')->value); |
||
1563 | } |
||
1564 | |||
1565 | /** |
||
1566 | * @see it('rejects an Enum type with incorrectly typed values') |
||
1567 | */ |
||
1568 | public function testRejectsAnEnumTypeWithIncorrectlyTypedValues() : void |
||
1569 | { |
||
1570 | $enumType = new EnumType([ |
||
1571 | 'name' => 'SomeEnum', |
||
1572 | 'values' => [['FOO' => 10]], |
||
1573 | ]); |
||
1574 | $this->expectException(InvariantViolation::class); |
||
1575 | $this->expectExceptionMessage( |
||
1576 | 'SomeEnum values must be an array with value names as keys.' |
||
1577 | ); |
||
1578 | $enumType->assertValid(); |
||
1579 | } |
||
1580 | |||
1581 | /** |
||
1582 | * @see it('does not allow isDeprecated without deprecationReason on enum') |
||
1583 | */ |
||
1584 | public function testDoesNotAllowIsDeprecatedWithoutDeprecationReasonOnEnum() : void |
||
1585 | { |
||
1586 | $enumType = new EnumType([ |
||
1587 | 'name' => 'SomeEnum', |
||
1588 | 'values' => [ |
||
1589 | 'FOO' => ['isDeprecated' => true], |
||
1590 | ], |
||
1591 | ]); |
||
1592 | $this->expectException(InvariantViolation::class); |
||
1593 | $this->expectExceptionMessage( |
||
1594 | 'SomeEnum.FOO should provide "deprecationReason" instead ' . |
||
1595 | 'of "isDeprecated".' |
||
1596 | ); |
||
1597 | $enumType->assertValid(); |
||
1598 | } |
||
1599 | |||
1600 | /** |
||
1601 | * @see it('rejects a Schema which redefines a built-in type') |
||
1602 | */ |
||
1603 | public function testRejectsASchemaWhichRedefinesABuiltInType() : void |
||
1604 | { |
||
1605 | $FakeString = new CustomScalarType([ |
||
1606 | 'name' => 'String', |
||
1607 | 'serialize' => static function () { |
||
1608 | }, |
||
1609 | ]); |
||
1610 | |||
1611 | $QueryType = new ObjectType([ |
||
1612 | 'name' => 'Query', |
||
1613 | 'fields' => [ |
||
1614 | 'normal' => ['type' => Type::string()], |
||
1615 | 'fake' => ['type' => $FakeString], |
||
1616 | ], |
||
1617 | ]); |
||
1618 | |||
1619 | $this->expectException(InvariantViolation::class); |
||
1620 | $this->expectExceptionMessage( |
||
1621 | 'Schema must contain unique named types but contains multiple types named "String" ' . |
||
1622 | '(see http://webonyx.github.io/graphql-php/type-system/#type-registry).' |
||
1623 | ); |
||
1624 | $schema = new Schema(['query' => $QueryType]); |
||
1625 | $schema->assertValid(); |
||
1626 | } |
||
1627 | |||
1628 | // Type System: A Schema must contain uniquely named types |
||
1629 | |||
1630 | /** |
||
1631 | * @see it('rejects a Schema which defines an object type twice') |
||
1632 | */ |
||
1633 | public function testRejectsASchemaWhichDefinesAnObjectTypeTwice() : void |
||
1634 | { |
||
1635 | $A = new ObjectType([ |
||
1636 | 'name' => 'SameName', |
||
1637 | 'fields' => ['f' => ['type' => Type::string()]], |
||
1638 | ]); |
||
1639 | |||
1640 | $B = new ObjectType([ |
||
1641 | 'name' => 'SameName', |
||
1642 | 'fields' => ['f' => ['type' => Type::string()]], |
||
1643 | ]); |
||
1644 | |||
1645 | $QueryType = new ObjectType([ |
||
1646 | 'name' => 'Query', |
||
1647 | 'fields' => [ |
||
1648 | 'a' => ['type' => $A], |
||
1649 | 'b' => ['type' => $B], |
||
1650 | ], |
||
1651 | ]); |
||
1652 | $this->expectException(InvariantViolation::class); |
||
1653 | $this->expectExceptionMessage( |
||
1654 | 'Schema must contain unique named types but contains multiple types named "SameName" ' . |
||
1655 | '(see http://webonyx.github.io/graphql-php/type-system/#type-registry).' |
||
1656 | ); |
||
1657 | $schema = new Schema(['query' => $QueryType]); |
||
1658 | $schema->assertValid(); |
||
1659 | } |
||
1660 | |||
1661 | /** |
||
1662 | * @see it('rejects a Schema which have same named objects implementing an interface') |
||
1663 | */ |
||
1664 | public function testRejectsASchemaWhichHaveSameNamedObjectsImplementingAnInterface() : void |
||
1665 | { |
||
1666 | $AnotherInterface = new InterfaceType([ |
||
1667 | 'name' => 'AnotherInterface', |
||
1668 | 'fields' => ['f' => ['type' => Type::string()]], |
||
1669 | ]); |
||
1670 | |||
1671 | $FirstBadObject = new ObjectType([ |
||
1672 | 'name' => 'BadObject', |
||
1673 | 'interfaces' => [$AnotherInterface], |
||
1674 | 'fields' => ['f' => ['type' => Type::string()]], |
||
1675 | ]); |
||
1676 | |||
1677 | $SecondBadObject = new ObjectType([ |
||
1678 | 'name' => 'BadObject', |
||
1679 | 'interfaces' => [$AnotherInterface], |
||
1680 | 'fields' => ['f' => ['type' => Type::string()]], |
||
1681 | ]); |
||
1682 | |||
1683 | $QueryType = new ObjectType([ |
||
1684 | 'name' => 'Query', |
||
1685 | 'fields' => [ |
||
1686 | 'iface' => ['type' => $AnotherInterface], |
||
1687 | ], |
||
1688 | ]); |
||
1689 | |||
1690 | $this->expectException(InvariantViolation::class); |
||
1691 | $this->expectExceptionMessage( |
||
1692 | 'Schema must contain unique named types but contains multiple types named "BadObject" ' . |
||
1693 | '(see http://webonyx.github.io/graphql-php/type-system/#type-registry).' |
||
1694 | ); |
||
1695 | $schema = new Schema([ |
||
1696 | 'query' => $QueryType, |
||
1697 | 'types' => [$FirstBadObject, $SecondBadObject], |
||
1698 | ]); |
||
1699 | $schema->assertValid(); |
||
1700 | } |
||
1701 | |||
1702 | public function objectWithIsTypeOf() : ObjectType |
||
1703 | { |
||
1704 | return new ObjectType([ |
||
1705 | 'name' => 'ObjectWithIsTypeOf', |
||
1706 | 'fields' => ['f' => ['type' => Type::string()]], |
||
1707 | ]); |
||
1710 |