Passed
Push — master ( cf54c3...3ae95d )
by Rafael
07:43
created

AllNodesWithPagination   A

Complexity

Total Complexity 33

Size/Duplication

Total Lines 193
Duplicated Lines 0 %

Test Coverage

Coverage 53.85%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 33
dl 0
loc 193
ccs 42
cts 78
cp 0.5385
rs 9.3999
c 1
b 0
f 0

6 Methods

Rating   Name   Duplication   Size   Complexity  
A createPaginator() 0 3 1
B search() 0 27 6
C __invoke() 0 61 11
A createConnection() 0 3 1
C applyFilters() 0 24 9
B applyFilterByParent() 0 27 5
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\Extension\PaginationDefinitionExtension;
17
use Ynlo\GraphQLBundle\Model\ConnectionInterface;
18
use Ynlo\GraphQLBundle\Model\ID;
19
use Ynlo\GraphQLBundle\Model\NodeConnection;
20
use Ynlo\GraphQLBundle\Model\NodeInterface;
21
use Ynlo\GraphQLBundle\Pagination\DoctrineCursorPaginatorInterface;
22
use Ynlo\GraphQLBundle\Pagination\DoctrineOffsetCursorPaginator;
23
use Ynlo\GraphQLBundle\Pagination\PaginationRequest;
24
25
/**
26
 * Base class to fetch nodes
27
 */
28
class AllNodesWithPagination extends AllNodes
29
{
30
    /**
31
     * @param array[] $args
32
     *
33
     * @return mixed
34
     *
35
     * @throws Error
36 10
     */
37
    public function __invoke($args = [])
38 10
    {
39 10
        $orderBy = $args['orderBy'] ?? [];
40 10
        $first = $args['first'] ?? null;
41 10
        $last = $args['last'] ?? null;
42 10
        $before = $args['before'] ?? null;
43 10
        $after = $args['after'] ?? null;
44
        $search = $args['search'] ?? null;
45 10
        $filters = $args['filters'] ?? null;
46
47 10
        $this->initialize();
48 10
49
        $qb = $this->createQuery();
50 10
        $this->applyOrderBy($qb, $orderBy);
51 3
52
        if ($this->getContext()->getRoot()) {
53
            $this->applyFilterByParent($qb, $this->getContext()->getRoot());
54 10
        }
55
56
        if ($search) {
57
            $this->search($qb, $search);
58 10
        }
59 10
60 4
        if ($filters) {
61
            $this->applyFilters($qb, $filters);
62
        }
63 10
64
        $this->configureQuery($qb);
65
        foreach ($this->extensions as $extension) {
66
            $extension->configureQuery($qb, $this, $this->context);
67
        }
68 10
69 10
        if (!$first && !$last) {
70
            $error = sprintf('You must provide a `first` or `last` value to properly paginate records in "%s" connection.', $this->queryDefinition->getName());
71 10
            throw new Error($error);
72
        }
73
74
        if ($this->queryDefinition->hasMeta('pagination')) {
75
            $limitAllowed = $this->queryDefinition->getMeta('pagination')['limit'];
76
77
            if ($first > $limitAllowed || $last > $limitAllowed) {
78
                $current = $first ?? $last;
79
                $where = $first ? 'first' : 'last';
80
                $error = sprintf(
81
                    'Requesting %s records for `%s` exceeds the `%s` limit of %s records for "%s" connection',
82
                    $current,
83
                    $this->queryDefinition->getName(),
84
                    $where,
85
                    $limitAllowed,
86 10
                    $this->queryDefinition->getName()
87
                );
88 10
                throw new Error($error);
89 10
            }
90
        }
91 10
92
        $paginator = $this->createPaginator();
93
94
        $connection = $this->createConnection();
95
        $paginator->paginate($qb, new PaginationRequest($first, $last, $after, $before), $connection);
96
97 10
        return $connection;
98
    }
99 10
100
    /**
101
     * @return ConnectionInterface
102
     */
103
    protected function createConnection(): ConnectionInterface
104
    {
105 10
        return new NodeConnection();
106
    }
107 10
108
    /**
109
     * @return DoctrineCursorPaginatorInterface
110
     */
111
    protected function createPaginator(): DoctrineCursorPaginatorInterface
112
    {
113
        return new DoctrineOffsetCursorPaginator();
114
    }
115
116
    /**
117
     * Apply advanced filters
118
     *
119
     * @param QueryBuilder $qb
120
     * @param array        $filters string to search
121
     */
122
    protected function applyFilters(QueryBuilder $qb, $filters)
123
    {
124
        $definition = $this->objectDefinition;
125
        foreach ($filters as $field => $value) {
126
            if ($definition->hasField($field)) {
127
                $prop = $definition->getField($field)->getOriginName();
128
                if ($prop) {
129
                    $entityField = sprintf('%s.%s', $this->queryAlias, $prop);
130
                    if (is_array($value)) {
131
                        foreach ($value as &$val) {
132
                            if ($val instanceof ID) {
133
                                $val = (int) $val->getDatabaseId();
134
                            }
135
                        }
136
                        if (empty($value)) {
137
                            $qb->andWhere($qb->expr()->isNull($entityField));
138
                        } else {
139
                            $qb->andWhere($qb->expr()->in($entityField, $value));
140
                        }
141
                    } else {
142
                        if (null === $value) {
143
                            $qb->andWhere($qb->expr()->isNull($entityField));
144
                        } else {
145
                            $qb->andWhere($qb->expr()->eq($entityField, $value));
146
                        }
147
                    }
148
                }
149
            }
150
        }
151 3
    }
152
153 3
    /**
154 3
     * Filter some columns with simple string.
155 3
     *
156
     * @param QueryBuilder $qb
157 3
     * @param string       $search string to search
158
     */
159
    protected function search(QueryBuilder $qb, $search)
160
    {
161
        //search every word separate
162
        $searchArray = explode(' ', $search);
163
164
        $alias = $qb->getRootAliases()[0];
165
166
        //TODO: allow some config to customize search fields
167 3
        $em = $this->getManager();
168 3
        $metadata = $em->getClassMetadata($this->entity);
169
        $searchFields = $metadata->getFieldNames();
170
171 3
        if (count($searchFields) > 0) {
172 3
            $meta = $qb->getEntityManager()->getClassMetadata($qb->getRootEntities()[0]);
173 2
            foreach ($searchArray as $q) {
174 2
                $q = trim(rtrim($q));
175
                $id = md5($q);
176 1
                $orx = new Orx();
177 1
                foreach ($searchFields as $field) {
178
                    if (strpos($field, '.') !== false && !isset($meta->embeddedClasses[explode('.', $field)[0]])) {
179 3
                        $orx->add("$field LIKE :search_$id");
180
                    } else { //append current alias
181
                        $orx->add("$alias.$field LIKE :search_$id");
182
                    }
183
                }
184
                $qb->andWhere($orx);
185
                $qb->setParameter("search_$id", "%$q%");
186
            }
187
        }
188
    }
189
190
    /**
191
     * @param QueryBuilder  $qb
192
     * @param NodeInterface $root
193
     */
194
    protected function applyFilterByParent(QueryBuilder $qb, NodeInterface $root)
195
    {
196
        $parentField = null;
197
        if ($this->queryDefinition->hasMeta('pagination')) {
198
            $parentField = $this->queryDefinition->getMeta('pagination')['parent_field'] ?? null;
199
        }
200
        if (!$parentField) {
201
            throw new \RuntimeException(
202
                sprintf(
203
                    'Missing parent field to filter "%s" by given parent.
204
             The "parent_field" should be specified.',
205
                    $this->queryDefinition->getName()
206
                )
207
            );
208
        }
209
210
        if ($this->objectDefinition->hasField($parentField)) {
211
            $parentField = $this->objectDefinition->getField($parentField)->getOriginName();
212
        }
213
214
        $paramName = 'root'.mt_rand();
0 ignored issues
show
Bug introduced by
The call to mt_rand() has too few arguments starting with min. ( Ignorable by Annotation )

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

214
        $paramName = 'root'./** @scrutinizer ignore-call */ mt_rand();

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
215
        if ($this->queryDefinition->getMeta('pagination')['parent_relation'] === PaginationDefinitionExtension::MANY_TO_MANY) {
216
            $qb->andWhere(sprintf(':%s MEMBER OF %s.%s', $paramName, $this->queryAlias, $parentField))
217
               ->setParameter($paramName, $root);
218
        } else {
219
            $qb->andWhere(sprintf('%s.%s = :%s', $this->queryAlias, $parentField, $paramName))
220
               ->setParameter($paramName, $root);
221
        }
222
    }
223
}
224