Completed
Pull Request — 8.x-3.x (#801)
by Sebastian
04:42 queued 03:15
created

EntityQuery::getQuery()   B

Complexity

Conditions 8
Paths 18

Size

Total Lines 29

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
nc 18
nop 4
dl 0
loc 29
rs 8.2114
c 0
b 0
f 0
1
<?php
2
3
namespace Drupal\graphql_core\Plugin\GraphQL\Fields\EntityQuery;
4
5
use Drupal\Core\Cache\CacheableMetadata;
6
use Drupal\Core\Config\ConfigFactoryInterface;
7
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
8
use Drupal\Core\Entity\EntityInterface;
9
use Drupal\Core\Entity\EntityTypeManagerInterface;
10
use Drupal\Core\Entity\Query\QueryInterface;
11
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
12
use Drupal\graphql\GraphQL\Execution\ResolveContext;
13
use Drupal\graphql\Plugin\GraphQL\Fields\FieldPluginBase;
14
use GraphQL\Error\Error;
15
use Symfony\Component\DependencyInjection\ContainerInterface;
16
use GraphQL\Type\Definition\ResolveInfo;
17
18
/**
19
 * @GraphQLField(
20
 *   id = "entity_query",
21
 *   secure = true,
22
 *   type = "EntityQueryResult",
23
 *   arguments = {
24
 *     "filter" = "EntityQueryFilterInput",
25
 *     "sort" = "[EntityQuerySortInput]",
26
 *     "offset" = {
27
 *       "type" = "Int",
28
 *       "default" = 0
29
 *     },
30
 *     "limit" = {
31
 *       "type" = "Int",
32
 *       "default" = 10
33
 *     },
34
 *     "revisions" = {
35
 *       "type" = "EntityQueryRevisionMode",
36
 *       "default" = "default"
37
 *     }
38
 *   },
39
 *   deriver = "Drupal\graphql_core\Plugin\Deriver\Fields\EntityQueryDeriver"
40
 * )
41
 */
42
class EntityQuery extends FieldPluginBase implements ContainerFactoryPluginInterface {
43
  use DependencySerializationTrait;
44
45
  /**
46
   * The entity type manager.
47
   *
48
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
49
   */
50
  protected $entityTypeManager;
51
52
  /**
53
   * Service config.factory.
54
   *
55
   * @var \Drupal\Core\Config\ConfigFactoryInterface
56
   */
57
  protected $config;
58
59
  /**
60
   * {@inheritdoc}
61
   */
62
  public static function create(ContainerInterface $container, array $configuration, $pluginId, $pluginDefinition) {
63
    return new static(
64
      $configuration,
65
      $pluginId,
66
      $pluginDefinition,
67
      $container->get('entity_type.manager'),
68
      $container->get('config.factory')
69
    );
70
  }
71
72
  /**
73
   * EntityQuery constructor.
74
   *
75
   * @param array $configuration
76
   *   The plugin configuration array.
77
   * @param string $pluginId
78
   *   The plugin id.
79
   * @param mixed $pluginDefinition
80
   *   The plugin definition.
81
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
82
   *   The entity type manager service.
83
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config
84
   *   Service config.factory.
85
   */
86
  public function __construct(array $configuration, $pluginId, $pluginDefinition, EntityTypeManagerInterface $entityTypeManager, ConfigFactoryInterface $config) {
87
    parent::__construct($configuration, $pluginId, $pluginDefinition);
88
    $this->entityTypeManager = $entityTypeManager;
89
    $this->config = $config;
90
  }
91
92
  /**
93
   * {@inheritdoc}
94
   */
95
  protected function getCacheDependencies(array $result, $value, array $args, ResolveContext $context, ResolveInfo $info) {
96
    $entityType = $this->getEntityType($value, $args, $context, $info);
97
    $type = $this->entityTypeManager->getDefinition($entityType);
98
99
    $metadata = new CacheableMetadata();
100
    $metadata->addCacheTags($type->getListCacheTags());
101
    $metadata->addCacheContexts($type->getListCacheContexts());
102
103
    return [$metadata];
104
  }
105
106
  /**
107
   * {@inheritdoc}
108
   */
109
  public function resolveValues($value, array $args, ResolveContext $context, ResolveInfo $info) {
110
    yield $this->getQuery($value, $args, $context, $info);
111
  }
112
113
  /**
114
   * Retrieve the target entity type of this plugin.
115
   *
116
   * @param mixed $value
117
   *   The parent value.
118
   * @param array $args
119
   *   The field arguments array.
120
   * @param \Drupal\graphql\GraphQL\Execution\ResolveContext $context
121
   *   The resolve context.
122
   * @param \GraphQL\Type\Definition\ResolveInfo $info
123
   *   The resolve info object.
124
   *
125
   * @return null|string
126
   *   The entity type object or NULL if none could be derived.
127
   */
128
  protected function getEntityType($value, array $args, ResolveContext $context, ResolveInfo $info) {
129
    $definition = $this->getPluginDefinition();
130
    if (isset($definition['entity_type'])) {
131
      return $definition['entity_type'];
132
    }
133
134
    if ($value instanceof EntityInterface) {
0 ignored issues
show
Bug introduced by
The class Drupal\Core\Entity\EntityInterface 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...
135
      return $value->getEntityType()->id();
136
    }
137
138
    return NULL;
139
  }
140
141
  /**
142
   * Create the full entity query for the plugin's entity type.
143
   *
144
   * @param mixed $value
145
   *   The parent entity type.
146
   * @param array $args
147
   *   The field arguments array.
148
   * @param \Drupal\graphql\GraphQL\Execution\ResolveContext $context
149
   *   The resolve context.
150
   * @param \GraphQL\Type\Definition\ResolveInfo $info
151
   *   The resolve info object.
152
   *
153
   * @return \Drupal\Core\Entity\Query\QueryInterface|null
154
   *   The entity query object.
155
   */
156
  protected function getQuery($value, array $args, ResolveContext $context, ResolveInfo $info) {
157
    if (!$query = $this->getBaseQuery($value, $args, $context, $info)) {
158
      return NULL;
159
    }
160
161
    $graphql_settings = $this->config->get('graphql.settings');
162
    if ($graphql_settings) {
163
      $limit_max = $graphql_settings->get('entity_query_limit_max');
164
      if ($limit_max && $args['limit'] > $limit_max) {
165
        throw new \InvalidArgumentException("EntityQuery with too big limit parameter, max is {$limit_max}, requested was {$args['limit']}.");
166
      }
167
    }
168
169
    $query->range($args['offset'], $args['limit']);
170
171
    if (array_key_exists('revisions', $args)) {
172
      $query = $this->applyRevisionsMode($query, $args['revisions']);
173
    }
174
175
    if (array_key_exists('filter', $args)) {
176
      $query = $this->applyFilter($query, $args['filter']);
177
    }
178
179
    if (array_key_exists('sort', $args)) {
180
      $query = $this->applySort($query, $args['sort']);
181
    }
182
183
    return $query;
184
  }
185
186
  /**
187
   * Create the basic entity query for the plugin's entity type.
188
   *
189
   * @param mixed $value
190
   *   The parent entity type.
191
   * @param array $args
192
   *   The field arguments array.
193
   * @param \Drupal\graphql\GraphQL\Execution\ResolveContext $context
194
   *   The resolve context.
195
   * @param \GraphQL\Type\Definition\ResolveInfo $info
196
   *   The resolve info object.
197
   *
198
   * @return \Drupal\Core\Entity\Query\QueryInterface|null
199
   *   The entity query object.
200
   */
201
  protected function getBaseQuery($value, array $args, ResolveContext $context, ResolveInfo $info) {
202
    $entityType = $this->getEntityType($value, $args, $context, $info);
203
    $entityStorage = $this->entityTypeManager->getStorage($entityType);
204
    $query = $entityStorage->getQuery();
205
    $query->accessCheck(TRUE);
206
207
    // The context object can e.g. transport the parent entity language.
208
    $query->addMetaData('graphql_context', $this->getQueryContext($value, $args, $context, $info));
209
210
    return $query;
211
  }
212
213
  /**
214
   * Retrieves an arbitrary value to write into the query metadata.
215
   *
216
   * @param mixed $value
217
   *   The parent value.
218
   * @param array $args
219
   *   The field arguments array.
220
   * @param \Drupal\graphql\GraphQL\Execution\ResolveContext $context
221
   *   The resolve context.
222
   * @param \GraphQL\Type\Definition\ResolveInfo $info
223
   *   The resolve info object.
224
   *
225
   * @return mixed
226
   *   The query context.
227
   */
228
  protected function getQueryContext($value, array $args, ResolveContext $context, ResolveInfo $info) {
229
    // Forward the whole set of arguments by default.
230
    return [
231
      'parent' => $value,
232
      'args' => $args,
233
      'info' => $info,
234
    ];
235
  }
236
237
  /**
238
   * Apply the specified revision filtering mode to the query.
239
   *
240
   * @param \Drupal\Core\Entity\Query\QueryInterface $query
241
   *   The entity query object.
242
   * @param mixed $mode
243
   *   The revision query mode.
244
   *
245
   * @return \Drupal\Core\Entity\Query\QueryInterface
246
   *   The entity query object.
247
   */
248
  protected function applyRevisionsMode(QueryInterface $query, $mode) {
249
    if ($mode === 'all') {
250
      // Mark the query as such and sort by the revision id too.
251
      $query->allRevisions();
252
      $query->addTag('revisions');
253
    }
254
    else if ($mode === 'latest') {
255
      // Mark the query to only include latest revision and sort by revision id.
256
      $query->latestRevision();
257
      $query->addTag('revisions');
258
    }
259
260
    return $query;
261
  }
262
263
  /**
264
   * Apply the specified sort directives to the query.
265
   *
266
   * @param \Drupal\Core\Entity\Query\QueryInterface $query
267
   *   The entity query object.
268
   * @param mixed $sort
269
   *   The sort definitions from the field arguments.
270
   *
271
   * @return \Drupal\Core\Entity\Query\QueryInterface
272
   *   The entity query object.
273
   */
274
  protected function applySort(QueryInterface $query, $sort) {
275
    if (!empty($sort) && is_array($sort)) {
276
      foreach ($sort as $item) {
277
        $direction = !empty($item['direction']) ? $item['direction'] : 'DESC';
278
        $language = !empty($item['language']) ? $item['language'] : null;
279
        $query->sort($item['field'], $direction, $language);
280
      }
281
    }
282
283
    return $query;
284
  }
285
286
  /**
287
   * Apply the specified filter conditions to the query.
288
   *
289
   * Recursively picks up all filters and aggregates them into condition groups
290
   * according to the nested structure of the filter argument.
291
   *
292
   * @param \Drupal\Core\Entity\Query\QueryInterface $query
293
   *   The entity query object.
294
   * @param mixed $filter
295
   *   The filter definitions from the field arguments.
296
   *
297
   * @return \Drupal\Core\Entity\Query\QueryInterface
298
   *   The entity query object.
299
   */
300
  protected function applyFilter(QueryInterface $query, $filter) {
301
    if (!empty($filter) && is_array($filter)) {
302
      //Conditions can be disabled. Check we are not adding an empty condition group.
303
      $filterConditions = $this->buildFilterConditions($query, $filter);
304
      if (count($filterConditions->conditions())) {
305
        $query->condition($filterConditions);
306
      }
307
    }
308
309
    return $query;
310
  }
311
312
  /**
313
   * Recursively builds the filter condition groups.
314
   *
315
   * @param \Drupal\Core\Entity\Query\QueryInterface $query
316
   *   The entity query object.
317
   * @param array $filter
318
   *   The filter definitions from the field arguments.
319
   *
320
   * @return \Drupal\Core\Entity\Query\ConditionInterface
321
   *   The generated condition group according to the given filter definitions.
322
   *
323
   * @throws \GraphQL\Error\Error
324
   *   If the given operator and value for a filter are invalid.
325
   */
326
  protected function buildFilterConditions(QueryInterface $query, array $filter) {
327
    $conjunction = !empty($filter['conjunction']) ? $filter['conjunction'] : 'AND';
328
    $group = $conjunction === 'AND' ? $query->andConditionGroup() : $query->orConditionGroup();
329
330
    // Apply filter conditions.
331
    $conditions = !empty($filter['conditions']) ? $filter['conditions'] : [];
332
    foreach ($conditions as $condition) {
333
      // Check if we need to disable this condition.
334
      if (isset($condition['enabled']) && empty($condition['enabled'])) {
335
        continue;
336
      }
337
338
      $field = $condition['field'];
339
      $value = !empty($condition['value']) ? $condition['value'] : NULL;
340
      $operator = !empty($condition['operator']) ? $condition['operator'] : NULL;
341
      $language = !empty($condition['language']) ? $condition['language'] : NULL;
342
343
      // We need at least a value or an operator.
344
      if (empty($operator) && empty($value)) {
345
        throw new Error(sprintf("Missing value and operator in filter for '%s'.", $field));
346
      }
347
      // Unary operators need a single value.
348
      else if (!empty($operator) && $this->isUnaryOperator($operator)) {
349 View Code Duplication
        if (empty($value) || count($value) > 1) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
350
          throw new Error(sprintf("Unary operators must be associated with a single value (field '%s').", $field));
351
        }
352
353
        // Pick the first item from the values.
354
        $value = reset($value);
355
      }
356
      // Range operators need exactly two values.
357
      else if (!empty($operator) && $this->isRangeOperator($operator)) {
358 View Code Duplication
        if (empty($value) || count($value) !== 2) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
359
          throw new Error(sprintf("Range operators must require exactly two values (field '%s').", $field));
360
        }
361
      }
362
      // Null operators can't have a value set.
363
      else if (!empty($operator) && $this->isNullOperator($operator)) {
364
        if (!empty($value)) {
365
          throw new Error(sprintf("Null operators must not be associated with a filter value (field '%s').", $field));
366
        }
367
      }
368
369
      // If no operator is set, however, we default to EQUALS or IN, depending
370
      // on whether the given value is an array with one or more than one items.
371
      if (empty($operator)) {
372
        $value = count($value) === 1 ? reset($value) : $value;
373
        $operator = is_array($value) ? 'IN' : '=';
374
      }
375
376
      // Add the condition for the current field.
377
      $group->condition($field, $value, $operator, $language);
378
    }
379
380
    // Apply nested filter group conditions.
381
    $groups = !empty($filter['groups']) ? $filter['groups'] : [];
382
    foreach ($groups as $args) {
383
      // By default, we use AND condition groups.
384
      // Conditions can be disabled. Check we are not adding an empty condition group.
385
      $filterConditions = $this->buildFilterConditions($query, $args);
386
      if (count($filterConditions->conditions())) {
387
        $group->condition($filterConditions);
388
      }
389
    }
390
391
    return $group;
392
  }
393
394
  /**
395
   * Checks if an operator is a unary operator.
396
   *
397
   * @param string $operator
398
   *   The query operator to check against.
399
   *
400
   * @return bool
401
   *   TRUE if the given operator is unary, FALSE otherwise.
402
   */
403
  protected function isUnaryOperator($operator) {
404
    $unary = ["=", "<>", "<", "<=", ">", ">=", "LIKE", "NOT LIKE"];
405
    return in_array($operator, $unary);
406
  }
407
408
  /**
409
   * Checks if an operator is a null 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 null operator, FALSE otherwise.
416
   */
417
  protected function isNullOperator($operator) {
418
    $null = ["IS NULL", "IS NOT NULL"];
419
    return in_array($operator, $null);
420
  }
421
422
  /**
423
   * Checks if an operator is a range operator.
424
   *
425
   * @param string $operator
426
   *   The query operator to check against.
427
   *
428
   * @return bool
429
   *   TRUE if the given operator is a range operator, FALSE otherwise.
430
   */
431
  protected function isRangeOperator($operator) {
432
    $null = ["BETWEEN", "NOT BETWEEN"];
433
    return in_array($operator, $null);
434
  }
435
436
}
437