1
|
|
|
<?php |
2
|
|
|
/******************************************************************************* |
3
|
|
|
* This file is part of the GraphQL Bundle package. |
4
|
|
|
* |
5
|
|
|
* (c) YnloUltratech <[email protected]> |
6
|
|
|
* |
7
|
|
|
* For the full copyright and license information, please view the LICENSE |
8
|
|
|
* file that was distributed with this source code. |
9
|
|
|
******************************************************************************/ |
10
|
|
|
|
11
|
|
|
namespace Ynlo\GraphQLBundle\Query\Node; |
12
|
|
|
|
13
|
|
|
use Doctrine\DBAL\Types\Type; |
14
|
|
|
use Doctrine\ORM\Mapping\MappingException; |
15
|
|
|
use Doctrine\ORM\Query\Expr\Orx; |
16
|
|
|
use Doctrine\ORM\QueryBuilder; |
17
|
|
|
use GraphQL\Error\Error; |
18
|
|
|
use Ynlo\GraphQLBundle\Definition\EnumDefinition; |
19
|
|
|
use Ynlo\GraphQLBundle\Definition\EnumValueDefinition; |
20
|
|
|
use Ynlo\GraphQLBundle\Definition\InputObjectDefinition; |
21
|
|
|
use Ynlo\GraphQLBundle\Definition\ObjectDefinition; |
22
|
|
|
use Ynlo\GraphQLBundle\Definition\Plugin\PaginationDefinitionPlugin; |
23
|
|
|
use Ynlo\GraphQLBundle\Filter\FilterContext; |
24
|
|
|
use Ynlo\GraphQLBundle\Filter\FilterInterface; |
25
|
|
|
use Ynlo\GraphQLBundle\Model\ConnectionInterface; |
26
|
|
|
use Ynlo\GraphQLBundle\Model\NodeConnection; |
27
|
|
|
use Ynlo\GraphQLBundle\Model\NodeInterface; |
28
|
|
|
use Ynlo\GraphQLBundle\Model\OrderBy; |
29
|
|
|
use Ynlo\GraphQLBundle\OrderBy\Common\OrderBySimpleField; |
30
|
|
|
use Ynlo\GraphQLBundle\OrderBy\OrderByContext; |
31
|
|
|
use Ynlo\GraphQLBundle\OrderBy\OrderByInterface; |
32
|
|
|
use Ynlo\GraphQLBundle\Pagination\DoctrineCursorPaginatorInterface; |
33
|
|
|
use Ynlo\GraphQLBundle\Pagination\DoctrineOffsetCursorPaginator; |
34
|
|
|
use Ynlo\GraphQLBundle\Pagination\PaginationRequest; |
35
|
|
|
use Ynlo\GraphQLBundle\SearchBy\Common\SearchByDoctrineColumn; |
36
|
|
|
use Ynlo\GraphQLBundle\SearchBy\SearchByContext; |
37
|
|
|
use Ynlo\GraphQLBundle\SearchBy\SearchByInterface; |
38
|
|
|
use Ynlo\GraphQLBundle\Util\FieldOptionsHelper; |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* Base class to fetch nodes |
42
|
|
|
*/ |
43
|
|
|
class AllNodesWithPagination extends AllNodes |
44
|
|
|
{ |
45
|
|
|
/** |
46
|
|
|
* @param array[] $args |
47
|
|
|
* |
48
|
|
|
* @return mixed |
49
|
|
|
* |
50
|
|
|
* @throws Error |
51
|
|
|
*/ |
52
|
|
|
public function __invoke($args = []) |
53
|
|
|
{ |
54
|
|
|
//keep orderBy for BC |
55
|
|
|
$orderBy = array_merge($args['orderBy'] ?? [], $args['order'] ?? []); |
56
|
|
|
$first = $args['first'] ?? null; |
57
|
|
|
$last = $args['last'] ?? null; |
58
|
|
|
$before = $args['before'] ?? null; |
59
|
|
|
$after = $args['after'] ?? null; |
60
|
|
|
$page = $args['page'] ?? null; |
61
|
|
|
$search = $args['search'] ?? null; |
62
|
|
|
$filters = $args['filters'] ?? null; |
63
|
|
|
$where = $args['where'] ?? null; |
64
|
|
|
|
65
|
|
|
$this->initialize(); |
66
|
|
|
|
67
|
|
|
$qb = $this->createQuery(); |
68
|
|
|
$this->applyOrderBy($qb, $orderBy); |
69
|
|
|
|
70
|
|
|
if ($this->getContext()->getRoot()) { |
71
|
|
|
$this->applyFilterByParent($qb, $this->getContext()->getRoot()); |
72
|
|
|
} |
73
|
|
|
|
74
|
|
|
if ($search) { |
75
|
|
|
$this->search($qb, $search); |
76
|
|
|
} |
77
|
|
|
|
78
|
|
|
if ($filters) { |
79
|
|
|
$this->applyFilters($qb, $filters); |
|
|
|
|
80
|
|
|
} |
81
|
|
|
|
82
|
|
|
if ($where) { |
83
|
|
|
$this->applyWhere($qb, $where); |
84
|
|
|
} |
85
|
|
|
|
86
|
|
|
$this->configureQuery($qb); |
87
|
|
|
foreach ($this->extensions as $extension) { |
88
|
|
|
$extension->configureQuery($qb, $this, $this->context); |
89
|
|
|
} |
90
|
|
|
|
91
|
|
|
if (!$first && !$last) { |
92
|
|
|
$error = sprintf('You must provide a `first` or `last` value to properly paginate records in "%s" connection.', $this->queryDefinition->getName()); |
93
|
|
|
throw new Error($error); |
94
|
|
|
} |
95
|
|
|
|
96
|
|
|
if ($this->queryDefinition->hasMeta('pagination')) { |
97
|
|
|
$limitAllowed = $this->queryDefinition->getMeta('pagination')['limit']; |
98
|
|
|
|
99
|
|
|
if ($first > $limitAllowed || $last > $limitAllowed) { |
100
|
|
|
$current = $first ?? $last; |
101
|
|
|
$where = $first ? 'first' : 'last'; |
102
|
|
|
$error = sprintf( |
103
|
|
|
'Requesting %s records for `%s` exceeds the `%s` limit of %s records for "%s" connection', |
104
|
|
|
$current, |
105
|
|
|
$this->queryDefinition->getName(), |
106
|
|
|
$where, |
107
|
|
|
$limitAllowed, |
108
|
|
|
$this->queryDefinition->getName() |
109
|
|
|
); |
110
|
|
|
throw new Error($error); |
111
|
|
|
} |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
$paginator = $this->createPaginator(); |
115
|
|
|
|
116
|
|
|
$connection = $this->createConnection(); |
117
|
|
|
$paginator->paginate($qb, new PaginationRequest($first, $last, $after, $before, $page), $connection); |
118
|
|
|
|
119
|
|
|
return $connection; |
120
|
|
|
} |
121
|
|
|
|
122
|
|
|
protected function createConnection(): ConnectionInterface |
123
|
|
|
{ |
124
|
|
|
return new NodeConnection(); |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
protected function createPaginator(): DoctrineCursorPaginatorInterface |
128
|
|
|
{ |
129
|
|
|
return new DoctrineOffsetCursorPaginator($this->getManager()); |
130
|
|
|
} |
131
|
|
|
|
132
|
|
|
/** |
133
|
|
|
* Apply advanced filters |
134
|
|
|
* |
135
|
|
|
* @deprecated since v1.1, `applyWhere` should be used instead |
136
|
|
|
*/ |
137
|
|
|
protected function applyFilters(QueryBuilder $qb, array $filters) |
138
|
|
|
{ |
139
|
|
|
$definition = $this->objectDefinition; |
140
|
|
|
foreach ($filters as $field => $value) { |
141
|
|
|
if (!$definition->hasField($field) || !$prop = $definition->getField($field)->getOriginName()) { |
142
|
|
|
continue; |
143
|
|
|
} |
144
|
|
|
|
145
|
|
|
$entityField = sprintf('%s.%s', $this->queryAlias, $prop); |
146
|
|
|
|
147
|
|
|
switch (gettype($value)) { |
148
|
|
|
case 'string': |
149
|
|
|
$qb->andWhere($qb->expr()->eq($entityField, $qb->expr()->literal($value))); |
150
|
|
|
break; |
151
|
|
|
case 'integer': |
152
|
|
|
case 'double': |
153
|
|
|
$qb->andWhere($qb->expr()->eq($entityField, $value)); |
154
|
|
|
break; |
155
|
|
|
case 'boolean': |
156
|
|
|
$qb->andWhere($qb->expr()->eq($entityField, (int) $value)); |
157
|
|
|
break; |
158
|
|
|
case 'array': |
159
|
|
|
if (empty($value)) { |
160
|
|
|
$qb->andWhere($qb->expr()->isNull($entityField)); |
161
|
|
|
} else { |
162
|
|
|
$qb->andWhere($qb->expr()->in($entityField, $value)); |
163
|
|
|
} |
164
|
|
|
break; |
165
|
|
|
case 'NULL': |
166
|
|
|
$qb->andWhere($qb->expr()->isNull($entityField)); |
167
|
|
|
break; |
168
|
|
|
} |
169
|
|
|
} |
170
|
|
|
} |
171
|
|
|
|
172
|
|
|
/** |
173
|
|
|
* @param QueryBuilder $qb |
174
|
|
|
* @param array|OrderBy[] $orderBy |
175
|
|
|
* |
176
|
|
|
* @throws Error |
177
|
|
|
*/ |
178
|
|
|
protected function applyOrderBy(QueryBuilder $qb, $orderBy) |
179
|
|
|
{ |
180
|
|
|
if (!$this->getContext()->getDefinition()->hasArgument('order')) { |
181
|
|
|
return; |
182
|
|
|
} |
183
|
|
|
|
184
|
|
|
$orderByType = $this->getContext()->getDefinition()->getArgument('order')->getType(); |
185
|
|
|
|
186
|
|
|
/** @var InputObjectDefinition $orderByDefinition */ |
187
|
|
|
$orderByDefinition = $this->getContext()->getEndpoint()->getType($orderByType); |
188
|
|
|
$orderByFieldName = $orderByDefinition->getField('field')->getType(); |
189
|
|
|
/** @var EnumDefinition $orderByFieldDefinition */ |
190
|
|
|
$orderByFieldDefinition = $this->getContext()->getEndpoint()->getType($orderByFieldName); |
191
|
|
|
/** @var ObjectDefinition $node */ |
192
|
|
|
$node = $this->getContext()->getEndpoint()->getType($this->getContext()->getDefinition()->getNode()); |
193
|
|
|
|
194
|
|
|
foreach ($orderBy as $order) { |
195
|
|
|
/** @var EnumValueDefinition $enumValueDeifinition */ |
196
|
|
|
$enumValueDefinition = $orderByFieldDefinition->getValues()[$order->getField()]; |
197
|
|
|
$orderByResolver = $enumValueDefinition->getMeta('resolver', OrderBySimpleField::class); |
198
|
|
|
|
199
|
|
|
//set with local name |
200
|
|
|
$order->setField($enumValueDefinition->getMeta('field', $order->getField())); |
201
|
|
|
|
202
|
|
|
/** @var OrderByInterface $orderByInstance */ |
203
|
|
|
$orderByInstance = (new \ReflectionClass($orderByResolver))->newInstanceWithoutConstructor(); |
204
|
|
|
|
205
|
|
|
if ($order->getField() && $node->hasField($order->getField())) { |
206
|
|
|
$relatedField = $node->getField($order->getField()); |
207
|
|
|
$context = new OrderByContext($this->getContext(), $node, $relatedField); |
208
|
|
|
} else { |
209
|
|
|
$context = new OrderByContext($this->getContext(), $node); |
210
|
|
|
} |
211
|
|
|
|
212
|
|
|
$orderByInstance($context, $qb, $this->queryAlias, $order); |
213
|
|
|
} |
214
|
|
|
} |
215
|
|
|
|
216
|
|
|
/** |
217
|
|
|
* @param QueryBuilder $qb |
218
|
|
|
* @param array $where |
219
|
|
|
* |
220
|
|
|
* @throws \ReflectionException |
221
|
|
|
*/ |
222
|
|
|
protected function applyWhere(QueryBuilder $qb, array $where): void |
223
|
|
|
{ |
224
|
|
|
$whereType = $this->getContext()->getDefinition()->getArgument('where')->getType(); |
225
|
|
|
|
226
|
|
|
/** @var InputObjectDefinition $whereDefinition */ |
227
|
|
|
$whereDefinition = $this->getContext()->getEndpoint()->getType($whereType); |
228
|
|
|
|
229
|
|
|
/** @var ObjectDefinition $node */ |
230
|
|
|
$node = $this->getContext()->getEndpoint()->getType($this->getContext()->getDefinition()->getNode()); |
231
|
|
|
|
232
|
|
|
foreach ($where as $filterName => $condition) { |
233
|
|
|
$filterDefinition = $whereDefinition->getField($filterName); |
234
|
|
|
|
235
|
|
|
/** @var FilterInterface $filter */ |
236
|
|
|
if ($this->container->has($filterDefinition->getResolver())) { |
237
|
|
|
$filter = $this->container->get($filterDefinition->getResolver()); |
238
|
|
|
} else { |
239
|
|
|
$filter = (new \ReflectionClass($filterDefinition->getResolver()))->newInstanceWithoutConstructor(); |
240
|
|
|
} |
241
|
|
|
|
242
|
|
|
$fieldName = $filterDefinition->getMeta('filter_field'); |
243
|
|
|
if ($fieldName && $node->hasField($fieldName)) { |
244
|
|
|
$relatedField = $node->getField($fieldName); |
245
|
|
|
$filterContext = new FilterContext($this->getContext(), $node, $relatedField); |
246
|
|
|
} else { |
247
|
|
|
$filterContext = new FilterContext($this->getContext(), $node); |
248
|
|
|
} |
249
|
|
|
|
250
|
|
|
$filter($filterContext, $qb, $condition); |
251
|
|
|
} |
252
|
|
|
} |
253
|
|
|
|
254
|
|
|
/** |
255
|
|
|
* @param QueryBuilder $qb |
256
|
|
|
* @param string $search |
257
|
|
|
* |
258
|
|
|
* @throws \Doctrine\ORM\Mapping\MappingException |
259
|
|
|
*/ |
260
|
|
|
protected function search(QueryBuilder $qb, string $search): void |
261
|
|
|
{ |
262
|
|
|
$query = $this->queryDefinition; |
263
|
|
|
$node = $this->objectDefinition; |
264
|
|
|
|
265
|
|
|
$em = $this->getManager(); |
266
|
|
|
$metadata = $em->getClassMetadata($this->entity); |
267
|
|
|
|
268
|
|
|
$node->getFields(); |
269
|
|
|
$columns = []; |
270
|
|
|
$searchFields = FieldOptionsHelper::normalize($query->getMeta('pagination')['search_fields'] ?? ['*']); |
271
|
|
|
foreach ($node->getFields() as $field) { |
272
|
|
|
if (!FieldOptionsHelper::isEnabled($searchFields, $field->getName())) { |
273
|
|
|
continue; |
274
|
|
|
} |
275
|
|
|
|
276
|
|
|
$config = FieldOptionsHelper::getConfig($searchFields, $field->getName(), null); |
277
|
|
|
$searchColumn = null; |
278
|
|
|
if ($metadata->hasField($field->getName())) { |
279
|
|
|
$searchColumn = $field->getName(); |
280
|
|
|
} |
281
|
|
|
|
282
|
|
|
if (!$searchColumn && $metadata->hasField($field->getOriginName())) { |
283
|
|
|
$searchColumn = $field->getOriginName(); |
284
|
|
|
} |
285
|
|
|
|
286
|
|
|
if ($searchColumn) { |
287
|
|
|
try { |
288
|
|
|
switch ($metadata->getFieldMapping($searchColumn)['type']) { |
289
|
|
|
case Type::STRING: |
290
|
|
|
case Type::TEXT: |
291
|
|
|
$columns[$searchColumn] = $config ?? SearchByInterface::PARTIAL_SEARCH; |
292
|
|
|
break; |
293
|
|
|
case Type::INTEGER: |
294
|
|
|
case Type::BIGINT: |
295
|
|
|
case Type::FLOAT: |
296
|
|
|
case Type::DECIMAL: |
297
|
|
|
case Type::SMALLINT: |
298
|
|
|
$columns[$searchColumn] = $config ?? SearchByInterface::EXACT_MATCH; |
299
|
|
|
break; |
300
|
|
|
} |
301
|
|
|
} catch (MappingException $exception) { |
302
|
|
|
continue; |
303
|
|
|
} |
304
|
|
|
} |
305
|
|
|
} |
306
|
|
|
|
307
|
|
|
foreach ($searchFields as $field => $mode) { |
308
|
|
|
if (\is_int($field)) { |
309
|
|
|
$field = $mode; |
310
|
|
|
$mode = SearchByInterface::EXACT_MATCH; |
311
|
|
|
} |
312
|
|
|
|
313
|
|
|
if ('*' === $field || $mode === false) { |
314
|
|
|
continue; |
315
|
|
|
} |
316
|
|
|
|
317
|
|
|
if (class_exists($field) && is_a($field, SearchByInterface::class, true)) { |
318
|
|
|
$mode = is_bool($mode) ? SearchByInterface::PARTIAL_SEARCH : $mode; |
319
|
|
|
} |
320
|
|
|
|
321
|
|
|
$columns[$field] = $mode; |
322
|
|
|
} |
323
|
|
|
|
324
|
|
|
if (\count($columns) > 0) { |
325
|
|
|
$orx = new Orx(); |
326
|
|
|
$searchContext = new SearchByContext($this->getContext(), $node); |
327
|
|
|
foreach ($columns as $column => $mode) { |
328
|
|
|
if (class_exists($column) && is_a($column, SearchByInterface::class, true) && $this->container->has($column)) { |
329
|
|
|
$searchBy = $this->container->get($column); |
330
|
|
|
} else { |
331
|
|
|
$searchBy = new SearchByDoctrineColumn(); |
332
|
|
|
} |
333
|
|
|
$searchBy($searchContext, $qb, $orx, $this->queryAlias, $column, $mode, $search); |
334
|
|
|
} |
335
|
|
|
|
336
|
|
|
$qb->andWhere($orx); |
337
|
|
|
} |
338
|
|
|
} |
339
|
|
|
|
340
|
|
|
/** |
341
|
|
|
* @param QueryBuilder $qb |
342
|
|
|
* @param NodeInterface $root |
343
|
|
|
*/ |
344
|
|
|
protected function applyFilterByParent(QueryBuilder $qb, NodeInterface $root) |
345
|
|
|
{ |
346
|
|
|
$parentField = null; |
347
|
|
|
if ($this->queryDefinition->hasMeta('pagination')) { |
348
|
|
|
$parentField = $this->queryDefinition->getMeta('pagination')['parent_field'] ?? null; |
349
|
|
|
} |
350
|
|
|
if (!$parentField) { |
351
|
|
|
throw new \RuntimeException( |
352
|
|
|
sprintf( |
353
|
|
|
'Missing parent field to filter "%s" by given parent. |
354
|
|
|
The "parentField" should be specified in @Pagination annotation.', |
355
|
|
|
$this->queryDefinition->getName() |
356
|
|
|
) |
357
|
|
|
); |
358
|
|
|
} |
359
|
|
|
|
360
|
|
|
if ($this->objectDefinition->hasField($parentField)) { |
361
|
|
|
$parentField = $this->objectDefinition->getField($parentField)->getOriginName(); |
362
|
|
|
} |
363
|
|
|
|
364
|
|
|
$paramName = 'root'.mt_rand(); |
365
|
|
|
if ($this->queryDefinition->getMeta('pagination')['parent_relation'] === PaginationDefinitionPlugin::MANY_TO_MANY) { |
366
|
|
|
$qb->andWhere(sprintf(':%s MEMBER OF %s.%s', $paramName, $this->queryAlias, $parentField)) |
367
|
|
|
->setParameter($paramName, $root); |
368
|
|
|
} else { |
369
|
|
|
$qb->andWhere(sprintf('%s.%s = :%s', $this->queryAlias, $parentField, $paramName)) |
370
|
|
|
->setParameter($paramName, $root); |
371
|
|
|
} |
372
|
|
|
} |
373
|
|
|
} |
374
|
|
|
|
This function has been deprecated. The supplier of the function has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.