From 59aa77167ef8e2fdbed2c5edae5a36514204cd3b Mon Sep 17 00:00:00 2001 From: Jeremiah VALERIE Date: Sun, 5 Feb 2017 21:37:16 +0100 Subject: [PATCH 1/2] Add support for Webonyx GraphQL Promise --- README.md | 210 ++++++++++++++++-- composer.json | 11 +- lib/promise-adapter/composer.json | 6 +- .../src/Adapter/ReactPromiseAdapter.php | 3 - .../WebonyxGraphQLSyncPromiseAdapter.php | 159 +++++++++++++ lib/promise-adapter/tests/AdapterTest.php | 6 +- .../Webonyx/GraphQL/SyncPromiseAdapter.php | 37 +++ tests/DataLoadTest.php | 4 +- tests/Functional/Webonyx/GraphQL/Schema.php | 86 +++++++ tests/Functional/Webonyx/GraphQL/TestCase.php | 112 ++++++++++ .../GraphQL/WithReactPhpPromiseTest.php | 28 +++ .../GraphQL/WithWebonyxGraphQLSyncTest.php | 29 +++ .../metrics.json | 7 + .../query.graphql | 26 +++ .../response.json | 158 +++++++++++++ .../characters-1000-1002-friends/metrics.json | 7 + .../query.graphql | 18 ++ .../response.json | 44 ++++ .../characters-1000-1002/metrics.json | 6 + .../characters-1000-1002/query.graphql | 10 + .../characters-1000-1002/response.json | 12 + tests/TestCase.php | 3 +- 22 files changed, 951 insertions(+), 31 deletions(-) create mode 100644 lib/promise-adapter/src/Adapter/WebonyxGraphQLSyncPromiseAdapter.php create mode 100644 src/Promise/Adapter/Webonyx/GraphQL/SyncPromiseAdapter.php create mode 100644 tests/Functional/Webonyx/GraphQL/Schema.php create mode 100644 tests/Functional/Webonyx/GraphQL/TestCase.php create mode 100644 tests/Functional/Webonyx/GraphQL/WithReactPhpPromiseTest.php create mode 100644 tests/Functional/Webonyx/GraphQL/WithWebonyxGraphQLSyncTest.php create mode 100644 tests/Functional/Webonyx/GraphQL/fixtures/characters-1000-1002-friends-friends/metrics.json create mode 100644 tests/Functional/Webonyx/GraphQL/fixtures/characters-1000-1002-friends-friends/query.graphql create mode 100644 tests/Functional/Webonyx/GraphQL/fixtures/characters-1000-1002-friends-friends/response.json create mode 100644 tests/Functional/Webonyx/GraphQL/fixtures/characters-1000-1002-friends/metrics.json create mode 100644 tests/Functional/Webonyx/GraphQL/fixtures/characters-1000-1002-friends/query.graphql create mode 100644 tests/Functional/Webonyx/GraphQL/fixtures/characters-1000-1002-friends/response.json create mode 100644 tests/Functional/Webonyx/GraphQL/fixtures/characters-1000-1002/metrics.json create mode 100644 tests/Functional/Webonyx/GraphQL/fixtures/characters-1000-1002/query.graphql create mode 100644 tests/Functional/Webonyx/GraphQL/fixtures/characters-1000-1002/response.json diff --git a/README.md b/README.md index 4682eb6..e5aa8dc 100644 --- a/README.md +++ b/README.md @@ -23,8 +23,10 @@ composer require "overblog/dataloader-php" To get started, create a `DataLoader` object. -Batching is not an advanced feature, it's DataLoaderPHP's primary feature. -Create loaders by providing a batch loading instance. +## Batching + +Batching is not an advanced feature, it's DataLoader's primary feature. +Create loaders by providing a batch loading function. ```php @@ -67,43 +69,101 @@ presented to your batch loading function. This allows your application to safely distribute data fetching requirements throughout your application and maintain minimal outgoing data requests. -### Caching (current PHP instance) +#### Batch Function + +A batch loading function accepts an Array of keys, and returns a Promise which +resolves to an Array of values. There are a few constraints that must be upheld: + + * The Array of values must be the same length as the Array of keys. + * Each index in the Array of values must correspond to the same index in the Array of keys. -After being loaded once, the resulting value is cached, eliminating -redundant requests. +For example, if your batch function was provided the Array of keys: `[ 2, 9, 6, 1 ]`, +and loading from a back-end service returned the values: + +```php +[ + ['id' => 9, 'name' => 'Chicago'], + ['id' => 1, 'name' => 'New York'], + ['id' => 2, 'name' => 'San Francisco'] +] +``` -In the example above, if User `1` was last invited by User `2`, only a single -round trip will occur. +Our back-end service returned results in a different order than we requested, likely +because it was more efficient for it to do so. Also, it omitted a result for key `6`, +which we can interpret as no value existing for that key. -Caching results in creating fewer objects which may relieve memory pressure on -your application: +To uphold the constraints of the batch function, it must return an Array of values +the same length as the Array of keys, and re-order them to ensure each index aligns +with the original keys `[ 2, 9, 6, 1 ]`: ```php +[ + ['id' => 2, 'name' => 'San Francisco'], + ['id' => 9, 'name' => 'Chicago'], + null, + ['id' => 1, 'name' => 'New York'] +] +``` + + +### Caching (current PHP instance) + +DataLoader provides a memoization cache for all loads which occur in a single +request to your application. After `->load()` is called once with a given key, +the resulting value is cached to eliminate redundant loads. + +In addition to reliving pressure on your data storage, caching results per-request +also creates fewer objects which may relieve memory pressure on your application: + +```php +$userLoader = new DataLoader(...); $promise1A = $userLoader->load(1); $promise1B = $userLoader->load(1); var_dump($promise1A === $promise1B); // bool(true) ``` -There are two common examples when clearing the loader's cache is necessary: +#### Clearing Cache + +In certain uncommon cases, clearing the request cache may be necessary. -*Mutations:* after a mutation or update, a cached value may be out of date. -Future loads should not use any possibly cached value. +The most common example when clearing the loader's cache is necessary is after +a mutation or update within the same request, when a cached value could be out of +date and future loads should not use any possibly cached value. Here's a simple example using SQL UPDATE to illustrate. ```php +use Overblog\DataLoader\DataLoader; + +// Request begins... +$userLoader = new DataLoader(...); + +// And a value happens to be loaded (and cached). +$userLoader->load(4)->then(...); + +// A mutation occurs, invalidating what might be in cache. $sql = 'UPDATE users WHERE id=4 SET username="zuck"'; if (true === $conn->query($sql)) { $userLoader->clear(4); } + +// Later the value load is loaded again so the mutated data appears. +$userLoader->load(4)->then(...); + +// Request completes. ``` -*Transient Errors:* A load may fail because it simply can't be loaded -(a permanent issue) or it may fail because of a transient issue such as a down -database or network issue. For transient errors, clear the cache: +#### Caching Errors + +If a batch load fails (that is, a batch function throws or returns a rejected +Promise), then the requested values will not be cached. However if a batch +function returns an `Error` instance for an individual value, that `Error` will +be cached to avoid frequently loading the same `Error`. + +In some circumstances you may wish to clear the cache for these individual Errors: ```php -$userLoader->load(1)->otherwise(function ($exception) { +$userLoader->load(1)->then(null, function ($exception) { if (/* determine if error is transient */) { $userLoader->clear(1); } @@ -111,6 +171,47 @@ $userLoader->load(1)->otherwise(function ($exception) { }); ``` +#### Disabling Cache + +In certain uncommon cases, a DataLoader which *does not* cache may be desirable. +Calling `new DataLoader(myBatchFn, new Option(['cache' => false ]))` will ensure that every +call to `->load()` will produce a *new* Promise, and requested keys will not be +saved in memory. + +However, when the memoization cache is disabled, your batch function will +receive an array of keys which may contain duplicates! Each key will be +associated with each call to `->load()`. Your batch loader should provide a value +for each instance of the requested key. + +For example: + +```php +$myLoader = new DataLoader(function ($keys) { + echo json_encode($keys); + return someBatchLoadFn($keys); +}, new Option(['cache' => false ])); + +$myLoader->load('A'); +$myLoader->load('B'); +$myLoader->load('A'); + +// [ 'A', 'B', 'A' ] +``` + +More complex cache behavior can be achieved by calling `->clear()` or `->clearAll()` +rather than disabling the cache completely. For example, this DataLoader will +provide unique keys to a batch function due to the memoization cache being +enabled, but will immediately clear its cache when the batch function is called +so later requests will load new values. + +```php +$myLoader = new DataLoader(function($keys) use ($identityLoader) { + $identityLoader->clearAll(); + return someBatchLoadFn($keys); +}); +``` + + ## API #### class DataLoader @@ -204,7 +305,82 @@ Await method process all waiting promise in all dataLoaderPHP instances. ## Using with Webonyx/GraphQL -Here [an example](https://github.com/mcg-web/sandbox-dataloader-graphql-php/blob/master/with-dataloader.php). +DataLoader pairs nicely well with [Webonyx/GraphQL](https://github.com/webonyx/graphql-php). GraphQL fields are +designed to be stand-alone functions. Without a caching or batching mechanism, +it's easy for a naive GraphQL server to issue new database requests each time a +field is resolved. + +Consider the following GraphQL request: + +```graphql +{ + me { + name + bestFriend { + name + } + friends(first: 5) { + name + bestFriend { + name + } + } + } +} +``` + +Naively, if `me`, `bestFriend` and `friends` each need to request the backend, +there could be at most 13 database requests! + +When using DataLoader, we could define the `User` type +at most 4 database requests, +and possibly fewer if there are cache hits. + +```php + 'User', + 'fields' => function () use (&$userType, $userLoader, $dbh) { + return [ + 'name' => ['type' => Type::string()], + 'bestFriend' => [ + 'type' => $userType, + 'resolve' => function ($user) use ($userLoader) { + $userLoader->load($user['bestFriendID']); + } + ], + 'friends' => [ + 'args' => [ + 'first' => ['type' => Type::int() ], + ], + 'type' => Type::listOf($userType), + 'resolve' => function ($user, $args) use ($userLoader, $dbh) { + $sth = $dbh->prepare('SELECT toID FROM friends WHERE fromID=:userID LIMIT :first'); + $sth->bindParam(':userID', $user['id'], PDO::PARAM_INT); + $sth->bindParam(':first', $args['first'], PDO::PARAM_INT); + $friendIDs = $sth->execute(); + + return $userLoader->loadMany($friendIDs); + } + ] + ]; + } +]); +``` +You can also see [an example](https://github.com/mcg-web/sandbox-dataloader-graphql-php/blob/master/with-dataloader.php). + +## Using with Symfony + +See the [bundle](https://github.com/overblog/dataloader-bundle). ## Credits diff --git a/composer.json b/composer.json index 450f0e4..5b1b4fa 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,10 @@ "psr-4": { "Overblog\\DataLoader\\Test\\": "tests/", "Overblog\\PromiseAdapter\\Test\\": "lib/promise-adapter/tests/" - } + }, + "files": [ + "vendor/webonyx/graphql-php/tests/StarWarsData.php" + ] }, "replace": { "overblog/promise-adapter": "self.version" @@ -29,11 +32,13 @@ "require-dev": { "guzzlehttp/promises": "^1.3.0", "phpunit/phpunit": "^4.1|^5.1", - "react/promise": "^2.5.0" + "react/promise": "^2.5.0", + "webonyx/graphql-php": "^0.9.0" }, "suggest": { "guzzlehttp/promises": "To use with Guzzle promise", - "react/promise": "To use with ReactPhp promise" + "react/promise": "To use with ReactPhp promise", + "webonyx/graphql-php": "To use with Webonyx GraphQL native promise" }, "extra": { "branch-alias": { diff --git a/lib/promise-adapter/composer.json b/lib/promise-adapter/composer.json index dcbbdf3..a1a9bc1 100644 --- a/lib/promise-adapter/composer.json +++ b/lib/promise-adapter/composer.json @@ -28,11 +28,13 @@ "require-dev": { "guzzlehttp/promises": "^1.3.0", "phpunit/phpunit": "^4.1|^5.1", - "react/promise": "^2.5.0" + "react/promise": "^2.5.0", + "webonyx/graphql-php": "^0.9.0" }, "suggest": { "guzzlehttp/promises": "To use with Guzzle promise", - "react/promise": "To use with ReactPhp promise" + "react/promise": "To use with ReactPhp promise", + "webonyx/graphql-php": "To use with Webonyx GraphQL native promise" }, "license": "MIT" } diff --git a/lib/promise-adapter/src/Adapter/ReactPromiseAdapter.php b/lib/promise-adapter/src/Adapter/ReactPromiseAdapter.php index aabc19f..545e55f 100644 --- a/lib/promise-adapter/src/Adapter/ReactPromiseAdapter.php +++ b/lib/promise-adapter/src/Adapter/ReactPromiseAdapter.php @@ -102,9 +102,6 @@ public function await($promise = null, $unwrap = false) $wait = false; }); - while ($wait) { - } - if ($exception instanceof \Exception) { if (!$unwrap) { return $exception; diff --git a/lib/promise-adapter/src/Adapter/WebonyxGraphQLSyncPromiseAdapter.php b/lib/promise-adapter/src/Adapter/WebonyxGraphQLSyncPromiseAdapter.php new file mode 100644 index 0000000..f118a33 --- /dev/null +++ b/lib/promise-adapter/src/Adapter/WebonyxGraphQLSyncPromiseAdapter.php @@ -0,0 +1,159 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Overblog\PromiseAdapter\Adapter; + +use GraphQL\Deferred; +use GraphQL\Executor\Promise\Adapter\SyncPromise; +use GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter; +use GraphQL\Executor\Promise\Promise; +use Overblog\PromiseAdapter\PromiseAdapterInterface; + +class WebonyxGraphQLSyncPromiseAdapter implements PromiseAdapterInterface +{ + /** @var callable[] */ + private $cancellers = []; + + /** + * @var SyncPromiseAdapter + */ + private $webonyxPromiseAdapter; + + public function __construct(SyncPromiseAdapter $webonyxPromiseAdapter = null) + { + $webonyxPromiseAdapter = $webonyxPromiseAdapter?:new SyncPromiseAdapter(); + $this->setWebonyxPromiseAdapter($webonyxPromiseAdapter); + } + + /** + * @return SyncPromiseAdapter + */ + public function getWebonyxPromiseAdapter() + { + return $this->webonyxPromiseAdapter; + } + + /** + * @param SyncPromiseAdapter $webonyxPromiseAdapter + */ + public function setWebonyxPromiseAdapter(SyncPromiseAdapter $webonyxPromiseAdapter) + { + $this->webonyxPromiseAdapter = $webonyxPromiseAdapter; + } + + /** + * {@inheritdoc} + */ + public function create(&$resolve = null, &$reject = null, callable $canceller = null) + { + $promise = $this->webonyxPromiseAdapter->create(function ($res, $rej) use (&$resolve, &$reject) { + $resolve = $res; + $reject = $rej; + }); + $this->cancellers[spl_object_hash($promise)] = $canceller; + + return $promise; + } + + /** + * {@inheritdoc} + */ + public function createFulfilled($promiseOrValue = null) + { + return $this->getWebonyxPromiseAdapter()->createFulfilled($promiseOrValue); + } + + /** + * {@inheritdoc} + */ + public function createRejected($reason) + { + return $this->getWebonyxPromiseAdapter()->createRejected($reason); + } + + /** + * {@inheritdoc} + */ + public function createAll($promisesOrValues) + { + return $this->getWebonyxPromiseAdapter()->all($promisesOrValues); + } + + /** + * {@inheritdoc} + */ + public function isPromise($value, $strict = false) + { + if ($value instanceof Promise) { + $value = $value->adoptedPromise; + } + $isStrictPromise = $value instanceof SyncPromise; + if ($strict) { + return $isStrictPromise; + } + + return $isStrictPromise || is_callable([$value, 'then']); + } + + /** + * {@inheritdoc} + */ + public function await($promise = null, $unwrap = false) + { + if (null === $promise) { + Deferred::runQueue(); + SyncPromise::runQueue(); + return null; + } + $promiseAdapter = $this->getWebonyxPromiseAdapter(); + + $resolvedValue = null; + $exception = null; + if (!$this->isPromise($promise)) { + throw new \InvalidArgumentException(sprintf('The "%s" method must be called with a Promise ("then" method).', __METHOD__)); + } + + try { + $resolvedValue = $promiseAdapter->wait($promise); + } catch (\Exception $reason) { + $exception = $reason; + } + if ($exception instanceof \Exception) { + if (!$unwrap) { + return $exception; + } + throw $exception; + } + + return $resolvedValue; + } + + /** + * {@inheritdoc} + */ + public function cancel($promise) + { + $hash = spl_object_hash($promise); + if (!$this->isPromise($promise) || !isset($this->cancellers[$hash])) { + throw new \InvalidArgumentException(sprintf('The "%s" method must be called with a compatible Promise.', __METHOD__)); + } + $canceller = $this->cancellers[$hash]; + $adoptedPromise = $promise; + if ($promise instanceof Promise) { + $adoptedPromise = $promise->adoptedPromise; + } + try { + $canceller([$adoptedPromise, 'resolve'], [$adoptedPromise, 'reject']); + } catch (\Exception $reason) { + $adoptedPromise->reject($reason); + } + } +} diff --git a/lib/promise-adapter/tests/AdapterTest.php b/lib/promise-adapter/tests/AdapterTest.php index f9d2119..738d39d 100644 --- a/lib/promise-adapter/tests/AdapterTest.php +++ b/lib/promise-adapter/tests/AdapterTest.php @@ -14,6 +14,7 @@ use Overblog\PromiseAdapter\Adapter\GuzzleHttpPromiseAdapter; use Overblog\PromiseAdapter\Adapter\ReactPromiseAdapter; +use Overblog\PromiseAdapter\Adapter\WebonyxGraphQLSyncPromiseAdapter; use Overblog\PromiseAdapter\PromiseAdapterInterface; class AdapterTest extends \PHPUnit_Framework_TestCase @@ -210,8 +211,9 @@ public function testCancelInvalidPromise(PromiseAdapterInterface $Adapter) public function AdapterDataProvider() { return [ - [new GuzzleHttpPromiseAdapter(), 'guzzle', '\\GuzzleHttp\\Promise\\PromiseInterface'], - [new ReactPromiseAdapter(), 'react', '\\React\\Promise\\PromiseInterface'], + [new GuzzleHttpPromiseAdapter(), 'guzzle', 'GuzzleHttp\\Promise\\PromiseInterface'], + [new ReactPromiseAdapter(), 'react', 'React\\Promise\\PromiseInterface'], + [new WebonyxGraphQLSyncPromiseAdapter(), 'webonyx', 'GraphQL\\Executor\\Promise\\Promise'], ]; } } diff --git a/src/Promise/Adapter/Webonyx/GraphQL/SyncPromiseAdapter.php b/src/Promise/Adapter/Webonyx/GraphQL/SyncPromiseAdapter.php new file mode 100644 index 0000000..f817ce6 --- /dev/null +++ b/src/Promise/Adapter/Webonyx/GraphQL/SyncPromiseAdapter.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Overblog\DataLoader\Promise\Adapter\Webonyx\GraphQL; + +use GraphQL\Deferred; +use GraphQL\Executor\Promise\Adapter\SyncPromise; +use GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter as BaseSyncPromiseAdapter; +use GraphQL\Executor\Promise\Promise; +use Overblog\DataLoader\DataLoader; + +class SyncPromiseAdapter extends BaseSyncPromiseAdapter +{ + /** + * Synchronously wait when promise completes + * + * @param Promise $promise + * @return mixed + */ + public function wait(Promise $promise) + { + DataLoader::await(); + Deferred::runQueue(); + SyncPromise::runQueue(); + DataLoader::await(); + + return parent::wait($promise); + } +} diff --git a/tests/DataLoadTest.php b/tests/DataLoadTest.php index 9371503..dca0d71 100644 --- a/tests/DataLoadTest.php +++ b/tests/DataLoadTest.php @@ -384,7 +384,7 @@ public function testCanClearValuesFromCacheAfterErrors() try { DataLoader::await( $errorLoader->load(1) - ->otherwise(function ($error) use (&$errorLoader) { + ->then(null, function ($error) use (&$errorLoader) { $errorLoader->clear(1); throw $error; }) @@ -399,7 +399,7 @@ public function testCanClearValuesFromCacheAfterErrors() try { DataLoader::await( $errorLoader->load(1) - ->otherwise(function ($error) use (&$errorLoader) { + ->then(null, function ($error) use (&$errorLoader) { $errorLoader->clear(1); throw $error; }) diff --git a/tests/Functional/Webonyx/GraphQL/Schema.php b/tests/Functional/Webonyx/GraphQL/Schema.php new file mode 100644 index 0000000..7219a54 --- /dev/null +++ b/tests/Functional/Webonyx/GraphQL/Schema.php @@ -0,0 +1,86 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Overblog\DataLoader\Test\Functional\Webonyx\GraphQL; + +use GraphQL\Type\Definition\NonNull; +use GraphQL\Type\Definition\ObjectType; +use GraphQL\Type\Definition\Type; +use Overblog\DataLoader\DataLoader; + +class Schema +{ + public static function build(DataLoader $dataLoader) + { + $characterType = null; + + /** + * This implements the following type system shorthand: + * type Character : Character { + * id: String! + * name: String + * friends: [Character] + * } + */ + $characterType = new ObjectType([ + 'name' => 'Character', + 'fields' => function () use (&$characterType, $dataLoader) { + return [ + 'id' => [ + 'type' => new NonNull(Type::string()), + 'description' => 'The id of the character.', + ], + 'name' => [ + 'type' => Type::string(), + 'description' => 'The name of the character.', + ], + 'friends' => [ + 'type' => Type::listOf($characterType), + 'description' => 'The friends of the character, or an empty list if they have none.', + 'resolve' => function ($character) use ($dataLoader) { + $promise = $dataLoader->loadMany($character['friends']); + return $promise; + }, + ], + ]; + }, + ]); + + /** + * This implements the following type system shorthand: + * type Query { + * character(id: String!): Character + * } + * + */ + $queryType = new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'character' => [ + 'type' => $characterType, + 'args' => [ + 'id' => [ + 'name' => 'id', + 'description' => 'id of the character', + 'type' => Type::nonNull(Type::string()) + ] + ], + 'resolve' => function ($root, $args) use ($dataLoader) { + $promise = $dataLoader->load($args['id']); + return $promise; + }, + ], + ] + ]); + + return new \GraphQL\Schema(['query' => $queryType]); + } +} diff --git a/tests/Functional/Webonyx/GraphQL/TestCase.php b/tests/Functional/Webonyx/GraphQL/TestCase.php new file mode 100644 index 0000000..1cc7ec1 --- /dev/null +++ b/tests/Functional/Webonyx/GraphQL/TestCase.php @@ -0,0 +1,112 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Overblog\DataLoader\Test\Functional\Webonyx\GraphQL; + +use GraphQL\Executor\Promise\Promise; +use GraphQL\Executor\Promise\PromiseAdapter; +use GraphQL\GraphQL; +use GraphQL\Tests\StarWarsData; +use Overblog\DataLoader\DataLoader; +use Overblog\PromiseAdapter\PromiseAdapterInterface; + +abstract class TestCase extends \PHPUnit_Framework_TestCase +{ + private static $fixtures = null; + + public function getFixtures() + { + if (null === self::$fixtures) { + $fixturesFiles = self::listFiles(__DIR__.'/fixtures'); + self::$fixtures = []; + foreach ($fixturesFiles as $file) { + $pathInfo = pathinfo($file); + $group = basename($pathInfo['dirname']); + $key = $pathInfo['filename']; + + $content = file_get_contents($file); + + if ('json' === $pathInfo['extension']) { + $content = json_decode($content, true); + } + self::$fixtures[$group][$key] = $content; + unset($group, $key, $content, $pathInfo); + } + unset($fixturesFiles); + } + + return self::$fixtures; + } + + private static function listFiles($dir, &$results = []) + { + $files = scandir($dir); + + foreach ($files as $key => $value) { + $path = realpath($dir . DIRECTORY_SEPARATOR . $value); + if (!is_dir($path)) { + $results[] = $path; + } elseif (!in_array($value, ['.', '..'])) { + self::listFiles($path, $results); + } + } + + return $results; + } + + /** + * @dataProvider getFixtures + * @param array $expectedMetrics + * @param string $query + * @param array $expectedResponse + */ + public function testExecute(array $expectedMetrics, $query, array $expectedResponse) + { + $metrics = [ + 'calls' => 0, + 'callsIds' => [], + ]; + + $graphQLPromiseAdapter = $this->createGraphQLPromiseAdapter(); + GraphQL::setPromiseAdapter($graphQLPromiseAdapter); + $dataLoaderPromiseAdapter = $this->createDataLoaderPromiseAdapter($graphQLPromiseAdapter); + $dataLoader = $this->createDataLoader($dataLoaderPromiseAdapter, $metrics['callsIds'], $metrics['calls']); + $schema = Schema::build($dataLoader); + + $response = GraphQL::execute($schema, $query); + if ($response instanceof Promise) { + $response = DataLoader::await($response); + } + + $this->assertEquals($expectedResponse, $response); + $this->assertEquals($expectedMetrics, $metrics); + + $dataLoader->clearAll(); + unset($dataLoader); + } + + abstract protected function createGraphQLPromiseAdapter(); + + abstract protected function createDataLoaderPromiseAdapter(PromiseAdapter $graphQLPromiseAdapter); + + protected function createDataLoader(PromiseAdapterInterface $dataLoaderPromiseAdapter, &$callsIds, &$calls) + { + $batchLoadFn = function ($ids) use (&$calls, &$callsIds, $dataLoaderPromiseAdapter) { + $callsIds[] = $ids; + ++$calls; + $allCharacters = StarWarsData::humans() + StarWarsData::droids(); + $characters = array_intersect_key($allCharacters, array_flip($ids)); + + return $dataLoaderPromiseAdapter->createAll(array_values($characters)); + }; + return new DataLoader($batchLoadFn, $dataLoaderPromiseAdapter); + } +} diff --git a/tests/Functional/Webonyx/GraphQL/WithReactPhpPromiseTest.php b/tests/Functional/Webonyx/GraphQL/WithReactPhpPromiseTest.php new file mode 100644 index 0000000..8d532af --- /dev/null +++ b/tests/Functional/Webonyx/GraphQL/WithReactPhpPromiseTest.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Overblog\DataLoader\Test\Functional\Webonyx\GraphQL; + +use GraphQL\Executor\Promise\Adapter\ReactPromiseAdapter; +use GraphQL\Executor\Promise\PromiseAdapter; + +class WithReactPhpPromiseTest extends TestCase +{ + protected function createGraphQLPromiseAdapter() + { + return new ReactPromiseAdapter(); + } + + protected function createDataLoaderPromiseAdapter(PromiseAdapter $graphQLPromiseAdapter) + { + return new \Overblog\PromiseAdapter\Adapter\ReactPromiseAdapter(); + } +} diff --git a/tests/Functional/Webonyx/GraphQL/WithWebonyxGraphQLSyncTest.php b/tests/Functional/Webonyx/GraphQL/WithWebonyxGraphQLSyncTest.php new file mode 100644 index 0000000..4aa4f13 --- /dev/null +++ b/tests/Functional/Webonyx/GraphQL/WithWebonyxGraphQLSyncTest.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Overblog\DataLoader\Test\Functional\Webonyx\GraphQL; + +use GraphQL\Executor\Promise\PromiseAdapter; +use Overblog\DataLoader\Promise\Adapter\Webonyx\GraphQL\SyncPromiseAdapter; +use Overblog\PromiseAdapter\Adapter\WebonyxGraphQLSyncPromiseAdapter; + +class WithWebonyxGraphQLSyncTest extends TestCase +{ + protected function createGraphQLPromiseAdapter() + { + return new SyncPromiseAdapter(); + } + + protected function createDataLoaderPromiseAdapter(PromiseAdapter $graphQLPromiseAdapter) + { + return new WebonyxGraphQLSyncPromiseAdapter($graphQLPromiseAdapter); + } +} diff --git a/tests/Functional/Webonyx/GraphQL/fixtures/characters-1000-1002-friends-friends/metrics.json b/tests/Functional/Webonyx/GraphQL/fixtures/characters-1000-1002-friends-friends/metrics.json new file mode 100644 index 0000000..ba10e84 --- /dev/null +++ b/tests/Functional/Webonyx/GraphQL/fixtures/characters-1000-1002-friends-friends/metrics.json @@ -0,0 +1,7 @@ +{ + "calls": 2, + "callsIds": [ + ["1000", "1002"], + ["1003", "2000", "2001"] + ] +} diff --git a/tests/Functional/Webonyx/GraphQL/fixtures/characters-1000-1002-friends-friends/query.graphql b/tests/Functional/Webonyx/GraphQL/fixtures/characters-1000-1002-friends-friends/query.graphql new file mode 100644 index 0000000..d13bb94 --- /dev/null +++ b/tests/Functional/Webonyx/GraphQL/fixtures/characters-1000-1002-friends-friends/query.graphql @@ -0,0 +1,26 @@ +{ + character1: character(id: "1000") { + id + name + friends { + id + name + friends { + id + name + } + } + } + character2: character(id: "1002") { + id + name + friends { + id + name + friends { + id + name + } + } + } +} diff --git a/tests/Functional/Webonyx/GraphQL/fixtures/characters-1000-1002-friends-friends/response.json b/tests/Functional/Webonyx/GraphQL/fixtures/characters-1000-1002-friends-friends/response.json new file mode 100644 index 0000000..0ace4cd --- /dev/null +++ b/tests/Functional/Webonyx/GraphQL/fixtures/characters-1000-1002-friends-friends/response.json @@ -0,0 +1,158 @@ +{ + "data": { + "character1": { + "id": "1000", + "name": "Luke Skywalker", + "friends": [ + { + "id": "1002", + "name": "Han Solo", + "friends": [ + { + "id": "1000", + "name": "Luke Skywalker" + }, + { + "id": "1003", + "name": "Leia Organa" + }, + { + "id": "2001", + "name": "R2-D2" + } + ] + }, + { + "id": "1003", + "name": "Leia Organa", + "friends": [ + { + "id": "1000", + "name": "Luke Skywalker" + }, + { + "id": "1002", + "name": "Han Solo" + }, + { + "id": "2000", + "name": "C-3PO" + }, + { + "id": "2001", + "name": "R2-D2" + } + ] + }, + { + "id": "2000", + "name": "C-3PO", + "friends": [ + { + "id": "1000", + "name": "Luke Skywalker" + }, + { + "id": "1002", + "name": "Han Solo" + }, + { + "id": "1003", + "name": "Leia Organa" + }, + { + "id": "2001", + "name": "R2-D2" + } + ] + }, + { + "id": "2001", + "name": "R2-D2", + "friends": [ + { + "id": "1000", + "name": "Luke Skywalker" + }, + { + "id": "1002", + "name": "Han Solo" + }, + { + "id": "1003", + "name": "Leia Organa" + } + ] + } + ] + }, + "character2": { + "id": "1002", + "name": "Han Solo", + "friends": [ + { + "id": "1000", + "name": "Luke Skywalker", + "friends": [ + { + "id": "1002", + "name": "Han Solo" + }, + { + "id": "1003", + "name": "Leia Organa" + }, + { + "id": "2000", + "name": "C-3PO" + }, + { + "id": "2001", + "name": "R2-D2" + } + ] + }, + { + "id": "1003", + "name": "Leia Organa", + "friends": [ + { + "id": "1000", + "name": "Luke Skywalker" + }, + { + "id": "1002", + "name": "Han Solo" + }, + { + "id": "2000", + "name": "C-3PO" + }, + { + "id": "2001", + "name": "R2-D2" + } + ] + }, + { + "id": "2001", + "name": "R2-D2", + "friends": [ + { + "id": "1000", + "name": "Luke Skywalker" + }, + { + "id": "1002", + "name": "Han Solo" + }, + { + "id": "1003", + "name": "Leia Organa" + } + ] + } + ] + } + } +} diff --git a/tests/Functional/Webonyx/GraphQL/fixtures/characters-1000-1002-friends/metrics.json b/tests/Functional/Webonyx/GraphQL/fixtures/characters-1000-1002-friends/metrics.json new file mode 100644 index 0000000..ba10e84 --- /dev/null +++ b/tests/Functional/Webonyx/GraphQL/fixtures/characters-1000-1002-friends/metrics.json @@ -0,0 +1,7 @@ +{ + "calls": 2, + "callsIds": [ + ["1000", "1002"], + ["1003", "2000", "2001"] + ] +} diff --git a/tests/Functional/Webonyx/GraphQL/fixtures/characters-1000-1002-friends/query.graphql b/tests/Functional/Webonyx/GraphQL/fixtures/characters-1000-1002-friends/query.graphql new file mode 100644 index 0000000..6d82891 --- /dev/null +++ b/tests/Functional/Webonyx/GraphQL/fixtures/characters-1000-1002-friends/query.graphql @@ -0,0 +1,18 @@ +{ + character1: character(id: "1000") { + id + name + friends { + id + name + } + } + character2: character(id: "1002") { + id + name + friends { + id + name + } + } +} diff --git a/tests/Functional/Webonyx/GraphQL/fixtures/characters-1000-1002-friends/response.json b/tests/Functional/Webonyx/GraphQL/fixtures/characters-1000-1002-friends/response.json new file mode 100644 index 0000000..918b64d --- /dev/null +++ b/tests/Functional/Webonyx/GraphQL/fixtures/characters-1000-1002-friends/response.json @@ -0,0 +1,44 @@ +{ + "data": { + "character1": { + "id": "1000", + "name": "Luke Skywalker", + "friends": [ + { + "id": "1002", + "name": "Han Solo" + }, + { + "id": "1003", + "name": "Leia Organa" + }, + { + "id": "2000", + "name": "C-3PO" + }, + { + "id": "2001", + "name": "R2-D2" + } + ] + }, + "character2": { + "id": "1002", + "name": "Han Solo", + "friends": [ + { + "id": "1000", + "name": "Luke Skywalker" + }, + { + "id": "1003", + "name": "Leia Organa" + }, + { + "id": "2001", + "name": "R2-D2" + } + ] + } + } +} diff --git a/tests/Functional/Webonyx/GraphQL/fixtures/characters-1000-1002/metrics.json b/tests/Functional/Webonyx/GraphQL/fixtures/characters-1000-1002/metrics.json new file mode 100644 index 0000000..07c2213 --- /dev/null +++ b/tests/Functional/Webonyx/GraphQL/fixtures/characters-1000-1002/metrics.json @@ -0,0 +1,6 @@ +{ + "calls": 1, + "callsIds": [ + ["1000", "1002"] + ] +} diff --git a/tests/Functional/Webonyx/GraphQL/fixtures/characters-1000-1002/query.graphql b/tests/Functional/Webonyx/GraphQL/fixtures/characters-1000-1002/query.graphql new file mode 100644 index 0000000..fa6ba42 --- /dev/null +++ b/tests/Functional/Webonyx/GraphQL/fixtures/characters-1000-1002/query.graphql @@ -0,0 +1,10 @@ +{ + character1: character(id: "1000") { + id + name + } + character2: character(id: "1002") { + id + name + } +} diff --git a/tests/Functional/Webonyx/GraphQL/fixtures/characters-1000-1002/response.json b/tests/Functional/Webonyx/GraphQL/fixtures/characters-1000-1002/response.json new file mode 100644 index 0000000..9588de0 --- /dev/null +++ b/tests/Functional/Webonyx/GraphQL/fixtures/characters-1000-1002/response.json @@ -0,0 +1,12 @@ +{ + "data": { + "character1": { + "id": "1000", + "name": "Luke Skywalker" + }, + "character2": { + "id": "1002", + "name": "Han Solo" + } + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index 6cbba2e..2359a79 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -11,11 +11,10 @@ namespace Overblog\DataLoader\Test; -use Overblog\PromiseAdapter\Adapter\GuzzleHttpPromiseAdapter; use Overblog\PromiseAdapter\Adapter\ReactPromiseAdapter; use Overblog\PromiseAdapter\PromiseAdapterInterface; -class TestCase extends \PHPUnit_Framework_TestCase +abstract class TestCase extends \PHPUnit_Framework_TestCase { /** * @var PromiseAdapterInterface From 95d16c59520a50706ee1278e4bbb27d78431beb5 Mon Sep 17 00:00:00 2001 From: Jeremiah VALERIE Date: Mon, 6 Feb 2017 17:11:14 +0100 Subject: [PATCH 2/2] Bump dev alias version --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 5b1b4fa..c6ebadc 100644 --- a/composer.json +++ b/composer.json @@ -42,7 +42,7 @@ }, "extra": { "branch-alias": { - "dev-master": "0.3-dev" + "dev-master": "0.4-dev" } } }