Failed Conditions
Push — master ( 9e41f7...697215 )
by Denis
02:46
created

ProxyQueryBuilder::addQueryFilters()   C

Complexity

Conditions 12
Paths 28

Size

Total Lines 45
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 156

Importance

Changes 0
Metric Value
cc 12
eloc 28
nc 28
nop 1
dl 0
loc 45
ccs 0
cts 39
cp 0
crap 156
rs 5.1612
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php declare(strict_types = 1);
2
3
namespace Artprima\QueryFilterBundle\Query;
4
5
use Artprima\QueryFilterBundle\Exception\InvalidArgumentException;
6
use Artprima\QueryFilterBundle\Exception\MissingArgumentException;
7
use Artprima\QueryFilterBundle\Query\Condition;
8
use Artprima\QueryFilterBundle\Query\Condition\ConditionInterface;
9
use Artprima\QueryFilterBundle\Query\Mysql\PaginationWalker;
10
use Doctrine\ORM\QueryBuilder;
11
use Doctrine\ORM\Query as DoctrineQuery;
12
13
/**
14
 * Class ProxyQueryBuilder
15
 *
16
 * @author Denis Voytyuk <[email protected]>
17
 */
18
class ProxyQueryBuilder
19
{
20
    /**
21
     * @var QueryBuilder
22
     */
23
    private $queryBuilder;
24
25
    /**
26
     * @var bool
27
     */
28
    private $calcRows;
29
30
    /**
31
     * @var ConditionInterface[]
32
     */
33
    private $conditions = [];
34
35
    public function __construct(QueryBuilder $queryBuilder, $calcRows = true)
36
    {
37
        $this->queryBuilder = $queryBuilder;
38
        $this->calcRows = $calcRows;
39
40
        // this way of registering does not seem to be too smart, but for now it can work
41
        $this->registerCondition(new Condition\Between());
42
        $this->registerCondition(new Condition\Eq());
43
        $this->registerCondition(new Condition\Gt());
44
        $this->registerCondition(new Condition\Gte());
45
        $this->registerCondition(new Condition\In());
46
        $this->registerCondition(new Condition\IsNotNull());
47
        $this->registerCondition(new Condition\IsNull());
48
        $this->registerCondition(new Condition\Like());
49
        $this->registerCondition(new Condition\Lt());
50
        $this->registerCondition(new Condition\Lte());
51
        $this->registerCondition(new Condition\MemberOf());
52
        $this->registerCondition(new Condition\NotBetween());
53
        $this->registerCondition(new Condition\NotEq());
54
        $this->registerCondition(new Condition\NotIn());
55
        $this->registerCondition(new Condition\NotLike());
56
    }
57
58
    /**
59
     * @param int $index parameter id
60
     * @param string $field field name
61
     * @param string $conditionName condition type (eq, like, etc.)
62
     * @param array $val condition parameters information
63
     * @return DoctrineQuery\Expr\Comparison|DoctrineQuery\Expr\Func|string
64
     * @throws InvalidArgumentException
65
     */
66
    private function getConditionExpr(int $index, string $field, string $conditionName, array $val)
67
    {
68
        if (!array_key_exists($conditionName, $this->conditions)) {
69
            throw new InvalidArgumentException(sprintf('Condition "%s" is not registered', $conditionName));
70
        }
71
72
        $expr = $this->conditions[$conditionName]->getExpr($this->queryBuilder, $field, $index, $val);
73
74
        return $expr;
75
    }
76
77
    /**
78
     * Get neighbor (prev or next) record id for use in navigation
79
     *
80
     * @param int $id record id
81
     * @param boolean $prev if true - get prev id, otherwise - next id
82
     * @param DoctrineQuery\Expr|null $extraAndWhereCondition
83
     * @return int|null neighbor id or null if empty result
84
     * @throws \RuntimeException
85
     * @throws \Doctrine\ORM\NonUniqueResultException
86
     */
87
    public function getNeighborRecordId(int $id, bool $prev, ?DoctrineQuery\Expr $extraAndWhereCondition = null): ?int
88
    {
89
        $sign = $prev ? '<' : '>';
90
        $order = $prev ? 'DESC' : 'ASC';
91
        $rootEntities = $this->queryBuilder->getRootEntities();
92
93
        if (count($rootEntities) >= 0) {
94
            throw new \RuntimeException('QueryBuilder must contain exactly one root entity');
95
        }
96
97
        $rootEntity = reset($rootEntities);
98
        $qb = new QueryBuilder($this->queryBuilder->getEntityManager());
99
        $qb
100
            ->select('c.id') // assuming that the entities index must be always called `id`
101
            ->from($rootEntity, 'c')
102
            ->where('c.id '.$sign.' :id')
103
            ->setParameter(':id', $id)
104
            ->orderBy('c.id', $order)
105
        ;
106
107
        if ($extraAndWhereCondition !== null) {
108
            $qb->andWhere($extraAndWhereCondition);
109
        }
110
111
        $query = $qb->getQuery();
112
        $query->setMaxResults(1);
113
        $result = $query->getOneOrNullResult();
114
115
        return $result;
116
    }
117
118
    /**
119
     * Get prev and next record ids for the given record id
120
     *
121
     * @param int $id record id
122
     * @return array prev and next records id in an array with 'prev' and 'next' keys. One or both items can be null in case of no records.
123
     * @throws \RuntimeException
124
     * @throws \Doctrine\ORM\NonUniqueResultException
125
     */
126
    public function getNeighborRecordIds(int $id): array
127
    {
128
        $prev = $this->getNeighborRecordId($id, true);
129
        $next = $this->getNeighborRecordId($id, false);
130
131
        return compact('prev', 'next');
132
    }
133
134
    /**
135
     * Get connector expression based on `and`, `or` or `null`
136
     *
137
     * @param $prev
138
     * @param $connector
139
     * @param $condition
140
     * @return DoctrineQuery\Expr\Andx|DoctrineQuery\Expr\Orx
141
     * @throws InvalidArgumentException
142
     */
143
    private function getConnectorExpr($prev, $connector, $condition)
144
    {
145
        $qb = $this->queryBuilder;
146
147
        if ($prev === null) {
148
            $expr = $condition;
149
        } elseif ($connector === null || $connector === 'and') {
150
            $expr = $qb->expr()->andX($prev, $condition);
151
        } elseif ($connector === 'or') {
152
            $expr = $qb->expr()->orX($prev, $condition);
153
        } else {
154
            throw new InvalidArgumentException(sprintf('Wrong connector type: %s', $connector));
155
        }
156
157
        return $expr;
158
    }
159
160
    private function registerCondition(ConditionInterface $condition)
161
    {
162
        $this->conditions[$condition->getName()] = $condition;
163
    }
164
165
    private function addQueryFilters(QueryBuilder $qb): QueryBuilder
166
    {
167
        if (empty($by)) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $by seems to never exist and therefore empty should always be true.
Loading history...
168
            return $qb;
169
        }
170
171
        $i = 0;
172
        $where = null;
173
        $having = null;
174
        foreach ($by as $key => $val) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $by seems to be never defined.
Loading history...
175
            $i++;
176
            if (is_scalar($val)) {
177
                $where = $this->getConnectorExpr($where, 'and', $qb->expr()->eq($key, '?'.$i));
178
                $qb->setParameter($i, $val);
179
            } elseif (is_array($val)) {
180
                if (!array_key_exists('x', $val) && !array_key_exists('y', $val)) {
181
                    if (!array_key_exists('val', $val)) {
182
                        throw new MissingArgumentException('Required "val" argument not given');
183
                    }
184
                    if (!is_scalar($val['val'])) {
185
                        throw new InvalidArgumentException(sprintf('Unexpected val php type ("%s")', gettype($val['val'])));
186
                    }
187
                }
188
189
                $condition = $this->getConditionExpr($i, $key, $val['type'], $val);
190
191
                if (empty($val['having'])) {
192
                    $where = $this->getConnectorExpr($where, $val['connector'] ?? 'and', $condition);
193
                } else {
194
                    $having = $this->getConnectorExpr($having, $val['connector'] ?? 'and', $condition);
195
                }
196
            } else {
197
                throw new InvalidArgumentException(sprintf('Unexpected val php type ("%s")', gettype($val)));
198
            }
199
        }
200
201
        if ($where) {
202
            $qb->add('where', $where);
203
        }
204
205
        if ($having) {
206
            $qb->add('having', $having);
207
        }
208
209
        return $qb;
210
    }
211
212
    /**
213
     * Add filter and order by conditions to the given QueryBuilder
214
     *
215
     * Example data
216
     *
217
     * array(
218
     *  'searchBy' => array(
219
     *    'e.name' => array(
220
     *      'type' => 'like',
221
     *      'val' => 'a',
222
     *    ),
223
     *    'e.city' => array(
224
     *      'type' => 'like',
225
     *      'val' => 'd',
226
     *    ),
227
     *    'c.name' => array(
228
     *      'type' => 'like',
229
     *      'val' => 'a',
230
     *    ),
231
     *    'concat(concat(concat(concat(p.firstname, ' '), p.middlename), ' '), p.lastname)' => array(
232
     *      'having' => TRUE
233
     *      'type' => 'like'
234
     *      'val' => 'a'
235
     *    )
236
     *    'year' => array(
237
     *      'type' => 'between',
238
     *      'val' => 2015,
239
     *      'x' => 'YEAR(e.startDate)',
240
     *      'y' => 'YEAR(e.endDate)'
241
     *    ),
242
     *  ),
243
     *  'sortData' => array(
244
     *      'e.name' => 'asc'
245
     *  )
246
     * )
247
     *
248
     * @param array $by
249
     * @param array $orderBy
250
     *
251
     * @throws MissingArgumentException
252
     * @return DoctrineQuery
253
     * @throws InvalidArgumentException
254
     */
255
    public function getSortedAndFilteredQuery(array $by, array $orderBy): DoctrineQuery
0 ignored issues
show
Unused Code introduced by
The parameter $by is not used and could be removed. ( Ignorable by Annotation )

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

255
    public function getSortedAndFilteredQuery(/** @scrutinizer ignore-unused */ array $by, array $orderBy): DoctrineQuery

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
256
    {
257
        $qb = $this->queryBuilder;
258
259
        foreach ($orderBy as $field => $dir) {
260
            $qb->addOrderBy($field, strtoupper($dir));
261
        }
262
263
        $query = $this->addQueryFilters($qb)->getQuery();
264
265
        if ($this->calcRows) {
266
            $query->setHint(DoctrineQuery::HINT_CUSTOM_OUTPUT_WALKER, PaginationWalker::class);
267
            $query->setHint('mysqlWalker.sqlCalcFoundRows', true);
268
        }
269
270
        return $query;
271
    }
272
}
273