Failed Conditions
Push — master ( 697215...5d660e )
by Denis
02:27
created

ProxyQueryBuilder::getConditionExpr()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 4
dl 0
loc 9
ccs 0
cts 7
cp 0
crap 6
rs 9.6666
c 0
b 0
f 0
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 checkFilterVal($val)
166
    {
167
        if (!is_scalar($val) && !is_array($val)) {
168
            throw new InvalidArgumentException(sprintf('Unexpected val php type ("%s")', gettype($val)));
169
        }
170
171
        if (is_scalar($val)) {
172
            return;
173
        }
174
175
        if (!array_key_exists('x', $val) || !array_key_exists('y', $val)) {
176
            if (!array_key_exists('val', $val)) {
177
                throw new MissingArgumentException('Required "val" argument not given');
178
            }
179
            if (!is_scalar($val['val'])) {
180
                throw new InvalidArgumentException(sprintf('Unexpected val php type ("%s")', gettype($val['val'])));
181
            }
182
        }
183
    }
184
185
    private function addQueryFilters(QueryBuilder $qb, array $by): QueryBuilder
186
    {
187
        if (empty($by)) {
188
            return $qb;
189
        }
190
191
        $i = 0;
192
        $where = null;
193
        $having = null;
194
        foreach ($by as $key => $val) {
195
            $this->checkFilterVal($val);
196
197
            $i++;
198
199
            if (is_scalar($val)) {
200
                $where = $this->getConnectorExpr($where, 'and', $qb->expr()->eq($key, '?'.$i));
201
                $qb->setParameter($i, $val);
202
                continue;
203
            }
204
205
            // elseif (is_array($val) === true):
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
206
207
            $condition = $this->getConditionExpr($i, $key, $val['type'], $val);
208
209
            if (empty($val['having'])) {
210
                $where = $this->getConnectorExpr($where, $val['connector'] ?? 'and', $condition);
211
            } else {
212
                $having = $this->getConnectorExpr($having, $val['connector'] ?? 'and', $condition);
213
            }
214
        }
215
216
        if ($where) {
217
            $qb->add('where', $where);
218
        }
219
220
        if ($having) {
221
            $qb->add('having', $having);
222
        }
223
224
        return $qb;
225
    }
226
227
    /**
228
     * Add filter and order by conditions to the given QueryBuilder
229
     *
230
     * Example data
231
     *
232
     * array(
233
     *  'searchBy' => array(
234
     *    'e.name' => array(
235
     *      'type' => 'like',
236
     *      'val' => 'a',
237
     *    ),
238
     *    'e.city' => array(
239
     *      'type' => 'like',
240
     *      'val' => 'd',
241
     *    ),
242
     *    'c.name' => array(
243
     *      'type' => 'like',
244
     *      'val' => 'a',
245
     *    ),
246
     *    'concat(concat(concat(concat(p.firstname, ' '), p.middlename), ' '), p.lastname)' => array(
247
     *      'having' => TRUE
248
     *      'type' => 'like'
249
     *      'val' => 'a'
250
     *    )
251
     *    'year' => array(
252
     *      'type' => 'between',
253
     *      'val' => 2015,
254
     *      'x' => 'YEAR(e.startDate)',
255
     *      'y' => 'YEAR(e.endDate)'
256
     *    ),
257
     *  ),
258
     *  'sortData' => array(
259
     *      'e.name' => 'asc'
260
     *  )
261
     * )
262
     *
263
     * @param array $by
264
     * @param array $orderBy
265
     *
266
     * @throws MissingArgumentException
267
     * @return DoctrineQuery
268
     * @throws InvalidArgumentException
269
     */
270
    public function getSortedAndFilteredQuery(array $by, array $orderBy): DoctrineQuery
271
    {
272
        $qb = $this->queryBuilder;
273
274
        foreach ($orderBy as $field => $dir) {
275
            $qb->addOrderBy($field, strtoupper($dir));
276
        }
277
278
        $query = $this->addQueryFilters($qb, $by)->getQuery();
279
280
        if ($this->calcRows) {
281
            $query->setHint(DoctrineQuery::HINT_CUSTOM_OUTPUT_WALKER, PaginationWalker::class);
282
            $query->setHint('mysqlWalker.sqlCalcFoundRows', true);
283
        }
284
285
        return $query;
286
    }
287
}
288