Failed Conditions
Push — master ( bc79fb...9e41f7 )
by Denis
02:47
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
    public function registerCondition(ConditionInterface $condition)
161
    {
162
        $this->conditions[$condition->getName()] = $condition;
163
    }
164
165
    /**
166
     * Add filter and order by conditions to the given QueryBuilder
167
     *
168
     * Example data
169
     *
170
     * array(
171
     *  'searchBy' => array(
172
     *    'e.name' => array(
173
     *      'type' => 'like',
174
     *      'val' => 'a',
175
     *    ),
176
     *    'e.city' => array(
177
     *      'type' => 'like',
178
     *      'val' => 'd',
179
     *    ),
180
     *    'c.name' => array(
181
     *      'type' => 'like',
182
     *      'val' => 'a',
183
     *    ),
184
     *    'concat(concat(concat(concat(p.firstname, ' '), p.middlename), ' '), p.lastname)' => array(
185
     *      'having' => TRUE
186
     *      'type' => 'like'
187
     *      'val' => 'a'
188
     *    )
189
     *    'year' => array(
190
     *      'type' => 'between',
191
     *      'val' => 2015,
192
     *      'x' => 'YEAR(e.startDate)',
193
     *      'y' => 'YEAR(e.endDate)'
194
     *    ),
195
     *  ),
196
     *  'sortData' => array(
197
     *      'e.name' => 'asc'
198
     *  )
199
     * )
200
     *
201
     * @param array $by
202
     * @param array $orderBy
203
     *
204
     * @throws MissingArgumentException
205
     * @return DoctrineQuery
206
     * @throws InvalidArgumentException
207
     */
208
    public function getSortedAndFilteredQuery(array $by, array $orderBy): DoctrineQuery
209
    {
210
        $qb = $this->queryBuilder;
211
212
        if (!empty($orderBy)) {
213
            foreach ($orderBy as $field => $dir) {
214
                $qb->addOrderBy($field, strtoupper($dir));
215
            }
216
        }
217
        if (!empty($by)) {
218
            $i = 0;
219
            $where = null;
220
            $having = null;
221
            foreach ($by as $key => $val) {
222
                $i++;
223
                if (is_scalar($val)) {
224
                    $where = $this->getConnectorExpr($where, 'and', $qb->expr()->eq($key, '?'.$i));
225
                    $qb->setParameter($i, $val);
226
                    // @todo: the following smells bad
227
                    //} elseif (is_callable($val)){
0 ignored issues
show
Unused Code Comprehensibility introduced by
73% 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...
228
                    //    call_user_func_array($val, array(&$where, &$having, &$qb));
0 ignored issues
show
Unused Code Comprehensibility introduced by
77% 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...
229
                } elseif (is_array($val)) {
230
                    if (!array_key_exists('x', $val) && !array_key_exists('y', $val)) {
231
                        if (!array_key_exists('val', $val)) {
232
                            throw new MissingArgumentException('Required "val" argument not given');
233
                        }
234
                        if (!is_scalar($val['val'])) {
235
                            throw new InvalidArgumentException(sprintf('Unexpected val php type ("%s")', gettype($val['val'])));
236
                        }
237
                    }
238
239
                    $condition = $this->getConditionExpr($i, $key, $val['type'], $val);
240
241
                    if (empty($val['having'])) {
242
                        $where = $this->getConnectorExpr($where, $val['connector'] ?? 'and', $condition);
243
                    } else {
244
                        $having = $this->getConnectorExpr($having, $val['connector'] ?? 'and', $condition);
245
                    }
246
                } else {
247
                    throw new InvalidArgumentException(sprintf('Unexpected val php type ("%s")', gettype($val)));
248
                }
249
            }
250
            if ($where) {
251
                $qb->add('where', $where);
252
            }
253
            if ($having) {
254
                $qb->add('having', $having);
255
            }
256
        }
257
258
        $query = $qb->getQuery();
259
260
        if ($this->calcRows) {
261
            $query->setHint(DoctrineQuery::HINT_CUSTOM_OUTPUT_WALKER, PaginationWalker::class);
262
            $query->setHint('mysqlWalker.sqlCalcFoundRows', true);
263
        }
264
265
        return $query;
266
    }
267
}
268