Skip to content

Commit ca53999

Browse files
committed
Allow context to be scoped
By implementing the ScopedContext marker interface in your context object, you can ensure that scope is only passed downwards and not shared with other fields. Example: ```graphql query { a { b { c } d { e { f } } } g { h { i } } } ``` In the above situation, the following will happen: * `a` receives the cloned context from the executor * `b` receives the cloned context from `a` * `c` receives the cloned context from `b` * `d` receives the cloned context from `d` * `f` receives the cloned context from `d` * `g` receives the cloned context from the executor * `h` receives the cloned context from `g` * `i` receives the cloned context from `h` For now I decided to use a marker interface `ScopedContext`. Although I'm not fan of marker interfaces, it was the easiest way to get this working. We could also make this an option that needs to be passed to the `Executor`. Something like `$useScopedContext = false` (disabled by default). References: graphql/graphql-js#2692 rmosolgo/graphql-ruby#2634
1 parent 01d7b25 commit ca53999

File tree

4 files changed

+325
-33
lines changed

4 files changed

+325
-33
lines changed

src/Executor/ReferenceExecutor.php

Lines changed: 71 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -294,8 +294,8 @@ protected function executeOperation(OperationDefinitionNode $operation, $rootVal
294294
// Similar to completeValueCatchingError.
295295
try {
296296
$result = $operation->operation === 'mutation'
297-
? $this->executeFieldsSerially($type, $rootValue, $path, $fields)
298-
: $this->executeFields($type, $rootValue, $path, $fields);
297+
? $this->executeFieldsSerially($type, $rootValue, $path, $fields, $this->exeContext->contextValue)
298+
: $this->executeFields($type, $rootValue, $path, $fields, $this->exeContext->contextValue);
299299

300300
$promise = $this->getPromise($result);
301301
if ($promise !== null) {
@@ -522,22 +522,23 @@ protected function doesFragmentConditionMatch(Node $fragment, ObjectType $type):
522522
*
523523
* @param mixed $rootValue
524524
* @param array<string|int> $path
525+
* @param mixed $contextValue
525526
*
526527
* @phpstan-param Fields $fields
527528
*
528529
* @return array<mixed>|Promise|\stdClass
529530
*/
530-
protected function executeFieldsSerially(ObjectType $parentType, $rootValue, array $path, \ArrayObject $fields)
531+
protected function executeFieldsSerially(ObjectType $parentType, $rootValue, array $path, \ArrayObject $fields, $contextValue)
531532
{
532533
$result = $this->promiseReduce(
533534
\array_keys($fields->getArrayCopy()),
534-
function ($results, $responseName) use ($path, $parentType, $rootValue, $fields) {
535+
function ($results, $responseName) use ($contextValue, $path, $parentType, $rootValue, $fields) {
535536
$fieldNodes = $fields[$responseName];
536537
assert($fieldNodes instanceof \ArrayObject, 'The keys of $fields populate $responseName');
537538

538539
$fieldPath = $path;
539540
$fieldPath[] = $responseName;
540-
$result = $this->resolveField($parentType, $rootValue, $fieldNodes, $fieldPath);
541+
$result = $this->resolveField($parentType, $rootValue, $fieldNodes, $fieldPath, $contextValue);
541542
if ($result === static::$UNDEFINED) {
542543
return $results;
543544
}
@@ -577,6 +578,7 @@ function ($results, $responseName) use ($path, $parentType, $rootValue, $fields)
577578
*
578579
* @param mixed $rootValue
579580
* @param array<int, string|int> $path
581+
* @param mixed $contextValue
580582
*
581583
* @phpstan-param Path $path
582584
*
@@ -587,7 +589,7 @@ function ($results, $responseName) use ($path, $parentType, $rootValue, $fields)
587589
*
588590
* @return array<mixed>|\Throwable|mixed|null
589591
*/
590-
protected function resolveField(ObjectType $parentType, $rootValue, \ArrayObject $fieldNodes, array $path)
592+
protected function resolveField(ObjectType $parentType, $rootValue, \ArrayObject $fieldNodes, array $path, $contextValue)
591593
{
592594
$exeContext = $this->exeContext;
593595
$fieldNode = $fieldNodes[0];
@@ -631,15 +633,17 @@ protected function resolveField(ObjectType $parentType, $rootValue, \ArrayObject
631633
$fieldNode,
632634
$resolveFn,
633635
$rootValue,
634-
$info
636+
$info,
637+
$contextValue
635638
);
636639

637640
return $this->completeValueCatchingError(
638641
$returnType,
639642
$fieldNodes,
640643
$info,
641644
$path,
642-
$result
645+
$result,
646+
$contextValue
643647
);
644648
}
645649

@@ -684,6 +688,7 @@ protected function getFieldDef(Schema $schema, ObjectType $parentType, string $f
684688
* Returns the result of resolveFn or the abrupt-return Error object.
685689
*
686690
* @param mixed $rootValue
691+
* @param mixed $contextValue
687692
*
688693
* @phpstan-param FieldResolver $resolveFn
689694
*
@@ -694,7 +699,8 @@ protected function resolveFieldValueOrError(
694699
FieldNode $fieldNode,
695700
callable $resolveFn,
696701
$rootValue,
697-
ResolveInfo $info
702+
ResolveInfo $info,
703+
$contextValue
698704
) {
699705
try {
700706
// Build a map of arguments from the field.arguments AST, using the
@@ -704,7 +710,6 @@ protected function resolveFieldValueOrError(
704710
$fieldNode,
705711
$this->exeContext->variableValues
706712
);
707-
$contextValue = $this->exeContext->contextValue;
708713

709714
return $resolveFn($rootValue, $args, $contextValue, $info);
710715
} catch (\Throwable $error) {
@@ -718,6 +723,7 @@ protected function resolveFieldValueOrError(
718723
*
719724
* @param \ArrayObject<int, FieldNode> $fieldNodes
720725
* @param array<string|int> $path
726+
* @param mixed $contextValue
721727
*
722728
* @phpstan-param Path $path
723729
*
@@ -732,18 +738,19 @@ protected function completeValueCatchingError(
732738
\ArrayObject $fieldNodes,
733739
ResolveInfo $info,
734740
array $path,
735-
$result
741+
$result,
742+
$contextValue
736743
) {
737744
// Otherwise, error protection is applied, logging the error and resolving
738745
// a null value for this field if one is encountered.
739746
try {
740747
$promise = $this->getPromise($result);
741748
if ($promise !== null) {
742-
$completed = $promise->then(function (&$resolved) use ($returnType, $fieldNodes, $info, $path) {
743-
return $this->completeValue($returnType, $fieldNodes, $info, $path, $resolved);
749+
$completed = $promise->then(function (&$resolved) use ($contextValue, $returnType, $fieldNodes, $info, $path) {
750+
return $this->completeValue($returnType, $fieldNodes, $info, $path, $resolved, $contextValue);
744751
});
745752
} else {
746-
$completed = $this->completeValue($returnType, $fieldNodes, $info, $path, $result);
753+
$completed = $this->completeValue($returnType, $fieldNodes, $info, $path, $result, $contextValue);
747754
}
748755

749756
$promise = $this->getPromise($completed);
@@ -811,6 +818,7 @@ protected function handleFieldError($rawError, \ArrayObject $fieldNodes, array $
811818
* @param \ArrayObject<int, FieldNode> $fieldNodes
812819
* @param array<string|int> $path
813820
* @param mixed $result
821+
* @param mixed $contextValue
814822
*
815823
* @throws \Throwable
816824
* @throws Error
@@ -822,7 +830,8 @@ protected function completeValue(
822830
\ArrayObject $fieldNodes,
823831
ResolveInfo $info,
824832
array $path,
825-
&$result
833+
&$result,
834+
$contextValue
826835
) {
827836
// If result is an Error, throw a located error.
828837
if ($result instanceof \Throwable) {
@@ -837,7 +846,8 @@ protected function completeValue(
837846
$fieldNodes,
838847
$info,
839848
$path,
840-
$result
849+
$result,
850+
$contextValue
841851
);
842852
if ($completed === null) {
843853
throw new InvariantViolation("Cannot return null for non-nullable field \"{$info->parentType}.{$info->fieldName}\".");
@@ -858,7 +868,7 @@ protected function completeValue(
858868
throw new InvariantViolation("Expected field {$info->parentType}.{$info->fieldName} to return iterable, but got: {$resultType}.");
859869
}
860870

861-
return $this->completeListValue($returnType, $fieldNodes, $info, $path, $result);
871+
return $this->completeListValue($returnType, $fieldNodes, $info, $path, $result, $contextValue);
862872
}
863873

864874
assert($returnType instanceof NamedType, 'Wrapping types should return early');
@@ -875,12 +885,12 @@ protected function completeValue(
875885
}
876886

877887
if ($returnType instanceof AbstractType) {
878-
return $this->completeAbstractValue($returnType, $fieldNodes, $info, $path, $result);
888+
return $this->completeAbstractValue($returnType, $fieldNodes, $info, $path, $result, $contextValue);
879889
}
880890

881891
// Field type must be and Object, Interface or Union and expect sub-selections.
882892
if ($returnType instanceof ObjectType) {
883-
return $this->completeObjectValue($returnType, $fieldNodes, $info, $path, $result);
893+
return $this->completeObjectValue($returnType, $fieldNodes, $info, $path, $result, $contextValue);
884894
}
885895

886896
$safeReturnType = Utils::printSafe($returnType);
@@ -949,17 +959,19 @@ function ($previous, $value) use ($callback) {
949959
* @param \ArrayObject<int, FieldNode> $fieldNodes
950960
* @param list<string|int> $path
951961
* @param iterable<mixed> $results
952-
*
953-
* @throws Error
962+
* @param mixed $contextValue
954963
*
955964
* @return array<mixed>|Promise|\stdClass
965+
*@throws Error
966+
*
956967
*/
957968
protected function completeListValue(
958969
ListOfType $returnType,
959970
\ArrayObject $fieldNodes,
960971
ResolveInfo $info,
961972
array $path,
962-
iterable &$results
973+
iterable &$results,
974+
$contextValue
963975
) {
964976
$itemType = $returnType->getWrappedType();
965977

@@ -970,7 +982,7 @@ protected function completeListValue(
970982
$fieldPath = [...$path, $i++];
971983
$info->path = $fieldPath;
972984

973-
$completedItem = $this->completeValueCatchingError($itemType, $fieldNodes, $info, $fieldPath, $item);
985+
$completedItem = $this->completeValueCatchingError($itemType, $fieldNodes, $info, $fieldPath, $item, $contextValue);
974986

975987
if (! $containsPromise && $this->getPromise($completedItem) !== null) {
976988
$containsPromise = true;
@@ -1016,6 +1028,7 @@ protected function completeLeafValue(LeafType $returnType, &$result)
10161028
* @param \ArrayObject<int, FieldNode> $fieldNodes
10171029
* @param array<string|int> $path
10181030
* @param array<mixed> $result
1031+
* @param mixed $contextValue
10191032
*
10201033
* @throws \Exception
10211034
* @throws Error
@@ -1028,7 +1041,8 @@ protected function completeAbstractValue(
10281041
\ArrayObject $fieldNodes,
10291042
ResolveInfo $info,
10301043
array $path,
1031-
&$result
1044+
&$result,
1045+
$contextValue
10321046
) {
10331047
$exeContext = $this->exeContext;
10341048
$typeCandidate = $returnType->resolveType($result, $exeContext->contextValue, $info);
@@ -1053,7 +1067,8 @@ protected function completeAbstractValue(
10531067
$fieldNodes,
10541068
$info,
10551069
$path,
1056-
$result
1070+
$result,
1071+
$contextValue
10571072
));
10581073
}
10591074

@@ -1067,7 +1082,8 @@ protected function completeAbstractValue(
10671082
$fieldNodes,
10681083
$info,
10691084
$path,
1070-
$result
1085+
$result,
1086+
$contextValue
10711087
);
10721088
}
10731089

@@ -1143,6 +1159,7 @@ protected function defaultTypeResolver($value, $contextValue, ResolveInfo $info,
11431159
* @param \ArrayObject<int, FieldNode> $fieldNodes
11441160
* @param array<string|int> $path
11451161
* @param mixed $result
1162+
* @param mixed $contextValue
11461163
*
11471164
* @throws \Exception
11481165
* @throws Error
@@ -1154,7 +1171,8 @@ protected function completeObjectValue(
11541171
\ArrayObject $fieldNodes,
11551172
ResolveInfo $info,
11561173
array $path,
1157-
&$result
1174+
&$result,
1175+
$contextValue
11581176
) {
11591177
// If there is an isTypeOf predicate function, call it with the
11601178
// current result. If isTypeOf returns false, then raise an error rather
@@ -1164,6 +1182,7 @@ protected function completeObjectValue(
11641182
$promise = $this->getPromise($isTypeOf);
11651183
if ($promise !== null) {
11661184
return $promise->then(function ($isTypeOfResult) use (
1185+
$contextValue,
11671186
$returnType,
11681187
$fieldNodes,
11691188
$path,
@@ -1177,7 +1196,8 @@ protected function completeObjectValue(
11771196
$returnType,
11781197
$fieldNodes,
11791198
$path,
1180-
$result
1199+
$result,
1200+
$contextValue
11811201
);
11821202
});
11831203
}
@@ -1192,7 +1212,8 @@ protected function completeObjectValue(
11921212
$returnType,
11931213
$fieldNodes,
11941214
$path,
1195-
$result
1215+
$result,
1216+
$contextValue
11961217
);
11971218
}
11981219

@@ -1217,6 +1238,7 @@ protected function invalidReturnTypeError(
12171238
* @param \ArrayObject<int, FieldNode> $fieldNodes
12181239
* @param array<string|int> $path
12191240
* @param mixed $result
1241+
* @param mixed $contextValue
12201242
*
12211243
* @throws \Exception
12221244
* @throws Error
@@ -1227,11 +1249,12 @@ protected function collectAndExecuteSubfields(
12271249
ObjectType $returnType,
12281250
\ArrayObject $fieldNodes,
12291251
array $path,
1230-
&$result
1252+
&$result,
1253+
$contextValue
12311254
) {
12321255
$subFieldNodes = $this->collectSubFields($returnType, $fieldNodes);
12331256

1234-
return $this->executeFields($returnType, $result, $path, $subFieldNodes);
1257+
return $this->executeFields($returnType, $result, $path, $subFieldNodes, $contextValue);
12351258
}
12361259

12371260
/**
@@ -1276,6 +1299,7 @@ protected function collectSubFields(ObjectType $returnType, \ArrayObject $fieldN
12761299
*
12771300
* @param mixed $rootValue
12781301
* @param array<string|int> $path
1302+
* @param mixed $contextValue
12791303
*
12801304
* @phpstan-param Fields $fields
12811305
*
@@ -1284,14 +1308,14 @@ protected function collectSubFields(ObjectType $returnType, \ArrayObject $fieldN
12841308
*
12851309
* @return Promise|\stdClass|array<mixed>
12861310
*/
1287-
protected function executeFields(ObjectType $parentType, $rootValue, array $path, \ArrayObject $fields)
1311+
protected function executeFields(ObjectType $parentType, $rootValue, array $path, \ArrayObject $fields, $contextValue)
12881312
{
12891313
$containsPromise = false;
12901314
$results = [];
12911315
foreach ($fields as $responseName => $fieldNodes) {
12921316
$fieldPath = $path;
12931317
$fieldPath[] = $responseName;
1294-
$result = $this->resolveField($parentType, $rootValue, $fieldNodes, $fieldPath);
1318+
$result = $this->resolveField($parentType, $rootValue, $fieldNodes, $fieldPath, $this->maybeScopeContext($contextValue));
12951319
if ($result === static::$UNDEFINED) {
12961320
continue;
12971321
}
@@ -1393,4 +1417,18 @@ protected function ensureValidRuntimeType(
13931417

13941418
return $runtimeType;
13951419
}
1420+
1421+
/**
1422+
* @param mixed $contextValue
1423+
*
1424+
* @return mixed
1425+
*/
1426+
private function maybeScopeContext($contextValue)
1427+
{
1428+
if ($contextValue instanceof ScopedContext) {
1429+
return clone $contextValue;
1430+
}
1431+
1432+
return $contextValue;
1433+
}
13961434
}

src/Executor/ScopedContext.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace GraphQL\Executor;
6+
7+
/**
8+
* This interface can be used to indicate that your context should be scoped.
9+
*
10+
* A scoped context will only delegate information to children.
11+
*/
12+
interface ScopedContext
13+
{
14+
}

0 commit comments

Comments
 (0)