EntityQuery   D
last analyzed

Complexity

Total Complexity 59

Size/Duplication

Total Lines 375
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 1
Metric Value
wmc 59
eloc 100
c 3
b 0
f 1
dl 0
loc 375
rs 4.08

15 Methods

Rating   Name   Duplication   Size   Complexity  
A isNullOperator() 0 3 1
A getCacheDependencies() 0 9 1
A applyRevisionsMode() 0 13 3
A getEntityType() 0 11 3
A __construct() 0 3 1
A getQueryContext() 0 6 1
A getQuery() 0 22 5
A isUnaryOperator() 0 3 1
A applyFilter() 0 10 4
A applySort() 0 10 6
A resolveValues() 0 2 1
F buildFilterConditions() 0 66 29
A isRangeOperator() 0 3 1
A create() 0 6 1
A getBaseQuery() 0 10 1

How to fix   Complexity   

Complex Class

Complex classes like EntityQuery often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use EntityQuery, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Drupal\graphql_core\Plugin\GraphQL\Fields\EntityQuery;
4
5
use Drupal\Core\Cache\CacheableMetadata;
0 ignored issues
show
Bug introduced by
The type Drupal\Core\Cache\CacheableMetadata was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
6
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
0 ignored issues
show
Bug introduced by
The type Drupal\Core\DependencyIn...dencySerializationTrait was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
7
use Drupal\Core\Entity\EntityInterface;
0 ignored issues
show
Bug introduced by
The type Drupal\Core\Entity\EntityInterface was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
8
use Drupal\Core\Entity\EntityTypeManagerInterface;
0 ignored issues
show
Bug introduced by
The type Drupal\Core\Entity\EntityTypeManagerInterface was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
9
use Drupal\Core\Entity\Query\QueryInterface;
0 ignored issues
show
Bug introduced by
The type Drupal\Core\Entity\Query\QueryInterface was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
10
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
0 ignored issues
show
Bug introduced by
The type Drupal\Core\Plugin\ContainerFactoryPluginInterface was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
11
use Drupal\graphql\GraphQL\Execution\ResolveContext;
12
use Drupal\graphql\Plugin\GraphQL\Fields\FieldPluginBase;
13
use GraphQL\Error\Error;
14
use Symfony\Component\DependencyInjection\ContainerInterface;
0 ignored issues
show
Bug introduced by
The type Symfony\Component\Depend...tion\ContainerInterface was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
15
use GraphQL\Type\Definition\ResolveInfo;
16
17
/**
18
 * @GraphQLField(
19
 *   id = "entity_query",
20
 *   secure = false,
21
 *   type = "EntityQueryResult",
22
 *   arguments = {
23
 *     "filter" = "EntityQueryFilterInput",
24
 *     "sort" = "[EntityQuerySortInput]",
25
 *     "offset" = {
26
 *       "type" = "Int",
27
 *       "default" = 0
28
 *     },
29
 *     "limit" = {
30
 *       "type" = "Int",
31
 *       "default" = 10
32
 *     },
33
 *     "revisions" = {
34
 *       "type" = "EntityQueryRevisionMode",
35
 *       "default" = "default"
36
 *     }
37
 *   },
38
 *   deriver = "Drupal\graphql_core\Plugin\Deriver\Fields\EntityQueryDeriver"
39
 * )
40
 *
41
 * This field is marked as not secure because it does not enforce entity field
42
 * access over a chain of filters. For example node.uid.pass could be used as
43
 * filter input which would disclose information about Drupal's password hashes.
44
 */
45
class EntityQuery extends FieldPluginBase implements ContainerFactoryPluginInterface {
46
  use DependencySerializationTrait;
47
48
  /**
49
   * The entity type manager.
50
   *
51
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
52
   */
53
  protected $entityTypeManager;
54
55
  /**
56
   * {@inheritdoc}
57
   */
58
  public static function create(ContainerInterface $container, array $configuration, $pluginId, $pluginDefinition) {
59
    return new static(
60
      $configuration,
61
      $pluginId,
62
      $pluginDefinition,
63
      $container->get('entity_type.manager')
64
    );
65
  }
66
67
  /**
68
   * EntityQuery constructor.
69
   *
70
   * @param array $configuration
71
   *   The plugin configuration array.
72
   * @param string $pluginId
73
   *   The plugin id.
74
   * @param mixed $pluginDefinition
75
   *   The plugin definition.
76
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
77
   *   The entity type manager service.
78
   */
79
  public function __construct(array $configuration, $pluginId, $pluginDefinition, EntityTypeManagerInterface $entityTypeManager) {
80
    parent::__construct($configuration, $pluginId, $pluginDefinition);
81
    $this->entityTypeManager = $entityTypeManager;
82
  }
83
84
  /**
85
   * {@inheritdoc}
86
   */
87
  protected function getCacheDependencies(array $result, $value, array $args, ResolveContext $context, ResolveInfo $info) {
88
    $entityType = $this->getEntityType($value, $args, $context, $info);
89
    $type = $this->entityTypeManager->getDefinition($entityType);
90
91
    $metadata = new CacheableMetadata();
92
    $metadata->addCacheTags($type->getListCacheTags());
93
    $metadata->addCacheContexts($type->getListCacheContexts());
94
95
    return [$metadata];
96
  }
97
98
  /**
99
   * {@inheritdoc}
100
   */
101
  public function resolveValues($value, array $args, ResolveContext $context, ResolveInfo $info) {
102
    yield $this->getQuery($value, $args, $context, $info);
103
  }
104
105
  /**
106
   * Retrieve the target entity type of this plugin.
107
   *
108
   * @param mixed $value
109
   *   The parent value.
110
   * @param array $args
111
   *   The field arguments array.
112
   * @param \Drupal\graphql\GraphQL\Execution\ResolveContext $context
113
   *   The resolve context.
114
   * @param \GraphQL\Type\Definition\ResolveInfo $info
115
   *   The resolve info object.
116
   *
117
   * @return null|string
118
   *   The entity type object or NULL if none could be derived.
119
   */
120
  protected function getEntityType($value, array $args, ResolveContext $context, ResolveInfo $info) {
121
    $definition = $this->getPluginDefinition();
122
    if (isset($definition['entity_type'])) {
123
      return $definition['entity_type'];
124
    }
125
126
    if ($value instanceof EntityInterface) {
127
      return $value->getEntityType()->id();
128
    }
129
130
    return NULL;
131
  }
132
133
  /**
134
   * Create the full entity query for the plugin's entity type.
135
   *
136
   * @param mixed $value
137
   *   The parent entity type.
138
   * @param array $args
139
   *   The field arguments array.
140
   * @param \Drupal\graphql\GraphQL\Execution\ResolveContext $context
141
   *   The resolve context.
142
   * @param \GraphQL\Type\Definition\ResolveInfo $info
143
   *   The resolve info object.
144
   *
145
   * @return \Drupal\Core\Entity\Query\QueryInterface|null
146
   *   The entity query object.
147
   */
148
  protected function getQuery($value, array $args, ResolveContext $context, ResolveInfo $info) {
149
    if (!$query = $this->getBaseQuery($value, $args, $context, $info)) {
150
      return NULL;
151
    }
152
153
    $query->addTag('graphql_entity_query');
154
155
    $query->range($args['offset'], $args['limit']);
156
157
    if (array_key_exists('revisions', $args)) {
158
      $query = $this->applyRevisionsMode($query, $args['revisions']);
159
    }
160
161
    if (array_key_exists('filter', $args)) {
162
      $query = $this->applyFilter($query, $args['filter']);
163
    }
164
165
    if (array_key_exists('sort', $args)) {
166
      $query = $this->applySort($query, $args['sort']);
167
    }
168
169
    return $query;
170
  }
171
172
  /**
173
   * Create the basic entity query for the plugin's entity type.
174
   *
175
   * @param mixed $value
176
   *   The parent entity type.
177
   * @param array $args
178
   *   The field arguments array.
179
   * @param \Drupal\graphql\GraphQL\Execution\ResolveContext $context
180
   *   The resolve context.
181
   * @param \GraphQL\Type\Definition\ResolveInfo $info
182
   *   The resolve info object.
183
   *
184
   * @return \Drupal\Core\Entity\Query\QueryInterface|null
185
   *   The entity query object.
186
   */
187
  protected function getBaseQuery($value, array $args, ResolveContext $context, ResolveInfo $info) {
188
    $entityType = $this->getEntityType($value, $args, $context, $info);
189
    $entityStorage = $this->entityTypeManager->getStorage($entityType);
190
    $query = $entityStorage->getQuery();
191
    $query->accessCheck(TRUE);
192
193
    // The context object can e.g. transport the parent entity language.
194
    $query->addMetaData('graphql_context', $this->getQueryContext($value, $args, $context, $info));
195
196
    return $query;
197
  }
198
199
  /**
200
   * Retrieves an arbitrary value to write into the query metadata.
201
   *
202
   * @param mixed $value
203
   *   The parent value.
204
   * @param array $args
205
   *   The field arguments array.
206
   * @param \Drupal\graphql\GraphQL\Execution\ResolveContext $context
207
   *   The resolve context.
208
   * @param \GraphQL\Type\Definition\ResolveInfo $info
209
   *   The resolve info object.
210
   *
211
   * @return mixed
212
   *   The query context.
213
   */
214
  protected function getQueryContext($value, array $args, ResolveContext $context, ResolveInfo $info) {
215
    // Forward the whole set of arguments by default.
216
    return [
217
      'parent' => $value,
218
      'args' => $args,
219
      'info' => $info,
220
    ];
221
  }
222
223
  /**
224
   * Apply the specified revision filtering mode to the query.
225
   *
226
   * @param \Drupal\Core\Entity\Query\QueryInterface $query
227
   *   The entity query object.
228
   * @param mixed $mode
229
   *   The revision query mode.
230
   *
231
   * @return \Drupal\Core\Entity\Query\QueryInterface
232
   *   The entity query object.
233
   */
234
  protected function applyRevisionsMode(QueryInterface $query, $mode) {
235
    if ($mode === 'all') {
236
      // Mark the query as such and sort by the revision id too.
237
      $query->allRevisions();
238
      $query->addTag('revisions');
239
    }
240
    else if ($mode === 'latest') {
241
      // Mark the query to only include latest revision and sort by revision id.
242
      $query->latestRevision();
243
      $query->addTag('revisions');
244
    }
245
246
    return $query;
247
  }
248
249
  /**
250
   * Apply the specified sort directives to the query.
251
   *
252
   * @param \Drupal\Core\Entity\Query\QueryInterface $query
253
   *   The entity query object.
254
   * @param mixed $sort
255
   *   The sort definitions from the field arguments.
256
   *
257
   * @return \Drupal\Core\Entity\Query\QueryInterface
258
   *   The entity query object.
259
   */
260
  protected function applySort(QueryInterface $query, $sort) {
261
    if (!empty($sort) && is_array($sort)) {
262
      foreach ($sort as $item) {
263
        $direction = !empty($item['direction']) ? $item['direction'] : 'DESC';
264
        $language = !empty($item['language']) ? $item['language'] : null;
265
        $query->sort($item['field'], $direction, $language);
266
      }
267
    }
268
269
    return $query;
270
  }
271
272
  /**
273
   * Apply the specified filter conditions to the query.
274
   *
275
   * Recursively picks up all filters and aggregates them into condition groups
276
   * according to the nested structure of the filter argument.
277
   *
278
   * @param \Drupal\Core\Entity\Query\QueryInterface $query
279
   *   The entity query object.
280
   * @param mixed $filter
281
   *   The filter definitions from the field arguments.
282
   *
283
   * @return \Drupal\Core\Entity\Query\QueryInterface
284
   *   The entity query object.
285
   */
286
  protected function applyFilter(QueryInterface $query, $filter) {
287
    if (!empty($filter) && is_array($filter)) {
288
      //Conditions can be disabled. Check we are not adding an empty condition group.
289
      $filterConditions = $this->buildFilterConditions($query, $filter);
290
      if (count($filterConditions->conditions())) {
291
        $query->condition($filterConditions);
292
      }
293
    }
294
295
    return $query;
296
  }
297
298
  /**
299
   * Recursively builds the filter condition groups.
300
   *
301
   * @param \Drupal\Core\Entity\Query\QueryInterface $query
302
   *   The entity query object.
303
   * @param array $filter
304
   *   The filter definitions from the field arguments.
305
   *
306
   * @return \Drupal\Core\Entity\Query\ConditionInterface
0 ignored issues
show
Bug introduced by
The type Drupal\Core\Entity\Query\ConditionInterface was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
307
   *   The generated condition group according to the given filter definitions.
308
   *
309
   * @throws \GraphQL\Error\Error
310
   *   If the given operator and value for a filter are invalid.
311
   */
312
  protected function buildFilterConditions(QueryInterface $query, array $filter) {
313
    $conjunction = !empty($filter['conjunction']) ? $filter['conjunction'] : 'AND';
314
    $group = $conjunction === 'AND' ? $query->andConditionGroup() : $query->orConditionGroup();
315
316
    // Apply filter conditions.
317
    $conditions = !empty($filter['conditions']) ? $filter['conditions'] : [];
318
    foreach ($conditions as $condition) {
319
      // Check if we need to disable this condition.
320
      if (isset($condition['enabled']) && empty($condition['enabled'])) {
321
        continue;
322
      }
323
324
      $field = $condition['field'];
325
      $value = !empty($condition['value']) ? $condition['value'] : NULL;
326
      $operator = !empty($condition['operator']) ? $condition['operator'] : NULL;
327
      $language = !empty($condition['language']) ? $condition['language'] : NULL;
328
329
      // We need at least a value or an operator.
330
      if (empty($operator) && empty($value)) {
331
        throw new Error(sprintf("Missing value and operator in filter for '%s'.", $field));
332
      }
333
      // Unary operators need a single value.
334
      else if (!empty($operator) && $this->isUnaryOperator($operator)) {
335
        if (empty($value) || count($value) > 1) {
336
          throw new Error(sprintf("Unary operators must be associated with a single value (field '%s').", $field));
337
        }
338
339
        // Pick the first item from the values.
340
        $value = reset($value);
341
      }
342
      // Range operators need exactly two values.
343
      else if (!empty($operator) && $this->isRangeOperator($operator)) {
344
        if (empty($value) || count($value) !== 2) {
345
          throw new Error(sprintf("Range operators must require exactly two values (field '%s').", $field));
346
        }
347
      }
348
      // Null operators can't have a value set.
349
      else if (!empty($operator) && $this->isNullOperator($operator)) {
350
        if (!empty($value)) {
351
          throw new Error(sprintf("Null operators must not be associated with a filter value (field '%s').", $field));
352
        }
353
      }
354
355
      // If no operator is set, however, we default to EQUALS or IN, depending
356
      // on whether the given value is an array with one or more than one items.
357
      if (empty($operator)) {
358
        $value = count($value) === 1 ? reset($value) : $value;
359
        $operator = is_array($value) ? 'IN' : '=';
360
      }
361
362
      // Add the condition for the current field.
363
      $group->condition($field, $value, $operator, $language);
364
    }
365
366
    // Apply nested filter group conditions.
367
    $groups = !empty($filter['groups']) ? $filter['groups'] : [];
368
    foreach ($groups as $args) {
369
      // By default, we use AND condition groups.
370
      // Conditions can be disabled. Check we are not adding an empty condition group.
371
      $filterConditions = $this->buildFilterConditions($query, $args);
372
      if (count($filterConditions->conditions())) {
373
        $group->condition($filterConditions);
374
      }
375
    }
376
377
    return $group;
378
  }
379
380
  /**
381
   * Checks if an operator is a unary operator.
382
   *
383
   * @param string $operator
384
   *   The query operator to check against.
385
   *
386
   * @return bool
387
   *   TRUE if the given operator is unary, FALSE otherwise.
388
   */
389
  protected function isUnaryOperator($operator) {
390
    $unary = ["=", "<>", "<", "<=", ">", ">=", "LIKE", "NOT LIKE"];
391
    return in_array($operator, $unary);
392
  }
393
394
  /**
395
   * Checks if an operator is a null operator.
396
   *
397
   * @param string $operator
398
   *   The query operator to check against.
399
   *
400
   * @return bool
401
   *   TRUE if the given operator is a null operator, FALSE otherwise.
402
   */
403
  protected function isNullOperator($operator) {
404
    $null = ["IS NULL", "IS NOT NULL"];
405
    return in_array($operator, $null);
406
  }
407
408
  /**
409
   * Checks if an operator is a range operator.
410
   *
411
   * @param string $operator
412
   *   The query operator to check against.
413
   *
414
   * @return bool
415
   *   TRUE if the given operator is a range operator, FALSE otherwise.
416
   */
417
  protected function isRangeOperator($operator) {
418
    $null = ["BETWEEN", "NOT BETWEEN"];
419
    return in_array($operator, $null);
420
  }
421
422
}
423