Execution: added SyncPromiseAdapter and made it default for Executor (+removed GenericPromiseAdapter)

This commit is contained in:
vladar 2016-12-03 02:14:14 +07:00
parent 48d78412ec
commit e97ca7f971
8 changed files with 424 additions and 73 deletions

53
src/Deferred.php Normal file
View File

@ -0,0 +1,53 @@
<?php
namespace GraphQL;
use GraphQL\Executor\Promise\Adapter\SyncPromise;
class Deferred
{
private static $queue;
private $callback;
/**
* @var SyncPromise
*/
public $promise;
public static function getQueue()
{
return self::$queue ?: self::$queue = new \SplQueue();
}
public static function runQueue()
{
$q = self::getQueue();
while (!$q->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);
}
}
}

View File

@ -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;
}

View File

@ -1,57 +0,0 @@
<?php
namespace GraphQL\Executor\Promise\Adapter;
use GraphQL\Executor\Promise\PromiseAdapter;
class GenericPromiseAdapter implements PromiseAdapter
{
public function isPromise($value)
{
return false;
}
/**
* Accepts value qualified by `isPromise` and returns other promise.
*
* @param $promise
* @param callable|null $onFullFilled
* @param callable|null $onRejected
* @return mixed
*/
public function then($promise, callable $onFullFilled = null, callable $onRejected = null)
{
try {
if (null !== $onFullFilled) {
$promise = $onFullFilled($promise);
}
return $this->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)
{
}
}

View File

@ -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;

View File

@ -0,0 +1,146 @@
<?php
namespace GraphQL\Executor\Promise\Adapter;
use GraphQL\Deferred;
use GraphQL\Error\InvariantViolation;
use GraphQL\Executor\Promise\Promise;
use GraphQL\Executor\Promise\PromiseAdapter;
use GraphQL\Utils;
/**
* Class SyncPromiseAdapter
*
* Allows changing order of field resolution even in sync environments
* (by leveraging queue of deferreds and promises)
*
* @package GraphQL\Executor\Promise\Adapter
*/
class SyncPromiseAdapter implements PromiseAdapter
{
/**
* @inheritdoc
*/
public function isThenable($value)
{
return $value instanceof Deferred;
}
/**
* @inheritdoc
*/
public function convert($value)
{
if (!$value instanceof Deferred) {
throw new InvariantViolation('Expected instance of GraphQL\Deferred, got ' . Utils::printSafe($value));
}
return new Promise($value->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;
}
}

View File

@ -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;
}

View File

@ -0,0 +1,203 @@
<?php
namespace GraphQL\Tests\Executor\Promise;
use GraphQL\Deferred;
use GraphQL\Error\InvariantViolation;
use GraphQL\Executor\Promise\Adapter\SyncPromise;
use GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter;
use GraphQL\Executor\Promise\Promise;
class SyncPromiseAdapterTest extends \PHPUnit_Framework_TestCase
{
/**
* @var SyncPromiseAdapter
*/
private $promises;
public function setUp()
{
$this->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);
}
}

View File

@ -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);