Skip to content

Commit c8848e0

Browse files
committed
Defer promise completion
1 parent 01fe2c6 commit c8848e0

File tree

3 files changed

+125
-34
lines changed

3 files changed

+125
-34
lines changed

src/Executor/Executor.php

Lines changed: 78 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -171,10 +171,13 @@ private static function executeOperation(ExecutionContext $exeContext, Operation
171171

172172
$path = [];
173173
if ($operation->operation === 'mutation') {
174-
return self::executeFieldsSerially($exeContext, $type, $rootValue, $path, $fields);
174+
$results = self::executeFieldsSerially($exeContext, $type, $rootValue, $path, $fields);
175+
} else {
176+
$results = self::executeFields($exeContext, $type, $rootValue, $path, $fields);
175177
}
178+
$finalResults = self::completePromiseIfNeeded($results);
176179

177-
return self::executeFields($exeContext, $type, $rootValue, $path, $fields);
180+
return $finalResults;
178181
}
179182

180183

@@ -223,18 +226,34 @@ private static function getOperationRootType(Schema $schema, OperationDefinition
223226
*/
224227
private static function executeFieldsSerially(ExecutionContext $exeContext, ObjectType $parentType, $sourceValue, $path, $fields)
225228
{
229+
if (self::isThenable($sourceValue)) {
230+
return $sourceValue->then(function($resolvedSourceValue) use ($exeContext, $parentType, $path, $fields) {
231+
return self::executeFieldsSerially($exeContext, $parentType, $resolvedSourceValue, $path, $fields);
232+
});
233+
}
234+
226235
$results = [];
227-
$sourceValue = self::completePromiseIfNeeded($sourceValue);
228236

229-
foreach ($fields as $responseName => $fieldASTs) {
237+
$process = function ($responseName, $fieldASTs, $results) use ($path, $exeContext, $parentType, $sourceValue) {
230238
$fieldPath = $path;
231239
$fieldPath[] = $responseName;
232240
$result = self::resolveField($exeContext, $parentType, $sourceValue, $fieldASTs, $fieldPath);
233241

234-
if ($result !== self::$UNDEFINED) {
235-
$result = self::completePromiseIfNeeded($result);
236-
// Undefined means that field is not defined in schema
237-
$results[$responseName] = $result;
242+
// Undefined means that field is not defined in schema
243+
if ($result === self::$UNDEFINED) {
244+
return $results;
245+
}
246+
$results[$responseName] = $result;
247+
return $results;
248+
};
249+
250+
foreach ($fields as $responseName => $fieldASTs) {
251+
if (self::isThenable($results)) {
252+
$results = $results->then(function ($resolvedResults) use ($responseName, $fieldASTs, $process) {
253+
return $process($responseName, $fieldASTs, $resolvedResults);
254+
});
255+
} else {
256+
$results = $process($responseName, $fieldASTs, $results);
238257
}
239258
}
240259
// see #59
@@ -250,12 +269,9 @@ private static function executeFieldsSerially(ExecutionContext $exeContext, Obje
250269
*/
251270
private static function executeFields(ExecutionContext $exeContext, ObjectType $parentType, $source, $path, $fields)
252271
{
253-
// Native PHP doesn't support promises.
254-
// Custom executor should be built for platforms like ReactPHP
255272
return self::executeFieldsSerially($exeContext, $parentType, $source, $path, $fields);
256273
}
257274

258-
259275
/**
260276
* Given a selectionSet, adds all of the fields in that selection to
261277
* the passed in map of fields, and returns it at the end.
@@ -454,8 +470,6 @@ private static function resolveField(ExecutionContext $exeContext, ObjectType $p
454470
// Get the resolve function, regardless of if its result is normal
455471
// or abrupt (error).
456472
$result = self::resolveOrError($resolveFn, $source, $args, $context, $info);
457-
$result = self::completePromiseIfNeeded($result);
458-
459473
$result = self::completeValueCatchingError(
460474
$exeContext,
461475
$returnType,
@@ -516,14 +530,26 @@ private static function completeValueCatchingError(
516530
// Otherwise, error protection is applied, logging the error and resolving
517531
// a null value for this field if one is encountered.
518532
try {
519-
return self::completeValueWithLocatedError(
533+
$completed = self::completeValueWithLocatedError(
520534
$exeContext,
521535
$returnType,
522536
$fieldASTs,
523537
$info,
524538
$path,
525539
$result
526540
);
541+
if (self::isThenable($completed)) {
542+
// If `completeValueWithLocatedError` returned a rejected promise, log
543+
// the rejection error and resolve to null.
544+
// Note: we don't rely on a `catch` method, but we do expect "thenable"
545+
// to take a second callback for the error case.
546+
return $completed->then(null, function ($err) use ($exeContext) {
547+
$exeContext->addError($err);
548+
return null;
549+
});
550+
}
551+
552+
return $completed;
527553
} catch (Error $err) {
528554
// If `completeValueWithLocatedError` returned abruptly (threw an error), log the error
529555
// and return null.
@@ -532,7 +558,6 @@ private static function completeValueCatchingError(
532558
}
533559
}
534560

535-
536561
/**
537562
* This is a small wrapper around completeValue which annotates errors with
538563
* location information.
@@ -546,7 +571,7 @@ private static function completeValueCatchingError(
546571
* @return array|null
547572
* @throws Error
548573
*/
549-
static function completeValueWithLocatedError(
574+
public static function completeValueWithLocatedError(
550575
ExecutionContext $exeContext,
551576
Type $returnType,
552577
$fieldASTs,
@@ -556,14 +581,20 @@ static function completeValueWithLocatedError(
556581
)
557582
{
558583
try {
559-
return self::completeValue(
584+
$completed = self::completeValue(
560585
$exeContext,
561586
$returnType,
562587
$fieldASTs,
563588
$info,
564589
$path,
565590
$result
566591
);
592+
if (self::isThenable($completed)) {
593+
return $completed->then(null, function ($error) use ($fieldASTs, $path) {
594+
throw Error::createLocatedError($error, $fieldASTs, $path);
595+
});
596+
}
597+
return $completed;
567598
} catch (\Exception $error) {
568599
throw Error::createLocatedError($error, $fieldASTs, $path);
569600
}
@@ -609,6 +640,20 @@ private static function completeValue(
609640
&$result
610641
)
611642
{
643+
// If result is a Promise, apply-lift over completeValue.
644+
if (self::isThenable($result)) {
645+
return $result->then(function ($resolved) use ($exeContext, $returnType, $fieldASTs, $info, $path) {
646+
return self::completeValue(
647+
$exeContext,
648+
$returnType,
649+
$fieldASTs,
650+
$info,
651+
$path,
652+
$resolved
653+
);
654+
});
655+
}
656+
612657
if ($result instanceof \Exception) {
613658
throw $result;
614659
}
@@ -879,12 +924,24 @@ private static function inferTypeOf($value, $context, ResolveInfo $info, Abstrac
879924
return null;
880925
}
881926

882-
private static function completePromiseIfNeeded($result)
927+
private static function isThenable($value)
883928
{
884-
if ($result instanceof PromiseInterface) {
885-
$result = $result->wait();
929+
return ($value instanceof PromiseInterface);
930+
}
931+
932+
private static function completePromiseIfNeeded($value)
933+
{
934+
if (self::isThenable($value)) {
935+
$results = self::completePromiseIfNeeded($value->wait());
936+
return $results;
937+
} elseif (is_array($value) || $value instanceof \Traversable) {
938+
$results = [];
939+
foreach ($value as $key => $item) {
940+
$results[$key] = self::completePromiseIfNeeded($item);
941+
}
942+
return $results;
886943
}
887944

888-
return $result;
945+
return $value;
889946
}
890947
}

src/Promise/PromiseWrapper.php

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public function wait()
4545
* @param callable $onFulfilled Invoked when the promise fulfills
4646
* @param callable $onRejected Invoked when the promise is rejected
4747
*
48-
* @return $this
48+
* @return self
4949
*
5050
* @throws \LogicException if the promise has no then function.
5151
*/
@@ -54,12 +54,15 @@ public function then(
5454
callable $onRejected = null
5555
)
5656
{
57+
if (!$this->getWrappedPromise()) {
58+
throw new \LogicException('No wrapped promise found!');
59+
}
60+
5761
if (null === $onFulfilled && null === $onRejected) {
5862
return $this;
5963
}
60-
$newWrappedPromise = $this->getWrappedPromise()->then($onFulfilled, $onRejected);
6164

62-
return $this->setWrappedPromise($newWrappedPromise);
65+
return $this->wrap($this->getWrappedPromise()->then($onFulfilled, $onRejected));
6366
}
6467

6568
public function getWrappedPromise()

tests/Promise/CliPromise.php

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ class CliPromise
55
{
66
private $pid;
77
private $outputFile;
8+
private $onFulfilledCallbacks = [];
9+
private $onRejectedCallbacks = [];
810

911
public function __construct($cmd)
1012
{
@@ -18,18 +20,22 @@ public function __construct($cmd)
1820

1921
public function wait()
2022
{
21-
while (!$this->isTerminated()) { usleep(1000); }
22-
23-
$log = new \stdClass();
24-
$log->pid = $this->pid;
25-
$log->output = shell_exec(sprintf('cat %s', $this->outputFile));
23+
try {
24+
while (!$this->isTerminated()) { usleep(1000); }
2625

27-
return $log;
28-
}
26+
$log = new \stdClass();
27+
$log->pid = $this->pid;
28+
$log->output = shell_exec(sprintf('cat %s', $this->outputFile));
2929

30-
public function then()
31-
{
32-
return $this;
30+
foreach ($this->onFulfilledCallbacks as $onFulfilled) {
31+
$log = call_user_func($onFulfilled, $log);
32+
}
33+
return $log;
34+
} catch (\Exception $e) {
35+
foreach ($this->onRejectedCallbacks as $onRejected) {
36+
call_user_func($onRejected, $e);
37+
}
38+
}
3339
}
3440

3541
private function isTerminated()
@@ -42,4 +48,29 @@ private function isTerminated()
4248

4349
return false;
4450
}
51+
52+
/**
53+
* Appends fulfillment and rejection handlers to the promise, and returns
54+
* a new promise resolving to the return value of the called handler.
55+
*
56+
* @param callable $onFulfilled Invoked when the promise fulfills
57+
* @param callable $onRejected Invoked when the promise is rejected
58+
*
59+
* @return $this
60+
*/
61+
public function then(
62+
callable $onFulfilled = null,
63+
callable $onRejected = null
64+
)
65+
{
66+
if (null !== $onFulfilled) {
67+
$this->onFulfilledCallbacks[] = $onFulfilled;
68+
}
69+
70+
if (null !== $onFulfilled) {
71+
$this->onRejectedCallbacks[] = $onRejected;
72+
}
73+
74+
return $this;
75+
}
4576
}

0 commit comments

Comments
 (0)