mirror of
https://github.com/retailcrm/graphql-php.git
synced 2025-02-11 10:09:24 +03:00
Execution: added SyncPromiseAdapter and made it default for Executor (+removed GenericPromiseAdapter)
This commit is contained in:
parent
48d78412ec
commit
e97ca7f971
53
src/Deferred.php
Normal file
53
src/Deferred.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
||||
|
146
src/Executor/Promise/Adapter/SyncPromiseAdapter.php
Normal file
146
src/Executor/Promise/Adapter/SyncPromiseAdapter.php
Normal 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
203
tests/Executor/Promise/SyncPromiseAdapterTest.php
Normal file
203
tests/Executor/Promise/SyncPromiseAdapterTest.php
Normal 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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user