vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php line 251

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace GraphQL\Executor;
  4. use ArrayAccess;
  5. use ArrayObject;
  6. use Exception;
  7. use GraphQL\Error\Error;
  8. use GraphQL\Error\InvariantViolation;
  9. use GraphQL\Error\Warning;
  10. use GraphQL\Executor\Promise\Promise;
  11. use GraphQL\Executor\Promise\PromiseAdapter;
  12. use GraphQL\Language\AST\DocumentNode;
  13. use GraphQL\Language\AST\FieldNode;
  14. use GraphQL\Language\AST\FragmentDefinitionNode;
  15. use GraphQL\Language\AST\FragmentSpreadNode;
  16. use GraphQL\Language\AST\InlineFragmentNode;
  17. use GraphQL\Language\AST\Node;
  18. use GraphQL\Language\AST\OperationDefinitionNode;
  19. use GraphQL\Language\AST\SelectionNode;
  20. use GraphQL\Language\AST\SelectionSetNode;
  21. use GraphQL\Type\Definition\AbstractType;
  22. use GraphQL\Type\Definition\Directive;
  23. use GraphQL\Type\Definition\FieldDefinition;
  24. use GraphQL\Type\Definition\InterfaceType;
  25. use GraphQL\Type\Definition\LeafType;
  26. use GraphQL\Type\Definition\ListOfType;
  27. use GraphQL\Type\Definition\NonNull;
  28. use GraphQL\Type\Definition\ObjectType;
  29. use GraphQL\Type\Definition\ResolveInfo;
  30. use GraphQL\Type\Definition\Type;
  31. use GraphQL\Type\Definition\UnionType;
  32. use GraphQL\Type\Introspection;
  33. use GraphQL\Type\Schema;
  34. use GraphQL\Utils\TypeInfo;
  35. use GraphQL\Utils\Utils;
  36. use RuntimeException;
  37. use SplObjectStorage;
  38. use stdClass;
  39. use Throwable;
  40. use Traversable;
  41. use function array_keys;
  42. use function array_merge;
  43. use function array_reduce;
  44. use function array_values;
  45. use function count;
  46. use function get_class;
  47. use function is_array;
  48. use function is_callable;
  49. use function is_string;
  50. use function sprintf;
  51. class ReferenceExecutor implements ExecutorImplementation
  52. {
  53.     /** @var object */
  54.     protected static $UNDEFINED;
  55.     /** @var ExecutionContext */
  56.     protected $exeContext;
  57.     /** @var SplObjectStorage */
  58.     protected $subFieldCache;
  59.     protected function __construct(ExecutionContext $context)
  60.     {
  61.         if (! static::$UNDEFINED) {
  62.             static::$UNDEFINED Utils::undefined();
  63.         }
  64.         $this->exeContext    $context;
  65.         $this->subFieldCache = new SplObjectStorage();
  66.     }
  67.     /**
  68.      * @param mixed                    $rootValue
  69.      * @param mixed                    $contextValue
  70.      * @param array<mixed>|Traversable $variableValues
  71.      */
  72.     public static function create(
  73.         PromiseAdapter $promiseAdapter,
  74.         Schema $schema,
  75.         DocumentNode $documentNode,
  76.         $rootValue,
  77.         $contextValue,
  78.         $variableValues,
  79.         ?string $operationName,
  80.         callable $fieldResolver
  81.     ) : ExecutorImplementation {
  82.         $exeContext = static::buildExecutionContext(
  83.             $schema,
  84.             $documentNode,
  85.             $rootValue,
  86.             $contextValue,
  87.             $variableValues,
  88.             $operationName,
  89.             $fieldResolver,
  90.             $promiseAdapter
  91.         );
  92.         if (is_array($exeContext)) {
  93.             return new class($promiseAdapter->createFulfilled(new ExecutionResult(null$exeContext))) implements ExecutorImplementation
  94.             {
  95.                 /** @var Promise */
  96.                 private $result;
  97.                 public function __construct(Promise $result)
  98.                 {
  99.                     $this->result $result;
  100.                 }
  101.                 public function doExecute() : Promise
  102.                 {
  103.                     return $this->result;
  104.                 }
  105.             };
  106.         }
  107.         return new static($exeContext);
  108.     }
  109.     /**
  110.      * Constructs an ExecutionContext object from the arguments passed to
  111.      * execute, which we will pass throughout the other execution methods.
  112.      *
  113.      * @param mixed                    $rootValue
  114.      * @param mixed                    $contextValue
  115.      * @param array<mixed>|Traversable $rawVariableValues
  116.      *
  117.      * @return ExecutionContext|array<Error>
  118.      */
  119.     protected static function buildExecutionContext(
  120.         Schema $schema,
  121.         DocumentNode $documentNode,
  122.         $rootValue,
  123.         $contextValue,
  124.         $rawVariableValues,
  125.         ?string $operationName null,
  126.         ?callable $fieldResolver null,
  127.         ?PromiseAdapter $promiseAdapter null
  128.     ) {
  129.         $errors    = [];
  130.         $fragments = [];
  131.         /** @var OperationDefinitionNode|null $operation */
  132.         $operation                    null;
  133.         $hasMultipleAssumedOperations false;
  134.         foreach ($documentNode->definitions as $definition) {
  135.             switch (true) {
  136.                 case $definition instanceof OperationDefinitionNode:
  137.                     if ($operationName === null && $operation !== null) {
  138.                         $hasMultipleAssumedOperations true;
  139.                     }
  140.                     if ($operationName === null ||
  141.                         (isset($definition->name) && $definition->name->value === $operationName)) {
  142.                         $operation $definition;
  143.                     }
  144.                     break;
  145.                 case $definition instanceof FragmentDefinitionNode:
  146.                     $fragments[$definition->name->value] = $definition;
  147.                     break;
  148.             }
  149.         }
  150.         if ($operation === null) {
  151.             if ($operationName === null) {
  152.                 $errors[] = new Error('Must provide an operation.');
  153.             } else {
  154.                 $errors[] = new Error(sprintf('Unknown operation named "%s".'$operationName));
  155.             }
  156.         } elseif ($hasMultipleAssumedOperations) {
  157.             $errors[] = new Error(
  158.                 'Must provide operation name if query contains multiple operations.'
  159.             );
  160.         }
  161.         $variableValues null;
  162.         if ($operation !== null) {
  163.             [$coercionErrors$coercedVariableValues] = Values::getVariableValues(
  164.                 $schema,
  165.                 $operation->variableDefinitions ?? [],
  166.                 $rawVariableValues ?? []
  167.             );
  168.             if (count($coercionErrors ?? []) === 0) {
  169.                 $variableValues $coercedVariableValues;
  170.             } else {
  171.                 $errors array_merge($errors$coercionErrors);
  172.             }
  173.         }
  174.         if (count($errors) > 0) {
  175.             return $errors;
  176.         }
  177.         Utils::invariant($operation'Has operation if no errors.');
  178.         Utils::invariant($variableValues !== null'Has variables if no errors.');
  179.         return new ExecutionContext(
  180.             $schema,
  181.             $fragments,
  182.             $rootValue,
  183.             $contextValue,
  184.             $operation,
  185.             $variableValues,
  186.             $errors,
  187.             $fieldResolver,
  188.             $promiseAdapter
  189.         );
  190.     }
  191.     public function doExecute() : Promise
  192.     {
  193.         // Return a Promise that will eventually resolve to the data described by
  194.         // the "Response" section of the GraphQL specification.
  195.         //
  196.         // If errors are encountered while executing a GraphQL field, only that
  197.         // field and its descendants will be omitted, and sibling fields will still
  198.         // be executed. An execution which encounters errors will still result in a
  199.         // resolved Promise.
  200.         $data   $this->executeOperation($this->exeContext->operation$this->exeContext->rootValue);
  201.         $result $this->buildResponse($data);
  202.         // Note: we deviate here from the reference implementation a bit by always returning promise
  203.         // But for the "sync" case it is always fulfilled
  204.         return $this->isPromise($result)
  205.             ? $result
  206.             $this->exeContext->promiseAdapter->createFulfilled($result);
  207.     }
  208.     /**
  209.      * @param mixed|Promise|null $data
  210.      *
  211.      * @return ExecutionResult|Promise
  212.      */
  213.     protected function buildResponse($data)
  214.     {
  215.         if ($this->isPromise($data)) {
  216.             return $data->then(function ($resolved) {
  217.                 return $this->buildResponse($resolved);
  218.             });
  219.         }
  220.         if ($data !== null) {
  221.             $data = (array) $data;
  222.         }
  223.         return new ExecutionResult($data$this->exeContext->errors);
  224.     }
  225.     /**
  226.      * Implements the "Evaluating operations" section of the spec.
  227.      *
  228.      * @param mixed $rootValue
  229.      *
  230.      * @return array<mixed>|Promise|stdClass|null
  231.      */
  232.     protected function executeOperation(OperationDefinitionNode $operation$rootValue)
  233.     {
  234.         $type   $this->getOperationRootType($this->exeContext->schema$operation);
  235.         $fields $this->collectFields($type$operation->selectionSet, new ArrayObject(), new ArrayObject());
  236.         $path   = [];
  237.         // Errors from sub-fields of a NonNull type may propagate to the top level,
  238.         // at which point we still log the error and null the parent field, which
  239.         // in this case is the entire response.
  240.         //
  241.         // Similar to completeValueCatchingError.
  242.         try {
  243.             $result $operation->operation === 'mutation'
  244.                 $this->executeFieldsSerially($type$rootValue$path$fields)
  245.                 : $this->executeFields($type$rootValue$path$fields);
  246.             if ($this->isPromise($result)) {
  247.                 return $result->then(
  248.                     null,
  249.                     function ($error) : ?Promise {
  250.                         if ($error instanceof Error) {
  251.                             $this->exeContext->addError($error);
  252.                             return $this->exeContext->promiseAdapter->createFulfilled(null);
  253.                         }
  254.                         return null;
  255.                     }
  256.                 );
  257.             }
  258.             return $result;
  259.         } catch (Error $error) {
  260.             $this->exeContext->addError($error);
  261.             return null;
  262.         }
  263.     }
  264.     /**
  265.      * Extracts the root type of the operation from the schema.
  266.      *
  267.      * @throws Error
  268.      */
  269.     protected function getOperationRootType(Schema $schemaOperationDefinitionNode $operation) : ObjectType
  270.     {
  271.         switch ($operation->operation) {
  272.             case 'query':
  273.                 $queryType $schema->getQueryType();
  274.                 if ($queryType === null) {
  275.                     throw new Error(
  276.                         'Schema does not define the required query root type.',
  277.                         [$operation]
  278.                     );
  279.                 }
  280.                 return $queryType;
  281.             case 'mutation':
  282.                 $mutationType $schema->getMutationType();
  283.                 if ($mutationType === null) {
  284.                     throw new Error(
  285.                         'Schema is not configured for mutations.',
  286.                         [$operation]
  287.                     );
  288.                 }
  289.                 return $mutationType;
  290.             case 'subscription':
  291.                 $subscriptionType $schema->getSubscriptionType();
  292.                 if ($subscriptionType === null) {
  293.                     throw new Error(
  294.                         'Schema is not configured for subscriptions.',
  295.                         [$operation]
  296.                     );
  297.                 }
  298.                 return $subscriptionType;
  299.             default:
  300.                 throw new Error(
  301.                     'Can only execute queries, mutations and subscriptions.',
  302.                     [$operation]
  303.                 );
  304.         }
  305.     }
  306.     /**
  307.      * Given a selectionSet, adds all of the fields in that selection to
  308.      * the passed in map of fields, and returns it at the end.
  309.      *
  310.      * CollectFields requires the "runtime type" of an object. For a field which
  311.      * returns an Interface or Union type, the "runtime type" will be the actual
  312.      * Object type returned by that field.
  313.      */
  314.     protected function collectFields(
  315.         ObjectType $runtimeType,
  316.         SelectionSetNode $selectionSet,
  317.         ArrayObject $fields,
  318.         ArrayObject $visitedFragmentNames
  319.     ) : ArrayObject {
  320.         $exeContext $this->exeContext;
  321.         foreach ($selectionSet->selections as $selection) {
  322.             switch (true) {
  323.                 case $selection instanceof FieldNode:
  324.                     if (! $this->shouldIncludeNode($selection)) {
  325.                         break;
  326.                     }
  327.                     $name = static::getFieldEntryKey($selection);
  328.                     if (! isset($fields[$name])) {
  329.                         $fields[$name] = new ArrayObject();
  330.                     }
  331.                     $fields[$name][] = $selection;
  332.                     break;
  333.                 case $selection instanceof InlineFragmentNode:
  334.                     if (! $this->shouldIncludeNode($selection) ||
  335.                         ! $this->doesFragmentConditionMatch($selection$runtimeType)
  336.                     ) {
  337.                         break;
  338.                     }
  339.                     $this->collectFields(
  340.                         $runtimeType,
  341.                         $selection->selectionSet,
  342.                         $fields,
  343.                         $visitedFragmentNames
  344.                     );
  345.                     break;
  346.                 case $selection instanceof FragmentSpreadNode:
  347.                     $fragName $selection->name->value;
  348.                     if (($visitedFragmentNames[$fragName] ?? false) === true || ! $this->shouldIncludeNode($selection)) {
  349.                         break;
  350.                     }
  351.                     $visitedFragmentNames[$fragName] = true;
  352.                     /** @var FragmentDefinitionNode|null $fragment */
  353.                     $fragment $exeContext->fragments[$fragName] ?? null;
  354.                     if ($fragment === null || ! $this->doesFragmentConditionMatch($fragment$runtimeType)) {
  355.                         break;
  356.                     }
  357.                     $this->collectFields(
  358.                         $runtimeType,
  359.                         $fragment->selectionSet,
  360.                         $fields,
  361.                         $visitedFragmentNames
  362.                     );
  363.                     break;
  364.             }
  365.         }
  366.         return $fields;
  367.     }
  368.     /**
  369.      * Determines if a field should be included based on the @include and @skip
  370.      * directives, where @skip has higher precedence than @include.
  371.      *
  372.      * @param FragmentSpreadNode|FieldNode|InlineFragmentNode $node
  373.      */
  374.     protected function shouldIncludeNode(SelectionNode $node) : bool
  375.     {
  376.         $variableValues $this->exeContext->variableValues;
  377.         $skipDirective  Directive::skipDirective();
  378.         $skip           Values::getDirectiveValues(
  379.             $skipDirective,
  380.             $node,
  381.             $variableValues
  382.         );
  383.         if (isset($skip['if']) && $skip['if'] === true) {
  384.             return false;
  385.         }
  386.         $includeDirective Directive::includeDirective();
  387.         $include          Values::getDirectiveValues(
  388.             $includeDirective,
  389.             $node,
  390.             $variableValues
  391.         );
  392.         return ! isset($include['if']) || $include['if'] !== false;
  393.     }
  394.     /**
  395.      * Implements the logic to compute the key of a given fields entry
  396.      */
  397.     protected static function getFieldEntryKey(FieldNode $node) : string
  398.     {
  399.         return $node->alias === null $node->name->value $node->alias->value;
  400.     }
  401.     /**
  402.      * Determines if a fragment is applicable to the given type.
  403.      *
  404.      * @param FragmentDefinitionNode|InlineFragmentNode $fragment
  405.      */
  406.     protected function doesFragmentConditionMatch(Node $fragmentObjectType $type) : bool
  407.     {
  408.         $typeConditionNode $fragment->typeCondition;
  409.         if ($typeConditionNode === null) {
  410.             return true;
  411.         }
  412.         $conditionalType TypeInfo::typeFromAST($this->exeContext->schema$typeConditionNode);
  413.         if ($conditionalType === $type) {
  414.             return true;
  415.         }
  416.         if ($conditionalType instanceof AbstractType) {
  417.             return $this->exeContext->schema->isSubType($conditionalType$type);
  418.         }
  419.         return false;
  420.     }
  421.     /**
  422.      * Implements the "Evaluating selection sets" section of the spec
  423.      * for "write" mode.
  424.      *
  425.      * @param mixed             $rootValue
  426.      * @param array<string|int> $path
  427.      *
  428.      * @return array<mixed>|Promise|stdClass
  429.      */
  430.     protected function executeFieldsSerially(ObjectType $parentType$rootValue, array $pathArrayObject $fields)
  431.     {
  432.         $result $this->promiseReduce(
  433.             array_keys($fields->getArrayCopy()),
  434.             function ($results$responseName) use ($path$parentType$rootValue$fields) {
  435.                 $fieldNodes  $fields[$responseName];
  436.                 $fieldPath   $path;
  437.                 $fieldPath[] = $responseName;
  438.                 $result      $this->resolveField($parentType$rootValue$fieldNodes$fieldPath);
  439.                 if ($result === static::$UNDEFINED) {
  440.                     return $results;
  441.                 }
  442.                 $promise $this->getPromise($result);
  443.                 if ($promise !== null) {
  444.                     return $promise->then(static function ($resolvedResult) use ($responseName$results) {
  445.                         $results[$responseName] = $resolvedResult;
  446.                         return $results;
  447.                     });
  448.                 }
  449.                 $results[$responseName] = $result;
  450.                 return $results;
  451.             },
  452.             []
  453.         );
  454.         if ($this->isPromise($result)) {
  455.             return $result->then(static function ($resolvedResults) {
  456.                 return static::fixResultsIfEmptyArray($resolvedResults);
  457.             });
  458.         }
  459.         return static::fixResultsIfEmptyArray($result);
  460.     }
  461.     /**
  462.      * Resolves the field on the given root value.
  463.      *
  464.      * In particular, this figures out the value that the field returns
  465.      * by calling its resolve function, then calls completeValue to complete promises,
  466.      * serialize scalars, or execute the sub-selection-set for objects.
  467.      *
  468.      * @param mixed             $rootValue
  469.      * @param array<string|int> $path
  470.      *
  471.      * @return array<mixed>|Throwable|mixed|null
  472.      */
  473.     protected function resolveField(ObjectType $parentType$rootValueArrayObject $fieldNodes, array $path)
  474.     {
  475.         $exeContext $this->exeContext;
  476.         $fieldNode  $fieldNodes[0];
  477.         $fieldName  $fieldNode->name->value;
  478.         $fieldDef   $this->getFieldDef($exeContext->schema$parentType$fieldName);
  479.         if ($fieldDef === null) {
  480.             return static::$UNDEFINED;
  481.         }
  482.         $returnType $fieldDef->getType();
  483.         // The resolve function's optional 3rd argument is a context value that
  484.         // is provided to every resolve function within an execution. It is commonly
  485.         // used to represent an authenticated user, or request-specific caches.
  486.         // The resolve function's optional 4th argument is a collection of
  487.         // information about the current execution state.
  488.         $info = new ResolveInfo(
  489.             $fieldDef,
  490.             $fieldNodes,
  491.             $parentType,
  492.             $path,
  493.             $exeContext->schema,
  494.             $exeContext->fragments,
  495.             $exeContext->rootValue,
  496.             $exeContext->operation,
  497.             $exeContext->variableValues
  498.         );
  499.         if ($fieldDef->resolveFn !== null) {
  500.             $resolveFn $fieldDef->resolveFn;
  501.         } elseif ($parentType->resolveFieldFn !== null) {
  502.             $resolveFn $parentType->resolveFieldFn;
  503.         } else {
  504.             $resolveFn $this->exeContext->fieldResolver;
  505.         }
  506.         // Get the resolve function, regardless of if its result is normal
  507.         // or abrupt (error).
  508.         $result $this->resolveFieldValueOrError(
  509.             $fieldDef,
  510.             $fieldNode,
  511.             $resolveFn,
  512.             $rootValue,
  513.             $info
  514.         );
  515.         $result $this->completeValueCatchingError(
  516.             $returnType,
  517.             $fieldNodes,
  518.             $info,
  519.             $path,
  520.             $result
  521.         );
  522.         return $result;
  523.     }
  524.     /**
  525.      * This method looks up the field on the given type definition.
  526.      *
  527.      * It has special casing for the two introspection fields, __schema
  528.      * and __typename. __typename is special because it can always be
  529.      * queried as a field, even in situations where no other fields
  530.      * are allowed, like on a Union. __schema could get automatically
  531.      * added to the query type, but that would require mutating type
  532.      * definitions, which would cause issues.
  533.      */
  534.     protected function getFieldDef(Schema $schemaObjectType $parentTypestring $fieldName) : ?FieldDefinition
  535.     {
  536.         static $schemaMetaFieldDef$typeMetaFieldDef$typeNameMetaFieldDef;
  537.         $schemaMetaFieldDef   $schemaMetaFieldDef ?? Introspection::schemaMetaFieldDef();
  538.         $typeMetaFieldDef     $typeMetaFieldDef ?? Introspection::typeMetaFieldDef();
  539.         $typeNameMetaFieldDef $typeNameMetaFieldDef ?? Introspection::typeNameMetaFieldDef();
  540.         if ($fieldName === $schemaMetaFieldDef->name && $schema->getQueryType() === $parentType) {
  541.             return $schemaMetaFieldDef;
  542.         }
  543.         if ($fieldName === $typeMetaFieldDef->name && $schema->getQueryType() === $parentType) {
  544.             return $typeMetaFieldDef;
  545.         }
  546.         if ($fieldName === $typeNameMetaFieldDef->name) {
  547.             return $typeNameMetaFieldDef;
  548.         }
  549.         return $parentType->findField($fieldName);
  550.     }
  551.     /**
  552.      * Isolates the "ReturnOrAbrupt" behavior to not de-opt the `resolveField` function.
  553.      * Returns the result of resolveFn or the abrupt-return Error object.
  554.      *
  555.      * @param mixed $rootValue
  556.      *
  557.      * @return Throwable|Promise|mixed
  558.      */
  559.     protected function resolveFieldValueOrError(
  560.         FieldDefinition $fieldDef,
  561.         FieldNode $fieldNode,
  562.         callable $resolveFn,
  563.         $rootValue,
  564.         ResolveInfo $info
  565.     ) {
  566.         try {
  567.             // Build a map of arguments from the field.arguments AST, using the
  568.             // variables scope to fulfill any variable references.
  569.             $args         Values::getArgumentValues(
  570.                 $fieldDef,
  571.                 $fieldNode,
  572.                 $this->exeContext->variableValues
  573.             );
  574.             $contextValue $this->exeContext->contextValue;
  575.             return $resolveFn($rootValue$args$contextValue$info);
  576.         } catch (Throwable $error) {
  577.             return $error;
  578.         }
  579.     }
  580.     /**
  581.      * This is a small wrapper around completeValue which detects and logs errors
  582.      * in the execution context.
  583.      *
  584.      * @param array<string|int> $path
  585.      * @param mixed             $result
  586.      *
  587.      * @return array<mixed>|Promise|stdClass|null
  588.      */
  589.     protected function completeValueCatchingError(
  590.         Type $returnType,
  591.         ArrayObject $fieldNodes,
  592.         ResolveInfo $info,
  593.         array $path,
  594.         $result
  595.     ) {
  596.         // Otherwise, error protection is applied, logging the error and resolving
  597.         // a null value for this field if one is encountered.
  598.         try {
  599.             $promise $this->getPromise($result);
  600.             if ($promise !== null) {
  601.                 $completed $promise->then(function (&$resolved) use ($returnType$fieldNodes$info$path) {
  602.                     return $this->completeValue($returnType$fieldNodes$info$path$resolved);
  603.                 });
  604.             } else {
  605.                 $completed $this->completeValue($returnType$fieldNodes$info$path$result);
  606.             }
  607.             $promise $this->getPromise($completed);
  608.             if ($promise !== null) {
  609.                 return $promise->then(null, function ($error) use ($fieldNodes$path$returnType) : void {
  610.                     $this->handleFieldError($error$fieldNodes$path$returnType);
  611.                 });
  612.             }
  613.             return $completed;
  614.         } catch (Throwable $err) {
  615.             $this->handleFieldError($err$fieldNodes$path$returnType);
  616.             return null;
  617.         }
  618.     }
  619.     /**
  620.      * @param mixed             $rawError
  621.      * @param array<string|int> $path
  622.      *
  623.      * @throws Error
  624.      */
  625.     protected function handleFieldError($rawErrorArrayObject $fieldNodes, array $pathType $returnType) : void
  626.     {
  627.         $error Error::createLocatedError(
  628.             $rawError,
  629.             $fieldNodes,
  630.             $path
  631.         );
  632.         // If the field type is non-nullable, then it is resolved without any
  633.         // protection from errors, however it still properly locates the error.
  634.         if ($returnType instanceof NonNull) {
  635.             throw $error;
  636.         }
  637.         // Otherwise, error protection is applied, logging the error and resolving
  638.         // a null value for this field if one is encountered.
  639.         $this->exeContext->addError($error);
  640.     }
  641.     /**
  642.      * Implements the instructions for completeValue as defined in the
  643.      * "Field entries" section of the spec.
  644.      *
  645.      * If the field type is Non-Null, then this recursively completes the value
  646.      * for the inner type. It throws a field error if that completion returns null,
  647.      * as per the "Nullability" section of the spec.
  648.      *
  649.      * If the field type is a List, then this recursively completes the value
  650.      * for the inner type on each item in the list.
  651.      *
  652.      * If the field type is a Scalar or Enum, ensures the completed value is a legal
  653.      * value of the type by calling the `serialize` method of GraphQL type
  654.      * definition.
  655.      *
  656.      * If the field is an abstract type, determine the runtime type of the value
  657.      * and then complete based on that type
  658.      *
  659.      * Otherwise, the field type expects a sub-selection set, and will complete the
  660.      * value by evaluating all sub-selections.
  661.      *
  662.      * @param array<string|int> $path
  663.      * @param mixed             $result
  664.      *
  665.      * @return array<mixed>|mixed|Promise|null
  666.      *
  667.      * @throws Error
  668.      * @throws Throwable
  669.      */
  670.     protected function completeValue(
  671.         Type $returnType,
  672.         ArrayObject $fieldNodes,
  673.         ResolveInfo $info,
  674.         array $path,
  675.         &$result
  676.     ) {
  677.         // If result is an Error, throw a located error.
  678.         if ($result instanceof Throwable) {
  679.             throw $result;
  680.         }
  681.         // If field type is NonNull, complete for inner type, and throw field error
  682.         // if result is null.
  683.         if ($returnType instanceof NonNull) {
  684.             $completed $this->completeValue(
  685.                 $returnType->getWrappedType(),
  686.                 $fieldNodes,
  687.                 $info,
  688.                 $path,
  689.                 $result
  690.             );
  691.             if ($completed === null) {
  692.                 throw new InvariantViolation(
  693.                     sprintf('Cannot return null for non-nullable field "%s.%s".'$info->parentType$info->fieldName)
  694.                 );
  695.             }
  696.             return $completed;
  697.         }
  698.         // If result is null-like, return null.
  699.         if ($result === null) {
  700.             return null;
  701.         }
  702.         // If field type is List, complete each item in the list with the inner type
  703.         if ($returnType instanceof ListOfType) {
  704.             return $this->completeListValue($returnType$fieldNodes$info$path$result);
  705.         }
  706.         // Account for invalid schema definition when typeLoader returns different
  707.         // instance than `resolveType` or $field->getType() or $arg->getType()
  708.         if ($returnType !== $this->exeContext->schema->getType($returnType->name)) {
  709.             $hint '';
  710.             if ($this->exeContext->schema->getConfig()->typeLoader !== null) {
  711.                 $hint sprintf(
  712.                     'Make sure that type loader returns the same instance as defined in %s.%s',
  713.                     $info->parentType,
  714.                     $info->fieldName
  715.                 );
  716.             }
  717.             throw new InvariantViolation(
  718.                 sprintf(
  719.                     'Schema must contain unique named types but contains multiple types named "%s". %s ' .
  720.                     '(see http://webonyx.github.io/graphql-php/type-system/#type-registry).',
  721.                     $returnType,
  722.                     $hint
  723.                 )
  724.             );
  725.         }
  726.         // If field type is Scalar or Enum, serialize to a valid value, returning
  727.         // null if serialization is not possible.
  728.         if ($returnType instanceof LeafType) {
  729.             return $this->completeLeafValue($returnType$result);
  730.         }
  731.         if ($returnType instanceof AbstractType) {
  732.             return $this->completeAbstractValue($returnType$fieldNodes$info$path$result);
  733.         }
  734.         // Field type must be Object, Interface or Union and expect sub-selections.
  735.         if ($returnType instanceof ObjectType) {
  736.             return $this->completeObjectValue($returnType$fieldNodes$info$path$result);
  737.         }
  738.         throw new RuntimeException(sprintf('Cannot complete value of unexpected type "%s".'$returnType));
  739.     }
  740.     /**
  741.      * @param mixed $value
  742.      */
  743.     protected function isPromise($value) : bool
  744.     {
  745.         return $value instanceof Promise || $this->exeContext->promiseAdapter->isThenable($value);
  746.     }
  747.     /**
  748.      * Only returns the value if it acts like a Promise, i.e. has a "then" function,
  749.      * otherwise returns null.
  750.      *
  751.      * @param mixed $value
  752.      */
  753.     protected function getPromise($value) : ?Promise
  754.     {
  755.         if ($value === null || $value instanceof Promise) {
  756.             return $value;
  757.         }
  758.         if ($this->exeContext->promiseAdapter->isThenable($value)) {
  759.             $promise $this->exeContext->promiseAdapter->convertThenable($value);
  760.             if (! $promise instanceof Promise) {
  761.                 throw new InvariantViolation(sprintf(
  762.                     '%s::convertThenable is expected to return instance of GraphQL\Executor\Promise\Promise, got: %s',
  763.                     get_class($this->exeContext->promiseAdapter),
  764.                     Utils::printSafe($promise)
  765.                 ));
  766.             }
  767.             return $promise;
  768.         }
  769.         return null;
  770.     }
  771.     /**
  772.      * Similar to array_reduce(), however the reducing callback may return
  773.      * a Promise, in which case reduction will continue after each promise resolves.
  774.      *
  775.      * If the callback does not return a Promise, then this function will also not
  776.      * return a Promise.
  777.      *
  778.      * @param array<mixed>       $values
  779.      * @param Promise|mixed|null $initialValue
  780.      *
  781.      * @return Promise|mixed|null
  782.      */
  783.     protected function promiseReduce(array $values, callable $callback$initialValue)
  784.     {
  785.         return array_reduce(
  786.             $values,
  787.             function ($previous$value) use ($callback) {
  788.                 $promise $this->getPromise($previous);
  789.                 if ($promise !== null) {
  790.                     return $promise->then(static function ($resolved) use ($callback$value) {
  791.                         return $callback($resolved$value);
  792.                     });
  793.                 }
  794.                 return $callback($previous$value);
  795.             },
  796.             $initialValue
  797.         );
  798.     }
  799.     /**
  800.      * Complete a list value by completing each item in the list with the inner type.
  801.      *
  802.      * @param array<string|int>        $path
  803.      * @param array<mixed>|Traversable $results
  804.      *
  805.      * @return array<mixed>|Promise|stdClass
  806.      *
  807.      * @throws Exception
  808.      */
  809.     protected function completeListValue(ListOfType $returnTypeArrayObject $fieldNodesResolveInfo $info, array $path, &$results)
  810.     {
  811.         $itemType $returnType->getWrappedType();
  812.         Utils::invariant(
  813.             is_array($results) || $results instanceof Traversable,
  814.             'User Error: expected iterable, but did not find one for field ' $info->parentType '.' $info->fieldName '.'
  815.         );
  816.         $containsPromise false;
  817.         $i               0;
  818.         $completedItems  = [];
  819.         foreach ($results as $item) {
  820.             $fieldPath     $path;
  821.             $fieldPath[]   = $i++;
  822.             $info->path    $fieldPath;
  823.             $completedItem $this->completeValueCatchingError($itemType$fieldNodes$info$fieldPath$item);
  824.             if (! $containsPromise && $this->getPromise($completedItem) !== null) {
  825.                 $containsPromise true;
  826.             }
  827.             $completedItems[] = $completedItem;
  828.         }
  829.         return $containsPromise
  830.             $this->exeContext->promiseAdapter->all($completedItems)
  831.             : $completedItems;
  832.     }
  833.     /**
  834.      * Complete a Scalar or Enum by serializing to a valid value, throwing if serialization is not possible.
  835.      *
  836.      * @param mixed $result
  837.      *
  838.      * @return mixed
  839.      *
  840.      * @throws Exception
  841.      */
  842.     protected function completeLeafValue(LeafType $returnType, &$result)
  843.     {
  844.         try {
  845.             return $returnType->serialize($result);
  846.         } catch (Throwable $error) {
  847.             throw new InvariantViolation(
  848.                 'Expected a value of type "' Utils::printSafe($returnType) . '" but received: ' Utils::printSafe($result),
  849.                 0,
  850.                 $error
  851.             );
  852.         }
  853.     }
  854.     /**
  855.      * Complete a value of an abstract type by determining the runtime object type
  856.      * of that value, then complete the value for that type.
  857.      *
  858.      * @param array<string|int> $path
  859.      * @param array<mixed>      $result
  860.      *
  861.      * @return array<mixed>|Promise|stdClass
  862.      *
  863.      * @throws Error
  864.      */
  865.     protected function completeAbstractValue(
  866.         AbstractType $returnType,
  867.         ArrayObject $fieldNodes,
  868.         ResolveInfo $info,
  869.         array $path,
  870.         &$result
  871.     ) {
  872.         $exeContext    $this->exeContext;
  873.         $typeCandidate $returnType->resolveType($result$exeContext->contextValue$info);
  874.         if ($typeCandidate === null) {
  875.             $runtimeType = static::defaultTypeResolver($result$exeContext->contextValue$info$returnType);
  876.         } elseif (is_callable($typeCandidate)) {
  877.             $runtimeType Schema::resolveType($typeCandidate);
  878.         } else {
  879.             $runtimeType $typeCandidate;
  880.         }
  881.         $promise $this->getPromise($runtimeType);
  882.         if ($promise !== null) {
  883.             return $promise->then(function ($resolvedRuntimeType) use (
  884.                 $returnType,
  885.                 $fieldNodes,
  886.                 $info,
  887.                 $path,
  888.                 &$result
  889.             ) {
  890.                 return $this->completeObjectValue(
  891.                     $this->ensureValidRuntimeType(
  892.                         $resolvedRuntimeType,
  893.                         $returnType,
  894.                         $info,
  895.                         $result
  896.                     ),
  897.                     $fieldNodes,
  898.                     $info,
  899.                     $path,
  900.                     $result
  901.                 );
  902.             });
  903.         }
  904.         return $this->completeObjectValue(
  905.             $this->ensureValidRuntimeType(
  906.                 $runtimeType,
  907.                 $returnType,
  908.                 $info,
  909.                 $result
  910.             ),
  911.             $fieldNodes,
  912.             $info,
  913.             $path,
  914.             $result
  915.         );
  916.     }
  917.     /**
  918.      * If a resolveType function is not given, then a default resolve behavior is
  919.      * used which attempts two strategies:
  920.      *
  921.      * First, See if the provided value has a `__typename` field defined, if so, use
  922.      * that value as name of the resolved type.
  923.      *
  924.      * Otherwise, test each possible type for the abstract type by calling
  925.      * isTypeOf for the object being coerced, returning the first type that matches.
  926.      *
  927.      * @param mixed|null              $value
  928.      * @param mixed|null              $contextValue
  929.      * @param InterfaceType|UnionType $abstractType
  930.      *
  931.      * @return Promise|Type|string|null
  932.      */
  933.     protected function defaultTypeResolver($value$contextValueResolveInfo $infoAbstractType $abstractType)
  934.     {
  935.         // First, look for `__typename`.
  936.         if ($value !== null &&
  937.             (is_array($value) || $value instanceof ArrayAccess) &&
  938.             isset($value['__typename']) &&
  939.             is_string($value['__typename'])
  940.         ) {
  941.             return $value['__typename'];
  942.         }
  943.         if ($abstractType instanceof InterfaceType && $info->schema->getConfig()->typeLoader !== null) {
  944.             Warning::warnOnce(
  945.                 sprintf(
  946.                     'GraphQL Interface Type `%s` returned `null` from its `resolveType` function ' .
  947.                     'for value: %s. Switching to slow resolution method using `isTypeOf` ' .
  948.                     'of all possible implementations. It requires full schema scan and degrades query performance significantly. ' .
  949.                     ' Make sure your `resolveType` always returns valid implementation or throws.',
  950.                     $abstractType->name,
  951.                     Utils::printSafe($value)
  952.                 ),
  953.                 Warning::WARNING_FULL_SCHEMA_SCAN
  954.             );
  955.         }
  956.         // Otherwise, test each possible type.
  957.         $possibleTypes           $info->schema->getPossibleTypes($abstractType);
  958.         $promisedIsTypeOfResults = [];
  959.         foreach ($possibleTypes as $index => $type) {
  960.             $isTypeOfResult $type->isTypeOf($value$contextValue$info);
  961.             if ($isTypeOfResult === null) {
  962.                 continue;
  963.             }
  964.             $promise $this->getPromise($isTypeOfResult);
  965.             if ($promise !== null) {
  966.                 $promisedIsTypeOfResults[$index] = $promise;
  967.             } elseif ($isTypeOfResult) {
  968.                 return $type;
  969.             }
  970.         }
  971.         if (count($promisedIsTypeOfResults) > 0) {
  972.             return $this->exeContext->promiseAdapter->all($promisedIsTypeOfResults)
  973.                 ->then(static function ($isTypeOfResults) use ($possibleTypes) : ?ObjectType {
  974.                     foreach ($isTypeOfResults as $index => $result) {
  975.                         if ($result) {
  976.                             return $possibleTypes[$index];
  977.                         }
  978.                     }
  979.                     return null;
  980.                 });
  981.         }
  982.         return null;
  983.     }
  984.     /**
  985.      * Complete an Object value by executing all sub-selections.
  986.      *
  987.      * @param array<string|int> $path
  988.      * @param mixed             $result
  989.      *
  990.      * @return array<mixed>|Promise|stdClass
  991.      *
  992.      * @throws Error
  993.      */
  994.     protected function completeObjectValue(
  995.         ObjectType $returnType,
  996.         ArrayObject $fieldNodes,
  997.         ResolveInfo $info,
  998.         array $path,
  999.         &$result
  1000.     ) {
  1001.         // If there is an isTypeOf predicate function, call it with the
  1002.         // current result. If isTypeOf returns false, then raise an error rather
  1003.         // than continuing execution.
  1004.         $isTypeOf $returnType->isTypeOf($result$this->exeContext->contextValue$info);
  1005.         if ($isTypeOf !== null) {
  1006.             $promise $this->getPromise($isTypeOf);
  1007.             if ($promise !== null) {
  1008.                 return $promise->then(function ($isTypeOfResult) use (
  1009.                     $returnType,
  1010.                     $fieldNodes,
  1011.                     $path,
  1012.                     &$result
  1013.                 ) {
  1014.                     if (! $isTypeOfResult) {
  1015.                         throw $this->invalidReturnTypeError($returnType$result$fieldNodes);
  1016.                     }
  1017.                     return $this->collectAndExecuteSubfields(
  1018.                         $returnType,
  1019.                         $fieldNodes,
  1020.                         $path,
  1021.                         $result
  1022.                     );
  1023.                 });
  1024.             }
  1025.             if (! $isTypeOf) {
  1026.                 throw $this->invalidReturnTypeError($returnType$result$fieldNodes);
  1027.             }
  1028.         }
  1029.         return $this->collectAndExecuteSubfields(
  1030.             $returnType,
  1031.             $fieldNodes,
  1032.             $path,
  1033.             $result
  1034.         );
  1035.     }
  1036.     /**
  1037.      * @param array<mixed> $result
  1038.      *
  1039.      * @return Error
  1040.      */
  1041.     protected function invalidReturnTypeError(
  1042.         ObjectType $returnType,
  1043.         $result,
  1044.         ArrayObject $fieldNodes
  1045.     ) {
  1046.         return new Error(
  1047.             'Expected value of type "' $returnType->name '" but got: ' Utils::printSafe($result) . '.',
  1048.             $fieldNodes
  1049.         );
  1050.     }
  1051.     /**
  1052.      * @param array<string|int> $path
  1053.      * @param mixed             $result
  1054.      *
  1055.      * @return array<mixed>|Promise|stdClass
  1056.      *
  1057.      * @throws Error
  1058.      */
  1059.     protected function collectAndExecuteSubfields(
  1060.         ObjectType $returnType,
  1061.         ArrayObject $fieldNodes,
  1062.         array $path,
  1063.         &$result
  1064.     ) {
  1065.         $subFieldNodes $this->collectSubFields($returnType$fieldNodes);
  1066.         return $this->executeFields($returnType$result$path$subFieldNodes);
  1067.     }
  1068.     /**
  1069.      * A memoized collection of relevant subfields with regard to the return
  1070.      * type. Memoizing ensures the subfields are not repeatedly calculated, which
  1071.      * saves overhead when resolving lists of values.
  1072.      */
  1073.     protected function collectSubFields(ObjectType $returnTypeArrayObject $fieldNodes) : ArrayObject
  1074.     {
  1075.         if (! isset($this->subFieldCache[$returnType])) {
  1076.             $this->subFieldCache[$returnType] = new SplObjectStorage();
  1077.         }
  1078.         if (! isset($this->subFieldCache[$returnType][$fieldNodes])) {
  1079.             // Collect sub-fields to execute to complete this value.
  1080.             $subFieldNodes        = new ArrayObject();
  1081.             $visitedFragmentNames = new ArrayObject();
  1082.             foreach ($fieldNodes as $fieldNode) {
  1083.                 if (! isset($fieldNode->selectionSet)) {
  1084.                     continue;
  1085.                 }
  1086.                 $subFieldNodes $this->collectFields(
  1087.                     $returnType,
  1088.                     $fieldNode->selectionSet,
  1089.                     $subFieldNodes,
  1090.                     $visitedFragmentNames
  1091.                 );
  1092.             }
  1093.             $this->subFieldCache[$returnType][$fieldNodes] = $subFieldNodes;
  1094.         }
  1095.         return $this->subFieldCache[$returnType][$fieldNodes];
  1096.     }
  1097.     /**
  1098.      * Implements the "Evaluating selection sets" section of the spec
  1099.      * for "read" mode.
  1100.      *
  1101.      * @param mixed             $rootValue
  1102.      * @param array<string|int> $path
  1103.      *
  1104.      * @return Promise|stdClass|array<mixed>
  1105.      */
  1106.     protected function executeFields(ObjectType $parentType$rootValue, array $pathArrayObject $fields)
  1107.     {
  1108.         $containsPromise false;
  1109.         $results         = [];
  1110.         foreach ($fields as $responseName => $fieldNodes) {
  1111.             $fieldPath   $path;
  1112.             $fieldPath[] = $responseName;
  1113.             $result      $this->resolveField($parentType$rootValue$fieldNodes$fieldPath);
  1114.             if ($result === static::$UNDEFINED) {
  1115.                 continue;
  1116.             }
  1117.             if (! $containsPromise && $this->isPromise($result)) {
  1118.                 $containsPromise true;
  1119.             }
  1120.             $results[$responseName] = $result;
  1121.         }
  1122.         // If there are no promises, we can just return the object
  1123.         if (! $containsPromise) {
  1124.             return static::fixResultsIfEmptyArray($results);
  1125.         }
  1126.         // Otherwise, results is a map from field name to the result of resolving that
  1127.         // field, which is possibly a promise. Return a promise that will return this
  1128.         // same map, but with any promises replaced with the values they resolved to.
  1129.         return $this->promiseForAssocArray($results);
  1130.     }
  1131.     /**
  1132.      * Differentiate empty objects from empty lists.
  1133.      *
  1134.      * @see https://github.com/webonyx/graphql-php/issues/59
  1135.      *
  1136.      * @param array<mixed>|mixed $results
  1137.      *
  1138.      * @return array<mixed>|stdClass|mixed
  1139.      */
  1140.     protected static function fixResultsIfEmptyArray($results)
  1141.     {
  1142.         if ($results === []) {
  1143.             return new stdClass();
  1144.         }
  1145.         return $results;
  1146.     }
  1147.     /**
  1148.      * Transform an associative array with Promises to a Promise which resolves to an
  1149.      * associative array where all Promises were resolved.
  1150.      *
  1151.      * @param array<string, Promise|mixed> $assoc
  1152.      */
  1153.     protected function promiseForAssocArray(array $assoc) : Promise
  1154.     {
  1155.         $keys              array_keys($assoc);
  1156.         $valuesAndPromises array_values($assoc);
  1157.         $promise           $this->exeContext->promiseAdapter->all($valuesAndPromises);
  1158.         return $promise->then(static function ($values) use ($keys) {
  1159.             $resolvedResults = [];
  1160.             foreach ($values as $i => $value) {
  1161.                 $resolvedResults[$keys[$i]] = $value;
  1162.             }
  1163.             return static::fixResultsIfEmptyArray($resolvedResults);
  1164.         });
  1165.     }
  1166.     /**
  1167.      * @param string|ObjectType|null  $runtimeTypeOrName
  1168.      * @param InterfaceType|UnionType $returnType
  1169.      * @param mixed                   $result
  1170.      */
  1171.     protected function ensureValidRuntimeType(
  1172.         $runtimeTypeOrName,
  1173.         AbstractType $returnType,
  1174.         ResolveInfo $info,
  1175.         &$result
  1176.     ) : ObjectType {
  1177.         $runtimeType is_string($runtimeTypeOrName)
  1178.             ? $this->exeContext->schema->getType($runtimeTypeOrName)
  1179.             : $runtimeTypeOrName;
  1180.         if (! $runtimeType instanceof ObjectType) {
  1181.             throw new InvariantViolation(
  1182.                 sprintf(
  1183.                     'Abstract type %s must resolve to an Object type at ' .
  1184.                     'runtime for field %s.%s with value "%s", received "%s". ' .
  1185.                     'Either the %s type should provide a "resolveType" ' .
  1186.                     'function or each possible type should provide an "isTypeOf" function.',
  1187.                     $returnType,
  1188.                     $info->parentType,
  1189.                     $info->fieldName,
  1190.                     Utils::printSafe($result),
  1191.                     Utils::printSafe($runtimeType),
  1192.                     $returnType
  1193.                 )
  1194.             );
  1195.         }
  1196.         if (! $this->exeContext->schema->isSubType($returnType$runtimeType)) {
  1197.             throw new InvariantViolation(
  1198.                 sprintf('Runtime Object type "%s" is not a possible type for "%s".'$runtimeType$returnType)
  1199.             );
  1200.         }
  1201.         if ($runtimeType !== $this->exeContext->schema->getType($runtimeType->name)) {
  1202.             throw new InvariantViolation(
  1203.                 sprintf(
  1204.                     'Schema must contain unique named types but contains multiple types named "%s". ' .
  1205.                     'Make sure that `resolveType` function of abstract type "%s" returns the same ' .
  1206.                     'type instance as referenced anywhere else within the schema ' .
  1207.                     '(see http://webonyx.github.io/graphql-php/type-system/#type-registry).',
  1208.                     $runtimeType,
  1209.                     $returnType
  1210.                 )
  1211.             );
  1212.         }
  1213.         return $runtimeType;
  1214.     }
  1215. }