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

QueryProcessor::executeOperation()   D

Complexity

Conditions 17
Paths 159

Size

Total Lines 103
Code Lines 56

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 17
eloc 56
nc 159
nop 4
dl 0
loc 103
rs 4.5364
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\CacheMetadataCalculator;
8
use Drupal\graphql\GraphQL\Visitors\ComplexityCalculator;
9
use Drupal\graphql\GraphQL\Visitors\QueryEdgeCollector;
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
class QueryProcessor {
33
34
  /**
35
   * The current user account.
36
   *
37
   * @var \Drupal\Core\Session\AccountProxyInterface
38
   */
39
  protected $currentUser;
40
41
  /**
42
   * The schema plugin manager.
43
   *
44
   * @var \Drupal\graphql\Plugin\SchemaPluginManager
45
   */
46
  protected $pluginManager;
47
48
  /**
49
   * The query provider service.
50
   *
51
   * @var \Drupal\graphql\GraphQL\QueryProvider\QueryProviderInterface
52
   */
53
  protected $queryProvider;
54
55
  /**
56
   * Processor constructor.
57
   *
58
   * @param \Drupal\Core\Session\AccountProxyInterface $currentUser
59
   *   The current user.
60
   * @param \Drupal\graphql\Plugin\SchemaPluginManager $pluginManager
61
   *   The schema plugin manager.
62
   * @param \Drupal\graphql\GraphQL\QueryProvider\QueryProviderInterface $queryProvider
63
   *   The query provider service.
64
   */
65
  public function __construct(
66
    AccountProxyInterface $currentUser,
67
    SchemaPluginManager $pluginManager,
68
    QueryProviderInterface $queryProvider
69
  ) {
70
    $this->currentUser = $currentUser;
71
    $this->pluginManager = $pluginManager;
72
    $this->queryProvider = $queryProvider;
73
  }
74
75
  /**
76
   * Processes one or multiple graphql operations.
77
   *
78
   * @param string $schema
79
   *   The plugin id of the schema to use.
80
   * @param \GraphQL\Server\OperationParams|\GraphQL\Server\OperationParams[] $params
81
   *   The graphql operation(s) to execute.
82
   * @param array $globals
83
   *   The query context.
84
   *
85
   * @return \Drupal\graphql\GraphQL\Execution\QueryResult|\Drupal\graphql\GraphQL\Execution\QueryResult[]
86
   *   The query result.
87
   *
88
   */
89
  public function processQuery($schema, $params, array $globals = []) {
90
    // Load the plugin from the schema manager.
91
    $plugin = $this->pluginManager->createInstance($schema);
92
    $schema = $plugin->getSchema();
93
94
    // If the current user has appropriate permissions, allow to bypass
95
    // the secure fields restriction.
96
    $globals['bypass field security'] = $this->currentUser->hasPermission('bypass graphql field security');
97
98
    // Create the server config.
99
    $config = ServerConfig::create();
100
    $config->setDebug(!empty($globals['development']));
101
    $config->setSchema($schema);
102
    $config->setQueryBatching(TRUE);
103
    $config->setContext(function () use ($globals) {
104
      // Each document (e.g. in a batch query) gets its own resolve context but
105
      // the global parameters are shared. This allows us to collect the cache
106
      // metadata and contextual values (e.g. inheritance for language) for each
107
      // query separately.
108
      return new ResolveContext($globals);
109
    });
110
111
    $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...
112
      if (!isset($params->queryId)) {
113
        // Assume that pre-parsed documents are already validated. This allows
114
        // us to store pre-validated query documents e.g. for persisted queries
115
        // effectively improving performance by skipping run-time validation.
116
        return [];
117
      }
118
119
      return DocumentValidator::allRules();
120
    });
121
122
    $config->setPersistentQueryLoader(function ($id, OperationParams $params) {
123
      if ($query = $this->queryProvider->getQuery($id, $params)) {
124
        return $query;
125
      }
126
127
      throw new RequestError(sprintf("Failed to load query map for id '%s'.", $id));
128
    });
129
130
    if (is_array($params)) {
131
      return $this->executeBatch($config, $params);
132
    }
133
134
    return $this->executeSingle($config, $params);
135
  }
136
137
  /**
138
   * @param \GraphQL\Server\ServerConfig $config
139
   * @param \GraphQL\Server\OperationParams $params
140
   *
141
   * @return mixed
142
   */
143
  public function executeSingle(ServerConfig $config, OperationParams $params) {
144
    $adapter = new SyncPromiseAdapter();
145
    $result = $this->executeOperation($adapter, $config, $params, FALSE);
146
    return $adapter->wait($result);
147
  }
148
149
  /**
150
   * @param \GraphQL\Server\ServerConfig $config
151
   * @param array $params
152
   *
153
   * @return mixed
154
   */
155
  public function executeBatch(ServerConfig $config, array $params) {
156
    $adapter = new SyncPromiseAdapter();
157
    $result = array_map(function ($params) use ($adapter, $config) {
158
      return $this->executeOperation($adapter, $config, $params, TRUE);
159
    }, $params);
160
161
    $result = $adapter->all($result);
162
    return $adapter->wait($result);
163
  }
164
165
  /**
166
   * @param \GraphQL\Executor\Promise\PromiseAdapter $adapter
167
   * @param \GraphQL\Server\ServerConfig $config
168
   * @param \GraphQL\Server\OperationParams $params
169
   * @param bool $batching
170
   *
171
   * @return \GraphQL\Executor\Promise\Promise
172
   */
173
  protected function executeOperation(PromiseAdapter $adapter, ServerConfig $config, OperationParams $params, $batching = FALSE) {
174
    try {
175
      if (!$config->getSchema()) {
176
        throw new \LogicException('Missing schema for query execution.');
177
      }
178
179
      if ($batching && !$config->getQueryBatching()) {
180
        throw new RequestError('Batched queries are not supported by this server.');
181
      }
182
183
      if ($errors = (new Helper())->validateOperationParams($params)) {
184
        $errors = Utils::map($errors, function (RequestError $err) {
185
          return Error::createLocatedError($err, NULL, NULL);
186
        });
187
188
        return $adapter->createFulfilled(new QueryResult(NULL, $errors));
189
      }
190
191
      $schema = $config->getSchema();
192
      $variables = $params->variables;
193
      $operation = $params->operation;
194
      $document = $params->queryId ? $this->loadPersistedQuery($config, $params) : $params->query;
195
      if (!$document instanceof DocumentNode) {
196
        $document = Parser::parse($document);
197
      }
198
199
      // Read the operation type from the document. Subscriptions and mutations
200
      // only work through POST requests. One cannot have mutations and queries
201
      // in the same document, hence this check is sufficient.
202
      $type = AST::getOperation($document, $operation);
203
      if ($params->isReadOnly() && $type !== 'query') {
204
        throw new RequestError('GET requests are only supported for query operations.');
205
      }
206
207
      $context = $this->resolveContextValue($config, $params, $document, $operation);
208
      $complexity = $this->resolveAllowedComplexity($config, $params, $document, $operation);
209
      $rules = $this->resolveValidationRules($config, $params, $document, $operation);
210
      $info = new TypeInfo($schema);
211
      $validation = new ValidationContext($schema, $document, $info);
212
213
      // Add a special visitor to the set of validation rules which will collect
214
      // all nodes and fields (all possible edges) from the document to allow
215
      // for static query analysis (e.g. for calculating the query complexity or
216
      // for determining the cache metadata of a query so we can perform cache
217
      // lookups).
218
      $visitors = array_map(function (AbstractValidationRule $rule) use ($validation, $context) {
219
        return $rule->getVisitor($validation);
220
      }, array_merge($rules, [new QueryEdgeCollector(array_filter([
221
        // Query operations can be cached. Collect static cache metadata from
222
        // the document during the visitor phase.
223
        $type === 'query' ? new CacheMetadataCalculator($context) : NULL,
224
        $complexity !== NULL ? new ComplexityCalculator($complexity) : NULL,
0 ignored issues
show
Unused Code introduced by
The call to ComplexityCalculator::__construct() has too many arguments starting with $complexity.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
225
      ]))]));
226
227
      // Run the query visitor with the prepared validation rules and the cache
228
      // metadata collector and query complexity calculator.
229
      Visitor::visit($document, Visitor::visitWithTypeInfo($info, Visitor::visitInParallel($visitors)));
230
231
      // If one of the validation rules found any problems, do not resolve the
232
      // query and bail out early instead.
233
      if ($errors = $context->getErrors()) {
234
        return $adapter->createFulfilled(new QueryResult(NULL, $errors));
235
      }
236
237
      // TODO: Perform a cach lookup.
238
239
      $resolver = $config->getFieldResolver();
240
      $root = $this->resolveRootValue($config, $params, $document, $operation);
241
      $promise = Executor::promiseToExecute(
242
        $adapter,
243
        $schema,
244
        $document,
245
        $root,
246
        $context,
247
        $variables,
248
        $operation,
249
        $resolver
250
      );
251
252
      return $promise->then(function (ExecutionResult $result) use ($context) {
253
        $metadata = (new CacheableMetadata())->addCacheableDependency($context);
254
        return new QueryResult($result->data, $result->errors, $result->extensions, $metadata);
255
      });
256
    }
257
    catch (RequestError $exception) {
258
      $result = $adapter->createFulfilled(new QueryResult(NULL, [Error::createLocatedError($exception)]));
259
    }
260
    catch (Error $exception) {
261
      $result = $adapter->createFulfilled(new QueryResult(NULL, [$exception]));
262
    }
263
264
    return $result->then(function(QueryResult $result) use ($config) {
265
      if ($config->getErrorsHandler()) {
266
        $result->setErrorsHandler($config->getErrorsHandler());
267
      }
268
269
      if ($config->getErrorFormatter() || $config->getDebug()) {
270
        $result->setErrorFormatter(FormattedError::prepareFormatter($config->getErrorFormatter(), $config->getDebug()));
271
      }
272
273
      return $result;
274
    });
275
  }
276
277
  /**
278
   * @param \GraphQL\Server\ServerConfig $config
279
   * @param \GraphQL\Server\OperationParams $params
280
   * @param \GraphQL\Language\AST\DocumentNode $document
281
   * @param $operation
282
   *
283
   * @return mixed
284
   */
285 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...
286
    $root = $config->getRootValue();
287
    if (is_callable($root)) {
288
      $root = $root($params, $document, $operation);
289
    }
290
291
    return $root;
292
  }
293
294
  /**
295
   * @param \GraphQL\Server\ServerConfig $config
296
   * @param \GraphQL\Server\OperationParams $params
297
   * @param \GraphQL\Language\AST\DocumentNode $document
298
   * @param $operation
299
   *
300
   * @return mixed
301
   */
302 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...
303
    $context = $config->getContext();
304
    if (is_callable($context)) {
305
      $context = $context($params, $document, $operation);
306
    }
307
308
    return $context;
309
  }
310
311
  /**
312
   * @param \GraphQL\Server\ServerConfig $config
313
   * @param \GraphQL\Server\OperationParams $params
314
   * @param \GraphQL\Language\AST\DocumentNode $document
315
   * @param $operation
316
   *
317
   * @return array
318
   */
319
  protected function resolveValidationRules(ServerConfig $config, OperationParams $params, DocumentNode $document, $operation) {
320
    // Allow customizing validation rules per operation:
321
    $rules = $config->getValidationRules();
322
    if (is_callable($rules)) {
323
      $rules = $rules($params, $document, $operation);
324
      if (!is_array($rules)) {
325
        throw new \LogicException(sprintf("Expecting validation rules to be array or callable returning array, but got: %s", Utils::printSafe($rules)));
326
      }
327
    }
328
329
    return $rules;
330
  }
331
332
  /**
333
   * @param \GraphQL\Server\ServerConfig $config
334
   * @param \GraphQL\Server\OperationParams $params
335
   * @param \GraphQL\Language\AST\DocumentNode $document
336
   * @param $operation
337
   *
338
   * @return int|null
339
   */
340
  protected function resolveAllowedComplexity(ServerConfig $config, OperationParams $params, DocumentNode $document, $operation) {
0 ignored issues
show
Unused Code introduced by
The parameter $config 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 $params 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 $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...
341
    return NULL;
342
  }
343
344
  /**
345
   * @param \GraphQL\Server\ServerConfig $config
346
   * @param \GraphQL\Server\OperationParams $params
347
   *
348
   * @return mixed
349
   * @throws \GraphQL\Server\RequestError
350
   */
351
  protected function loadPersistedQuery(ServerConfig $config, OperationParams $params) {
352
    if (!$loader = $config->getPersistentQueryLoader()) {
353
      throw new RequestError('Persisted queries are not supported by this server.');
354
    }
355
356
    $source = $loader($params->queryId, $params);
357
    if (!is_string($source) && !$source instanceof DocumentNode) {
358
      throw new \LogicException(sprintf('The persisted query loader must return query string or instance of %s but got: %s.', DocumentNode::class, Utils::printSafe($source)));
359
    }
360
361
    return $source;
362
  }
363
364
}
365