From b8dba829e00d5c21dad52cb8e09940ea186a21aa Mon Sep 17 00:00:00 2001 From: Evgeny Grik Date: Wed, 10 Apr 2019 11:24:37 +0300 Subject: [PATCH 1/4] add data external source --- .gitignore | 4 +- .../GraphQL/DataSource/AbstractDataSource.php | 129 ++++++++++++++++++ .../DataSource/DataSourceInterface.php | 10 ++ .../DataSourceRelationInterface.php | 8 ++ .../GraphQL/DataSource/DataSourceResolver.php | 69 ++++++++++ composer.json | 10 +- 6 files changed, 228 insertions(+), 2 deletions(-) create mode 100644 Service/GraphQL/DataSource/AbstractDataSource.php create mode 100644 Service/GraphQL/DataSource/DataSourceInterface.php create mode 100644 Service/GraphQL/DataSource/DataSourceRelationInterface.php create mode 100644 Service/GraphQL/DataSource/DataSourceResolver.php diff --git a/.gitignore b/.gitignore index 62c8935..072705c 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -.idea/ \ No newline at end of file +.idea/ +vendor/ +composer.lock diff --git a/Service/GraphQL/DataSource/AbstractDataSource.php b/Service/GraphQL/DataSource/AbstractDataSource.php new file mode 100644 index 0000000..f8a3b81 --- /dev/null +++ b/Service/GraphQL/DataSource/AbstractDataSource.php @@ -0,0 +1,129 @@ +graphQLService = $graphQLService; + } + + public function enqueue(array $args): self + { + if ($this->result !== null) { + $this->clear(); + } + + $this->buffer[] = $args; + + return $this; + } + + public function resolve(array $args, array $fields) + { + if ($this->result === null) { + try { + $this->result = $this->applyMap($this->fetch($fields)); + } catch (\Exception $exception) { + $this->result = []; + } + } + + return $this->result[$this->makePKByArgs($args)] ?? null; + } + + abstract public function getQueryName():string; + + protected function clear(): void + { + $this->buffer = []; + $this->result = null; + } + + protected function getPrimaryKey(): array + { + return ['id']; + } + + protected function getSelectSource(array $fields): array + { + return array_unique(array_merge($fields, $this->getPrimaryKey())); + } + + protected function getResponseRootPath(): string + { + return $this->packPath(['data', $this->getQueryName()]); + } + + protected function fetch(array $fields): array + { + $this->graphQLService->createQuery($this->getQueryName()) + ->select($this->getSelectSource($fields)) + ->where(array_merge_recursive(...$this->buffer)); + + try { + return $this->graphQLService->fetch(); + } catch (\Exception $exception) { + /* + * todo[egrik]: add logging exception and retry request + */ + } + + return []; + } + + protected function makePKByArgs(array $args): string + { + $data = ArrayHelper::mapByKeys($args, $this->getPrimaryKey()); + + return implode($this->glueKey, array_map(function (...$items) { + return implode($this->glueKey, $items); + }, array_keys($data), $data)); + } + + protected function applyMap(array $result): array + { + $rootItem = ArrayHelper::get($result, $this->getResponseRootPath(), []); + $columns = array_map([$this, 'makePKByArgs'], $rootItem); + + return array_map($callback = function ($array) use (&$callback) { + if (is_array($array)) { + return (object)array_map($callback, $array); + } + + return $array; + }, array_combine($columns, $rootItem)); + } + + protected function packPath($items): string + { + return implode($this->glueKey, $items); + } +} diff --git a/Service/GraphQL/DataSource/DataSourceInterface.php b/Service/GraphQL/DataSource/DataSourceInterface.php new file mode 100644 index 0000000..c1ef627 --- /dev/null +++ b/Service/GraphQL/DataSource/DataSourceInterface.php @@ -0,0 +1,10 @@ +getContainer(); + + return (new static($container->get($dataSourceAlias), $container->get($relationAlias)))(...func_get_args()); + }; + } + + /** + * DataSourceResolver constructor. + */ + public function __construct(DataSourceInterface $dataSource, DataSourceRelationInterface $relation) + { + $this->dataSource = $dataSource; + $this->relation = $relation; + } + + public function __invoke($value, $args, ResolveInfo $resolveInfo): DeferredResolver + { + $this->resolverInfo = $resolveInfo; + $this->value = $value; + + $this->dataSource->enqueue($this->handleRelationCallback()); + + return new DeferredResolver(function () { + return $this->dataSource->resolve($this->handleRelationCallback(), $this->getFields()); + }); + } + + protected function handleRelationCallback(): array + { + return $this->relation->relation($this->value); + } + + protected function getFields(): array + { + return array_map(function (Field $field) { + return $field->getName(); + }, $this->resolverInfo->getFieldASTList()); + } +} diff --git a/composer.json b/composer.json index 24d1022..bd7ac29 100755 --- a/composer.json +++ b/composer.json @@ -15,6 +15,12 @@ "Garlic\\Wrapper\\": "" } }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/egrik/helpers.git" + } + ], "require": { "php": ">=7.1", "garlic/bus": "1.*", @@ -22,6 +28,8 @@ "enqueue/enqueue-bundle": "^0.8", "enqueue/amqp-ext": "^0.8", "dflydev/dot-access-data": "^2.0", - "ext-json": "*" + "ext-json": "*", + "garlic/graphql": "^1.2", + "garlic/helpers": "^1.0" } } From 4d167b01ba4d9f955839611c8f530fc900222507 Mon Sep 17 00:00:00 2001 From: Evgeny Grik Date: Wed, 10 Apr 2019 16:22:54 +0300 Subject: [PATCH 2/4] upd readme - add using deferrer resolver and dataSource --- README.md | 112 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/README.md b/README.md index a82ce78..fbe9fe7 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,118 @@ $apartmentQuery $result = $graphQLService->fetch(); ``` +### Querying external related objects using a deferred resolver +Using deferred resolvers allows to getting relate data by one query after receiving the data of the base query, which solve the N + 1 problem. + +Suppose we have next GraphQL query: +```json +{ + wallet { + WalletFind { + items { + id + amount + company { + id + title + } + } + } + } +} +``` + +Where _company_ object is part of an external service, wich we'd like to get by one query to the external service. +To solve this problem, we need: +1. Create CompanyType, which we'd like use in our schema: + ```php + addField(new Field(['name' => 'id', 'type' => new StringType()])) + ->addField(new Field(['name' => 'title', 'type' => new StringType()])); + } + } + ``` + +2. Create CompanyDataSource, which we'd like use to get external data: + ```php + $entity->getCompanyId()]; + } + } + ``` +4. Add the Company field to WalletType: + ```php + addField('id', new IdType()) + ->addField('amount', new FloatType(), ['argument' => false]) + ->addField('company', new CompanyType(), [ + 'argument' => false, + 'resolve' => DataSourceResolver::build(CompanyDataSource::class, WalletCompanyRelation::class), + ]); + } + //... + ``` + ### GraphQL mutations Mutation is the way to change service data by sending some kinds of query. What this queries are and how they could created read below. From dc33830a72ec4ab96d5ab6632cc56cf4e9cff53e Mon Sep 17 00:00:00 2001 From: Evgeny Grik Date: Tue, 16 Apr 2019 17:52:18 +0300 Subject: [PATCH 3/4] add getWhereSource --- Service/GraphQL/DataSource/AbstractDataSource.php | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Service/GraphQL/DataSource/AbstractDataSource.php b/Service/GraphQL/DataSource/AbstractDataSource.php index f8a3b81..f771dd8 100644 --- a/Service/GraphQL/DataSource/AbstractDataSource.php +++ b/Service/GraphQL/DataSource/AbstractDataSource.php @@ -77,6 +77,13 @@ protected function getSelectSource(array $fields): array return array_unique(array_merge($fields, $this->getPrimaryKey())); } + protected function getWhereSource($where): array + { + return array_map(function($item) { + return array_unique(ArrayHelper::wrap($item)); + }, array_merge_recursive(...$where)); + } + protected function getResponseRootPath(): string { return $this->packPath(['data', $this->getQueryName()]); @@ -85,8 +92,8 @@ protected function getResponseRootPath(): string protected function fetch(array $fields): array { $this->graphQLService->createQuery($this->getQueryName()) - ->select($this->getSelectSource($fields)) - ->where(array_merge_recursive(...$this->buffer)); + ->select($this->getSelectSource($fields)) + ->where($this->getWhereSource($this->buffer)); try { return $this->graphQLService->fetch(); From c24c2f657d53dd33bd5362964ca805d61550f795 Mon Sep 17 00:00:00 2001 From: Evgeny Grik Date: Wed, 17 Apr 2019 18:03:32 +0300 Subject: [PATCH 4/4] fix related conditions --- .editorconfig | 2 ++ .../GraphQL/DataSource/AbstractDataSource.php | 18 +++++++++--------- 2 files changed, 11 insertions(+), 9 deletions(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..027a44a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,2 @@ +[*.php] +insert_final_newline = true \ No newline at end of file diff --git a/Service/GraphQL/DataSource/AbstractDataSource.php b/Service/GraphQL/DataSource/AbstractDataSource.php index f771dd8..fc18ca3 100644 --- a/Service/GraphQL/DataSource/AbstractDataSource.php +++ b/Service/GraphQL/DataSource/AbstractDataSource.php @@ -59,7 +59,7 @@ public function resolve(array $args, array $fields) return $this->result[$this->makePKByArgs($args)] ?? null; } - abstract public function getQueryName():string; + abstract public function getQueryName(): string; protected function clear(): void { @@ -72,16 +72,16 @@ protected function getPrimaryKey(): array return ['id']; } - protected function getSelectSource(array $fields): array + protected function getRelatedFields(array $fields): array { - return array_unique(array_merge($fields, $this->getPrimaryKey())); + return array_values(array_unique(array_merge($fields, $this->getPrimaryKey()))); } - protected function getWhereSource($where): array + protected function getRelatedConditions(): array { - return array_map(function($item) { - return array_unique(ArrayHelper::wrap($item)); - }, array_merge_recursive(...$where)); + return array_map(function ($item) { + return array_values(array_unique(ArrayHelper::wrap($item))); + }, array_merge_recursive(...$this->buffer)); } protected function getResponseRootPath(): string @@ -92,8 +92,8 @@ protected function getResponseRootPath(): string protected function fetch(array $fields): array { $this->graphQLService->createQuery($this->getQueryName()) - ->select($this->getSelectSource($fields)) - ->where($this->getWhereSource($this->buffer)); + ->select($this->getRelatedFields($fields)) + ->where($this->getRelatedConditions()); try { return $this->graphQLService->fetch();