Completed
Push — master ( becf03...491f37 )
by Rafael
08:53
created

AllNodesWithPagination::applyOrderBy()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 33
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
eloc 17
c 0
b 0
f 0
dl 0
loc 33
ccs 0
cts 14
cp 0
rs 9.7
cc 4
nc 3
nop 2
crap 20
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\Query\Expr\Andx;
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\Util\FieldOptionsHelper;
36
37
/**
38
 * Base class to fetch nodes
39
 */
40
class AllNodesWithPagination extends AllNodes
41
{
42
    /**
43
     * @param array[] $args
44
     *
45
     * @return mixed
46
     *
47
     * @throws Error
48
     */
49
    public function __invoke($args = [])
50
    {
51
        //keep orderBy for BC
52
        $orderBy = array_merge($args['orderBy'] ?? [], $args['order'] ?? []);
53
        $first = $args['first'] ?? null;
54
        $last = $args['last'] ?? null;
55
        $before = $args['before'] ?? null;
56
        $after = $args['after'] ?? null;
57
        $page = $args['page'] ?? null;
58
        $search = $args['search'] ?? null;
59
        $filters = $args['filters'] ?? null;
60
        $where = $args['where'] ?? null;
61
62
        $this->initialize();
63
64
        $qb = $this->createQuery();
65
        $this->applyOrderBy($qb, $orderBy);
66
67
        if ($this->getContext()->getRoot()) {
68
            $this->applyFilterByParent($qb, $this->getContext()->getRoot());
69
        }
70
71
        if ($search) {
72
            $this->search($qb, $search);
73
        }
74
75
        if ($filters) {
76
            $this->applyFilters($qb, $filters);
0 ignored issues
show
Deprecated Code introduced by
The function Ynlo\GraphQLBundle\Query...ination::applyFilters() has been deprecated: since v1.1, `applyWhere` should be used instead ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

76
            /** @scrutinizer ignore-deprecated */ $this->applyFilters($qb, $filters);

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.

Loading history...
77
        }
78
79
        if ($where) {
80
            $this->applyWhere($qb, $where);
81
        }
82
83
        $this->configureQuery($qb);
84
        foreach ($this->extensions as $extension) {
85
            $extension->configureQuery($qb, $this, $this->context);
86
        }
87
88
        if (!$first && !$last) {
89
            $error = sprintf('You must provide a `first` or `last` value to properly paginate records in "%s" connection.', $this->queryDefinition->getName());
90
            throw new Error($error);
91
        }
92
93
        if ($this->queryDefinition->hasMeta('pagination')) {
94
            $limitAllowed = $this->queryDefinition->getMeta('pagination')['limit'];
95
96
            if ($first > $limitAllowed || $last > $limitAllowed) {
97
                $current = $first ?? $last;
98
                $where = $first ? 'first' : 'last';
99
                $error = sprintf(
100
                    'Requesting %s records for `%s` exceeds the `%s` limit of %s records for "%s" connection',
101
                    $current,
102
                    $this->queryDefinition->getName(),
103
                    $where,
104
                    $limitAllowed,
105
                    $this->queryDefinition->getName()
106
                );
107
                throw new Error($error);
108
            }
109
        }
110
111
        $paginator = $this->createPaginator();
112
113
        $connection = $this->createConnection();
114
        $paginator->paginate($qb, new PaginationRequest($first, $last, $after, $before, $page), $connection);
115
116
        return $connection;
117
    }
118
119
    protected function createConnection(): ConnectionInterface
120
    {
121
        return new NodeConnection();
122
    }
123
124
    protected function createPaginator(): DoctrineCursorPaginatorInterface
125
    {
126
        return new DoctrineOffsetCursorPaginator();
127
    }
128
129
    /**
130
     * Apply advanced filters
131
     *
132
     * @deprecated since v1.1, `applyWhere` should be used instead
133
     */
134
    protected function applyFilters(QueryBuilder $qb, array $filters)
135
    {
136
        $definition = $this->objectDefinition;
137
        foreach ($filters as $field => $value) {
138
            if (!$definition->hasField($field) || !$prop = $definition->getField($field)->getOriginName()) {
139
                continue;
140
            }
141
142
            $entityField = sprintf('%s.%s', $this->queryAlias, $prop);
143
144
            switch (gettype($value)) {
145
                case 'string':
146
                    $qb->andWhere($qb->expr()->eq($entityField, $qb->expr()->literal($value)));
147
                    break;
148
                case 'integer':
149
                case 'double':
150
                    $qb->andWhere($qb->expr()->eq($entityField, $value));
151
                    break;
152
                case 'boolean':
153
                    $qb->andWhere($qb->expr()->eq($entityField, (int) $value));
154
                    break;
155
                case 'array':
156
                    if (empty($value)) {
157
                        $qb->andWhere($qb->expr()->isNull($entityField));
158
                    } else {
159
                        $qb->andWhere($qb->expr()->in($entityField, $value));
160
                    }
161
                    break;
162
                case 'NULL':
163
                    $qb->andWhere($qb->expr()->isNull($entityField));
164
                    break;
165
            }
166
        }
167
    }
168
169
    /**
170
     * @param QueryBuilder    $qb
171
     * @param array|OrderBy[] $orderBy
172
     *
173
     * @throws Error
174
     */
175
    protected function applyOrderBy(QueryBuilder $qb, $orderBy)
176
    {
177
        $query = $this->queryDefinition;
0 ignored issues
show
Unused Code introduced by
The assignment to $query is dead and can be removed.
Loading history...
178
179
        $orderByType = $this->getContext()->getDefinition()->getArgument('order')->getType();
180
181
        /** @var InputObjectDefinition $orderByDefinition */
182
        $orderByDefinition = $this->getContext()->getEndpoint()->getType($orderByType);
183
        $orderByFieldName = $orderByDefinition->getField('field')->getType();
184
        /** @var EnumDefinition $orderByFieldDefinition */
185
        $orderByFieldDefinition = $this->getContext()->getEndpoint()->getType($orderByFieldName);
186
        /** @var ObjectDefinition $node */
187
        $node = $this->getContext()->getEndpoint()->getType($this->getContext()->getDefinition()->getNode());
188
189
        foreach ($orderBy as $order) {
190
            /** @var EnumValueDefinition $enumValueDeifinition */
191
            $enumValueDefinition = $orderByFieldDefinition->getValues()[$order->getField()];
192
            $orderByResolver = $enumValueDefinition->getMeta('resolver', OrderBySimpleField::class);
193
194
            //set with local name
195
            $order->setField($enumValueDefinition->getMeta('field', $order->getField()));
196
197
            /** @var OrderByInterface $orderByInstance */
198
            $orderByInstance = (new \ReflectionClass($orderByResolver))->newInstanceWithoutConstructor();
199
200
            if ($order->getField() && $node->hasField($order->getField())) {
201
                $relatedField = $node->getField($order->getField());
202
                $context = new OrderByContext($this->getContext()->getEndpoint(), $node, $relatedField);
203
            } else {
204
                $context = new OrderByContext($this->getContext()->getEndpoint(), $node);
205
            }
206
207
            $orderByInstance($context, $qb, $this->queryAlias, $order);
208
        }
209
    }
210
211
    /**
212
     * @param QueryBuilder    $qb
213
     * @param array           $where
214
     *
215
     * @throws \ReflectionException
216
     */
217
    protected function applyWhere(QueryBuilder $qb, array $where): void
218
    {
219
        $whereType = $this->getContext()->getDefinition()->getArgument('where')->getType();
220
221
        /** @var InputObjectDefinition $whereDefinition */
222
        $whereDefinition = $this->getContext()->getEndpoint()->getType($whereType);
223
224
        /** @var ObjectDefinition $node */
225
        $node = $this->getContext()->getEndpoint()->getType($this->getContext()->getDefinition()->getNode());
226
227
        foreach ($where as $filterName => $condition) {
228
            $filterDefinition = $whereDefinition->getField($filterName);
229
230
            //TODO: load filters from services
231
            /** @var FilterInterface $filter */
232
            $filter = (new \ReflectionClass($filterDefinition->getResolver()))->newInstanceWithoutConstructor();
233
234
            $fieldName = $filterDefinition->getMeta('filter_field');
235
            if ($fieldName && $node->hasField($fieldName)) {
236
                $relatedField = $node->getField($fieldName);
237
                $filterContext = new FilterContext($this->getContext()->getEndpoint(), $node, $relatedField);
238
            } else {
239
                $filterContext = new FilterContext($this->getContext()->getEndpoint(), $node);
240
            }
241
242
            $filter($filterContext, $qb, $condition);
243
        }
244
    }
245
246
    /**
247
     * @param QueryBuilder $qb
248
     * @param string       $search
249
     *
250
     * @throws \Doctrine\ORM\Mapping\MappingException
251
     */
252
    protected function search(QueryBuilder $qb, string $search): void
253
    {
254
        $query = $this->queryDefinition;
255
        $node = $this->objectDefinition;
256
257
        $em = $this->getManager();
258
        $metadata = $em->getClassMetadata($this->entity);
259
260
        $node->getFields();
261
        $columns = [];
262
        $searchFields = FieldOptionsHelper::normalize($query->getMeta('pagination')['search_fields'] ?? ['*']);
263
        foreach ($node->getFields() as $field) {
264
            if (!FieldOptionsHelper::isEnabled($searchFields, $field->getName())) {
265
                continue;
266
            }
267
268
            $config = FieldOptionsHelper::getConfig($searchFields, $field->getName(), null);
269
            $searchColumn = null;
270
            if ($metadata->hasField($field->getName())) {
271
                $searchColumn = $field->getName();
272
            }
273
274
            if (!$searchColumn && $metadata->hasField($field->getOriginName())) {
275
                $searchColumn = $field->getOriginName();
276
            }
277
278
            if ($searchColumn) {
279
                switch ($metadata->getFieldMapping($searchColumn)['type']) {
280
                    case Type::STRING:
281
                    case Type::TEXT:
282
                        $columns[$searchColumn] = $config ?? 'partial';
283
                        break;
284
                    case Type::INTEGER:
285
                    case Type::BIGINT:
286
                    case Type::FLOAT:
287
                    case Type::DECIMAL:
288
                    case Type::SMALLINT:
289
                        $columns[$searchColumn] = $config ?? 'exact';
290
                        break;
291
                }
292
            }
293
        }
294
295
        foreach ($searchFields as $field => $mode) {
296
            if (\is_int($field)) {
297
                $field = $mode;
298
                $mode = 'exact';
299
            }
300
301
            if ('*' === $field) {
302
                continue;
303
            }
304
            $columns[$field] = $mode;
305
        }
306
307
308
        if (\count($columns) > 0) {
309
            $joins = [];
310
            $orx = new Orx();
311
            foreach ($columns as $column => $mode) {
312
                $alias = $qb->getRootAliases()[0];
313
                if (strpos($column, '.') !== false) {
314
                    [$child, $column] = explode('.', $column);
315
                    $parentAlias = $alias;
316
                    $alias = 'searchJoin'.ucfirst($child);
317
                    if (!\in_array($alias, $joins, true)) {
318
                        $qb->leftJoin("{$parentAlias}.{$child}", $alias);
319
                        $joins[] = $alias;
320
                    }
321
                }
322
323
                if ('partial' === $mode) {
324
                    //search each word separate
325
                    $searchArray = explode(' ', $search);
326
327
                    $partialAnd = new Andx();
328
                    foreach ($searchArray as $index => $q) {
329
                        $partialAnd->add("$alias.$column LIKE :query_search_$index");
330
                        $qb->setParameter("query_search_$index", '%'.addcslashes($q, '%_').'%');
331
                    }
332
                    $orx->add($partialAnd);
333
                } else {
334
                    $orx->add("$alias.$column LIKE :query_search");
335
                    $qb->setParameter('query_search', trim($search));
336
                }
337
            }
338
339
            $qb->andWhere($orx);
340
        }
341
    }
342
343
    /**
344
     * @param QueryBuilder  $qb
345
     * @param NodeInterface $root
346
     */
347
    protected function applyFilterByParent(QueryBuilder $qb, NodeInterface $root)
348
    {
349
        $parentField = null;
350
        if ($this->queryDefinition->hasMeta('pagination')) {
351
            $parentField = $this->queryDefinition->getMeta('pagination')['parent_field'] ?? null;
352
        }
353
        if (!$parentField) {
354
            throw new \RuntimeException(
355
                sprintf(
356
                    'Missing parent field to filter "%s" by given parent.
357
             The "parentField" should be specified in @Pagination annotation.',
358
                    $this->queryDefinition->getName()
359
                )
360
            );
361
        }
362
363
        if ($this->objectDefinition->hasField($parentField)) {
364
            $parentField = $this->objectDefinition->getField($parentField)->getOriginName();
365
        }
366
367
        $paramName = 'root'.mt_rand();
368
        if ($this->queryDefinition->getMeta('pagination')['parent_relation'] === PaginationDefinitionPlugin::MANY_TO_MANY) {
369
            $qb->andWhere(sprintf(':%s MEMBER OF %s.%s', $paramName, $this->queryAlias, $parentField))
370
                ->setParameter($paramName, $root);
371
        } else {
372
            $qb->andWhere(sprintf('%s.%s = :%s', $this->queryAlias, $parentField, $paramName))
373
                ->setParameter($paramName, $root);
374
        }
375
    }
376
}
377