Completed
Push — master ( 13833e...80f050 )
by Denis
03:20
created

ProxyQueryBuilder   A

Complexity

Total Complexity 28

Size/Duplication

Total Lines 233
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
dl 0
loc 233
ccs 0
cts 111
cp 0
rs 10
c 0
b 0
f 0
wmc 28

9 Methods

Rating   Name   Duplication   Size   Complexity  
B getConnectorExpr() 0 15 5
A registerCondition() 0 3 1
A getConditionExpr() 0 9 2
A getNeighborRecordIds() 0 6 1
A __construct() 0 3 1
B getNeighborRecordId() 0 29 5
A checkFilterVal() 0 7 3
C addQueryFilters() 0 40 7
A getSortedAndFilteredQuery() 0 16 3
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 ConditionInterface[]
27
     */
28
    private $conditions = [];
29
30
    public function __construct(QueryBuilder $queryBuilder)
31
    {
32
        $this->queryBuilder = $queryBuilder;
33
    }
34
35
    /**
36
     * @param int $index parameter id
37
     * @param string $field field name
38
     * @param string $conditionName condition type (eq, like, etc.)
39
     * @param array $val condition parameters information
40
     * @return DoctrineQuery\Expr\Comparison|DoctrineQuery\Expr\Func|string
41
     * @throws InvalidArgumentException
42
     */
43
    private function getConditionExpr(int $index, string $field, string $conditionName, array $val)
44
    {
45
        if (!array_key_exists($conditionName, $this->conditions)) {
46
            throw new InvalidArgumentException(sprintf('Condition "%s" is not registered', $conditionName));
47
        }
48
49
        $expr = $this->conditions[$conditionName]->getExpr($this->queryBuilder, $field, $index, $val);
50
51
        return $expr;
52
    }
53
54
    /**
55
     * Get neighbor (prev or next) record id for use in navigation
56
     *
57
     * @param int $id record id
58
     * @param boolean $prev if true - get prev id, otherwise - next id
59
     * @param DoctrineQuery\Expr|null $extraAndWhereCondition
60
     * @return int|null neighbor id or null if empty result
61
     * @throws \RuntimeException
62
     * @throws \Doctrine\ORM\NonUniqueResultException
63
     */
64
    public function getNeighborRecordId(int $id, bool $prev, ?DoctrineQuery\Expr $extraAndWhereCondition = null): ?int
65
    {
66
        $sign = $prev ? '<' : '>';
67
        $order = $prev ? 'DESC' : 'ASC';
68
        $rootEntities = $this->queryBuilder->getRootEntities();
69
70
        if (count($rootEntities) >= 0) {
71
            throw new \RuntimeException('QueryBuilder must contain exactly one root entity');
72
        }
73
74
        $rootEntity = reset($rootEntities);
75
        $qb = new QueryBuilder($this->queryBuilder->getEntityManager());
76
        $qb
77
            ->select('c.id') // assuming that the entities index must be always called `id`
78
            ->from($rootEntity, 'c')
79
            ->where('c.id '.$sign.' :id')
80
            ->setParameter(':id', $id)
81
            ->orderBy('c.id', $order)
82
        ;
83
84
        if ($extraAndWhereCondition !== null) {
85
            $qb->andWhere($extraAndWhereCondition);
86
        }
87
88
        $query = $qb->getQuery();
89
        $query->setMaxResults(1);
90
        $result = $query->getOneOrNullResult();
91
92
        return $result;
93
    }
94
95
    /**
96
     * Get prev and next record ids for the given record id
97
     *
98
     * @param int $id record id
99
     * @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.
100
     * @throws \RuntimeException
101
     * @throws \Doctrine\ORM\NonUniqueResultException
102
     */
103
    public function getNeighborRecordIds(int $id): array
104
    {
105
        $prev = $this->getNeighborRecordId($id, true);
106
        $next = $this->getNeighborRecordId($id, false);
107
108
        return compact('prev', 'next');
109
    }
110
111
    /**
112
     * Get connector expression based on `and`, `or` or `null`
113
     *
114
     * @param $prev
115
     * @param $connector
116
     * @param $condition
117
     * @return DoctrineQuery\Expr\Andx|DoctrineQuery\Expr\Orx
118
     * @throws InvalidArgumentException
119
     */
120
    private function getConnectorExpr($prev, $connector, $condition)
121
    {
122
        $qb = $this->queryBuilder;
123
124
        if ($prev === null) {
125
            $expr = $condition;
126
        } elseif ($connector === null || $connector === 'and') {
127
            $expr = $qb->expr()->andX($prev, $condition);
128
        } elseif ($connector === 'or') {
129
            $expr = $qb->expr()->orX($prev, $condition);
130
        } else {
131
            throw new InvalidArgumentException(sprintf('Wrong connector type: %s', $connector));
132
        }
133
134
        return $expr;
135
    }
136
137
    public function registerCondition(ConditionInterface $condition)
138
    {
139
        $this->conditions[$condition->getName()] = $condition;
140
    }
141
142
    private function checkFilterVal($val)
143
    {
144
        if (is_scalar($val) || is_array($val)) {
145
            return;
146
        }
147
148
        throw new InvalidArgumentException(sprintf('Unexpected val php type ("%s")', gettype($val)));
149
    }
150
151
    private function addQueryFilters(QueryBuilder $qb, array $by): QueryBuilder
152
    {
153
        if (empty($by)) {
154
            return $qb;
155
        }
156
157
        $i = 0;
158
        $where = null;
159
        $having = null;
160
        foreach ($by as $key => $val) {
161
            $this->checkFilterVal($val);
162
163
            $i++;
164
165
            if (is_scalar($val)) {
166
                $where = $this->getConnectorExpr($where, 'and', $qb->expr()->eq($key, '?'.$i));
167
                $qb->setParameter($i, $val);
168
                continue;
169
            }
170
171
            // otherwise $val is array
172
173
            $condition = $this->getConditionExpr($i, $key, $val['type'], $val);
174
175
            if (empty($val['having'])) {
176
                $where = $this->getConnectorExpr($where, $val['connector'] ?? 'and', $condition);
177
            } else {
178
                $having = $this->getConnectorExpr($having, $val['connector'] ?? 'and', $condition);
179
            }
180
        }
181
182
        if ($where) {
183
            $qb->add('where', $where);
184
        }
185
186
        if ($having) {
187
            $qb->add('having', $having);
188
        }
189
190
        return $qb;
191
    }
192
193
    /**
194
     * Add filter and order by conditions to the given QueryBuilder
195
     *
196
     * Example data
197
     *
198
     * array(
199
     *  'searchBy' => array(
200
     *    'e.name' => array(
201
     *      'type' => 'like',
202
     *      'val' => 'a',
203
     *    ),
204
     *    'e.city' => array(
205
     *      'type' => 'like',
206
     *      'val' => 'd',
207
     *    ),
208
     *    'c.name' => array(
209
     *      'type' => 'like',
210
     *      'val' => 'a',
211
     *    ),
212
     *    'concat(concat(concat(concat(p.firstname, ' '), p.middlename), ' '), p.lastname)' => array(
213
     *      'having' => TRUE
214
     *      'type' => 'like'
215
     *      'val' => 'a'
216
     *    )
217
     *    'year' => array(
218
     *      'type' => 'between',
219
     *      'val' => 2015,
220
     *      'x' => 'YEAR(e.startDate)',
221
     *      'y' => 'YEAR(e.endDate)'
222
     *    ),
223
     *  ),
224
     *  'sortData' => array(
225
     *      'e.name' => 'asc'
226
     *  )
227
     * )
228
     *
229
     * @param array $by
230
     * @param array $orderBy
231
     *
232
     * @param bool $calcRows
233
     * @return DoctrineQuery
234
     */
235
    public function getSortedAndFilteredQuery(array $by, array $orderBy, $calcRows = true): DoctrineQuery
236
    {
237
        $qb = $this->queryBuilder;
238
239
        foreach ($orderBy as $field => $dir) {
240
            $qb->addOrderBy($field, strtoupper($dir));
241
        }
242
243
        $query = $this->addQueryFilters($qb, $by)->getQuery();
244
245
        if ($calcRows) {
246
            $query->setHint(DoctrineQuery::HINT_CUSTOM_OUTPUT_WALKER, PaginationWalker::class);
247
            $query->setHint('mysqlWalker.sqlCalcFoundRows', true);
248
        }
249
250
        return $query;
251
    }
252
}
253