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