Completed
Pull Request — 8.x-3.x (#519)
by Sebastian
03:53
created

EntityQuery::calculateCost()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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