Passed
Pull Request — master (#190)
by Sebastian
03:03
created

ExecutionStrategy::doesFragmentConditionMatch()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 23
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 23
rs 8.7972
c 0
b 0
f 0
cc 4
eloc 11
nc 4
nop 2
1
<?php
2
3
namespace Digia\GraphQL\Execution;
4
5
use Digia\GraphQL\Error\ExecutionException;
6
use Digia\GraphQL\Error\GraphQLException;
7
use Digia\GraphQL\Error\InvalidTypeException;
8
use Digia\GraphQL\Error\UndefinedException;
9
use Digia\GraphQL\Language\Node\FieldNode;
10
use Digia\GraphQL\Language\Node\FragmentDefinitionNode;
11
use Digia\GraphQL\Language\Node\FragmentSpreadNode;
12
use Digia\GraphQL\Language\Node\InlineFragmentNode;
13
use Digia\GraphQL\Language\Node\NodeInterface;
14
use Digia\GraphQL\Language\Node\OperationDefinitionNode;
15
use Digia\GraphQL\Language\Node\SelectionSetNode;
16
use Digia\GraphQL\Schema\Schema;
17
use Digia\GraphQL\Type\Definition\AbstractTypeInterface;
18
use Digia\GraphQL\Type\Definition\Field;
19
use Digia\GraphQL\Type\Definition\InterfaceType;
20
use Digia\GraphQL\Type\Definition\LeafTypeInterface;
21
use Digia\GraphQL\Type\Definition\ListType;
22
use Digia\GraphQL\Type\Definition\NonNullType;
23
use Digia\GraphQL\Type\Definition\ObjectType;
24
use Digia\GraphQL\Type\Definition\TypeInterface;
25
use Digia\GraphQL\Type\Definition\UnionType;
26
use React\Promise\ExtendedPromiseInterface;
27
use React\Promise\PromiseInterface;
28
use function Digia\GraphQL\Type\SchemaMetaFieldDefinition;
29
use function Digia\GraphQL\Type\TypeMetaFieldDefinition;
30
use function Digia\GraphQL\Type\TypeNameMetaFieldDefinition;
31
use function Digia\GraphQL\Util\toString;
32
use function Digia\GraphQL\Util\typeFromAST;
33
34
/**
35
 * Class AbstractStrategy
36
 *
37
 * @package Digia\GraphQL\Execution\Strategies
38
 */
39
abstract class ExecutionStrategy
40
{
41
42
    /**
43
     * @var array
44
     */
45
    protected static $defaultFieldResolver = [
46
      __CLASS__,
47
      'defaultFieldResolver',
48
    ];
49
50
    /**
51
     * @var ExecutionContext
52
     */
53
    protected $context;
54
55
    /**
56
     * @var OperationDefinitionNode
57
     */
58
    protected $operation;
59
60
    /**
61
     * @var mixed
62
     */
63
    protected $rootValue;
64
65
    /**
66
     * @var array
67
     */
68
    protected $finalResult;
69
70
    /**
71
     * AbstractStrategy constructor.
72
     *
73
     * @param ExecutionContext $context
74
     *
75
     * @param OperationDefinitionNode $operation
76
     */
77
    public function __construct(
78
      ExecutionContext $context,
79
      OperationDefinitionNode $operation,
80
      $rootValue
81
    ) {
82
        $this->context = $context;
83
        $this->operation = $operation;
84
        $this->rootValue = $rootValue;
85
    }
86
87
    /**
88
     * Try to resolve a field without any field resolver function.
89
     *
90
     * @param array|object $rootValue
91
     * @param              $args
92
     * @param              $context
93
     * @param ResolveInfo $info
94
     *
95
     * @return mixed|null
96
     */
97
    public static function defaultFieldResolver(
98
      $rootValue,
99
      $args,
100
      $context,
101
      ResolveInfo $info
102
    ) {
103
        $fieldName = $info->getFieldName();
104
        $property = null;
105
106
        if (is_array($rootValue) && isset($rootValue[$fieldName])) {
107
            $property = $rootValue[$fieldName];
108
        }
109
110
        if (is_object($rootValue)) {
111
            $getter = 'get' . ucfirst($fieldName);
112
            if (method_exists($rootValue, $getter)) {
113
                $property = $rootValue->{$getter}();
114
            } elseif (property_exists($rootValue, $fieldName)) {
115
                $property = $rootValue->{$fieldName};
116
            }
117
        }
118
119
120
        return $property instanceof \Closure ? $property($rootValue, $args,
121
          $context, $info) : $property;
122
    }
123
124
    /**
125
     * @return array|null
126
     */
127
    abstract function execute(): ?array;
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
128
129
    /**
130
     * Implements the "Evaluating selection sets" section of the spec for
131
     * "write" mode.
132
     *
133
     * @param ObjectType $objectType
134
     * @param            $rootValue
135
     * @param            $path
136
     * @param            $fields
137
     *
138
     * @return array
139
     * @throws InvalidTypeException
140
     * @throws \Digia\GraphQL\Error\ExecutionException
141
     * @throws \Digia\GraphQL\Error\InvariantException
142
     * @throws \Throwable
143
     */
144
    public function executeFieldsSerially(
145
      ObjectType $objectType,
146
      $rootValue,
147
      $path,
148
      $fields
149
    ) {
150
151
        $finalResults = [];
152
153
        $promise = new \React\Promise\FulfilledPromise([]);
154
155
        $resolve = function (
156
          $results,
157
          $fieldName,
158
          $path,
159
          $objectType,
160
          $rootValue,
161
          $fieldNodes
162
        ) {
163
            $fieldPath = $path;
164
            $fieldPath[] = $fieldName;
165
            try {
166
                $result = $this->resolveField($objectType, $rootValue,
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $result is correct as $this->resolveField($obj...fieldNodes, $fieldPath) targeting Digia\GraphQL\Execution\...trategy::resolveField() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
167
                  $fieldNodes, $fieldPath);
168
            } catch (UndefinedException $ex) {
169
                return null;
170
            }
171
172
            if ($this->isPromise($result)) {
173
                /** @var ExtendedPromiseInterface $result */
174
                return $result->then(function ($resolvedResult) use (
175
                  $fieldName,
176
                  $results
177
                ) {
178
                    $results[$fieldName] = $resolvedResult;
179
                    return $results;
180
                });
181
            }
182
183
            $results[$fieldName] = $result;
184
185
            return $results;
186
        };
187
188
        foreach ($fields as $fieldName => $fieldNodes) {
189
            $promise = $promise->then(function ($resolvedResults) use (
190
              $resolve,
191
              $fieldName,
192
              $path,
193
              $objectType,
194
              $rootValue,
195
              $fieldNodes
196
            ) {
197
                return $resolve($resolvedResults, $fieldName, $path,
198
                  $objectType, $rootValue, $fieldNodes);
199
            });
200
        }
201
202
        $promise->then(function ($resolvedResults) use (&$finalResults) {
203
            $finalResults = $resolvedResults ?? [];
204
        });
205
206
        return $finalResults;
207
    }
208
209
    /**
210
     * Implements the "Evaluating selection sets" section of the spec for
211
     * "read" mode.
212
     *
213
     * @param ObjectType $objectType
214
     * @param            $rootValue
215
     * @param            $path
216
     * @param            $fields
217
     *
218
     * @return array
219
     * @throws InvalidTypeException
220
     * @throws \Digia\GraphQL\Error\ExecutionException
221
     * @throws \Digia\GraphQL\Error\InvariantException
222
     * @throws \Throwable
223
     */
224
    protected function executeFields(
225
      ObjectType $objectType,
226
      $rootValue,
227
      $path,
228
      $fields
229
    ): array {
230
        $finalResults = [];
231
        $isContainsPromise = false;
232
233
        foreach ($fields as $fieldName => $fieldNodes) {
234
            $fieldPath = $path;
235
            $fieldPath[] = $fieldName;
236
237
            try {
238
                $result = $this->resolveField($objectType, $rootValue,
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $result is correct as $this->resolveField($obj...fieldNodes, $fieldPath) targeting Digia\GraphQL\Execution\...trategy::resolveField() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
239
                  $fieldNodes, $fieldPath);
240
            } catch (UndefinedException $ex) {
241
                continue;
242
            }
243
244
            $isContainsPromise = $isContainsPromise || $this->isPromise($result);
245
246
            $finalResults[$fieldName] = $result;
247
        }
248
249
        if ($isContainsPromise) {
250
            $keys = array_keys($finalResults);
251
            $promise = \React\Promise\all(array_values($finalResults));
252
            $promise->then(function ($values) use ($keys, &$finalResults) {
253
                foreach ($values as $i => $value) {
254
                    $finalResults[$keys[$i]] = $value;
255
                }
256
            });
257
        }
258
259
        return $finalResults;
260
    }
261
262
    /**
263
     * @param ObjectType $parentType
264
     * @param            $rootValue
265
     * @param            $fieldNodes
266
     * @param            $path
267
     *
268
     * @return array|null|\Throwable
269
     * @throws InvalidTypeException
270
     * @throws \Digia\GraphQL\Error\ExecutionException
271
     * @throws \Digia\GraphQL\Error\InvariantException
272
     * @throws \Throwable
273
     */
274
    protected function resolveField(
275
      ObjectType $parentType,
276
      $rootValue,
277
      $fieldNodes,
278
      $path
279
    ) {
280
        /** @var FieldNode $fieldNode */
281
        $fieldNode = $fieldNodes[0];
282
283
        $field = $this->getFieldDefinition($this->context->getSchema(),
284
          $parentType, $fieldNode->getNameValue());
285
286
        if (null === $field) {
287
            throw new UndefinedException('Undefined field definition.');
288
        }
289
290
        $info = $this->buildResolveInfo($fieldNodes, $fieldNode, $field,
291
          $parentType, $path, $this->context);
292
293
        $resolveFunction = $this->determineResolveFunction($field, $parentType,
294
          $this->context);
295
296
        $result = $this->resolveFieldValueOrError(
297
          $field,
298
          $fieldNode,
299
          $resolveFunction,
300
          $rootValue,
301
          $this->context,
302
          $info
303
        );
304
305
        $result = $this->completeValueCatchingError(
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $result is correct as $this->completeValueCatc... $info, $path, $result) targeting Digia\GraphQL\Execution\...eteValueCatchingError() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
306
          $field->getType(),
307
          $fieldNodes,
308
          $info,
309
          $path,
310
          $result// $result is passed as $source
311
        );
312
313
        return $result;
314
    }
315
316
    /**
317
     * @param Schema $schema
318
     * @param ObjectType $parentType
319
     * @param string $fieldName
320
     *
321
     * @return \Digia\GraphQL\Type\Definition\Field|null
322
     * @throws InvalidTypeException
323
     * @throws \Digia\GraphQL\Error\InvariantException
324
     */
325
    public function getFieldDefinition(
326
      Schema $schema,
327
      ObjectType $parentType,
328
      string $fieldName
329
    ) {
330
        $schemaMetaFieldDefinition = SchemaMetaFieldDefinition();
331
        $typeMetaFieldDefinition = TypeMetaFieldDefinition();
332
        $typeNameMetaFieldDefinition = TypeNameMetaFieldDefinition();
333
334
        if ($fieldName === $schemaMetaFieldDefinition->getName() && $schema->getQueryType() === $parentType) {
335
            return $schemaMetaFieldDefinition;
336
        }
337
338
        if ($fieldName === $typeMetaFieldDefinition->getName() && $schema->getQueryType() === $parentType) {
339
            return $typeMetaFieldDefinition;
340
        }
341
342
        if ($fieldName === $typeNameMetaFieldDefinition->getName()) {
343
            return $typeNameMetaFieldDefinition;
344
        }
345
346
        $fields = $parentType->getFields();
347
348
        return $fields[$fieldName] ?? null;
349
    }
350
351
    /**
352
     * @param array $fieldNodes
353
     * @param FieldNode $fieldNode
354
     * @param Field $field
355
     * @param ObjectType $parentType
356
     * @param array|null $path
357
     * @param ExecutionContext $context
358
     *
359
     * @return ResolveInfo
360
     */
361
    private function buildResolveInfo(
362
      array $fieldNodes,
363
      FieldNode $fieldNode,
364
      Field $field,
365
      ObjectType $parentType,
366
      ?array $path,
367
      ExecutionContext $context
368
    ) {
369
        return new ResolveInfo(
370
          $fieldNode->getNameValue(),
371
          $fieldNodes,
372
          $field->getType(),
373
          $parentType,
374
          $path,
375
          $context->getSchema(),
376
          $context->getFragments(),
377
          $context->getRootValue(),
378
          $context->getOperation(),
379
          $context->getVariableValues()
380
        );
381
    }
382
383
    /**
384
     * @param Field $field
385
     * @param ObjectType $objectType
386
     * @param ExecutionContext $context
387
     *
388
     * @return callable|mixed|null
389
     */
390
    private function determineResolveFunction(
391
      Field $field,
392
      ObjectType $objectType,
393
      ExecutionContext $context
394
    ) {
395
396
        if ($field->hasResolve()) {
397
            return $field->getResolve();
398
        }
399
400
        if ($objectType->hasResolve()) {
401
            return $objectType->getResolve();
402
        }
403
404
        return $this->context->getFieldResolver() ?? self::$defaultFieldResolver;
405
    }
406
407
    /**
408
     * @param Field $field
409
     * @param FieldNode $fieldNode
410
     * @param callable $resolveFunction
411
     * @param                  $rootValue
412
     * @param ExecutionContext $context
413
     * @param ResolveInfo $info
414
     *
415
     * @return array|\Throwable
416
     */
417
    private function resolveFieldValueOrError(
418
      Field $field,
419
      FieldNode $fieldNode,
420
      ?callable $resolveFunction,
421
      $rootValue,
422
      ExecutionContext $context,
423
      ResolveInfo $info
424
    ) {
425
        try {
426
            $args = coerceArgumentValues($field, $fieldNode,
427
              $context->getVariableValues());
428
429
            return $resolveFunction($rootValue, $args,
430
              $context->getContextValue(), $info);
431
        } catch (\Throwable $error) {
432
            return $error;
433
        }
434
    }
435
436
    /**
437
     * @param TypeInterface $fieldType
438
     * @param               $fieldNodes
439
     * @param ResolveInfo $info
440
     * @param               $path
441
     * @param               $result
442
     *
443
     * @return null
444
     * @throws \Throwable
445
     */
446
    public function completeValueCatchingError(
447
      TypeInterface $fieldType,
448
      $fieldNodes,
449
      ResolveInfo $info,
450
      $path,
451
      &$result
452
    ) {
453
        if ($fieldType instanceof NonNullType) {
454
            return $this->completeValueWithLocatedError(
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->completeVa... $info, $path, $result) also could return the type array which is incompatible with the documented return type null.
Loading history...
455
              $fieldType,
456
              $fieldNodes,
457
              $info,
458
              $path,
459
              $result
460
            );
461
        }
462
463
        try {
464
            $completed = $this->completeValueWithLocatedError(
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $completed is correct as $this->completeValueWith... $info, $path, $result) targeting Digia\GraphQL\Execution\...ValueWithLocatedError() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
465
              $fieldType,
466
              $fieldNodes,
467
              $info,
468
              $path,
469
              $result
470
            );
471
472
            if ($this->isPromise($completed)) {
473
                $context = $this->context;
474
                /** @var ExtendedPromiseInterface $completed */
475
                return $completed->then(null,
0 ignored issues
show
Bug Best Practice introduced by
The expression return $completed->then(...ion(...) { /* ... */ }) returns the type React\Promise\PromiseInterface which is incompatible with the documented return type null.
Loading history...
476
                  function ($error) use ($context, $fieldNodes, $path) {
477
                      //@TODO Handle $error better
478
                      if ($error instanceof \Exception) {
479
                          $context->addError($this->buildLocatedError($error,
480
                            $fieldNodes, $path));
481
                      } else {
482
                          $context->addError(
483
                            $this->buildLocatedError(
484
                              new ExecutionException($error ?? 'An unknown error occurred.'),
485
                              $fieldNodes,
486
                              $path
487
                            )
488
                          );
489
                      }
490
                      return new \React\Promise\FulfilledPromise(null);
491
                  });
492
            }
493
494
            return $completed;
495
        } catch (ExecutionException $ex) {
496
            $this->context->addError($ex);
497
            return null;
498
        } catch (\Exception $ex) {
499
            $this->context->addError($this->buildLocatedError($ex, $fieldNodes,
500
              $path));
501
            return null;
502
        }
503
    }
504
505
    /**
506
     * @param TypeInterface $fieldType
507
     * @param               $fieldNodes
508
     * @param ResolveInfo $info
509
     * @param               $path
510
     * @param               $result
511
     *
512
     * @throws \Throwable
513
     */
514
    public function completeValueWithLocatedError(
515
      TypeInterface $fieldType,
516
      $fieldNodes,
517
      ResolveInfo $info,
518
      $path,
519
      $result
520
    ) {
521
        try {
522
            $completed = $this->completeValue(
523
              $fieldType,
524
              $fieldNodes,
525
              $info,
526
              $path,
527
              $result
528
            );
529
530
            return $completed;
531
        } catch (\Throwable $ex) {
532
            throw $this->buildLocatedError($ex, $fieldNodes, $path);
533
        }
534
    }
535
536
    /**
537
     * @param TypeInterface $returnType
538
     * @param               $fieldNodes
539
     * @param ResolveInfo $info
540
     * @param               $path
541
     * @param               $result
542
     *
543
     * @return array|mixed
544
     * @throws ExecutionException
545
     * @throws \Throwable
546
     */
547
    private function completeValue(
548
      TypeInterface $returnType,
549
      $fieldNodes,
550
      ResolveInfo $info,
551
      $path,
552
      &$result
553
    ) {
554
        if ($this->isPromise($result)) {
555
            /** @var ExtendedPromiseInterface $result */
556
            return $result->then(function (&$value) use (
557
              $returnType,
558
              $fieldNodes,
559
              $info,
560
              $path
561
            ) {
562
                return $this->completeValue($returnType, $fieldNodes, $info,
563
                  $path, $value);
564
            });
565
        }
566
567
        if ($result instanceof \Throwable) {
568
            throw $result;
569
        }
570
571
        // If result is null-like, return null.
572
        if (null === $result) {
573
            return null;
574
        }
575
576
        if ($returnType instanceof NonNullType) {
577
            $completed = $this->completeValue(
578
              $returnType->getOfType(),
579
              $fieldNodes,
580
              $info,
581
              $path,
582
              $result
583
            );
584
585
            if ($completed === null) {
586
                throw new ExecutionException(
587
                  sprintf(
588
                    'Cannot return null for non-nullable field %s.%s.',
589
                    $info->getParentType(), $info->getFieldName()
590
                  )
591
                );
592
            }
593
594
            return $completed;
595
        }
596
597
        // If field type is List, complete each item in the list with the inner type
598
        if ($returnType instanceof ListType) {
599
            return $this->completeListValue($returnType, $fieldNodes, $info,
600
              $path, $result);
601
        }
602
603
604
        // If field type is Scalar or Enum, serialize to a valid value, returning
605
        // null if serialization is not possible.
606
        if ($returnType instanceof LeafTypeInterface) {
607
            return $this->completeLeafValue($returnType, $result);
608
        }
609
610
        //@TODO Make a function for checking abstract type?
611
        if ($returnType instanceof InterfaceType || $returnType instanceof UnionType) {
612
            return $this->completeAbstractValue($returnType, $fieldNodes, $info,
613
              $path, $result);
614
        }
615
616
        // Field type must be Object, Interface or Union and expect sub-selections.
617
        if ($returnType instanceof ObjectType) {
618
            return $this->completeObjectValue($returnType, $fieldNodes, $info,
619
              $path, $result);
620
        }
621
622
        throw new ExecutionException("Cannot complete value of unexpected type \"{$returnType}\".");
623
    }
624
625
    /**
626
     * @param $value
627
     *
628
     * @return bool
629
     */
630
    protected function isPromise($value): bool
631
    {
632
        return $value instanceof ExtendedPromiseInterface;
633
    }
634
635
    /**
636
     * @param ListType $returnType
637
     * @param             $fieldNodes
638
     * @param ResolveInfo $info
639
     * @param             $path
640
     * @param             $result
641
     *
642
     * @return array|\React\Promise\Promise
643
     * @throws \Throwable
644
     */
645
    private function completeListValue(
646
      ListType $returnType,
647
      $fieldNodes,
648
      ResolveInfo $info,
649
      $path,
650
      &$result
651
    ) {
652
        $itemType = $returnType->getOfType();
653
654
        $completedItems = [];
655
        $containsPromise = false;
656
657
        if (!is_array($result) && !($result instanceof \Traversable)) {
658
            throw new \Exception(
659
              sprintf(
660
                'Expected Array or Traversable, but did not find one for field %s.%s.',
661
                $info->getParentType()->getName(),
662
                $info->getFieldName()
663
              )
664
            );
665
        }
666
667
        foreach ($result as $key => $item) {
668
            $fieldPath = $path;
669
            $fieldPath[] = $key;
670
            $completedItem = $this->completeValueCatchingError($itemType,
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $completedItem is correct as $this->completeValueCatc...nfo, $fieldPath, $item) targeting Digia\GraphQL\Execution\...eteValueCatchingError() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
671
              $fieldNodes, $info, $fieldPath, $item);
672
            $completedItems[] = $completedItem;
673
            $containsPromise = $containsPromise || $this->isPromise($completedItem);
674
        }
675
676
        return $containsPromise ? \React\Promise\all($completedItems) : $completedItems;
677
    }
678
679
    /**
680
     * @param LeafTypeInterface $returnType
681
     * @param                   $result
682
     *
683
     * @return mixed
684
     * @throws ExecutionException
685
     */
686
    private function completeLeafValue(LeafTypeInterface $returnType, &$result)
687
    {
688
        $serializedResult = $returnType->serialize($result);
0 ignored issues
show
Bug introduced by
The method serialize() does not exist on Digia\GraphQL\Type\Definition\LeafTypeInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Digia\GraphQL\Type\Definition\LeafTypeInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

688
        /** @scrutinizer ignore-call */ 
689
        $serializedResult = $returnType->serialize($result);
Loading history...
689
690
        if ($serializedResult === null) {
691
            //@TODO Make a function for this type of exception
692
            throw new ExecutionException(
693
              sprintf('Expected value of type "%s" but received: %s.',
694
                (string)$returnType, toString($result))
695
            );
696
        }
697
698
        return $serializedResult;
699
    }
700
701
    /**
702
     * @param AbstractTypeInterface $returnType
703
     * @param                       $fieldNodes
704
     * @param ResolveInfo $info
705
     * @param                       $path
706
     * @param                       $result
707
     *
708
     * @return array|PromiseInterface
709
     * @throws ExecutionException
710
     * @throws InvalidTypeException
711
     * @throws \Digia\GraphQL\Error\InvariantException
712
     * @throws \Throwable
713
     */
714
    private function completeAbstractValue(
715
      AbstractTypeInterface $returnType,
716
      $fieldNodes,
717
      ResolveInfo $info,
718
      $path,
719
      &$result
720
    ) {
721
        $runtimeType = $returnType->resolveType($result,
722
          $this->context->getContextValue(), $info);
0 ignored issues
show
Bug introduced by
$info of type Digia\GraphQL\Execution\ResolveInfo is incompatible with the type array expected by parameter $args of Digia\GraphQL\Type\Defin...nterface::resolveType(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

722
          $this->context->getContextValue(), /** @scrutinizer ignore-type */ $info);
Loading history...
723
724
        if (null === $runtimeType) {
725
            //@TODO Show warning
726
            $runtimeType = $this->defaultTypeResolver($result,
727
              $this->context->getContextValue(), $info, $returnType);
728
        }
729
730
        if ($this->isPromise($runtimeType)) {
731
            /** @var ExtendedPromiseInterface $runtimeType */
732
            return $runtimeType->then(function ($resolvedRuntimeType) use (
733
              $returnType,
734
              $fieldNodes,
735
              $info,
736
              $path,
737
              &$result
738
            ) {
739
                return $this->completeObjectValue(
740
                  $this->ensureValidRuntimeType(
741
                    $resolvedRuntimeType,
742
                    $returnType,
743
                    $fieldNodes,
744
                    $info,
745
                    $result
746
                  ),
747
                  $fieldNodes,
748
                  $info,
749
                  $path,
750
                  $result
751
                );
752
            });
753
        }
754
755
        return $this->completeObjectValue(
756
          $this->ensureValidRuntimeType(
757
            $runtimeType,
758
            $returnType,
759
            $fieldNodes,
760
            $info,
761
            $result
762
          ),
763
          $fieldNodes,
764
          $info,
765
          $path,
766
          $result
767
        );
768
    }
769
770
    /**
771
     * @param                       $value
772
     * @param                       $context
773
     * @param ResolveInfo $info
774
     * @param AbstractTypeInterface $abstractType
775
     *
776
     * @return TypeInterface|null
777
     * @throws ExecutionException
778
     */
779
    private function defaultTypeResolver(
780
      $value,
781
      $context,
782
      ResolveInfo $info,
783
      AbstractTypeInterface $abstractType
784
    ) {
785
        $possibleTypes = $info->getSchema()->getPossibleTypes($abstractType);
786
        $promisedIsTypeOfResults = [];
787
788
        foreach ($possibleTypes as $index => $type) {
789
            $isTypeOfResult = $type->isTypeOf($value, $context, $info);
790
            if (null !== $isTypeOfResult) {
791
                if ($this->isPromise($isTypeOfResult)) {
792
                    $promisedIsTypeOfResults[$index] = $isTypeOfResult;
793
                    continue;
794
                }
795
796
                if ($isTypeOfResult === true) {
797
                    return $type;
798
                }
799
800
                if (\is_array($value)) {
801
                    //@TODO Make `type` configurable
802
                    if (isset($value['type']) && $value['type'] === $type->getName()) {
803
                        return $type;
804
                    }
805
                }
806
            }
807
        }
808
809
        if (!empty($promisedIsTypeOfResults)) {
810
            return \React\Promise\all($promisedIsTypeOfResults)
0 ignored issues
show
Bug Best Practice introduced by
The expression return all($promisedIsTy...ion(...) { /* ... */ }) also could return the type React\Promise\Promise which is incompatible with the documented return type null|Digia\GraphQL\Type\Definition\TypeInterface.
Loading history...
811
              ->then(function ($isTypeOfResults) use ($possibleTypes) {
812
                  foreach ($isTypeOfResults as $index => $result) {
813
                      if ($result) {
814
                          return $possibleTypes[$index];
815
                      }
816
                  }
817
                  return null;
818
              });
819
        }
820
821
        return null;
822
    }
823
824
    /**
825
     * @param ObjectType $returnType
826
     * @param             $fieldNodes
827
     * @param ResolveInfo $info
828
     * @param             $path
829
     * @param             $result
830
     *
831
     * @return array
832
     * @throws ExecutionException
833
     * @throws InvalidTypeException
834
     * @throws \Digia\GraphQL\Error\InvariantException
835
     * @throws \Throwable
836
     */
837
    private function completeObjectValue(
838
      ObjectType $returnType,
839
      $fieldNodes,
840
      ResolveInfo $info,
841
      $path,
842
      &$result
843
    ) {
844
845
        if (null !== $returnType->getIsTypeOf()) {
846
            $isTypeOf = $returnType->isTypeOf($result,
847
              $this->context->getContextValue(), $info);
848
            //@TODO check for promise?
849
            if (!$isTypeOf) {
850
                throw new ExecutionException(
851
                  sprintf('Expected value of type "%s" but received: %s.',
852
                    (string)$returnType, toString($result))
853
                );
854
            }
855
        }
856
857
        return $this->collectAndExecuteSubFields(
858
          $returnType,
859
          $fieldNodes,
860
          $info,
861
          $path,
862
          $result
863
        );
864
    }
865
866
    /**
867
     * @param ObjectType $returnType
868
     * @param             $fieldNodes
869
     * @param ResolveInfo $info
870
     * @param             $path
871
     * @param             $result
872
     *
873
     * @return array
874
     * @throws InvalidTypeException
875
     * @throws \Digia\GraphQL\Error\ExecutionException
876
     * @throws \Digia\GraphQL\Error\InvariantException
877
     * @throws \Throwable
878
     */
879
    private function collectAndExecuteSubFields(
880
      ObjectType $returnType,
881
      $fieldNodes,
882
      ResolveInfo $info,
883
      $path,
884
      &$result
885
    ) {
886
        $subFields = [];
887
        $visitedFragmentNames = [];
888
889
        foreach ($fieldNodes as $fieldNode) {
890
            /** @var FieldNode $fieldNode */
891
            if ($fieldNode->getSelectionSet() !== null) {
892
                $subFields = $this->collectFields(
893
                  $returnType,
894
                  $fieldNode->getSelectionSet(),
895
                  $subFields,
896
                  $visitedFragmentNames
897
                );
898
            }
899
        }
900
901
        if (!empty($subFields)) {
902
            return $this->executeFields($returnType, $result, $path,
903
              $subFields);
904
        }
905
906
        return $result;
907
    }
908
909
    /**
910
     * @param ObjectType $runtimeType
911
     * @param SelectionSetNode $selectionSet
912
     * @param                  $fields
913
     * @param                  $visitedFragmentNames
914
     *
915
     * @return mixed
916
     * @throws InvalidTypeException
917
     * @throws \Digia\GraphQL\Error\ExecutionException
918
     * @throws \Digia\GraphQL\Error\InvariantException
919
     */
920
    protected function collectFields(
921
      ObjectType $runtimeType,
922
      SelectionSetNode $selectionSet,
923
      &$fields,
924
      &$visitedFragmentNames
925
    ) {
926
        foreach ($selectionSet->getSelections() as $selection) {
927
            // Check if this Node should be included first
928
            if (!$this->shouldIncludeNode($selection)) {
929
                continue;
930
            }
931
            // Collect fields
932
            if ($selection instanceof FieldNode) {
933
                $fieldName = $this->getFieldNameKey($selection);
934
935
                if (!isset($fields[$fieldName])) {
936
                    $fields[$fieldName] = [];
937
                }
938
939
                $fields[$fieldName][] = $selection;
940
            } elseif ($selection instanceof InlineFragmentNode) {
941
                if (!$this->doesFragmentConditionMatch($selection,
942
                  $runtimeType)) {
943
                    continue;
944
                }
945
946
                $this->collectFields($runtimeType,
947
                  $selection->getSelectionSet(), $fields,
948
                  $visitedFragmentNames);
949
            } elseif ($selection instanceof FragmentSpreadNode) {
950
                $fragmentName = $selection->getNameValue();
951
952
                if (!empty($visitedFragmentNames[$fragmentName])) {
953
                    continue;
954
                }
955
956
                $visitedFragmentNames[$fragmentName] = true;
957
                /** @var FragmentDefinitionNode $fragment */
958
                $fragment = $this->context->getFragments()[$fragmentName];
959
                $this->collectFields($runtimeType, $fragment->getSelectionSet(),
960
                  $fields, $visitedFragmentNames);
961
            }
962
        }
963
964
        return $fields;
965
    }
966
967
    /**
968
     * @param $node
969
     *
970
     * @return bool
971
     * @throws InvalidTypeException
972
     * @throws \Digia\GraphQL\Error\ExecutionException
973
     * @throws \Digia\GraphQL\Error\InvariantException
974
     */
975
    private function shouldIncludeNode(NodeInterface $node): bool
976
    {
977
978
        $contextVariables = $this->context->getVariableValues();
979
980
        $skip = coerceDirectiveValues(SkipDirective(), $node,
981
          $contextVariables);
982
983
        if ($skip && $skip['if'] === true) {
984
            return false;
985
        }
986
987
        $include = coerceDirectiveValues(IncludeDirective(), $node,
988
          $contextVariables);
989
990
        if ($include && $include['if'] === false) {
991
            return false;
992
        }
993
994
        return true;
995
    }
996
997
    /**
998
     * @TODO: consider to move this to FieldNode
999
     *
1000
     * @param FieldNode $node
1001
     *
1002
     * @return string
1003
     */
1004
    private function getFieldNameKey(FieldNode $node): string
1005
    {
1006
        return $node->getAlias() ? $node->getAlias()
0 ignored issues
show
Bug Best Practice introduced by
The expression return $node->getAlias()...: $node->getNameValue() could return the type null which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
1007
          ->getValue() : $node->getNameValue();
1008
    }
1009
1010
    /**
1011
     * @param FragmentDefinitionNode|InlineFragmentNode $fragment
1012
     * @param ObjectType $type
1013
     *
1014
     * @return bool
1015
     * @throws InvalidTypeException
1016
     */
1017
    private function doesFragmentConditionMatch(
1018
      NodeInterface $fragment,
1019
      ObjectType $type
1020
    ): bool {
1021
        $typeConditionNode = $fragment->getTypeCondition();
0 ignored issues
show
Bug introduced by
The method getTypeCondition() does not exist on Digia\GraphQL\Language\Node\NodeInterface. It seems like you code against a sub-type of Digia\GraphQL\Language\Node\NodeInterface such as Digia\GraphQL\Language\Node\InlineFragmentNode or Digia\GraphQL\Language\Node\FragmentDefinitionNode. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1021
        /** @scrutinizer ignore-call */ 
1022
        $typeConditionNode = $fragment->getTypeCondition();
Loading history...
1022
1023
        if (!$typeConditionNode) {
1024
            return true;
1025
        }
1026
1027
        $conditionalType = typeFromAST($this->context->getSchema(),
1028
          $typeConditionNode);
1029
1030
        if ($conditionalType === $type) {
1031
            return true;
1032
        }
1033
1034
        if ($conditionalType instanceof AbstractTypeInterface) {
1035
            return $this->context->getSchema()
1036
              ->isPossibleType($conditionalType, $type);
1037
        }
1038
1039
        return false;
1040
    }
1041
1042
    /**
1043
     * @param                       $runtimeTypeOrName
1044
     * @param AbstractTypeInterface $returnType
1045
     * @param                       $fieldNodes
1046
     * @param ResolveInfo $info
1047
     * @param                       $result
1048
     *
1049
     * @return TypeInterface|ObjectType|null
1050
     * @throws ExecutionException
1051
     */
1052
    private function ensureValidRuntimeType(
1053
      $runtimeTypeOrName,
1054
      AbstractTypeInterface $returnType,
1055
      $fieldNodes,
1056
      ResolveInfo $info,
1057
      &$result
1058
    ) {
1059
        $runtimeType = is_string($runtimeTypeOrName)
1060
          ? $this->context->getSchema()->getType($runtimeTypeOrName)
1061
          : $runtimeTypeOrName;
1062
1063
        $runtimeTypeName = is_string($runtimeType) ?: $runtimeType->getName();
0 ignored issues
show
Bug introduced by
The method getName() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1063
        $runtimeTypeName = is_string($runtimeType) ?: $runtimeType->/** @scrutinizer ignore-call */ getName();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug introduced by
The method getName() does not exist on Digia\GraphQL\Type\Definition\TypeInterface. It seems like you code against a sub-type of said class. However, the method does not exist in Digia\GraphQL\Type\Defin...n\AbstractTypeInterface or Digia\GraphQL\Type\Defin...n\WrappingTypeInterface. Are you sure you never get one of those? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1063
        $runtimeTypeName = is_string($runtimeType) ?: $runtimeType->/** @scrutinizer ignore-call */ getName();
Loading history...
1064
        $returnTypeName = $returnType->getName();
0 ignored issues
show
Bug introduced by
The method getName() does not exist on Digia\GraphQL\Type\Defin...n\AbstractTypeInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Digia\GraphQL\Type\Defin...n\AbstractTypeInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1064
        /** @scrutinizer ignore-call */ 
1065
        $returnTypeName = $returnType->getName();
Loading history...
1065
1066
        if (!$runtimeType instanceof ObjectType) {
1067
            $parentTypeName = $info->getParentType()->getName();
1068
            $fieldName = $info->getFieldName();
1069
1070
            throw new ExecutionException(
1071
              "Abstract type {$returnTypeName} must resolve to an Object type at runtime " .
1072
              "for field {$parentTypeName}.{$fieldName} with " .
1073
              'value "' . $result . '", received "{$runtimeTypeName}".'
1074
            );
1075
        }
1076
1077
        if (!$this->context->getSchema()
1078
          ->isPossibleType($returnType, $runtimeType)) {
1079
            throw new ExecutionException(
1080
              "Runtime Object type \"{$runtimeTypeName}\" is not a possible type for \"{$returnTypeName}\"."
1081
            );
1082
        }
1083
1084
        if ($runtimeType !== $this->context->getSchema()
1085
            ->getType($runtimeType->getName())) {
1086
            throw new ExecutionException(
1087
              "Schema must contain unique named types but contains multiple types named \"{$runtimeTypeName}\". " .
1088
              "Make sure that `resolveType` function of abstract type \"{$returnTypeName}\" returns the same " .
1089
              "type instance as referenced anywhere else within the schema."
1090
            );
1091
        }
1092
1093
        return $runtimeType;
1094
    }
1095
1096
    /**
1097
     * @param \Throwable $originalException
1098
     * @param array $nodes
1099
     * @param string|array $path
1100
     *
1101
     * @return GraphQLException
1102
     */
1103
    protected function buildLocatedError(
1104
      \Throwable $originalException,
1105
      array $nodes = [],
1106
      array $path = []
1107
    ): ExecutionException {
1108
        return new ExecutionException(
1109
          $originalException->getMessage(),
1110
          $originalException instanceof GraphQLException ? $originalException->getNodes() : $nodes,
1111
          $originalException instanceof GraphQLException ? $originalException->getSource() : null,
1112
          $originalException instanceof GraphQLException ? $originalException->getPositions() : null,
1113
          $originalException instanceof GraphQLException ? ($originalException->getPath() ?? $path) : $path,
1114
          $originalException
1115
        );
1116
    }
1117
}
1118