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

QueryProcessor::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 15
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 13
nc 1
nop 6
dl 0
loc 15
rs 9.4285
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\GraphQL\Visitors\CacheContextsCollector;
11
use Drupal\graphql\Plugin\SchemaPluginManager;
12
use Drupal\graphql\GraphQL\QueryProvider\QueryProviderInterface;
13
use GraphQL\Error\Error;
14
use GraphQL\Error\FormattedError;
15
use GraphQL\Executor\ExecutionResult;
16
use GraphQL\Executor\Executor;
17
use GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter;
18
use GraphQL\Executor\Promise\PromiseAdapter;
19
use GraphQL\Language\AST\DocumentNode;
20
use GraphQL\Language\Parser;
21
use GraphQL\Language\Visitor;
22
use GraphQL\Server\Helper;
23
use GraphQL\Server\OperationParams;
24
use GraphQL\Server\RequestError;
25
use GraphQL\Server\ServerConfig;
26
use GraphQL\Utils\AST;
27
use GraphQL\Utils\TypeInfo;
28
use GraphQL\Utils\Utils;
29
use GraphQL\Validator\DocumentValidator;
30
use GraphQL\Validator\Rules\AbstractValidationRule;
31
use GraphQL\Validator\ValidationContext;
32
33
class QueryProcessor {
34
35
  /**
36
   * The current user account.
37
   *
38
   * @var \Drupal\Core\Session\AccountProxyInterface
39
   */
40
  protected $currentUser;
41
42
  /**
43
   * The schema plugin manager.
44
   *
45
   * @var \Drupal\graphql\Plugin\SchemaPluginManager
46
   */
47
  protected $pluginManager;
48
49
  /**
50
   * The query provider service.
51
   *
52
   * @var \Drupal\graphql\GraphQL\QueryProvider\QueryProviderInterface
53
   */
54
  protected $queryProvider;
55
56
  /**
57
   * The cache backend for caching query results.
58
   *
59
   * @var \Drupal\Core\Cache\CacheBackendInterface
60
   */
61
  protected $cacheBackend;
62
63
  /**
64
   * The cache contexts manager service.
65
   *
66
   * @var \Drupal\Core\Cache\Context\CacheContextsManager
67
   */
68
  protected $contextsManager;
69
70
  /**
71
   * GraphQL configuration.
72
   *
73
   * @var array
74
   */
75
  protected $config;
76
77
  /**
78
   * Processor constructor.
79
   *
80
   * @param \Drupal\Core\Session\AccountProxyInterface $currentUser
81
   *   The current user.
82
   * @param \Drupal\Core\Cache\Context\CacheContextsManager $contextsManager
83
   *   The cache contexts manager service.
84
   * @param \Drupal\graphql\Plugin\SchemaPluginManager $pluginManager
85
   *   The schema plugin manager.
86
   * @param \Drupal\graphql\GraphQL\QueryProvider\QueryProviderInterface $queryProvider
87
   *   The query provider service.
88
   * @param \Drupal\Core\Cache\CacheBackendInterface $cacheBackend
89
   *   The cache backend for caching query results.
90
   */
91
  public function __construct(
92
    AccountProxyInterface $currentUser,
93
    CacheContextsManager $contextsManager,
94
    SchemaPluginManager $pluginManager,
95
    QueryProviderInterface $queryProvider,
96
    CacheBackendInterface $cacheBackend,
97
    array $config
98
  ) {
99
    $this->currentUser = $currentUser;
100
    $this->contextsManager = $contextsManager;
101
    $this->pluginManager = $pluginManager;
102
    $this->queryProvider = $queryProvider;
103
    $this->cacheBackend = $cacheBackend;
104
    $this->config = $config;
105
  }
106
107
  /**
108
   * Processes one or multiple graphql operations.
109
   *
110
   * @param string $schema
111
   *   The plugin id of the schema to use.
112
   * @param \GraphQL\Server\OperationParams|\GraphQL\Server\OperationParams[] $params
113
   *   The graphql operation(s) to execute.
114
   * @param array $globals
115
   *   The query context.
116
   *
117
   * @return \Drupal\graphql\GraphQL\Execution\QueryResult|\Drupal\graphql\GraphQL\Execution\QueryResult[]
118
   *   The query result.
119
   *
120
   */
121
  public function processQuery($schema, $params, array $globals = []) {
122
    // Load the plugin from the schema manager.
123
    $plugin = $this->pluginManager->createInstance($schema);
124
    $schema = $plugin->getSchema();
125
126
    // If the current user has appropriate permissions, allow to bypass
127
    // the secure fields restriction.
128
    $globals['bypass field security'] = $this->currentUser->hasPermission('bypass graphql field security');
129
130
    // Create the server config.
131
    $config = ServerConfig::create();
132
    $config->setDebug(!empty($this->config['development']));
133
    $config->setSchema($schema);
134
    $config->setQueryBatching(TRUE);
135
    $config->setContext(function () use ($globals, $plugin) {
136
      // Each document (e.g. in a batch query) gets its own resolve context but
137
      // the global parameters are shared. This allows us to collect the cache
138
      // metadata and contextual values (e.g. inheritance for language) for each
139
      // query separately.
140
      $context = new ResolveContext($globals);
141
      if ($plugin instanceof CacheableDependencyInterface) {
0 ignored issues
show
Bug introduced by
The class Drupal\Core\Cache\CacheableDependencyInterface does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
142
        $context->addCacheableDependency($plugin)->addCacheTags(['graphql_response']);
143
      }
144
      return $context;
145
    });
146
147
    $config->setValidationRules(function (OperationParams $params, DocumentNode $document, $operation) {
0 ignored issues
show
Unused Code introduced by
The parameter $document is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $operation is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
148
      if (isset($params->queryId)) {
149
        // Assume that pre-parsed documents are already validated. This allows
150
        // us to store pre-validated query documents e.g. for persisted queries
151
        // effectively improving performance by skipping run-time validation.
152
        return [];
153
      }
154
155
      return array_values(DocumentValidator::defaultRules());
156
    });
157
158
    $config->setPersistentQueryLoader(function ($id, OperationParams $params) {
159
      if ($query = $this->queryProvider->getQuery($id, $params)) {
160
        return $query;
161
      }
162
163
      throw new RequestError(sprintf("Failed to load query map for id '%s'.", $id));
164
    });
165
166
    if (is_array($params)) {
167
      return $this->executeBatch($config, $params);
168
    }
169
170
    return $this->executeSingle($config, $params);
171
  }
172
173
  /**
174
   * @param \GraphQL\Server\ServerConfig $config
175
   * @param \GraphQL\Server\OperationParams $params
176
   *
177
   * @return mixed
178
   */
179
  public function executeSingle(ServerConfig $config, OperationParams $params) {
180
    $adapter = new SyncPromiseAdapter();
181
    $result = $this->executeOperationWithReporting($adapter, $config, $params, FALSE);
182
    return $adapter->wait($result);
183
  }
184
185
  /**
186
   * @param \GraphQL\Server\ServerConfig $config
187
   * @param array $params
188
   *
189
   * @return mixed
190
   */
191
  public function executeBatch(ServerConfig $config, array $params) {
192
    $adapter = new SyncPromiseAdapter();
193
    $result = array_map(function ($params) use ($adapter, $config) {
194
      return $this->executeOperationWithReporting($adapter, $config, $params, TRUE);
195
    }, $params);
196
197
    $result = $adapter->all($result);
198
    return $adapter->wait($result);
199
  }
200
201
  /**
202
   * @param \GraphQL\Executor\Promise\PromiseAdapter $adapter
203
   * @param \GraphQL\Server\ServerConfig $config
204
   * @param \GraphQL\Server\OperationParams $params
205
   * @param bool $batching
206
   *
207
   * @return \GraphQL\Executor\Promise\Promise
208
   */
209
  protected function executeOperationWithReporting(PromiseAdapter $adapter, ServerConfig $config, OperationParams $params, $batching = FALSE) {
210
    $result = $this->executeOperation($adapter, $config, $params, $batching);
211
212
    // Format and print errors.
213
    return $result->then(function(QueryResult $result) use ($config) {
214
      if ($config->getErrorsHandler()) {
215
        $result->setErrorsHandler($config->getErrorsHandler());
216
      }
217
218
      if ($config->getErrorFormatter() || $config->getDebug()) {
219
        $result->setErrorFormatter(FormattedError::prepareFormatter($config->getErrorFormatter(), $config->getDebug()));
220
      }
221
222
      return $result;
223
    });
224
  }
225
226
  /**
227
   * @param \GraphQL\Executor\Promise\PromiseAdapter $adapter
228
   * @param \GraphQL\Server\ServerConfig $config
229
   * @param \GraphQL\Server\OperationParams $params
230
   * @param bool $batching
231
   *
232
   * @return \GraphQL\Executor\Promise\Promise
233
   */
234
  protected function executeOperation(PromiseAdapter $adapter, ServerConfig $config, OperationParams $params, $batching = FALSE) {
235
    try {
236
      if (!$config->getSchema()) {
237
        throw new \LogicException('Missing schema for query execution.');
238
      }
239
240
      if ($batching && !$config->getQueryBatching()) {
241
        throw new RequestError('Batched queries are not supported by this server.');
242
      }
243
244
      if ($errors = $this->validateOperationParams($params)) {
245
        return $adapter->createFulfilled(new QueryResult(NULL, $errors));
246
      }
247
248
      $document = $params->queryId ? $this->loadPersistedQuery($config, $params) : $params->query;
249
      if (!$document instanceof DocumentNode) {
250
        $document = Parser::parse($document);
251
      }
252
253
      // Read the operation type from the document. Subscriptions and mutations
254
      // only work through POST requests. One cannot have mutations and queries
255
      // in the same document, hence this check is sufficient.
256
      $operation = $params->operation;
257
      $type = AST::getOperation($document, $operation);
258
      if ($params->isReadOnly() && $type !== 'query') {
259
        throw new RequestError('GET requests are only supported for query operations.');
260
      }
261
262
      // If one of the validation rules found any problems, do not resolve the
263
      // query and bail out early instead.
264
      if ($errors = $this->validateOperation($config, $params, $document)) {
265
        return $adapter->createFulfilled(new QueryResult(NULL, $errors));
266
      }
267
268
      // Only queries can be cached (mutations and subscriptions can't).
269
      if ($type === 'query') {
270
        return $this->executeCacheableOperation($adapter, $config, $params, $document);
271
      }
272
273
      return $this->executeUncachableOperation($adapter, $config, $params, $document);
274
    }
275
    catch (RequestError $exception) {
276
      return $adapter->createFulfilled(new QueryResult(NULL, [Error::createLocatedError($exception)]));
277
    }
278
    catch (Error $exception) {
279
      return $adapter->createFulfilled(new QueryResult(NULL, [$exception]));
280
    }
281
  }
282
283
  /**
284
   * @param \GraphQL\Executor\Promise\PromiseAdapter $adapter
285
   * @param \GraphQL\Server\ServerConfig $config
286
   * @param \GraphQL\Server\OperationParams $params
287
   * @param \GraphQL\Language\AST\DocumentNode $document
288
   *
289
   * @return \GraphQL\Executor\Promise\Promise|mixed
290
   */
291
  protected function executeCacheableOperation(PromiseAdapter $adapter, ServerConfig $config, OperationParams $params, DocumentNode $document) {
292
    $schema = $config->getSchema();
293
294
    // Collect cache contexts from the query document.
295
    $contexts = [];
296
    $info = new TypeInfo($schema);
297
    $visitor = (new CacheContextsCollector())->getVisitor($info, $contexts);
298
    Visitor::visit($document, Visitor::visitWithTypeInfo($info, $visitor));
299
300
    // Generate a cache identifier from the collected contexts.
301
    $metadata = (new CacheableMetadata())->addCacheContexts($contexts);
302
    $cid = $this->cacheIdentifier($params, $document, $metadata);
303
    if (!$config->getDebug() && ($cache = $this->cacheBackend->get($cid)) && $result = $cache->data) {
304
      return $adapter->createFulfilled($result);
305
    }
306
307
    $result = $this->doExecuteOperation($adapter, $config, $params, $document);
308
    return $result->then(function (QueryResult $result) use ($cid, $metadata) {
309
      if ($missing = array_diff($result->getCacheContexts(), $metadata->getCacheContexts())) {
310
        throw new \LogicException(sprintf(
311
          'The query result yielded cache contexts (%s) that were not part of the static query analysis.',
312
          implode(', ', $missing)
313
        ));
314
      }
315
316
      // Add the statically collected cache contexts.
317
      $result->addCacheableDependency($metadata);
318
      // Write this query into the cache if it is cacheable.
319
      if ($result->getCacheMaxAge() !== 0) {
320
        $this->cacheBackend->set($cid, $result, $result->getCacheMaxAge(), $result->getCacheTags());
321
      }
322
323
      return $result;
324
    });
325
  }
326
327
  /**
328
   * @param \GraphQL\Executor\Promise\PromiseAdapter $adapter
329
   * @param \GraphQL\Server\ServerConfig $config
330
   * @param \GraphQL\Server\OperationParams $params
331
   * @param \GraphQL\Language\AST\DocumentNode $document
332
   *
333
   * @return \GraphQL\Executor\Promise\Promise
334
   */
335
  protected function executeUncachableOperation(PromiseAdapter $adapter, ServerConfig $config, OperationParams $params, DocumentNode $document) {
336
    $result = $this->doExecuteOperation($adapter, $config, $params, $document);
337
    return $result->then(function (QueryResult $result) {
338
      // Mark the query result as uncacheable.
339
      $result->mergeCacheMaxAge(0);
340
      return $result;
341
    });
342
  }
343
344
  /**
345
   * @param \GraphQL\Executor\Promise\PromiseAdapter $adapter
346
   * @param \GraphQL\Server\ServerConfig $config
347
   * @param \GraphQL\Server\OperationParams $params
348
   * @param \GraphQL\Language\AST\DocumentNode $document
349
   *
350
   * @return \GraphQL\Executor\Promise\Promise
351
   */
352
  protected function doExecuteOperation(PromiseAdapter $adapter, ServerConfig $config, OperationParams $params, DocumentNode $document) {
353
    $operation = $params->operation;
354
    $variables = $params->variables;
355
    $context = $this->resolveContextValue($config, $params, $document, $operation);
356
    $root = $this->resolveRootValue($config, $params, $document, $operation);
357
    $resolver = $config->getFieldResolver();
358
    $schema = $config->getSchema();
359
360
    $promise = Executor::promiseToExecute(
361
      $adapter,
362
      $schema,
363
      $document,
364
      $root,
365
      $context,
366
      $variables,
367
      $operation,
368
      $resolver
369
    );
370
371
    return $promise->then(function (ExecutionResult $result) use ($context) {
372
      // Add the collected cache metadata to the result.
373
      $metadata = (new CacheableMetadata())->addCacheableDependency($context);
374
375
      // Do not cache in development mode or if there are any errors.
376
      if ($context->getGlobal('development') || !empty($result->errors)) {
377
        $metadata->setCacheMaxAge(0);
378
      }
379
380
      return new QueryResult($result->data, $result->errors, $result->extensions, $metadata);
381
    });
382
  }
383
384
  /**
385
   * @param \GraphQL\Server\OperationParams $params
386
   *
387
   * @return array
388
   */
389
  protected function validateOperationParams(OperationParams $params) {
390
    $errors = (new Helper())->validateOperationParams($params);
391
    return array_map(function (RequestError $error) {
392
      return Error::createLocatedError($error, NULL, NULL);
393
    }, $errors);
394
  }
395
396
  /**
397
   * @param \GraphQL\Server\ServerConfig $config
398
   * @param \GraphQL\Server\OperationParams $params
399
   * @param \GraphQL\Language\AST\DocumentNode $document
400
   *
401
   * @return \GraphQL\Error\Error[]
402
   */
403
  protected function validateOperation(ServerConfig $config, OperationParams $params, DocumentNode $document) {
404
    $operation = $params->operation;
405
    // Skip validation if there are no validation rules to be applied.
406
    if (!$rules = $this->resolveValidationRules($config, $params, $document, $operation)) {
407
      return [];
408
    }
409
410
    $schema = $config->getSchema();
411
    $info = new TypeInfo($schema);
412
    $validation = new ValidationContext($schema, $document, $info);
413
    $visitors = array_values(array_map(function (AbstractValidationRule $rule) use ($validation) {
414
      return $rule($validation);
415
    }, $rules));
416
417
    // Run the query visitor with the prepared validation rules and the cache
418
    // metadata collector and query complexity calculator.
419
    Visitor::visit($document, Visitor::visitWithTypeInfo($info, Visitor::visitInParallel($visitors)));
420
421
    // Return any possible errors collected during validation.
422
    return $validation->getErrors();
423
  }
424
425
  /**
426
   * @param \GraphQL\Server\ServerConfig $config
427
   * @param \GraphQL\Server\OperationParams $params
428
   * @param \GraphQL\Language\AST\DocumentNode $document
429
   * @param $operation
430
   *
431
   * @return mixed
432
   */
433 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...
434
    $root = $config->getRootValue();
435
    if (is_callable($root)) {
436
      $root = $root($params, $document, $operation);
437
    }
438
439
    return $root;
440
  }
441
442
  /**
443
   * @param \GraphQL\Server\ServerConfig $config
444
   * @param \GraphQL\Server\OperationParams $params
445
   * @param \GraphQL\Language\AST\DocumentNode $document
446
   * @param $operation
447
   *
448
   * @return mixed
449
   */
450 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...
451
    $context = $config->getContext();
452
    if (is_callable($context)) {
453
      $context = $context($params, $document, $operation);
454
    }
455
456
    return $context;
457
  }
458
459
  /**
460
   * @param \GraphQL\Server\ServerConfig $config
461
   * @param \GraphQL\Server\OperationParams $params
462
   * @param \GraphQL\Language\AST\DocumentNode $document
463
   * @param $operation
464
   *
465
   * @return array
466
   */
467
  protected function resolveValidationRules(ServerConfig $config, OperationParams $params, DocumentNode $document, $operation) {
468
    // Allow customizing validation rules per operation:
469
    $rules = $config->getValidationRules();
470
    if (is_callable($rules)) {
471
      $rules = $rules($params, $document, $operation);
472
      if (!is_array($rules)) {
473
        throw new \LogicException(sprintf("Expecting validation rules to be array or callable returning array, but got: %s", Utils::printSafe($rules)));
474
      }
475
    }
476
477
    return $rules;
478
  }
479
480
  /**
481
   * @param \GraphQL\Server\ServerConfig $config
482
   * @param \GraphQL\Server\OperationParams $params
483
   *
484
   * @return mixed
485
   * @throws \GraphQL\Server\RequestError
486
   */
487
  protected function loadPersistedQuery(ServerConfig $config, OperationParams $params) {
488
    if (!$loader = $config->getPersistentQueryLoader()) {
489
      throw new RequestError('Persisted queries are not supported by this server.');
490
    }
491
492
    $source = $loader($params->queryId, $params);
493
    if (!is_string($source) && !$source instanceof DocumentNode) {
494
      throw new \LogicException(sprintf('The persisted query loader must return query string or instance of %s but got: %s.', DocumentNode::class, Utils::printSafe($source)));
495
    }
496
497
    return $source;
498
  }
499
500
  /**
501
   * @param \GraphQL\Language\AST\DocumentNode $document
502
   *
503
   * @return array
504
   */
505
  protected function serializeDocument(DocumentNode $document) {
506
    return $this->sanitizeRecursive(AST::toArray($document));
507
  }
508
509
  /**
510
   * @param array $item
511
   *
512
   * @return array
513
   */
514
  protected function sanitizeRecursive(array $item) {
515
    unset($item['loc']);
516
517
    foreach ($item as &$value) {
518
      if (is_array($value)) {
519
        $value = $this->sanitizeRecursive($value);
520
      }
521
    }
522
523
    return $item;
524
  }
525
526
  /**
527
   * @param \GraphQL\Server\OperationParams $params
528
   * @param \GraphQL\Language\AST\DocumentNode $document
529
   * @param \Drupal\Core\Cache\CacheableMetadata $metadata
530
   *
531
   * @return string
532
   */
533
  protected function cacheIdentifier(OperationParams $params, DocumentNode $document, CacheableMetadata $metadata) {
534
    $contexts = $metadata->getCacheContexts();
535
    $keys = $this->contextsManager->convertTokensToKeys($contexts)->getKeys();
536
537
    // Sorting the variables will cause fewer cache vectors.
538
    $variables = $params->variables ?: [];
539
    ksort($variables);
540
541
    // Prepend the hash of the serialized document to the cache contexts.
542
    $hash = hash('sha256', json_encode([
543
      'query' => $this->serializeDocument($document),
544
      'variables' => $variables,
545
    ]));
546
547
    return implode(':', array_values(array_merge([$hash], $keys)));
548
  }
549
}
550