Completed
Pull Request — 8.x-3.x (#505)
by Sebastian
02:40
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\EntityTypeManagerInterface;
8
use Drupal\Core\Entity\Query\QueryInterface;
9
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
10
use Drupal\graphql\Plugin\GraphQL\Fields\FieldPluginBase;
11
use Symfony\Component\DependencyInjection\ContainerInterface;
12
use Youshido\GraphQL\Exception\ResolveException;
13
use Youshido\GraphQL\Execution\ResolveInfo;
14
15
/**
16
 * @GraphQLField(
17
 *   id = "entity_query",
18
 *   secure = true,
19
 *   type = "EntityQueryResult!",
20
 *   arguments = {
21
 *     "filter" = "EntityQueryFilterInput",
22
 *     "sort" = "[EntityQuerySortInput]",
23
 *     "offset" = {
24
 *       "type" = "Int",
25
 *       "default" = 0
26
 *     },
27
 *     "limit" = {
28
 *       "type" = "Int",
29
 *       "default" = 10
30
 *     },
31
 *     "revisions" = {
32
 *       "type" = "EntityQueryRevisionMode",
33
 *       "default" = "default"
34
 *     }
35
 *   },
36
 *   deriver = "Drupal\graphql_core\Plugin\Deriver\Fields\EntityQueryDeriver"
37
 * )
38
 */
39
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...
40
  use DependencySerializationTrait;
41
42
  /**
43
   * The entity type manager.
44
   *
45
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
46
   */
47
  protected $entityTypeManager;
48
49
  /**
50
   * {@inheritdoc}
51
   */
52
  public function __construct(array $configuration, $pluginId, $pluginDefinition, EntityTypeManagerInterface $entityTypeManager) {
53
    $this->entityTypeManager = $entityTypeManager;
54
    parent::__construct($configuration, $pluginId, $pluginDefinition);
55
  }
56
57
  /**
58
   * {@inheritdoc}
59
   */
60
  public static function create(ContainerInterface $container, array $configuration, $pluginId, $pluginDefinition) {
61
    return new static(
62
      $configuration,
63
      $pluginId,
64
      $pluginDefinition,
65
      $container->get('entity_type.manager')
66
    );
67
  }
68
69
  /**
70
   * {@inheritdoc}
71
   */
72
  protected function getCacheDependencies(array $result, $value, array $args, ResolveInfo $info) {
73
    $entityTypeId = $this->pluginDefinition['entity_type'];
74
    $type = $this->entityTypeManager->getDefinition($entityTypeId);
75
76
    $metadata = new CacheableMetadata();
77
    $metadata->addCacheTags($type->getListCacheTags());
78
    $metadata->addCacheContexts($type->getListCacheContexts());
79
80
    return [$metadata];
81
  }
82
83
  /**
84
   * {@inheritdoc}
85
   */
86
  public function resolveValues($value, array $args, ResolveInfo $info) {
87
    yield $this->getQuery($value, $args, $info);
88
  }
89
90
  /**
91
   * Create an entity query for the plugin's entity type.
92
   *
93
   * @param mixed $value
94
   *   The parent entity type.
95
   * @param array $args
96
   *   The field arguments array.
97
   * @param \Youshido\GraphQL\Execution\ResolveInfo $info
98
   *   The resolve info object.
99
   *
100
   * @return \Drupal\Core\Entity\Query\QueryInterface
101
   *   The entity query object.
102
   */
103
  protected function getQuery($value, array $args, ResolveInfo $info) {
104
    $entityTypeId = $this->pluginDefinition['entity_type'];
105
    $entityStorage = $this->entityTypeManager->getStorage($entityTypeId);
106
107
    $query = $entityStorage->getQuery();
108
    $query->range($args['offset'], $args['limit']);
109
    $query->accessCheck(TRUE);
110
111
    // Check if this is a query for all entity revisions.
112
    if (!empty($args['revisions']) && $args['revisions'] === 'all') {
113
      // Mark the query as such and sort by the revision id too.
114
      $query->allRevisions();
115
      $query->addTag('revisions');
116
    }
117
118
    if (!empty($args['filter'])) {
119
      $this->applyFilter($query, $args['filter']);
120
    }
121
122
    if (!empty($args['sort'])) {
123
      $this->applySort($query, $args['sort']);
124
    }
125
126
    return $query;
127
  }
128
129
  /**
130
   * Apply the specified sort directives to the query.
131
   *
132
   * @param \Drupal\Core\Entity\Query\QueryInterface $query
133
   *   The entity query object.
134
   * @param array $sort
135
   *   The sort definitions from the field arguments.
136
   */
137
  protected function applySort(QueryInterface $query, array $sort) {
138
    foreach ($sort as $item) {
139
      $direction = !empty($item['direction']) ? $item['direction'] : 'DESC';
140
      $query->sort($item['field'], $direction);
141
    }
142
  }
143
144
  /**
145
   * Apply the specified filter conditions to the query.
146
   *
147
   * Recursively picks up all filters and aggregates them into condition groups
148
   * according to the nested structure of the filter argument.
149
   *
150
   * @param \Drupal\Core\Entity\Query\QueryInterface $query
151
   *   The entity query object.
152
   * @param array $filter
153
   *   The filter definitions from the field arguments.
154
   */
155
  protected function applyFilter(QueryInterface $query, array $filter) {
156
    $query->condition($this->buildFilterConditions($query, $filter));
157
  }
158
159
  /**
160
   * Recursively builds the filter condition groups.
161
   *
162
   * @param \Drupal\Core\Entity\Query\QueryInterface $query
163
   *   The entity query object.
164
   * @param array $filter
165
   *   The filter definitions from the field arguments.
166
   *
167
   * @return \Drupal\Core\Entity\Query\ConditionInterface
168
   *   The generated condition group according to the given filter definitions.
169
   *
170
   * @throws \Youshido\GraphQL\Exception\ResolveException
171
   *   If the given operator and value for a filter are invalid.
172
   */
173
  protected function buildFilterConditions(QueryInterface $query, array $filter) {
174
    $conjunction = !empty($args['conjunction']) ? $args['conjunction'] : 'AND';
0 ignored issues
show
Bug introduced by
The variable $args seems only to be defined at a later point. As such the call to empty() seems to always evaluate to true.

This check marks calls to isset(...) or empty(...) that are found before the variable itself is defined. These will always have the same result.

This is likely the result of code being shifted around. Consider removing these calls.

Loading history...
175
    $group = $conjunction === 'AND' ? $query->andConditionGroup() : $query->orConditionGroup();
176
177
    // Apply filter conditions.
178
    $conditions = !empty($filter['conditions']) ? $filter['conditions'] : [];
179
    foreach ($conditions as $condition) {
180
      $field = $condition['field'];
181
      $value = !empty($condition['value']) ? $condition['value'] : NULL;
182
      $operator = !empty($condition['operator']) ? $condition['operator'] : NULL;
183
      $language = !empty($condition['language']) ? $condition['language'] : NULL;
184
185
      // We need at least a value or an operator.
186
      if (empty($operator) && empty($value)) {
187
        throw new ResolveException('Missing value and operator in filter.');
188
      }
189
      // Unary operators need a single value.
190
      else if (!empty($operator) && $this->isUnaryOperator($operator)) {
191
        if (empty($value) || count($value) > 1) {
192
          throw new ResolveException('Unary operators must be associated with a single value.');
193
        }
194
195
        // Pick the first item from the values.
196
        $value = reset($value);
197
      }
198
      // Range operators need exactly two values.
199
      else if (!empty($operator) && $this->isRangeOperator($operator)) {
200
        if (empty($value) || count($value) !== 2) {
201
          throw new ResolveException('Range operators must require exactly two values.');
202
        }
203
      }
204
      // Null operators can't have a value set.
205
      else if (!empty($operator) && $this->isNullOperator($operator)) {
206
        if (!empty($value)) {
207
          throw new ResolveException('Null operators must not be associated with a filter value.');
208
        }
209
      }
210
211
      // If no operator is set, however, we default to EQUALS or IN, depending
212
      // on whether the given value is an array with one or more than one items.
213
      if (empty($operator)) {
214
        $value = count($value) === 1 ? reset($value) : $value;
215
        $operator = is_array($value) ? 'IN' : '=';
216
      }
217
218
      // Add the condition for the current field.
219
      $group->condition($field, $value, $operator, $language);
220
    }
221
222
    // Apply nested filter group conditions.
223
    $groups = !empty($filter['groups']) ? $filter['groups'] : [];
224
    foreach ($groups as $args) {
225
      // By default, we use AND condition groups.
226
      $group->condition($this->buildFilterConditions($query, $args));
227
    }
228
229
    return $group;
230
  }
231
232
  /**
233
   * Checks if an operator is a unary operator.
234
   *
235
   * @param string $operator
236
   *   The query operator to check against.
237
   *
238
   * @return bool
239
   *   TRUE if the given operator is unary, FALSE otherwise.
240
   */
241
  protected function isUnaryOperator($operator) {
242
    $unary = ["=", "<>", "<", "<=", ">", ">=", "LIKE", "NOT LIKE"];
243
    return in_array($operator, $unary);
244
  }
245
246
  /**
247
   * Checks if an operator is a null operator.
248
   *
249
   * @param string $operator
250
   *   The query operator to check against.
251
   *
252
   * @return bool
253
   *   TRUE if the given operator is a null operator, FALSE otherwise.
254
   */
255
  protected function isNullOperator($operator) {
256
    $null = ["IS NULL", "IS NOT NULL"];
257
    return in_array($operator, $null);
258
  }
259
260
  /**
261
   * Checks if an operator is a range operator.
262
   *
263
   * @param string $operator
264
   *   The query operator to check against.
265
   *
266
   * @return bool
267
   *   TRUE if the given operator is a range operator, FALSE otherwise.
268
   */
269
  protected function isRangeOperator($operator) {
270
    $null = ["BETWEEN", "NOT BETWEEN"];
271
    return in_array($operator, $null);
272
  }
273
274
}
275