Completed
Push — master ( cbfed7...c39fb6 )
by Rafael
05:06
created

AllNodes::modifyQuery()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 0

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 0
nc 1
nop 1
dl 0
loc 2
ccs 1
cts 1
cp 1
crap 1
rs 10
c 0
b 0
f 0
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\QueryBuilder;
14
use GraphQL\Error\Error;
15
use Ynlo\GraphQLBundle\Definition\ObjectDefinition;
16
use Ynlo\GraphQLBundle\Definition\QueryDefinition;
17
use Ynlo\GraphQLBundle\Model\NodeInterface;
18
use Ynlo\GraphQLBundle\Model\OrderBy;
19
use Ynlo\GraphQLBundle\Resolver\AbstractResolver;
20
21
/**
22
 * Base class to fetch nodes
23
 */
24
class AllNodes extends AbstractResolver
25
{
26
    /**
27
     * @var string
28
     */
29
    protected $queryAlias = 'o';
30
31
    /**
32
     * @var string
33
     */
34
    protected $entity;
35
36
    /**
37
     * @var QueryDefinition
38
     */
39
    protected $queryDefinition;
40
41
    /**
42
     * @var ObjectDefinition
43
     */
44
    protected $objectDefinition;
45
46
    /**
47
     * @param NodeInterface|null $root
48
     * @param int|null           $first
49
     * @param int|null           $last
50
     * @param OrderBy[]          $orderBy
51
     *
52
     * @return mixed
53
     */
54 3
    public function __invoke(NodeInterface $root = null, $first = null, $last = null, $orderBy = [])
55
    {
56 3
        $objectType = $this->context->getDefinition()->getType();
57 3
        $this->queryDefinition = $this->context->getDefinition();
58 3
        $this->objectDefinition = $this->context->getDefinitionManager()->getType($objectType);
59 3
        $this->entity = $this->context->getDefinitionManager()->getType($objectType)->getClass();
60
61 3
        $qb = $this->createQuery();
62 3
        $this->applyOrderBy($qb, $orderBy);
63 3
        $this->applyLimits($qb, $first, $last);
0 ignored issues
show
Bug introduced by
It seems like $first can also be of type integer; however, parameter $first of Ynlo\GraphQLBundle\Query...AllNodes::applyLimits() does only seem to accept null, maybe add an additional type check? ( Ignorable by Annotation )

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

63
        $this->applyLimits($qb, /** @scrutinizer ignore-type */ $first, $last);
Loading history...
Bug introduced by
It seems like $last can also be of type integer; however, parameter $last of Ynlo\GraphQLBundle\Query...AllNodes::applyLimits() does only seem to accept null, maybe add an additional type check? ( Ignorable by Annotation )

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

63
        $this->applyLimits($qb, $first, /** @scrutinizer ignore-type */ $last);
Loading history...
64
65 3
        if ($root) {
66 1
            $this->applyFilterByParent($qb, $root);
67
        }
68
69 3
        $this->modifyQuery($qb);
70
71 3
        return $qb->getQuery()->execute();
72
    }
73
74
    /**
75
     * @param QueryBuilder $qb
76
     */
77 3
    public function modifyQuery(QueryBuilder $qb)
78
    {
79
        //implements on childs to customize the query
80 3
    }
81
82
    /**
83
     * @param QueryBuilder  $qb
84
     * @param NodeInterface $root
85
     */
86 1
    protected function applyFilterByParent(QueryBuilder $qb, NodeInterface $root)
87
    {
88 1
        $parentField = null;
89 1
        if ($this->queryDefinition->hasMeta('connection_parent_field')) {
90 1
            $parentField = $this->queryDefinition->getMeta('connection_parent_field');
91
        }
92 1
        if (!$parentField) {
93
            throw new \RuntimeException(sprintf('Missing parent field to filter "%s" by given parent. The parentField should be specified in the connection.', $this->queryDefinition->getName()));
94
        }
95
96 1
        if ($this->objectDefinition->hasField($parentField)) {
97 1
            $parentField = $this->objectDefinition->getField($parentField)->getOriginName();
98
        }
99
100 1
        $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

100
        $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...
101 1
        $qb->andWhere(sprintf('%s.%s = :%s', $this->queryAlias, $parentField, $paramName))
102 1
           ->setParameter($paramName, $root);
103 1
    }
104
105
    /**
106
     * @param QueryBuilder $qb
107
     * @param null         $first
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $first is correct as it would always require null to be passed?
Loading history...
Documentation Bug introduced by
Are you sure the doc-type for parameter $last is correct as it would always require null to be passed?
Loading history...
108
     * @param null         $last
109
     *
110
     * @throws Error
111
     */
112 3
    protected function applyLimits(QueryBuilder $qb, $first = null, $last = null)
113
    {
114 3
        if (!$first && !$last) {
115
            $error = sprintf('You must provide a `first` or `last` value to properly paginate records in "%s" connection.', $this->queryDefinition->getName());
116
            throw new Error($error);
117
        }
118
119 3
        if ($this->queryDefinition->hasMeta('connection_limit')) {
120 3
            $limit = $this->queryDefinition->getMeta('connection_limit');
121 3
            if ($first > $limit || $last > $limit) {
122
                $current = $first ?? $last;
123
                $where = $first ? 'first' : 'last';
124
                $error = sprintf(
125
                    'Requesting %s records for `%s` exceeds the `%s` limit of %s records for "%s" connection',
126
                    $current,
127
                    $this->queryDefinition->getName(),
128
                    $where,
129
                    $limit,
130
                    $this->queryDefinition->getName()
131
                );
132
                throw new Error($error);
133
            }
134
        }
135
136 3
        if ($first) {
137 3
            $qb->setMaxResults(abs($first));
0 ignored issues
show
Bug introduced by
It seems like abs($first) can also be of type double; however, parameter $maxResults of Doctrine\ORM\QueryBuilder::setMaxResults() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

137
            $qb->setMaxResults(/** @scrutinizer ignore-type */ abs($first));
Loading history...
138
        } elseif ($last) {
139
            $qb->setMaxResults(abs($last));
140
141
            //invert all orders
142
            /** @var \Doctrine\ORM\Query\Expr\OrderBy[] $orders */
143
            $orders = $qb->getDQLPart('orderBy');
144
            $qb->resetDQLPart('orderBy');
145
            foreach ($orders as &$order) {
146
                foreach ($order->getParts() as &$part) {
0 ignored issues
show
Bug introduced by
The expression $order->getParts() cannot be used as a reference.

Let?s assume that you have the following foreach statement:

foreach ($array as &$itemValue) { }

$itemValue is assigned by reference. This is possible because the expression (in the example $array) can be used as a reference target.

However, if we were to replace $array with something different like the result of a function call as in

foreach (getArray() as &$itemValue) { }

then assigning by reference is not possible anymore as there is no target that could be modified.

Available Fixes

1. Do not assign by reference
foreach (getArray() as $itemValue) { }
2. Assign to a local variable first
$array = getArray();
foreach ($array as &$itemValue) {}
3. Return a reference
function &getArray() { $array = array(); return $array; }

foreach (getArray() as &$itemValue) { }
Loading history...
147
                    if (strpos($part, 'ASC')) {
148
                        $part = str_replace('ASC', '', $part);
149
                        $qb->addOrderBy($part, 'DESC');
150
                    } else {
151
                        $part = str_replace('DESC', '', $part);
152
                        $qb->addOrderBy($part, 'ASC');
153
                    }
154
                }
155
            }
156
        }
157 3
    }
158
159
    /**
160
     * @param QueryBuilder $qb
161
     * @param array        $orderBy
162
     *
163
     * @throws Error
164
     */
165 3
    protected function applyOrderBy(QueryBuilder $qb, $orderBy)
166
    {
167 3
        $refClass = new \ReflectionClass($this->entity);
168
        //TODO: allow sort using nested entities, e.g. profile.username
169 3
        foreach ($orderBy as $order) {
170 1
            $order->getField();
171 1
            if ($this->objectDefinition->hasField($order->getField())) {
172 1
                $fieldDefinition = $this->objectDefinition->getField($order->getField());
173 1
                if ($fieldDefinition->getOriginType() === \ReflectionProperty::class) {
174 1
                    if ($refClass->hasProperty($fieldDefinition->getOriginName())) {
175 1
                        $qb->addOrderBy($this->queryAlias.'.'.$fieldDefinition->getOriginName(), $order->getDirection());
176
177 1
                        continue;
178
                    }
179
                }
180
            }
181
182
            throw new Error(sprintf('The field "%s" its not valid to order in "%s" connection', $order->getField(), $this->queryDefinition->getName()));
183
        }
184 3
    }
185
186
    /**
187
     * @return QueryBuilder
188
     */
189 3
    protected function createQuery(): QueryBuilder
190
    {
191 3
        return $this->getManager()
192 3
                    ->getRepository($this->entity)
193 3
                    ->createQueryBuilder($this->queryAlias);
194
    }
195
}
196