From e97ca7f971662de1973c67e7f04c56f68a6d330f Mon Sep 17 00:00:00 2001 From: vladar Date: Sat, 3 Dec 2016 02:14:14 +0700 Subject: [PATCH] Execution: added SyncPromiseAdapter and made it default for Executor (+removed GenericPromiseAdapter) --- src/Deferred.php | 53 +++++ src/Executor/Executor.php | 8 +- .../Promise/Adapter/GenericPromiseAdapter.php | 57 ----- src/Executor/Promise/Adapter/SyncPromise.php | 21 +- .../Promise/Adapter/SyncPromiseAdapter.php | 146 +++++++++++++ src/Utils.php | 3 + .../Promise/SyncPromiseAdapterTest.php | 203 ++++++++++++++++++ tests/Executor/Promise/SyncPromiseTest.php | 6 +- 8 files changed, 424 insertions(+), 73 deletions(-) create mode 100644 src/Deferred.php delete mode 100644 src/Executor/Promise/Adapter/GenericPromiseAdapter.php create mode 100644 src/Executor/Promise/Adapter/SyncPromiseAdapter.php create mode 100644 tests/Executor/Promise/SyncPromiseAdapterTest.php diff --git a/src/Deferred.php b/src/Deferred.php new file mode 100644 index 0000000..2fc6c1d --- /dev/null +++ b/src/Deferred.php @@ -0,0 +1,53 @@ +isEmpty()) { + /** @var self $dfd */ + $dfd = $q->dequeue(); + $dfd->run(); + } + } + + public function __construct(callable $callback) + { + $this->callback = $callback; + $this->promise = new SyncPromise(); + self::getQueue()->enqueue($this); + } + + public function then($onFulfilled = null, $onRejected = null) + { + return $this->promise->then($onFulfilled, $onRejected); + } + + private function run() + { + try { + $cb = $this->callback; + $this->promise->resolve($cb()); + } catch (\Exception $e) { + $this->promise->reject($e); + } + } +} diff --git a/src/Executor/Executor.php b/src/Executor/Executor.php index b601102..eba8b76 100644 --- a/src/Executor/Executor.php +++ b/src/Executor/Executor.php @@ -3,6 +3,7 @@ namespace GraphQL\Executor; use GraphQL\Error\Error; use GraphQL\Error\InvariantViolation; +use GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter; use GraphQL\Executor\Promise\Promise; use GraphQL\Language\AST\DocumentNode; use GraphQL\Language\AST\FieldNode; @@ -10,7 +11,6 @@ use GraphQL\Language\AST\FragmentDefinitionNode; use GraphQL\Language\AST\NodeKind; use GraphQL\Language\AST\OperationDefinitionNode; use GraphQL\Language\AST\SelectionSetNode; -use GraphQL\Executor\Promise\Adapter\GenericPromiseAdapter; use GraphQL\Executor\Promise\PromiseAdapter; use GraphQL\Schema; use GraphQL\Type\Definition\AbstractType; @@ -68,7 +68,7 @@ class Executor */ public static function getPromiseAdapter() { - return self::$promiseAdapter ?: (self::$promiseAdapter = new GenericPromiseAdapter()); + return self::$promiseAdapter ?: (self::$promiseAdapter = new SyncPromiseAdapter()); } /** @@ -112,6 +112,10 @@ class Executor $executor = new self($exeContext, $promiseAdapter); $result = $executor->executeQuery(); + if ($result instanceof Promise && $promiseAdapter instanceof SyncPromiseAdapter) { + $result = $promiseAdapter->wait($result); + } + return $result; } diff --git a/src/Executor/Promise/Adapter/GenericPromiseAdapter.php b/src/Executor/Promise/Adapter/GenericPromiseAdapter.php deleted file mode 100644 index 0a4d79a..0000000 --- a/src/Executor/Promise/Adapter/GenericPromiseAdapter.php +++ /dev/null @@ -1,57 +0,0 @@ -createResolvedPromise($promise); - } catch (\Exception $e) { - if (null !== $onRejected) { - $onRejected($e); - } - return $this->createRejectedPromise($e); - } - } - - public function createPromise(callable $resolver) - { - return $resolver(function ($value) { - return $value; - }); - } - - public function createResolvedPromise($promiseOrValue = null) - { - return $promiseOrValue; - } - - public function createRejectedPromise($reason) - { - } - - public function createPromiseAll($promisesOrValues) - { - } -} diff --git a/src/Executor/Promise/Adapter/SyncPromise.php b/src/Executor/Promise/Adapter/SyncPromise.php index 65d3111..8b8f6e7 100644 --- a/src/Executor/Promise/Adapter/SyncPromise.php +++ b/src/Executor/Promise/Adapter/SyncPromise.php @@ -22,10 +22,16 @@ class SyncPromise */ public static $queue; + public static function getQueue() + { + return self::$queue ?: self::$queue = new \SplQueue(); + } + public static function runQueue() { - while (self::$queue && !self::$queue->isEmpty()) { - $task = self::$queue->dequeue(); + $q = self::getQueue(); + while (!$q->isEmpty()) { + $task = $q->dequeue(); $task(); } } @@ -40,13 +46,6 @@ class SyncPromise */ private $waiting = []; - public function __construct() - { - if (!self::$queue) { - self::$queue = new \SplQueue(); - } - } - public function reject(\Exception $reason) { switch ($this->state) { @@ -73,7 +72,7 @@ class SyncPromise if ($value === $this) { throw new \Exception("Cannot resolve promise with self"); } - if ($value instanceof self) { + if (is_object($value) && method_exists($value, 'then')) { $value->then( function($resolvedValue) { $this->resolve($resolvedValue); @@ -123,7 +122,7 @@ class SyncPromise Utils::invariant($this->state !== self::PENDING, 'Cannot enqueue derived promises when parent is still pending'); foreach ($this->waiting as $descriptor) { - self::$queue->enqueue(function () use ($descriptor) { + self::getQueue()->enqueue(function () use ($descriptor) { /** @var $promise self */ list($promise, $onFulfilled, $onRejected) = $descriptor; diff --git a/src/Executor/Promise/Adapter/SyncPromiseAdapter.php b/src/Executor/Promise/Adapter/SyncPromiseAdapter.php new file mode 100644 index 0000000..08100d4 --- /dev/null +++ b/src/Executor/Promise/Adapter/SyncPromiseAdapter.php @@ -0,0 +1,146 @@ +promise, $this); + } + + /** + * @inheritdoc + */ + public function then(Promise $promise, callable $onFulfilled = null, callable $onRejected = null) + { + /** @var SyncPromise $promise */ + $promise = $promise->adoptedPromise; + return new Promise($promise->then($onFulfilled, $onRejected), $this); + } + + /** + * @inheritdoc + */ + public function createPromise(callable $resolver) + { + $promise = new SyncPromise(); + + $resolver( + [$promise, 'resolve'], + [$promise, 'reject'] + ); + + return new Promise($promise, $this); + } + + /** + * @inheritdoc + */ + public function createResolvedPromise($value = null) + { + $promise = new SyncPromise(); + return new Promise($promise->resolve($value), $this); + } + + /** + * @inheritdoc + */ + public function createRejectedPromise(\Exception $reason) + { + $promise = new SyncPromise(); + return new Promise($promise->reject($reason), $this); + } + + /** + * @inheritdoc + */ + public function createPromiseAll(array $promisesOrValues) + { + $all = new SyncPromise(); + + $total = count($promisesOrValues); + $count = 0; + $result = []; + + foreach ($promisesOrValues as $index => $promiseOrValue) { + if ($promiseOrValue instanceof Promise) { + $promiseOrValue->then( + function($value) use ($index, &$count, $total, &$result, $all) { + $result[$index] = $value; + $count++; + if ($count >= $total) { + $all->resolve($result); + } + }, + [$all, 'reject'] + ); + } else { + $result[$index] = $promiseOrValue; + $count++; + } + } + if ($count === $total) { + $all->resolve($result); + } + return new Promise($all, $this); + } + + public function wait(Promise $promise) + { + $fulfilledValue = null; + $rejectedReason = null; + + $promise->then( + function ($value) use (&$fulfilledValue) { + $fulfilledValue = $value; + }, + function ($reason) use (&$rejectedReason) { + $rejectedReason = $reason; + } + ); + + while ( + $promise->adoptedPromise->state === SyncPromise::PENDING && + !(Deferred::getQueue()->isEmpty() && SyncPromise::getQueue()->isEmpty()) + ) { + Deferred::runQueue(); + SyncPromise::runQueue(); + } + + if ($promise->adoptedPromise->state === SyncPromise::PENDING) { + throw new InvariantViolation("Could not resolve promise"); + } + if ($rejectedReason instanceof \Exception) { + throw $rejectedReason; + } + return $fulfilledValue; + } +} diff --git a/src/Utils.php b/src/Utils.php index 903ffc9..358e6ae 100644 --- a/src/Utils.php +++ b/src/Utils.php @@ -242,6 +242,9 @@ class Utils if (is_object($var)) { return 'instance of ' . get_class($var); } + if ('' === $var) { + return '(empty string)'; + } if (is_scalar($var)) { return (string) $var; } diff --git a/tests/Executor/Promise/SyncPromiseAdapterTest.php b/tests/Executor/Promise/SyncPromiseAdapterTest.php new file mode 100644 index 0000000..863c146 --- /dev/null +++ b/tests/Executor/Promise/SyncPromiseAdapterTest.php @@ -0,0 +1,203 @@ +promises = new SyncPromiseAdapter(); + } + + public function testIsThenable() + { + $this->assertEquals(true, $this->promises->isThenable(new Deferred(function() {}))); + $this->assertEquals(false, $this->promises->isThenable(false)); + $this->assertEquals(false, $this->promises->isThenable(true)); + $this->assertEquals(false, $this->promises->isThenable(1)); + $this->assertEquals(false, $this->promises->isThenable(0)); + $this->assertEquals(false, $this->promises->isThenable('test')); + $this->assertEquals(false, $this->promises->isThenable('')); + $this->assertEquals(false, $this->promises->isThenable([])); + $this->assertEquals(false, $this->promises->isThenable(new \stdClass())); + } + + public function testConvert() + { + $dfd = new Deferred(function() {}); + $result = $this->promises->convert($dfd); + + $this->assertInstanceOf('GraphQL\Executor\Promise\Promise', $result); + $this->assertInstanceOf('GraphQL\Executor\Promise\Adapter\SyncPromise', $result->adoptedPromise); + + try { + $this->promises->convert(''); + $this->fail('Expected exception no thrown'); + } catch (InvariantViolation $e) { + $this->assertEquals('Expected instance of GraphQL\Deferred, got (empty string)', $e->getMessage()); + } + } + + public function testThen() + { + $dfd = new Deferred(function() {}); + $promise = $this->promises->convert($dfd); + + $result = $this->promises->then($promise); + + $this->assertInstanceOf('GraphQL\Executor\Promise\Promise', $result); + $this->assertInstanceOf('GraphQL\Executor\Promise\Adapter\SyncPromise', $result->adoptedPromise); + } + + public function testCreatePromise() + { + $promise = $this->promises->createPromise(function($resolve, $reject) {}); + + $this->assertInstanceOf('GraphQL\Executor\Promise\Promise', $promise); + $this->assertInstanceOf('GraphQL\Executor\Promise\Adapter\SyncPromise', $promise->adoptedPromise); + + $promise = $this->promises->createPromise(function($resolve, $reject) { + $resolve('A'); + }); + + $this->assertValidPromise($promise, null, 'A', SyncPromise::FULFILLED); + } + + public function testCreateFulfilledPromise() + { + $promise = $this->promises->createResolvedPromise('test'); + $this->assertValidPromise($promise, null, 'test', SyncPromise::FULFILLED); + } + + public function testCreateRejectedPromise() + { + $promise = $this->promises->createRejectedPromise(new \Exception('test reason')); + $this->assertValidPromise($promise, 'test reason', null, SyncPromise::REJECTED); + } + + public function testCreatePromiseAll() + { + $promise = $this->promises->createPromiseAll([]); + $this->assertValidPromise($promise, null, [], SyncPromise::FULFILLED); + + $promise = $this->promises->createPromiseAll(['1']); + $this->assertValidPromise($promise, null, ['1'], SyncPromise::FULFILLED); + + $promise1 = new SyncPromise(); + $promise2 = new SyncPromise(); + $promise3 = $promise2->then( + function($value) { + return $value .'-value3'; + } + ); + + $data = [ + '1', + new Promise($promise1, $this->promises), + new Promise($promise2, $this->promises), + 3, + new Promise($promise3, $this->promises), + [] + ]; + + $promise = $this->promises->createPromiseAll($data); + $this->assertValidPromise($promise, null, null, SyncPromise::PENDING); + + $promise1->resolve('value1'); + $this->assertValidPromise($promise, null, null, SyncPromise::PENDING); + $promise2->resolve('value2'); + $this->assertValidPromise($promise, null, ['1', 'value1', 'value2', 3, 'value2-value3', []], SyncPromise::FULFILLED); + } + + public function testWait() + { + $called = []; + + $deferred1 = new Deferred(function() use (&$called) { + $called[] = 1; + return 1; + }); + $deferred2 = new Deferred(function() use (&$called) { + $called[] = 2; + return 2; + }); + + $p1 = $this->promises->convert($deferred1); + $p2 = $this->promises->convert($deferred2); + + $p3 = $p2->then(function() use (&$called) { + $dfd = new Deferred(function() use (&$called) { + $called[] = 3; + return 3; + }); + return $this->promises->convert($dfd); + }); + + $p4 = $p3->then(function() use (&$called) { + return new Deferred(function() use (&$called) { + $called[] = 4; + return 4; + }); + }); + + $all = $this->promises->createPromiseAll([0, $p1, $p2, $p3, $p4]); + + $result = $this->promises->wait($p2); + $this->assertEquals(2, $result); + $this->assertEquals(SyncPromise::PENDING, $p3->adoptedPromise->state); + $this->assertEquals(SyncPromise::PENDING, $all->adoptedPromise->state); + $this->assertEquals([1, 2], $called); + + $expectedResult = [0,1,2,3,4]; + $result = $this->promises->wait($all); + $this->assertEquals($expectedResult, $result); + $this->assertEquals([1, 2, 3, 4], $called); + $this->assertValidPromise($all, null, [0,1,2,3,4], SyncPromise::FULFILLED); + } + + private function assertValidPromise($promise, $expectedNextReason, $expectedNextValue, $expectedNextState) + { + $this->assertInstanceOf('GraphQL\Executor\Promise\Promise', $promise); + $this->assertInstanceOf('GraphQL\Executor\Promise\Adapter\SyncPromise', $promise->adoptedPromise); + + $actualNextValue = null; + $actualNextReason = null; + $onFulfilledCalled = false; + $onRejectedCalled = false; + + $promise->then( + function($nextValue) use (&$actualNextValue, &$onFulfilledCalled) { + $onFulfilledCalled = true; + $actualNextValue = $nextValue; + }, + function(\Exception $reason) use (&$actualNextReason, &$onRejectedCalled) { + $onRejectedCalled = true; + $actualNextReason = $reason->getMessage(); + } + ); + + $this->assertEquals($onFulfilledCalled, false); + $this->assertEquals($onRejectedCalled, false); + + SyncPromise::runQueue(); + + if ($expectedNextState !== SyncPromise::PENDING) { + $this->assertEquals(!$expectedNextReason, $onFulfilledCalled); + $this->assertEquals(!!$expectedNextReason, $onRejectedCalled); + } + + $this->assertEquals($expectedNextValue, $actualNextValue); + $this->assertEquals($expectedNextReason, $actualNextReason); + $this->assertEquals($expectedNextState, $promise->adoptedPromise->state); + } +} diff --git a/tests/Executor/Promise/SyncPromiseTest.php b/tests/Executor/Promise/SyncPromiseTest.php index e0566dc..f91ba91 100644 --- a/tests/Executor/Promise/SyncPromiseTest.php +++ b/tests/Executor/Promise/SyncPromiseTest.php @@ -252,12 +252,12 @@ class SyncPromiseTest extends \PHPUnit_Framework_TestCase $nextPromise3 = $promise->then($onFulfilled, $onRejected); $nextPromise4 = $promise->then($onFulfilled, $onRejected); - $this->assertEquals(SyncPromise::$queue->count(), 0); + $this->assertEquals(SyncPromise::getQueue()->count(), 0); $this->assertEquals($onFulfilledCount, 0); $this->assertEquals($onRejectedCount, 0); $promise->resolve(1); - $this->assertEquals(SyncPromise::$queue->count(), 4); + $this->assertEquals(SyncPromise::getQueue()->count(), 4); $this->assertEquals($onFulfilledCount, 0); $this->assertEquals($onRejectedCount, 0); $this->assertEquals(SyncPromise::PENDING, $nextPromise->state); @@ -266,7 +266,7 @@ class SyncPromiseTest extends \PHPUnit_Framework_TestCase $this->assertEquals(SyncPromise::PENDING, $nextPromise4->state); SyncPromise::runQueue(); - $this->assertEquals(SyncPromise::$queue->count(), 0); + $this->assertEquals(SyncPromise::getQueue()->count(), 0); $this->assertEquals($onFulfilledCount, 3); $this->assertEquals($onRejectedCount, 0); $this->assertValidPromise($nextPromise, null, 1, SyncPromise::FULFILLED);