@@ -147,10 +147,12 @@ private static function executeOperation(ExecutionContext $exeContext, Operation
147147 $ fields = self ::collectFields ($ exeContext , $ type , $ operation ->selectionSet , new \ArrayObject (), new \ArrayObject ());
148148
149149 if ($ operation ->operation === 'mutation ' ) {
150- return self ::executeFieldsSerially ($ exeContext , $ type , $ rootValue , $ fields ->getArrayCopy ());
150+ $ result = self ::executeFieldsSerially ($ exeContext , $ type , [$ rootValue ], $ fields );
151+ } else {
152+ $ result = self ::executeFields ($ exeContext , $ type , [$ rootValue ], $ fields );
151153 }
152154
153- return self :: executeFields ( $ exeContext , $ type , $ rootValue , $ fields ) ;
155+ return null === $ result || $ result === [] ? [] : $ result [ 0 ] ;
154156 }
155157
156158
@@ -187,30 +189,40 @@ private static function getOperationRootType(Schema $schema, OperationDefinition
187189 /**
188190 * Implements the "Evaluating selection sets" section of the spec
189191 * for "write" mode.
192+ *
193+ * @param ExecutionContext $exeContext
194+ * @param ObjectType $parentType
195+ * @param $sourceList
196+ * @param $sourceIsList
197+ * @param $fields
198+ * @return array
199+ * @throws Error
200+ * @throws \Exception
190201 */
191- private static function executeFieldsSerially (ExecutionContext $ exeContext , ObjectType $ parentType , $ sourceValue , $ fields )
202+ private static function executeFieldsSerially (ExecutionContext $ exeContext , ObjectType $ parentType , $ sourceList , $ fields )
192203 {
193204 $ results = [];
194205 foreach ($ fields as $ responseName => $ fieldASTs ) {
195- $ result = self ::resolveField ($ exeContext , $ parentType , $ sourceValue , $ fieldASTs );
196-
197- if ($ result !== self ::$ UNDEFINED ) {
198- // Undefined means that field is not defined in schema
199- $ results [$ responseName ] = $ result ;
200- }
206+ self ::resolveField ($ exeContext , $ parentType , $ sourceList , $ fieldASTs , $ responseName , $ results );
201207 }
208+
202209 return $ results ;
203210 }
204211
205212 /**
206213 * Implements the "Evaluating selection sets" section of the spec
207214 * for "read" mode.
215+ * @param ExecutionContext $exeContext
216+ * @param ObjectType $parentType
217+ * @param $sourceList
218+ * @param $fields
219+ * @return array
208220 */
209- private static function executeFields (ExecutionContext $ exeContext , ObjectType $ parentType , $ source , $ fields )
221+ private static function executeFields (ExecutionContext $ exeContext , ObjectType $ parentType , $ sourceList , $ fields )
210222 {
211223 // Native PHP doesn't support promises.
212224 // Custom executor should be built for platforms like ReactPHP
213- return self ::executeFieldsSerially ($ exeContext , $ parentType , $ source , $ fields );
225+ return self ::executeFieldsSerially ($ exeContext , $ parentType , $ sourceList , $ fields );
214226 }
215227
216228
@@ -345,32 +357,33 @@ private static function getFieldEntryKey(Field $node)
345357 }
346358
347359 /**
348- * Resolves the field on the given source object. In particular, this
349- * figures out the value that the field returns by calling its resolve function,
350- * then calls completeValue to complete promises, serialize scalars, or execute
351- * the sub-selection-set for objects.
360+ * Given list of parent type values returns corresponding list of field values
361+ *
362+ * In particular, this
363+ * figures out the value that the field returns by calling its `resolve` or `map` function,
364+ * then calls `completeValue` on each value to serialize scalars, or execute the sub-selection-set
365+ * for objects.
366+ *
367+ * @param ExecutionContext $exeContext
368+ * @param ObjectType $parentType
369+ * @param $sourceValueList
370+ * @param $fieldASTs
371+ * @return array
372+ * @throws Error
352373 */
353- private static function resolveField (ExecutionContext $ exeContext , ObjectType $ parentType , $ source , $ fieldASTs )
374+ private static function resolveField (ExecutionContext $ exeContext , ObjectType $ parentType , $ sourceValueList , $ fieldASTs, $ responseName , & $ resolveResult )
354375 {
355376 $ fieldAST = $ fieldASTs [0 ];
356377 $ fieldName = $ fieldAST ->name ->value ;
357378
358379 $ fieldDef = self ::getFieldDef ($ exeContext ->schema , $ parentType , $ fieldName );
359380
360381 if (!$ fieldDef ) {
361- return self :: $ UNDEFINED ;
382+ return ;
362383 }
363384
364385 $ returnType = $ fieldDef ->getType ();
365386
366- if (isset ($ fieldDef ->resolve )) {
367- $ resolveFn = $ fieldDef ->resolve ;
368- } else if (isset ($ parentType ->resolveField )) {
369- $ resolveFn = $ parentType ->resolveField ;
370- } else {
371- $ resolveFn = self ::$ defaultResolveFn ;
372- }
373-
374387 // Build hash of arguments from the field.arguments AST, using the
375388 // variables scope to fulfill any variable references.
376389 // TODO: find a way to memoize, in case this field is within a List type.
@@ -394,30 +407,74 @@ private static function resolveField(ExecutionContext $exeContext, ObjectType $p
394407 'variableValues ' => $ exeContext ->variableValues ,
395408 ]);
396409
397- // If an error occurs while calling the field `resolve` function, ensure that
410+ $ mapFn = $ fieldDef ->mapFn ;
411+
412+ // If an error occurs while calling the field `map` or `resolve` function, ensure that
398413 // it is wrapped as a GraphQLError with locations. Log this error and return
399414 // null if allowed, otherwise throw the error so the parent field can handle
400415 // it.
401- try {
402- $ result = call_user_func ($ resolveFn , $ source , $ args , $ info );
403- } catch (\Exception $ error ) {
404- $ reportedError = Error::createLocatedError ($ error , $ fieldASTs );
416+ if ($ mapFn ) {
417+ try {
418+ $ mapped = call_user_func ($ mapFn , $ sourceValueList , $ args , $ info );
419+
420+ Utils::invariant (
421+ is_array ($ mapped ) && count ($ mapped ) === count ($ sourceValueList ),
422+ "Function `map` of $ parentType. $ fieldName is expected to return array " .
423+ "with exact same number of items as list being mapped (first argument of `map`) "
424+ );
425+
426+ } catch (\Exception $ error ) {
427+ $ reportedError = Error::createLocatedError ($ error , $ fieldASTs );
428+
429+ if ($ returnType instanceof NonNull) {
430+ throw $ reportedError ;
431+ }
405432
406- if ( $ returnType instanceof NonNull) {
407- throw $ reportedError ;
433+ $ exeContext -> addError ( $ reportedError );
434+ return null ;
408435 }
409436
410- $ exeContext ->addError ($ reportedError );
411- return null ;
412- }
437+ foreach ($ mapped as $ index => $ value ) {
438+ $ resolveResult [$ index ][$ responseName ] = self ::completeValueCatchingError (
439+ $ exeContext ,
440+ $ returnType ,
441+ $ fieldASTs ,
442+ $ info ,
443+ $ value
444+ );
445+ }
446+ } else {
447+ if (isset ($ fieldDef ->resolveFn )) {
448+ $ resolveFn = $ fieldDef ->resolveFn ;
449+ } else if (isset ($ parentType ->resolveFieldFn )) {
450+ $ resolveFn = $ parentType ->resolveFieldFn ;
451+ } else {
452+ $ resolveFn = self ::$ defaultResolveFn ;
453+ }
413454
414- return self ::completeValueCatchingError (
415- $ exeContext ,
416- $ returnType ,
417- $ fieldASTs ,
418- $ info ,
419- $ result
420- );
455+ foreach ($ sourceValueList as $ index => $ value ) {
456+ try {
457+ $ resolved = call_user_func ($ resolveFn , $ value , $ args , $ info );
458+ } catch (\Exception $ error ) {
459+ $ reportedError = Error::createLocatedError ($ error , $ fieldASTs );
460+
461+ if ($ returnType instanceof NonNull) {
462+ throw $ reportedError ;
463+ }
464+
465+ $ exeContext ->addError ($ reportedError );
466+ $ resolved = null ;
467+ }
468+
469+ $ resolveResult [$ index ][$ responseName ] = self ::completeValueCatchingError (
470+ $ exeContext ,
471+ $ returnType ,
472+ $ fieldASTs ,
473+ $ info ,
474+ $ resolved
475+ );
476+ }
477+ }
421478 }
422479
423480
@@ -489,32 +546,112 @@ private static function completeValue(ExecutionContext $exeContext, Type $return
489546 return null ;
490547 }
491548
492- // If field type is List, complete each item in the list with the inner type
549+ // If field type is Scalar or Enum, serialize to a valid value, returning
550+ // null if serialization is not possible.
551+ if ($ returnType instanceof ScalarType ||
552+ $ returnType instanceof EnumType) {
553+ return $ returnType ->serialize ($ result );
554+ }
555+
556+ // If field type is List, and return type is Composite - complete by executing these fields with list value as parameter
493557 if ($ returnType instanceof ListOfType) {
494558 $ itemType = $ returnType ->getWrappedType ();
559+
495560 Utils::invariant (
496561 is_array ($ result ) || $ result instanceof \Traversable,
497562 'User Error: expected iterable, but did not find one. '
498563 );
499564
500- $ tmp = [];
501- foreach ($ result as $ item ) {
502- $ tmp [] = self ::completeValueCatchingError ($ exeContext , $ itemType , $ fieldASTs , $ info , $ item );
565+ // For Object[]:
566+ // Allow all object fields to process list value in it's `map` callback:
567+ if ($ itemType instanceof ObjectType) {
568+ // Filter out nulls (as `map` doesn't expect it):
569+ $ list = [];
570+ foreach ($ result as $ index => $ item ) {
571+ if (null !== $ item ) {
572+ $ list [] = $ item ;
573+ }
574+ }
575+
576+ $ subFieldASTs = self ::collectSubFields ($ exeContext , $ itemType , $ fieldASTs );
577+ $ mapped = self ::executeFields ($ exeContext , $ itemType , $ list , $ subFieldASTs );
578+
579+ $ i = 0 ;
580+ $ completed = [];
581+ foreach ($ result as $ index => $ item ) {
582+ if (null === $ item ) {
583+ // Complete nulls separately
584+ $ completed [] = self ::completeValueCatchingError ($ exeContext , $ itemType , $ fieldASTs , $ info , $ item );
585+ } else {
586+ // Assuming same order of mapped values
587+ $ completed [] = $ mapped [$ i ++];
588+ }
589+ }
590+ return $ completed ;
591+
592+ } else if ($ itemType instanceof AbstractType) {
593+
594+ // Values sharded by ObjectType
595+ $ listPerObjectType = [];
596+
597+ // Helper structures to restore ordering after resolve calls
598+ $ resultTypeMap = [];
599+ $ typeNameMap = [];
600+ $ cursors = [];
601+
602+ foreach ($ result as $ index => $ item ) {
603+ if (null !== $ item ) {
604+ $ objectType = $ itemType ->getObjectType ($ item , $ info );
605+
606+ if ($ objectType && !$ itemType ->isPossibleType ($ objectType )) {
607+ $ exeContext ->addError (new Error (
608+ "Runtime Object type \"$ objectType \" is not a possible type for \"$ itemType \". "
609+ ));
610+ $ result [$ index ] = null ;
611+ } else {
612+ $ listPerObjectType [$ objectType ->name ][] = $ item ;
613+ $ resultTypeMap [$ index ] = $ objectType ->name ;
614+ $ typeNameMap [$ objectType ->name ] = $ objectType ;
615+ }
616+ }
617+ }
618+
619+ $ mapped = [];
620+ foreach ($ listPerObjectType as $ typeName => $ list ) {
621+ $ objectType = $ typeNameMap [$ typeName ];
622+ $ subFieldASTs = self ::collectSubFields ($ exeContext , $ objectType , $ fieldASTs );
623+ $ mapped [$ typeName ] = self ::executeFields ($ exeContext , $ objectType , $ list , $ subFieldASTs );
624+ $ cursors [$ typeName ] = 0 ;
625+ }
626+
627+ // Restore order:
628+ $ completed = [];
629+ foreach ($ result as $ index => $ item ) {
630+ if (null === $ item ) {
631+ // Complete nulls separately
632+ $ completed [] = self ::completeValueCatchingError ($ exeContext , $ itemType , $ fieldASTs , $ info , $ item );
633+ } else {
634+ $ typeName = $ resultTypeMap [$ index ];
635+ $ completed [] = $ mapped [$ typeName ][$ cursors [$ typeName ]++];
636+ }
637+ }
638+
639+ return $ completed ;
640+ } else {
641+
642+ // For simple lists:
643+ $ tmp = [];
644+ foreach ($ result as $ item ) {
645+ $ tmp [] = self ::completeValueCatchingError ($ exeContext , $ itemType , $ fieldASTs , $ info , $ item );
646+ }
647+ return $ tmp ;
503648 }
504- return $ tmp ;
505- }
506649
507- // If field type is Scalar or Enum, serialize to a valid value, returning
508- // null if serialization is not possible.
509- if ($ returnType instanceof ScalarType ||
510- $ returnType instanceof EnumType) {
511- Utils::invariant (method_exists ($ returnType , 'serialize ' ), 'Missing serialize method on type ' );
512- return $ returnType ->serialize ($ result );
513650 }
514651
515- // Field type must be Object, Interface or Union and expect sub-selections.
516652 if ($ returnType instanceof ObjectType) {
517653 $ objectType = $ returnType ;
654+
518655 } else if ($ returnType instanceof AbstractType) {
519656 $ objectType = $ returnType ->getObjectType ($ result , $ info );
520657
@@ -542,6 +679,18 @@ private static function completeValue(ExecutionContext $exeContext, Type $return
542679 }
543680
544681 // Collect sub-fields to execute to complete this value.
682+ $ subFieldASTs = self ::collectSubFields ($ exeContext , $ objectType , $ fieldASTs );
683+ return self ::executeFields ($ exeContext , $ objectType , [$ result ], $ subFieldASTs )[0 ];
684+ }
685+
686+ /**
687+ * @param ExecutionContext $exeContext
688+ * @param ObjectType $objectType
689+ * @param $fieldASTs
690+ * @return \ArrayObject
691+ */
692+ private static function collectSubFields (ExecutionContext $ exeContext , ObjectType $ objectType , $ fieldASTs )
693+ {
545694 $ subFieldASTs = new \ArrayObject ();
546695 $ visitedFragmentNames = new \ArrayObject ();
547696 for ($ i = 0 ; $ i < count ($ fieldASTs ); $ i ++) {
@@ -556,11 +705,9 @@ private static function completeValue(ExecutionContext $exeContext, Type $return
556705 );
557706 }
558707 }
559-
560- return self ::executeFields ($ exeContext , $ objectType , $ result , $ subFieldASTs );
708+ return $ subFieldASTs ;
561709 }
562710
563-
564711 /**
565712 * If a resolve function is not given, then a default resolve behavior is used
566713 * which takes the property of the source object of the same name as the field
0 commit comments