Completed
Pull Request — 8.x-3.x (#525)
by Philipp
03:19
created

QueryProcessor::processQuery()   B

Complexity

Conditions 3
Paths 2

Size

Total Lines 25
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 15
nc 2
nop 4
dl 0
loc 25
rs 8.8571
c 0
b 0
f 0
1
<?php
2
3
namespace Drupal\graphql\GraphQL\Execution;
4
5
use Drupal\Core\Cache\CacheableMetadata;
6
use Drupal\Core\Session\AccountProxyInterface;
7
use Drupal\graphql\GraphQL\Visitors\CacheMetadataCollector;
8
use Drupal\graphql\Plugin\SchemaPluginManager;
9
use Drupal\graphql\GraphQL\QueryProvider\QueryProviderInterface;
10
use GraphQL\Error\Error;
11
use GraphQL\Error\FormattedError;
12
use GraphQL\Executor\ExecutionResult;
13
use GraphQL\Executor\Executor;
14
use GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter;
15
use GraphQL\Executor\Promise\PromiseAdapter;
16
use GraphQL\Language\AST\DocumentNode;
17
use GraphQL\Language\Parser;
18
use GraphQL\Server\Helper;
19
use GraphQL\Server\OperationParams;
20
use GraphQL\Server\RequestError;
21
use GraphQL\Server\ServerConfig;
22
use GraphQL\Type\Schema;
23
use GraphQL\Utils\AST;
24
use GraphQL\Utils\Utils;
25
use GraphQL\Validator\DocumentValidator;
26
27
class QueryProcessor {
28
29
  /**
30
   * The current user account.
31
   *
32
   * @var \Drupal\Core\Session\AccountProxyInterface
33
   */
34
  protected $currentUser;
35
36
  /**
37
   * The schema plugin manager.
38
   *
39
   * @var \Drupal\graphql\Plugin\SchemaPluginManager
40
   */
41
  protected $pluginManager;
42
43
  /**
44
   * The query provider service.
45
   *
46
   * @var \Drupal\graphql\GraphQL\QueryProvider\QueryProviderInterface
47
   */
48
  protected $queryProvider;
49
50
  /**
51
   * Processor constructor.
52
   *
53
   * @param \Drupal\Core\Session\AccountProxyInterface $currentUser
54
   *   The current user.
55
   * @param \Drupal\graphql\Plugin\SchemaPluginManager $pluginManager
56
   *   The schema plugin manager.
57
   * @param \Drupal\graphql\GraphQL\QueryProvider\QueryProviderInterface $queryProvider
58
   *   The query provider service.
59
   */
60
  public function __construct(
61
    AccountProxyInterface $currentUser,
62
    SchemaPluginManager $pluginManager,
63
    QueryProviderInterface $queryProvider
64
  ) {
65
    $this->currentUser = $currentUser;
66
    $this->pluginManager = $pluginManager;
67
    $this->queryProvider = $queryProvider;
68
  }
69
70
  /**
71
   * Processes one or multiple graphql operations.
72
   *
73
   * @param string $schema
74
   *   The plugin id of the schema to use.
75
   * @param \GraphQL\Server\OperationParams|\GraphQL\Server\OperationParams[] $params
76
   *   The graphql operation(s) to execute.
77
   * @param mixed $context
78
   *   The query context.
79
   * @param bool $debug
80
   *   Whether to run this query in debugging mode.
81
   *
82
   * @return \Drupal\graphql\GraphQL\Execution\QueryResult|\Drupal\graphql\GraphQL\Execution\QueryResult[]
83
   *   The query result.
84
   */
85
  public function processQuery($schema, $params, $context = NULL, $debug = FALSE) {
86
    // Load the plugin from the schema manager.
87
    $plugin = $this->pluginManager->createInstance($schema);
88
    $schema = $plugin->getSchema();
89
90
    // Create the server config.
91
    $config = ServerConfig::create();
92
    $config->setDebug($debug);
93
    $config->setSchema($schema);
94
    $config->setContext($context);
95
    $config->setQueryBatching(TRUE);
96
    $config->setPersistentQueryLoader(function ($id, OperationParams $params) {
97
      if ($query = $this->queryProvider->getQuery($id, $params)) {
98
        return $query;
99
      }
100
101
      throw new RequestError(sprintf("Failed to load query map for id '%s'.", $id));
102
    });
103
104
    if (is_array($params)) {
105
      return $this->executeBatch($config, $params);
106
    }
107
108
    return $this->executeSingle($config, $params);
109
  }
110
111
  /**
112
   * @param \GraphQL\Server\ServerConfig $config
113
   * @param \GraphQL\Server\OperationParams $params
114
   *
115
   * @return mixed
116
   */
117
  public function executeSingle(ServerConfig $config, OperationParams $params) {
118
    $adapter = new SyncPromiseAdapter();
119
    $result = $this->promiseToExecuteOperation($adapter, $config, $params, FALSE);
120
    return $adapter->wait($result);
121
  }
122
123
  /**
124
   * @param \GraphQL\Server\ServerConfig $config
125
   * @param array $params
126
   *
127
   * @return mixed
128
   */
129
  public function executeBatch(ServerConfig $config, array $params) {
130
    $adapter = new SyncPromiseAdapter();
131
    $result = array_map(function ($params) use ($adapter, $config) {
132
      return $this->promiseToExecuteOperation($adapter, $config, $params, TRUE);
133
    }, $params);
134
135
    $result = $adapter->all($result);
136
    return $adapter->wait($result);
137
  }
138
139
  /**
140
   * @param \GraphQL\Executor\Promise\PromiseAdapter $adapter
141
   * @param \GraphQL\Server\ServerConfig $config
142
   * @param \GraphQL\Server\OperationParams $params
143
   * @param bool $batching
144
   *
145
   * @return \GraphQL\Executor\Promise\Promise
146
   */
147
  protected function promiseToExecuteOperation(PromiseAdapter $adapter, ServerConfig $config, OperationParams $params, $batching = FALSE) {
148
    try {
149
      if (!$config->getSchema()) {
150
        throw new \LogicException('Missing schema for query execution.');
151
      }
152
153
      if ($batching && !$config->getQueryBatching()) {
154
        throw new RequestError('Batched queries are not supported by this server.');
155
      }
156
157
      if ($errors = (new Helper())->validateOperationParams($params)) {
158
        $errors = Utils::map($errors, function (RequestError $err) {
159
          return Error::createLocatedError($err, NULL, NULL);
160
        });
161
162
        return $adapter->createFulfilled(new QueryResult(NULL, $errors));
163
      }
164
165
      $variables = $params->variables;
166
      $operation = $params->operation;
167
      $document = $params->queryId ? $this->loadPersistedQuery($config, $params) : $params->query;
168
      if (!$document instanceof DocumentNode) {
169
        $document = Parser::parse($document);
170
      }
171
172
      if ($params->isReadOnly() && AST::getOperation($document, $operation) !== 'query') {
173
        throw new RequestError('GET requests are only supported for query operations.');
174
      }
175
176
      $schema = $config->getSchema();
177
      $resolver = $config->getFieldResolver();
178
      $root = $this->resolveRootValue($config, $params, $document, $operation);
179
      $context = $this->resolveContextValue($config, $params, $document, $operation);
180
      $rules = $this->resolveValidationRules($config, $params, $document, $operation);
181
      $result = $this->promiseToExecute(
182
        $adapter,
183
        $schema,
184
        $document,
185
        $root,
186
        $context,
187
        $variables,
188
        $operation,
189
        $resolver,
190
        $rules
191
      );
192
    }
193
    catch (RequestError $exception) {
194
      $result = $adapter->createFulfilled(new QueryResult(NULL, [Error::createLocatedError($exception)]));
195
    }
196
    catch (Error $exception) {
197
      $result = $adapter->createFulfilled(new QueryResult(NULL, [$exception]));
198
    }
199
200
    return $result->then(function(QueryResult $result) use ($config) {
201
      if ($config->getErrorsHandler()) {
202
        $result->setErrorsHandler($config->getErrorsHandler());
203
      }
204
205
      if ($config->getErrorFormatter() || $config->getDebug()) {
206
        $result->setErrorFormatter(FormattedError::prepareFormatter($config->getErrorFormatter(), $config->getDebug()));
207
      }
208
209
      return $result;
210
    });
211
  }
212
213
  /**
214
   * @param \GraphQL\Executor\Promise\PromiseAdapter $adapter
215
   * @param \GraphQL\Type\Schema $schema
216
   * @param \GraphQL\Language\AST\DocumentNode $document
217
   * @param null $root
218
   * @param null $context
219
   * @param null $variables
220
   * @param null $operation
221
   * @param callable|NULL $resolver
222
   * @param array|NULL $rules
223
   *
224
   * @return \GraphQL\Executor\Promise\Promise
225
   */
226
  protected function promiseToExecute(
227
    PromiseAdapter $adapter,
228
    Schema $schema,
229
    DocumentNode $document,
230
    $root = NULL,
231
    $context = NULL,
232
    $variables = NULL,
233
    $operation = NULL,
234
    callable $resolver = NULL,
235
    array $rules = NULL
236
  ) {
237
    try {
238
      $metadata = new CacheableMetadata();
239
      $visitor = new CacheMetadataCollector($metadata, $variables);
240
      $rules = array_merge($rules ?: DocumentValidator::allRules(), [$visitor]);
241
      if ($errors = DocumentValidator::validate($schema, $document, $rules)) {
242
        return $adapter->createFulfilled(new QueryResult(NULL, $errors));
243
      }
244
245
      // TODO: Implement cache backend lookup with collected cache metadata.
246
247
      // Add the metadata bag to the context so fields can
248
249
      return (Executor::promiseToExecute(
250
        $adapter,
251
        $schema,
252
        $document,
253
        $root,
254
        $context,
255
        $variables,
256
        $operation,
257
        $resolver
258
      ))->then(function (ExecutionResult $result) use ($metadata) {
259
        return new QueryResult($result->data, $result->errors, $result->extensions, $metadata);
260
      });
261
    }
262
    catch (Error $exception) {
263
      return $adapter->createFulfilled(new QueryResult(NULL, [$exception]));
264
    }
265
  }
266
267
  /**
268
   * @param \GraphQL\Server\ServerConfig $config
269
   * @param \GraphQL\Server\OperationParams $params
270
   * @param \GraphQL\Language\AST\DocumentNode $document
271
   * @param $operation
272
   *
273
   * @return callable|mixed
274
   */
275 View Code Duplication
  protected function resolveRootValue(ServerConfig $config, OperationParams $params, DocumentNode $document, $operation) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
276
    $root = $config->getRootValue();
277
    if (is_callable($root)) {
278
      $root = $root($params, $document, $operation);
279
    }
280
281
    return $root;
282
  }
283
284
  /**
285
   * @param \GraphQL\Server\ServerConfig $config
286
   * @param \GraphQL\Server\OperationParams $params
287
   * @param \GraphQL\Language\AST\DocumentNode $document
288
   * @param $operation
289
   *
290
   * @return callable|mixed
291
   */
292 View Code Duplication
  protected function resolveContextValue(ServerConfig $config, OperationParams $params, DocumentNode $document, $operation) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
293
    $context = $config->getContext();
294
    if (is_callable($context)) {
295
      $context = $context($params, $document, $operation);
296
    }
297
298
    return $context;
299
  }
300
301
  /**
302
   * @param \GraphQL\Server\ServerConfig $config
303
   * @param \GraphQL\Server\OperationParams $params
304
   * @param \GraphQL\Language\AST\DocumentNode $document
305
   * @param $operation
306
   *
307
   * @return array|callable
308
   */
309
  protected function resolveValidationRules(ServerConfig $config, OperationParams $params, DocumentNode $document, $operation) {
310
    // Allow customizing validation rules per operation:
311
    $rules = $config->getValidationRules();
312
    if (is_callable($rules)) {
313
      $rules = $rules($params, $document, $operation);
314
      if (!is_array($rules)) {
315
        throw new \LogicException(sprintf("Expecting validation rules to be array or callable returning array, but got: %s", Utils::printSafe($rules)));
316
      }
317
    }
318
319
    return $rules;
320
  }
321
322
  /**
323
   * @param \GraphQL\Server\ServerConfig $config
324
   * @param \GraphQL\Server\OperationParams $params
325
   *
326
   * @return mixed
327
   * @throws \GraphQL\Server\RequestError
328
   */
329
  protected function loadPersistedQuery(ServerConfig $config, OperationParams $params) {
330
    if (!$loader = $config->getPersistentQueryLoader()) {
331
      throw new RequestError('Persisted queries are not supported by this server.');
332
    }
333
334
    $source = $loader($params->queryId, $params);
335
    if (!is_string($source) && !$source instanceof DocumentNode) {
336
      throw new \LogicException(sprintf('The persisted query loader must return query string or instance of %s but got: %s.', DocumentNode::class, Utils::printSafe($source)));
337
    }
338
339
    return $source;
340
  }
341
342
}
343