Completed
Push — 8.x-3.x ( 496612...b9394b )
by Sebastian
25:04 queued 17:23
created

EntityQuery::resolveValues()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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