Completed
Push — 8.x-3.x ( 10877b...7496af )
by Sebastian
03:40 queued 01:27
created

EntityQuery::isUnaryOperator()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 4
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
142
   *   The entity query object.
143
   */
144
  protected function getQuery($value, array $args, ResolveContext $context, ResolveInfo $info) {
145
    $query = $this->getBaseQuery($value, $args, $context, $info);
146
    $query->range($args['offset'], $args['limit']);
147
148
    if (array_key_exists('revisions', $args)) {
149
      $query = $this->applyRevisionsMode($query, $args['revisions']);
150
    }
151
152
    if (array_key_exists('filter', $args)) {
153
      $query = $this->applyFilter($query, $args['filter']);
154
    }
155
156
    if (array_key_exists('sort', $args)) {
157
      $query = $this->applySort($query, $args['sort']);
158
    }
159
160
    return $query;
161
  }
162
163
  /**
164
   * Create the basic entity query for the plugin's entity type.
165
   *
166
   * @param mixed $value
167
   *   The parent entity type.
168
   * @param array $args
169
   *   The field arguments array.
170
   * @param \Drupal\graphql\GraphQL\Execution\ResolveContext $context
171
   *   The resolve context.
172
   * @param \GraphQL\Type\Definition\ResolveInfo $info
173
   *   The resolve info object.
174
   *
175
   * @return \Drupal\Core\Entity\Query\QueryInterface
176
   *   The entity query object.
177
   */
178
  protected function getBaseQuery($value, array $args, ResolveContext $context, ResolveInfo $info) {
179
    $entityType = $this->getEntityType($value, $args, $context, $info);
180
    $entityStorage = $this->entityTypeManager->getStorage($entityType);
181
    $query = $entityStorage->getQuery();
182
    $query->accessCheck(TRUE);
183
184
    // The context object can e.g. transport the parent entity language.
185
    $query->addMetaData('graphql_context', $this->getQueryContext($value, $args, $context, $info));
186
187
    return $query;
188
  }
189
190
  /**
191
   * Retrieves an arbitrary value to write into the query metadata.
192
   *
193
   * @param mixed $value
194
   *   The parent value.
195
   * @param array $args
196
   *   The field arguments array.
197
   * @param \Drupal\graphql\GraphQL\Execution\ResolveContext $context
198
   *   The resolve context.
199
   * @param \GraphQL\Type\Definition\ResolveInfo $info
200
   *   The resolve info object.
201
   *
202
   * @return mixed
203
   *   The query context.
204
   */
205
  protected function getQueryContext($value, array $args, ResolveContext $context, ResolveInfo $info) {
206
    // Forward the whole set of arguments by default.
207
    return [
208
      'parent' => $value,
209
      'args' => $args,
210
      'info' => $info,
211
    ];
212
  }
213
214
  /**
215
   * Apply the specified revision filtering mode to the query.
216
   *
217
   * @param \Drupal\Core\Entity\Query\QueryInterface $query
218
   *   The entity query object.
219
   * @param mixed $mode
220
   *   The revision query mode.
221
   *
222
   * @return \Drupal\Core\Entity\Query\QueryInterface
223
   *   The entity query object.
224
   */
225
  protected function applyRevisionsMode(QueryInterface $query, $mode) {
226
    if ($mode === 'all') {
227
      // Mark the query as such and sort by the revision id too.
228
      $query->allRevisions();
229
      $query->addTag('revisions');
230
    }
231
232
    return $query;
233
  }
234
235
  /**
236
   * Apply the specified sort directives to the query.
237
   *
238
   * @param \Drupal\Core\Entity\Query\QueryInterface $query
239
   *   The entity query object.
240
   * @param mixed $sort
241
   *   The sort definitions from the field arguments.
242
   *
243
   * @return \Drupal\Core\Entity\Query\QueryInterface
244
   *   The entity query object.
245
   */
246
  protected function applySort(QueryInterface $query, $sort) {
247
    if (!empty($sort) && is_array($sort)) {
248
      foreach ($sort as $item) {
249
        $direction = !empty($item['direction']) ? $item['direction'] : 'DESC';
250
        $query->sort($item['field'], $direction);
251
      }
252
    }
253
254
    return $query;
255
  }
256
257
  /**
258
   * Apply the specified filter conditions to the query.
259
   *
260
   * Recursively picks up all filters and aggregates them into condition groups
261
   * according to the nested structure of the filter argument.
262
   *
263
   * @param \Drupal\Core\Entity\Query\QueryInterface $query
264
   *   The entity query object.
265
   * @param mixed $filter
266
   *   The filter definitions from the field arguments.
267
   *
268
   * @return \Drupal\Core\Entity\Query\QueryInterface
269
   *   The entity query object.
270
   */
271
  protected function applyFilter(QueryInterface $query, $filter) {
272
    if (!empty($filter) && is_array($filter)) {
273
      //Conditions can be disabled. Check we are not adding an empty condition group.
274
      $filterConditions = $this->buildFilterConditions($query, $filter);
275
      if (count($filterConditions->conditions())) {
276
        $query->condition($filterConditions);
277
      }      
278
    }
279
280
    return $query;
281
  }
282
283
  /**
284
   * Recursively builds the filter condition groups.
285
   *
286
   * @param \Drupal\Core\Entity\Query\QueryInterface $query
287
   *   The entity query object.
288
   * @param array $filter
289
   *   The filter definitions from the field arguments.
290
   *
291
   * @return \Drupal\Core\Entity\Query\ConditionInterface
292
   *   The generated condition group according to the given filter definitions.
293
   *
294
   * @throws \GraphQL\Error\Error
295
   *   If the given operator and value for a filter are invalid.
296
   */
297
  protected function buildFilterConditions(QueryInterface $query, array $filter) {
298
    $conjunction = !empty($filter['conjunction']) ? $filter['conjunction'] : 'AND';
299
    $group = $conjunction === 'AND' ? $query->andConditionGroup() : $query->orConditionGroup();
300
301
    // Apply filter conditions.
302
    $conditions = !empty($filter['conditions']) ? $filter['conditions'] : [];
303
    foreach ($conditions as $condition) {
304
      // Check if we need to disable this condition.
305
      if (!$condition['enabled']) {
306
        continue;
307
      }
308
      
309
      $field = $condition['field'];
310
      $value = !empty($condition['value']) ? $condition['value'] : NULL;
311
      $operator = !empty($condition['operator']) ? $condition['operator'] : NULL;
312
      $language = !empty($condition['language']) ? $condition['language'] : NULL;
313
314
      // We need at least a value or an operator.
315
      if (empty($operator) && empty($value)) {
316
        throw new Error(sprintf("Missing value and operator in filter for '%s'.", $field));
317
      }
318
      // Unary operators need a single value.
319
      else if (!empty($operator) && $this->isUnaryOperator($operator)) {
320 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...
321
          throw new Error(sprintf("Unary operators must be associated with a single value (field '%s').", $field));
322
        }
323
324
        // Pick the first item from the values.
325
        $value = reset($value);
326
      }
327
      // Range operators need exactly two values.
328
      else if (!empty($operator) && $this->isRangeOperator($operator)) {
329 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...
330
          throw new Error(sprintf("Range operators must require exactly two values (field '%s').", $field));
331
        }
332
      }
333
      // Null operators can't have a value set.
334
      else if (!empty($operator) && $this->isNullOperator($operator)) {
335
        if (!empty($value)) {
336
          throw new Error(sprintf("Null operators must not be associated with a filter value (field '%s').", $field));
337
        }
338
      }
339
340
      // If no operator is set, however, we default to EQUALS or IN, depending
341
      // on whether the given value is an array with one or more than one items.
342
      if (empty($operator)) {
343
        $value = count($value) === 1 ? reset($value) : $value;
344
        $operator = is_array($value) ? 'IN' : '=';
345
      }
346
347
      // Add the condition for the current field.
348
      $group->condition($field, $value, $operator, $language);
349
    }
350
351
    // Apply nested filter group conditions.
352
    $groups = !empty($filter['groups']) ? $filter['groups'] : [];
353
    foreach ($groups as $args) {
354
      // By default, we use AND condition groups.
355
      // Conditions can be disabled. Check we are not adding an empty condition group.
356
      $filterConditions = $this->buildFilterConditions($query, $args);
357
      if (count($filterConditions->conditions())) {
358
        $group->condition($filterConditions);
359
      }      
360
    }
361
362
    return $group;
363
  }
364
365
  /**
366
   * Checks if an operator is a unary operator.
367
   *
368
   * @param string $operator
369
   *   The query operator to check against.
370
   *
371
   * @return bool
372
   *   TRUE if the given operator is unary, FALSE otherwise.
373
   */
374
  protected function isUnaryOperator($operator) {
375
    $unary = ["=", "<>", "<", "<=", ">", ">=", "LIKE", "NOT LIKE"];
376
    return in_array($operator, $unary);
377
  }
378
379
  /**
380
   * Checks if an operator is a null operator.
381
   *
382
   * @param string $operator
383
   *   The query operator to check against.
384
   *
385
   * @return bool
386
   *   TRUE if the given operator is a null operator, FALSE otherwise.
387
   */
388
  protected function isNullOperator($operator) {
389
    $null = ["IS NULL", "IS NOT NULL"];
390
    return in_array($operator, $null);
391
  }
392
393
  /**
394
   * Checks if an operator is a range operator.
395
   *
396
   * @param string $operator
397
   *   The query operator to check against.
398
   *
399
   * @return bool
400
   *   TRUE if the given operator is a range operator, FALSE otherwise.
401
   */
402
  protected function isRangeOperator($operator) {
403
    $null = ["BETWEEN", "NOT BETWEEN"];
404
    return in_array($operator, $null);
405
  }
406
407
}
408