Completed
Push — 8.x-3.x ( 6608c9...0ba9eb )
by Philipp
02:38
created

QueryProcessor::processQuery()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 2
dl 0
loc 11
rs 9.9
c 0
b 0
f 0
1
<?php
2
3
namespace Drupal\graphql\GraphQL\Execution;
4
5
use Drupal\Core\Cache\CacheableDependencyInterface;
6
use Drupal\Core\Cache\CacheableMetadata;
7
use Drupal\Core\Cache\CacheBackendInterface;
8
use Drupal\Core\Cache\Context\CacheContextsManager;
9
use Drupal\Core\Session\AccountProxyInterface;
10
use Drupal\graphql\Plugin\SchemaPluginManager;
11
use Drupal\graphql\GraphQL\QueryProvider\QueryProviderInterface;
12
use GraphQL\Error\Error;
13
use GraphQL\Error\FormattedError;
14
use GraphQL\Executor\ExecutionResult;
15
use GraphQL\Executor\Executor;
16
use GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter;
17
use GraphQL\Executor\Promise\PromiseAdapter;
18
use GraphQL\Language\AST\DocumentNode;
19
use GraphQL\Language\Parser;
20
use GraphQL\Language\Visitor;
21
use GraphQL\Server\Helper;
22
use GraphQL\Server\OperationParams;
23
use GraphQL\Server\RequestError;
24
use GraphQL\Server\ServerConfig;
25
use GraphQL\Utils\AST;
26
use GraphQL\Utils\TypeInfo;
27
use GraphQL\Utils\Utils;
28
use GraphQL\Validator\DocumentValidator;
29
use GraphQL\Validator\Rules\AbstractValidationRule;
30
use GraphQL\Validator\ValidationContext;
31
32
// TODO: Refactor this and clean it up.
33
class QueryProcessor {
34
35
  /**
36
   * The schema plugin manager.
37
   *
38
   * @var \Drupal\graphql\Plugin\SchemaPluginManager
39
   */
40
  protected $pluginManager;
41
42
  /**
43
   * The cache backend for caching query results.
44
   *
45
   * @var \Drupal\Core\Cache\CacheBackendInterface
46
   */
47
  protected $cacheBackend;
48
49
  /**
50
   * The cache contexts manager service.
51
   *
52
   * @var \Drupal\Core\Cache\Context\CacheContextsManager
53
   */
54
  protected $contextsManager;
55
56
  /**
57
   * Processor constructor.
58
   *
59
   * @param \Drupal\Core\Cache\Context\CacheContextsManager $contextsManager
60
   *   The cache contexts manager service.
61
   * @param \Drupal\graphql\Plugin\SchemaPluginManager $pluginManager
62
   *   The schema plugin manager.
63
   * @param \Drupal\Core\Cache\CacheBackendInterface $cacheBackend
64
   *   The cache backend for caching query results.
65
   */
66
  public function __construct(
67
    CacheContextsManager $contextsManager,
68
    SchemaPluginManager $pluginManager,
69
    CacheBackendInterface $cacheBackend
70
  ) {
71
    $this->contextsManager = $contextsManager;
72
    $this->pluginManager = $pluginManager;
73
    $this->cacheBackend = $cacheBackend;
74
  }
75
76
  /**
77
   * Processes one or multiple graphql operations.
78
   *
79
   * @param string $schema
80
   *   The plugin id of the schema to use.
81
   * @param \GraphQL\Server\OperationParams|\GraphQL\Server\OperationParams[] $params
82
   *   The graphql operation(s) to execute.
83
   *
84
   * @return \Drupal\graphql\GraphQL\Execution\QueryResult|\Drupal\graphql\GraphQL\Execution\QueryResult[]
85
   *   The query result.
86
   *
87
   * @throws \Drupal\Component\Plugin\Exception\PluginException
88
   */
89
  public function processQuery($schema, $params) {
90
    // Load the plugin from the schema manager.
91
    $plugin = $this->pluginManager->createInstance($schema);
92
    $config = $plugin->getServer();
93
94
    if (is_array($params)) {
95
      return $this->executeBatch($config, $params);
96
    }
97
98
    return $this->executeSingle($config, $params);
99
  }
100
101
  /**
102
   * @param \GraphQL\Server\ServerConfig $config
103
   * @param \GraphQL\Server\OperationParams $params
104
   *
105
   * @return mixed
106
   */
107
  public function executeSingle(ServerConfig $config, OperationParams $params) {
108
    $adapter = new SyncPromiseAdapter();
109
    $result = $this->executeOperationWithReporting($adapter, $config, $params, FALSE);
110
    return $adapter->wait($result);
111
  }
112
113
  /**
114
   * @param \GraphQL\Server\ServerConfig $config
115
   * @param array $params
116
   *
117
   * @return mixed
118
   */
119
  public function executeBatch(ServerConfig $config, array $params) {
120
    $adapter = new SyncPromiseAdapter();
121
    $result = array_map(function ($params) use ($adapter, $config) {
122
      return $this->executeOperationWithReporting($adapter, $config, $params, TRUE);
123
    }, $params);
124
125
    $result = $adapter->all($result);
126
    return $adapter->wait($result);
127
  }
128
129
  /**
130
   * @param \GraphQL\Executor\Promise\PromiseAdapter $adapter
131
   * @param \GraphQL\Server\ServerConfig $config
132
   * @param \GraphQL\Server\OperationParams $params
133
   * @param bool $batching
134
   *
135
   * @return \GraphQL\Executor\Promise\Promise
136
   */
137
  protected function executeOperationWithReporting(PromiseAdapter $adapter, ServerConfig $config, OperationParams $params, $batching = FALSE) {
138
    $result = $this->executeOperation($adapter, $config, $params, $batching);
139
140
    // Format and print errors.
141
    return $result->then(function(QueryResult $result) use ($config) {
142
      if ($config->getErrorsHandler()) {
143
        $result->setErrorsHandler($config->getErrorsHandler());
144
      }
145
146
      if ($config->getErrorFormatter() || $config->getDebug()) {
147
        $result->setErrorFormatter(FormattedError::prepareFormatter($config->getErrorFormatter(), $config->getDebug()));
148
      }
149
150
      return $result;
151
    });
152
  }
153
154
  /**
155
   * @param \GraphQL\Executor\Promise\PromiseAdapter $adapter
156
   * @param \GraphQL\Server\ServerConfig $config
157
   * @param \GraphQL\Server\OperationParams $params
158
   * @param bool $batching
159
   *
160
   * @return \GraphQL\Executor\Promise\Promise
161
   */
162
  protected function executeOperation(PromiseAdapter $adapter, ServerConfig $config, OperationParams $params, $batching = FALSE) {
163
    try {
164
      if (!$config->getSchema()) {
165
        throw new \LogicException('Missing schema for query execution.');
166
      }
167
168
      if ($batching && !$config->getQueryBatching()) {
169
        throw new RequestError('Batched queries are not supported by this server.');
170
      }
171
172
      if ($errors = $this->validateOperationParams($params)) {
173
        return $adapter->createFulfilled(new QueryResult(NULL, $errors));
174
      }
175
176
      $document = $params->queryId ? $this->loadPersistedQuery($config, $params) : $params->query;
177
      if (!$document instanceof DocumentNode) {
178
        $document = Parser::parse($document);
179
      }
180
181
      // Read the operation type from the document. Subscriptions and mutations
182
      // only work through POST requests. One cannot have mutations and queries
183
      // in the same document, hence this check is sufficient.
184
      $operation = $params->operation;
185
      $type = AST::getOperation($document, $operation);
186
      if ($params->isReadOnly() && $type !== 'query') {
187
        throw new RequestError('GET requests are only supported for query operations.');
188
      }
189
190
      // If one of the validation rules found any problems, do not resolve the
191
      // query and bail out early instead.
192
      if ($errors = $this->validateOperation($config, $params, $document)) {
193
        return $adapter->createFulfilled(new QueryResult(NULL, $errors));
194
      }
195
196
      // Only queries can be cached (mutations and subscriptions can't).
197
      if ($type === 'query') {
198
        return $this->executeCacheableOperation($adapter, $config, $params, $document);
199
      }
200
201
      return $this->executeUncachableOperation($adapter, $config, $params, $document);
202
    }
203
    catch (RequestError $exception) {
204
      return $adapter->createFulfilled(new QueryResult(NULL, [Error::createLocatedError($exception)]));
205
    }
206
    catch (Error $exception) {
207
      return $adapter->createFulfilled(new QueryResult(NULL, [$exception]));
208
    }
209
  }
210
211
  /**
212
   * @param \GraphQL\Executor\Promise\PromiseAdapter $adapter
213
   * @param \GraphQL\Server\ServerConfig $config
214
   * @param \GraphQL\Server\OperationParams $params
215
   * @param \GraphQL\Language\AST\DocumentNode $document
216
   *
217
   * @return \GraphQL\Executor\Promise\Promise|mixed
218
   */
219
  protected function executeCacheableOperation(PromiseAdapter $adapter, ServerConfig $config, OperationParams $params, DocumentNode $document) {
220
    $contextCacheId = 'ccid:' . $this->cacheIdentifier($params, $document, new CacheableMetadata());
221
222
    if (!$config->getDebug() && ($contextCache = $this->cacheBackend->get($contextCacheId)) && $contexts = $contextCache->data) {
223
      $cacheId = 'cid:' . $this->cacheIdentifier($params, $document, (new CacheableMetadata())->addCacheContexts($contexts));
224
      if (($cache = $this->cacheBackend->get($cacheId)) && $result = $cache->data) {
225
        return $adapter->createFulfilled($result);
226
      }
227
    }
228
229
    $result = $this->doExecuteOperation($adapter, $config, $params, $document);
230
231
    return $result->then(function (QueryResult $result) use ($contextCacheId, $params, $document) {
232
      // Write this query into the cache if it is cacheable.
233
      if ($result->getCacheMaxAge() !== 0) {
234
        $cacheId = 'cid:' . $this->cacheIdentifier($params, $document, (new CacheableMetadata())->addCacheContexts($result->getCacheContexts()));
235
        $this->cacheBackend->set($contextCacheId, $result->getCacheContexts(), $result->getCacheMaxAge(), $result->getCacheTags());
236
        $this->cacheBackend->set($cacheId, $result, $result->getCacheMaxAge(), $result->getCacheTags());
237
      }
238
      return $result;
239
    });
240
  }
241
242
  /**
243
   * @param \GraphQL\Executor\Promise\PromiseAdapter $adapter
244
   * @param \GraphQL\Server\ServerConfig $config
245
   * @param \GraphQL\Server\OperationParams $params
246
   * @param \GraphQL\Language\AST\DocumentNode $document
247
   *
248
   * @return \GraphQL\Executor\Promise\Promise
249
   */
250
  protected function executeUncachableOperation(PromiseAdapter $adapter, ServerConfig $config, OperationParams $params, DocumentNode $document) {
251
    $result = $this->doExecuteOperation($adapter, $config, $params, $document);
252
    return $result->then(function (QueryResult $result) {
253
      // Mark the query result as uncacheable.
254
      $result->mergeCacheMaxAge(0);
255
      return $result;
256
    });
257
  }
258
259
  /**
260
   * @param \GraphQL\Executor\Promise\PromiseAdapter $adapter
261
   * @param \GraphQL\Server\ServerConfig $config
262
   * @param \GraphQL\Server\OperationParams $params
263
   * @param \GraphQL\Language\AST\DocumentNode $document
264
   *
265
   * @return \GraphQL\Executor\Promise\Promise
266
   */
267
  protected function doExecuteOperation(PromiseAdapter $adapter, ServerConfig $config, OperationParams $params, DocumentNode $document) {
268
    $operation = $params->operation;
269
    $variables = $params->variables;
270
    $context = $this->resolveContextValue($config, $params, $document, $operation);
271
    $root = $this->resolveRootValue($config, $params, $document, $operation);
272
    $resolver = $config->getFieldResolver();
273
    $schema = $config->getSchema();
274
275
    $promise = Executor::promiseToExecute(
276
      $adapter,
277
      $schema,
278
      $document,
279
      $root,
280
      $context,
281
      $variables,
282
      $operation,
283
      $resolver
284
    );
285
286
    return $promise->then(function (ExecutionResult $result) use ($context) {
287
288
      $metadata = (new CacheableMetadata())
289
        ->addCacheContexts($this->filterCacheContexts($context->getCacheContexts()))
290
        ->addCacheTags($context->getCacheTags())
291
        ->setCacheMaxAge($context->getCacheMaxAge());
292
293
      // Do not cache in development mode or if there are any errors.
294
      if ($context->getGlobal('development') || !empty($result->errors)) {
295
        $metadata->setCacheMaxAge(0);
296
      }
297
298
      return new QueryResult($result->data, $result->errors, $result->extensions, $metadata);
299
    });
300
  }
301
302
  /**
303
   * @param \GraphQL\Server\OperationParams $params
304
   *
305
   * @return array
306
   */
307
  protected function validateOperationParams(OperationParams $params) {
308
    $errors = (new Helper())->validateOperationParams($params);
309
    return array_map(function (RequestError $error) {
310
      return Error::createLocatedError($error, NULL, NULL);
311
    }, $errors);
312
  }
313
314
  /**
315
   * @param \GraphQL\Server\ServerConfig $config
316
   * @param \GraphQL\Server\OperationParams $params
317
   * @param \GraphQL\Language\AST\DocumentNode $document
318
   *
319
   * @return \GraphQL\Error\Error[]
320
   * @throws \Exception
321
   */
322
  protected function validateOperation(ServerConfig $config, OperationParams $params, DocumentNode $document) {
323
    $operation = $params->operation;
324
    // Skip validation if there are no validation rules to be applied.
325
    if (!$rules = $this->resolveValidationRules($config, $params, $document, $operation)) {
326
      return [];
327
    }
328
329
    $schema = $config->getSchema();
330
    $info = new TypeInfo($schema);
331
    $validation = new ValidationContext($schema, $document, $info);
332
    $visitors = array_values(array_map(function (AbstractValidationRule $rule) use ($validation) {
333
      return $rule($validation);
334
    }, $rules));
335
336
    // Run the query visitor with the prepared validation rules and the cache
337
    // metadata collector and query complexity calculator.
338
    Visitor::visit($document, Visitor::visitWithTypeInfo($info, Visitor::visitInParallel($visitors)));
339
340
    // Return any possible errors collected during validation.
341
    return $validation->getErrors();
342
  }
343
344
  /**
345
   * @param \GraphQL\Server\ServerConfig $config
346
   * @param \GraphQL\Server\OperationParams $params
347
   * @param \GraphQL\Language\AST\DocumentNode $document
348
   * @param $operation
349
   *
350
   * @return mixed
351
   */
352 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...
353
    $root = $config->getRootValue();
354
    if (is_callable($root)) {
355
      $root = $root($params, $document, $operation);
356
    }
357
358
    return $root;
359
  }
360
361
  /**
362
   * @param \GraphQL\Server\ServerConfig $config
363
   * @param \GraphQL\Server\OperationParams $params
364
   * @param \GraphQL\Language\AST\DocumentNode $document
365
   * @param $operation
366
   *
367
   * @return mixed
368
   */
369 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...
370
    $context = $config->getContext();
371
    if (is_callable($context)) {
372
      $context = $context($params, $document, $operation);
373
    }
374
375
    return $context;
376
  }
377
378
  /**
379
   * @param \GraphQL\Server\ServerConfig $config
380
   * @param \GraphQL\Server\OperationParams $params
381
   * @param \GraphQL\Language\AST\DocumentNode $document
382
   * @param $operation
383
   *
384
   * @return array
385
   */
386
  protected function resolveValidationRules(ServerConfig $config, OperationParams $params, DocumentNode $document, $operation) {
387
    // Allow customizing validation rules per operation:
388
    $rules = $config->getValidationRules();
389
    if (is_callable($rules)) {
390
      $rules = $rules($params, $document, $operation);
391
      if (!is_array($rules)) {
392
        throw new \LogicException(sprintf("Expecting validation rules to be array or callable returning array, but got: %s", Utils::printSafe($rules)));
393
      }
394
    }
395
396
    return $rules;
397
  }
398
399
  /**
400
   * @param \GraphQL\Server\ServerConfig $config
401
   * @param \GraphQL\Server\OperationParams $params
402
   *
403
   * @return mixed
404
   * @throws \GraphQL\Server\RequestError
405
   */
406
  protected function loadPersistedQuery(ServerConfig $config, OperationParams $params) {
407
    if (!$loader = $config->getPersistentQueryLoader()) {
408
      throw new RequestError('Persisted queries are not supported by this server.');
409
    }
410
411
    $source = $loader($params->queryId, $params);
412
    if (!is_string($source) && !$source instanceof DocumentNode) {
413
      throw new \LogicException(sprintf('The persisted query loader must return query string or instance of %s but got: %s.', DocumentNode::class, Utils::printSafe($source)));
414
    }
415
416
    return $source;
417
  }
418
419
  /**
420
   * @param \GraphQL\Language\AST\DocumentNode $document
421
   *
422
   * @return array
423
   */
424
  protected function serializeDocument(DocumentNode $document) {
425
    return $this->sanitizeRecursive(AST::toArray($document));
426
  }
427
428
  /**
429
   * @param array $item
430
   *
431
   * @return array
432
   */
433
  protected function sanitizeRecursive(array $item) {
434
    unset($item['loc']);
435
436
    foreach ($item as &$value) {
437
      if (is_array($value)) {
438
        $value = $this->sanitizeRecursive($value);
439
      }
440
    }
441
442
    return $item;
443
  }
444
445
  /**
446
   * @param \GraphQL\Server\OperationParams $params
447
   * @param \GraphQL\Language\AST\DocumentNode $document
448
   * @param \Drupal\Core\Cache\CacheableMetadata $metadata
449
   *
450
   * @return string
451
   */
452
  protected function cacheIdentifier(OperationParams $params, DocumentNode $document, CacheableMetadata $metadata) {
453
    // Ignore language contexts since they are handled by graphql internally.
454
    $contexts = $this->filterCacheContexts($metadata->getCacheContexts());
455
    $keys = $this->contextsManager->convertTokensToKeys($contexts)->getKeys();
456
457
    // Sorting the variables will cause fewer cache vectors.
458
    $variables = $params->variables ?: [];
459
    ksort($variables);
460
461
    // Prepend the hash of the serialized document to the cache contexts.
462
    $hash = hash('sha256', json_encode([
463
      'query' => $this->serializeDocument($document),
464
      'variables' => $variables,
465
    ]));
466
467
    return implode(':', array_values(array_merge([$hash], $keys)));
468
  }
469
470
  /**
471
   * Filter unused contexts.
472
   *
473
   * Removes the language contexts from a list of context ids.
474
   *
475
   * @param string[] $contexts
476
   *   The list of context id's.
477
   *
478
   * @return string[]
479
   *   The filtered list of context id's.
480
   */
481
  protected function filterCacheContexts(array $contexts) {
482
    return array_filter($contexts, function ($context) {
483
      return strpos($context, 'languages:') !== 0;
484
    });
485
  }
486
}
487