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

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