Completed
Pull Request — 8.x-3.x (#517)
by Sebastian
07:28 queued 02:24
created

EntityQuery::getQueryContext()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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