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\ORM\Query\Expr\Orx; |
14
|
|
|
use Doctrine\ORM\QueryBuilder; |
15
|
|
|
use GraphQL\Error\Error; |
16
|
|
|
use Ynlo\GraphQLBundle\Definition\InputObjectDefinition; |
17
|
|
|
use Ynlo\GraphQLBundle\Definition\ObjectDefinition; |
18
|
|
|
use Ynlo\GraphQLBundle\Definition\Plugin\PaginationDefinitionPlugin; |
19
|
|
|
use Ynlo\GraphQLBundle\Filter\FilterContext; |
20
|
|
|
use Ynlo\GraphQLBundle\Filter\FilterInterface; |
21
|
|
|
use Ynlo\GraphQLBundle\Model\ConnectionInterface; |
22
|
|
|
use Ynlo\GraphQLBundle\Model\NodeConnection; |
23
|
|
|
use Ynlo\GraphQLBundle\Model\NodeInterface; |
24
|
|
|
use Ynlo\GraphQLBundle\Pagination\DoctrineCursorPaginatorInterface; |
25
|
|
|
use Ynlo\GraphQLBundle\Pagination\DoctrineOffsetCursorPaginator; |
26
|
|
|
use Ynlo\GraphQLBundle\Pagination\PaginationRequest; |
27
|
|
|
use Ynlo\GraphQLBundle\Resolver\QueryExecutionContext; |
28
|
|
|
|
29
|
|
|
/** |
30
|
|
|
* Base class to fetch nodes |
31
|
|
|
*/ |
32
|
|
|
class AllNodesWithPagination extends AllNodes |
33
|
|
|
{ |
34
|
|
|
/** |
35
|
|
|
* @param array[] $args |
36
|
|
|
* @param QueryExecutionContext $context |
37
|
|
|
* |
38
|
|
|
* @return mixed |
39
|
|
|
* |
40
|
|
|
* @throws Error |
41
|
|
|
*/ |
42
|
|
|
public function __invoke($args = [], QueryExecutionContext $context) |
43
|
|
|
{ |
44
|
|
|
$orderBy = $args['orderBy'] ?? []; |
45
|
|
|
$first = $args['first'] ?? null; |
46
|
|
|
$last = $args['last'] ?? null; |
47
|
|
|
$before = $args['before'] ?? null; |
48
|
|
|
$after = $args['after'] ?? null; |
49
|
|
|
$search = $args['search'] ?? null; |
50
|
|
|
$filters = $args['filters'] ?? null; |
51
|
|
|
$where = $args['where'] ?? null; |
52
|
|
|
|
53
|
|
|
$this->initialize(); |
54
|
|
|
|
55
|
|
|
$qb = $this->createQuery(); |
56
|
|
|
$this->applyOrderBy($qb, $orderBy); |
57
|
|
|
|
58
|
|
|
if ($this->getContext()->getRoot()) { |
59
|
|
|
$this->applyFilterByParent($qb, $this->getContext()->getRoot()); |
60
|
|
|
} |
61
|
|
|
|
62
|
|
|
if ($search) { |
63
|
|
|
$this->search($qb, $search); |
64
|
|
|
} |
65
|
|
|
|
66
|
|
|
if ($filters) { |
67
|
|
|
$this->applyFilters($qb, $filters); |
|
|
|
|
68
|
|
|
} |
69
|
|
|
|
70
|
|
|
if ($where) { |
71
|
|
|
$this->applyWhere($context, $qb, $where); |
72
|
|
|
} |
73
|
|
|
|
74
|
|
|
$this->configureQuery($qb); |
75
|
|
|
foreach ($this->extensions as $extension) { |
76
|
|
|
$extension->configureQuery($qb, $this, $this->context); |
77
|
|
|
} |
78
|
|
|
|
79
|
|
|
if (!$first && !$last) { |
80
|
|
|
$error = sprintf('You must provide a `first` or `last` value to properly paginate records in "%s" connection.', $this->queryDefinition->getName()); |
81
|
|
|
throw new Error($error); |
82
|
|
|
} |
83
|
|
|
|
84
|
|
|
if ($this->queryDefinition->hasMeta('pagination')) { |
85
|
|
|
$limitAllowed = $this->queryDefinition->getMeta('pagination')['limit']; |
86
|
|
|
|
87
|
|
|
if ($first > $limitAllowed || $last > $limitAllowed) { |
88
|
|
|
$current = $first ?? $last; |
89
|
|
|
$where = $first ? 'first' : 'last'; |
90
|
|
|
$error = sprintf( |
91
|
|
|
'Requesting %s records for `%s` exceeds the `%s` limit of %s records for "%s" connection', |
92
|
|
|
$current, |
93
|
|
|
$this->queryDefinition->getName(), |
94
|
|
|
$where, |
95
|
|
|
$limitAllowed, |
96
|
|
|
$this->queryDefinition->getName() |
97
|
|
|
); |
98
|
|
|
throw new Error($error); |
99
|
|
|
} |
100
|
|
|
} |
101
|
|
|
|
102
|
|
|
$paginator = $this->createPaginator(); |
103
|
|
|
|
104
|
|
|
$connection = $this->createConnection(); |
105
|
|
|
$paginator->paginate($qb, new PaginationRequest($first, $last, $after, $before), $connection); |
106
|
|
|
|
107
|
|
|
return $connection; |
108
|
|
|
} |
109
|
|
|
|
110
|
|
|
protected function createConnection(): ConnectionInterface |
111
|
|
|
{ |
112
|
|
|
return new NodeConnection(); |
113
|
|
|
} |
114
|
|
|
|
115
|
|
|
protected function createPaginator(): DoctrineCursorPaginatorInterface |
116
|
|
|
{ |
117
|
|
|
return new DoctrineOffsetCursorPaginator(); |
118
|
|
|
} |
119
|
|
|
|
120
|
|
|
/** |
121
|
|
|
* Apply advanced filters |
122
|
|
|
* |
123
|
|
|
* @deprecated since v1.1, `applyWhere` should be used instead |
124
|
|
|
*/ |
125
|
|
|
protected function applyFilters(QueryBuilder $qb, array $filters) |
126
|
|
|
{ |
127
|
|
|
$definition = $this->objectDefinition; |
128
|
|
|
foreach ($filters as $field => $value) { |
129
|
|
|
if (!$definition->hasField($field) || !$prop = $definition->getField($field)->getOriginName()) { |
130
|
|
|
continue; |
131
|
|
|
} |
132
|
|
|
|
133
|
|
|
$entityField = sprintf('%s.%s', $this->queryAlias, $prop); |
134
|
|
|
|
135
|
|
|
switch (gettype($value)) { |
136
|
|
|
case 'string': |
137
|
|
|
$qb->andWhere($qb->expr()->eq($entityField, $qb->expr()->literal($value))); |
138
|
|
|
break; |
139
|
|
|
case 'integer': |
140
|
|
|
case 'double': |
141
|
|
|
$qb->andWhere($qb->expr()->eq($entityField, $value)); |
142
|
|
|
break; |
143
|
|
|
case 'boolean': |
144
|
|
|
$qb->andWhere($qb->expr()->eq($entityField, (int) $value)); |
145
|
|
|
break; |
146
|
|
|
case 'array': |
147
|
|
|
if (empty($value)) { |
148
|
|
|
$qb->andWhere($qb->expr()->isNull($entityField)); |
149
|
|
|
} else { |
150
|
|
|
$qb->andWhere($qb->expr()->in($entityField, $value)); |
151
|
|
|
} |
152
|
|
|
break; |
153
|
|
|
case 'NULL': |
154
|
|
|
$qb->andWhere($qb->expr()->isNull($entityField)); |
155
|
|
|
break; |
156
|
|
|
} |
157
|
|
|
} |
158
|
|
|
} |
159
|
|
|
|
160
|
|
|
/** |
161
|
|
|
* @param QueryExecutionContext $context |
162
|
|
|
* @param QueryBuilder $qb |
163
|
|
|
* @param array $where |
164
|
|
|
* |
165
|
|
|
* @throws \ReflectionException |
166
|
|
|
*/ |
167
|
|
|
protected function applyWhere(QueryExecutionContext $context, QueryBuilder $qb, array $where): void |
168
|
|
|
{ |
169
|
|
|
$whereType = $context->getDefinition()->getArgument('where')->getType(); |
170
|
|
|
|
171
|
|
|
/** @var InputObjectDefinition $whereDefinition */ |
172
|
|
|
$whereDefinition = $context->getEndpoint()->getType($whereType); |
173
|
|
|
|
174
|
|
|
/** @var ObjectDefinition $node */ |
175
|
|
|
$node = $context->getEndpoint()->getType($context->getDefinition()->getNode()); |
176
|
|
|
|
177
|
|
|
foreach ($where as $filterName => $condition) { |
178
|
|
|
$filterDefinition = $whereDefinition->getField($filterName); |
179
|
|
|
|
180
|
|
|
//TODO: load filters from services |
181
|
|
|
/** @var FilterInterface $filter */ |
182
|
|
|
$filter = (new \ReflectionClass($filterDefinition->getResolver()))->newInstanceWithoutConstructor(); |
183
|
|
|
|
184
|
|
|
$fieldName = $filterDefinition->getMeta('filter_field'); |
185
|
|
|
if ($fieldName && $node->hasField($fieldName)) { |
186
|
|
|
$relatedField = $node->getField($fieldName); |
187
|
|
|
$filterContext = new FilterContext($context->getEndpoint(), $node, $relatedField); |
188
|
|
|
} else { |
189
|
|
|
$filterContext = new FilterContext($context->getEndpoint(), $node); |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
$filter($filterContext, $qb, $condition); |
193
|
|
|
} |
194
|
|
|
} |
195
|
|
|
|
196
|
|
|
/** |
197
|
|
|
* Filter some columns with simple string. |
198
|
|
|
*/ |
199
|
|
|
protected function search(QueryBuilder $qb, string $search) |
200
|
|
|
{ |
201
|
|
|
//search every word separate |
202
|
|
|
$searchArray = explode(' ', $search); |
203
|
|
|
|
204
|
|
|
$alias = $qb->getRootAliases()[0]; |
205
|
|
|
|
206
|
|
|
//TODO: allow some config to customize search fields |
207
|
|
|
$em = $this->getManager(); |
208
|
|
|
$metadata = $em->getClassMetadata($this->entity); |
209
|
|
|
$searchFields = $metadata->getFieldNames(); |
210
|
|
|
|
211
|
|
|
if (\count($searchFields) > 0) { |
212
|
|
|
$meta = $qb->getEntityManager()->getClassMetadata($qb->getRootEntities()[0]); |
213
|
|
|
foreach ($searchArray as $q) { |
214
|
|
|
$q = trim(rtrim($q)); |
215
|
|
|
$id = md5($q); |
216
|
|
|
$orx = new Orx(); |
217
|
|
|
foreach ($searchFields as $field) { |
218
|
|
|
if (strpos($field, '.') !== false && !isset($meta->embeddedClasses[explode('.', $field)[0]])) { |
219
|
|
|
$orx->add("$field LIKE :search_$id"); |
220
|
|
|
} else { //append current alias |
221
|
|
|
$orx->add("$alias.$field LIKE :search_$id"); |
222
|
|
|
} |
223
|
|
|
} |
224
|
|
|
$qb->andWhere($orx); |
225
|
|
|
$qb->setParameter("search_$id", "%$q%"); |
226
|
|
|
} |
227
|
|
|
} |
228
|
|
|
} |
229
|
|
|
|
230
|
|
|
/** |
231
|
|
|
* @param QueryBuilder $qb |
232
|
|
|
* @param NodeInterface $root |
233
|
|
|
*/ |
234
|
|
|
protected function applyFilterByParent(QueryBuilder $qb, NodeInterface $root) |
235
|
|
|
{ |
236
|
|
|
$parentField = null; |
237
|
|
|
if ($this->queryDefinition->hasMeta('pagination')) { |
238
|
|
|
$parentField = $this->queryDefinition->getMeta('pagination')['parent_field'] ?? null; |
239
|
|
|
} |
240
|
|
|
if (!$parentField) { |
241
|
|
|
throw new \RuntimeException( |
242
|
|
|
sprintf( |
243
|
|
|
'Missing parent field to filter "%s" by given parent. |
244
|
|
|
The "parent_field" should be specified.', |
245
|
|
|
$this->queryDefinition->getName() |
246
|
|
|
) |
247
|
|
|
); |
248
|
|
|
} |
249
|
|
|
|
250
|
|
|
if ($this->objectDefinition->hasField($parentField)) { |
251
|
|
|
$parentField = $this->objectDefinition->getField($parentField)->getOriginName(); |
252
|
|
|
} |
253
|
|
|
|
254
|
|
|
$paramName = 'root'.mt_rand(); |
255
|
|
|
if ($this->queryDefinition->getMeta('pagination')['parent_relation'] === PaginationDefinitionPlugin::MANY_TO_MANY) { |
256
|
|
|
$qb->andWhere(sprintf(':%s MEMBER OF %s.%s', $paramName, $this->queryAlias, $parentField)) |
257
|
|
|
->setParameter($paramName, $root); |
258
|
|
|
} else { |
259
|
|
|
$qb->andWhere(sprintf('%s.%s = :%s', $this->queryAlias, $parentField, $paramName)) |
260
|
|
|
->setParameter($paramName, $root); |
261
|
|
|
} |
262
|
|
|
} |
263
|
|
|
} |
264
|
|
|
|
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.