Completed
Pull Request — 8.x-3.x (#511)
by Sebastian
03:14 queued 45s
created

EntityQuery::resolveValues()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 3
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\EntityTypeInterface;
9
use Drupal\Core\Entity\EntityTypeManagerInterface;
10
use Drupal\Core\Entity\Query\QueryInterface;
11
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
12
use Drupal\graphql\Plugin\GraphQL\Fields\FieldPluginBase;
13
use Symfony\Component\DependencyInjection\ContainerInterface;
14
use Youshido\GraphQL\Exception\ResolveException;
15
use Youshido\GraphQL\Execution\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 {
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...
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 function __construct(array $configuration, $pluginId, $pluginDefinition, EntityTypeManagerInterface $entityTypeManager) {
55
    $this->entityTypeManager = $entityTypeManager;
56
    parent::__construct($configuration, $pluginId, $pluginDefinition);
57
  }
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
    );
69
  }
70
71
  /**
72
   * {@inheritdoc}
73
   */
74
  protected function getCacheDependencies(array $result, $value, array $args, ResolveInfo $info) {
75
    $entityType = $this->getEntityType($value, $args, $info);
76
    $type = $this->entityTypeManager->getDefinition($entityType);
77
78
    $metadata = new CacheableMetadata();
79
    $metadata->addCacheTags($type->getListCacheTags());
80
    $metadata->addCacheContexts($type->getListCacheContexts());
81
82
    return [$metadata];
83
  }
84
85
  /**
86
   * {@inheritdoc}
87
   */
88
  public function resolveValues($value, array $args, ResolveInfo $info) {
89
    yield $this->getQuery($value, $args, $info);
90
  }
91
92
  /**
93
   * Retrieve the target entity type of this plugin.
94
   *
95
   * @param mixed $value
96
   *   The parent entity type.
97
   * @param array $args
98
   *   The field arguments array.
99
   * @param \Youshido\GraphQL\Execution\ResolveInfo $info
100
   *   The resolve info object.
101
   *
102
   * @return string|null
103
   *   The entity type object or NULL if none could be derived.
104
   */
105
  protected function getEntityType($value, array $args, ResolveInfo $info) {
106
    $definition = $this->getPluginDefinition();
107
    if (isset($definition['entity_type'])) {
108
      return $definition['entity_type'];
109
    }
110
111
    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...
112
      return $value->getEntityType()->id();
113
    }
114
115
    return NULL;
116
  }
117
118
  /**
119
   * Create the full entity query for the plugin's entity type.
120
   *
121
   * @param mixed $value
122
   *   The parent entity type.
123
   * @param array $args
124
   *   The field arguments array.
125
   * @param \Youshido\GraphQL\Execution\ResolveInfo $info
126
   *   The resolve info object.
127
   *
128
   * @return \Drupal\Core\Entity\Query\QueryInterface
129
   *   The entity query object.
130
   */
131
  protected function getQuery($value, array $args, ResolveInfo $info) {
132
    $query = $this->getBaseQuery($value, $args, $info);
133
    $query->range($args['offset'], $args['limit']);
134
135
    if (array_key_exists('revisions', $args)) {
136
      $query = $this->applyRevisionsMode($query, $args['revisions']);
137
    }
138
139
    if (array_key_exists('filter', $args)) {
140
      $query = $this->applyFilter($query, $args['filter']);
141
    }
142
143
    if (array_key_exists('sort', $args)) {
144
      $query = $this->applySort($query, $args['sort']);
145
    }
146
147
    return $query;
148
  }
149
150
  /**
151
   * Create the basic entity query for the plugin's entity type.
152
   *
153
   * @param mixed $value
154
   *   The parent entity type.
155
   * @param array $args
156
   *   The field arguments array.
157
   * @param \Youshido\GraphQL\Execution\ResolveInfo $info
158
   *   The resolve info object.
159
   *
160
   * @return \Drupal\Core\Entity\Query\QueryInterface
161
   *   The entity query object.
162
   */
163
  protected function getBaseQuery($value, array $args, ResolveInfo $info) {
164
    $entityType = $this->getEntityType($value, $args, $info);
165
    $entityStorage = $this->entityTypeManager->getStorage($entityType);
166
    $query = $entityStorage->getQuery();
167
    $query->accessCheck(TRUE);
168
169
    return $query;
170
  }
171
172
173
  /**
174
   * Apply the specified revision filtering mode to the query.
175
   *
176
   * @param \Drupal\Core\Entity\Query\QueryInterface $query
177
   *   The entity query object.
178
   * @param mixed $mode
179
   *   The revision query mode.
180
   *
181
   * @return \Drupal\Core\Entity\Query\QueryInterface
182
   *   The entity query object.
183
   */
184
  protected function applyRevisionsMode(QueryInterface $query, $mode) {
185
    if ($mode === 'all') {
186
      // Mark the query as such and sort by the revision id too.
187
      $query->allRevisions();
188
      $query->addTag('revisions');
189
    }
190
191
    return $query;
192
  }
193
194
  /**
195
   * Apply the specified sort directives to the query.
196
   *
197
   * @param \Drupal\Core\Entity\Query\QueryInterface $query
198
   *   The entity query object.
199
   * @param mixed $sort
200
   *   The sort definitions from the field arguments.
201
   *
202
   * @return \Drupal\Core\Entity\Query\QueryInterface
203
   *   The entity query object.
204
   */
205
  protected function applySort(QueryInterface $query, $sort) {
206
    if (!empty($sort) && is_array($sort)) {
207
      foreach ($sort as $item) {
208
        $direction = !empty($item['direction']) ? $item['direction'] : 'DESC';
209
        $query->sort($item['field'], $direction);
210
      }
211
    }
212
213
    return $query;
214
  }
215
216
  /**
217
   * Apply the specified filter conditions to the query.
218
   *
219
   * Recursively picks up all filters and aggregates them into condition groups
220
   * according to the nested structure of the filter argument.
221
   *
222
   * @param \Drupal\Core\Entity\Query\QueryInterface $query
223
   *   The entity query object.
224
   * @param mixed $filter
225
   *   The filter definitions from the field arguments.
226
   *
227
   * @return \Drupal\Core\Entity\Query\QueryInterface
228
   *   The entity query object.
229
   */
230
  protected function applyFilter(QueryInterface $query, $filter) {
231
    if (!empty($filter) && is_array($filter)) {
232
      $query->condition($this->buildFilterConditions($query, $filter));
233
    }
234
235
    return $query;
236
  }
237
238
  /**
239
   * Recursively builds the filter condition groups.
240
   *
241
   * @param \Drupal\Core\Entity\Query\QueryInterface $query
242
   *   The entity query object.
243
   * @param array $filter
244
   *   The filter definitions from the field arguments.
245
   *
246
   * @return \Drupal\Core\Entity\Query\ConditionInterface
247
   *   The generated condition group according to the given filter definitions.
248
   *
249
   * @throws \Youshido\GraphQL\Exception\ResolveException
250
   *   If the given operator and value for a filter are invalid.
251
   */
252
  protected function buildFilterConditions(QueryInterface $query, array $filter) {
253
    $conjunction = !empty($filter['conjunction']) ? $filter['conjunction'] : 'AND';
254
    $group = $conjunction === 'AND' ? $query->andConditionGroup() : $query->orConditionGroup();
255
256
    // Apply filter conditions.
257
    $conditions = !empty($filter['conditions']) ? $filter['conditions'] : [];
258
    foreach ($conditions as $condition) {
259
      $field = $condition['field'];
260
      $value = !empty($condition['value']) ? $condition['value'] : NULL;
261
      $operator = !empty($condition['operator']) ? $condition['operator'] : NULL;
262
      $language = !empty($condition['language']) ? $condition['language'] : NULL;
263
264
      // We need at least a value or an operator.
265
      if (empty($operator) && empty($value)) {
266
        throw new ResolveException(sprintf("Missing value and operator in filter for '%s'.", $field));
267
      }
268
      // Unary operators need a single value.
269
      else if (!empty($operator) && $this->isUnaryOperator($operator)) {
270 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...
271
          throw new ResolveException(sprintf("Unary operators must be associated with a single value (field '%s').", $field));
272
        }
273
274
        // Pick the first item from the values.
275
        $value = reset($value);
276
      }
277
      // Range operators need exactly two values.
278
      else if (!empty($operator) && $this->isRangeOperator($operator)) {
279 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...
280
          throw new ResolveException(sprintf("Range operators must require exactly two values (field '%s').", $field));
281
        }
282
      }
283
      // Null operators can't have a value set.
284
      else if (!empty($operator) && $this->isNullOperator($operator)) {
285
        if (!empty($value)) {
286
          throw new ResolveException(sprintf("Null operators must not be associated with a filter value (field '%s').", $field));
287
        }
288
      }
289
290
      // If no operator is set, however, we default to EQUALS or IN, depending
291
      // on whether the given value is an array with one or more than one items.
292
      if (empty($operator)) {
293
        $value = count($value) === 1 ? reset($value) : $value;
294
        $operator = is_array($value) ? 'IN' : '=';
295
      }
296
297
      // Add the condition for the current field.
298
      $group->condition($field, $value, $operator, $language);
299
    }
300
301
    // Apply nested filter group conditions.
302
    $groups = !empty($filter['groups']) ? $filter['groups'] : [];
303
    foreach ($groups as $args) {
304
      // By default, we use AND condition groups.
305
      $group->condition($this->buildFilterConditions($query, $args));
306
    }
307
308
    return $group;
309
  }
310
311
  /**
312
   * Checks if an operator is a unary operator.
313
   *
314
   * @param string $operator
315
   *   The query operator to check against.
316
   *
317
   * @return bool
318
   *   TRUE if the given operator is unary, FALSE otherwise.
319
   */
320
  protected function isUnaryOperator($operator) {
321
    $unary = ["=", "<>", "<", "<=", ">", ">=", "LIKE", "NOT LIKE"];
322
    return in_array($operator, $unary);
323
  }
324
325
  /**
326
   * Checks if an operator is a null operator.
327
   *
328
   * @param string $operator
329
   *   The query operator to check against.
330
   *
331
   * @return bool
332
   *   TRUE if the given operator is a null operator, FALSE otherwise.
333
   */
334
  protected function isNullOperator($operator) {
335
    $null = ["IS NULL", "IS NOT NULL"];
336
    return in_array($operator, $null);
337
  }
338
339
  /**
340
   * Checks if an operator is a range operator.
341
   *
342
   * @param string $operator
343
   *   The query operator to check against.
344
   *
345
   * @return bool
346
   *   TRUE if the given operator is a range operator, FALSE otherwise.
347
   */
348
  protected function isRangeOperator($operator) {
349
    $null = ["BETWEEN", "NOT BETWEEN"];
350
    return in_array($operator, $null);
351
  }
352
353
}
354