AllNodesWithPagination   F
last analyzed

Complexity

Total Complexity 67

Size/Duplication

Total Lines 328
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 11
Bugs 6 Features 1
Metric Value
wmc 67
eloc 174
c 11
b 6
f 1
dl 0
loc 328
ccs 0
cts 170
cp 0
rs 3.04

8 Methods

Rating   Name   Duplication   Size   Complexity  
A createPaginator() 0 3 1
A applyWhere() 0 29 5
A applyOrderBy() 0 35 5
C __invoke() 0 68 12
A createConnection() 0 3 1
B applyFilters() 0 31 11
A applyFilterByParent() 0 27 5
F search() 0 77 27

How to fix   Complexity   

Complex Class

Complex classes like AllNodesWithPagination often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AllNodesWithPagination, and based on these observations, apply Extract Interface, too.

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);
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

79
            /** @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...
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