Test Failed
Push — master ( 2ad047...e74f61 )
by Mathieu
04:00 queued 10s
created

Datatable::result()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 9
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 3
1
<?php
2
3
namespace DoctrineDatatable;
4
5
use Doctrine\ORM\QueryBuilder;
6
use DoctrineDatatable\Exception\GlobalFilterWithHavingColumn;
7
use DoctrineDatatable\Exception\MinimumColumn;
8
9
/**
10
 * Class Datatable.
11
 *
12
 * @author Mathieu Petrini <[email protected]>
13
 */
14
class Datatable
15
{
16
    /**
17
     * @var string
18
     */
19
    private $identifier;
20
21
    /**
22
     * @var int
23
     */
24
    private $resultPerPage;
25
26
    /**
27
     * @var string
28
     */
29
    private $nameIdentifier;
30
31
    /**
32
     * @var bool
33
     */
34
    private $globalSearch;
35
36
    /**
37
     * @var QueryBuilder
38
     */
39
    protected $query;
40
41
    /**
42
     * @var Column[]
43
     */
44
    protected $columns;
45
46
    private const DEFAULT_NAME_IDENTIFIER = 'DT_RowId';
47
48
    public const RESULT_PER_PAGE = 30;
49
50
    /**
51
     * Datatable constructor.
52
     *
53
     * @author Mathieu Petrini <[email protected]>
54
     *
55
     * @param QueryBuilder $query
56
     * @param string       $identifier
57
     * @param Column[]     $columns
58
     * @param int|null     $resultPerPage
59
     *
60
     * @throws MinimumColumn
61
     */
62
    public function __construct(
63
        QueryBuilder $query,
64
        string $identifier,
65
        array $columns,
66
        ?int $resultPerPage = self::RESULT_PER_PAGE
67
    ) {
68
        if (empty($columns)) {
69
            throw new MinimumColumn();
70
        }
71
        $this->query = $query;
72
        $this->identifier = $identifier;
73
        $this->columns = $columns;
74
        $this->resultPerPage = $resultPerPage ?? self::RESULT_PER_PAGE;
75
        $this->nameIdentifier = self::DEFAULT_NAME_IDENTIFIER;
76
        $this->globalSearch = false;
77
    }
78
79
    /**
80
     * PRIVATE METHODS.
81
     */
82
83
    /**
84
     * @author Mathieu Petrini <[email protected]>
85
     *
86
     * @param mixed[] $filters
87
     *
88
     * @return mixed[]
89
     */
90
    private function createGlobalFilters(array $filters): array
91
    {
92
        $temp = array(
93
            'columns' => array(),
94
        );
95
        array_map(function () use ($filters, &$temp) {
96
            $temp['columns'][] = array(
97
                'search' => array(
98
                    'value' => $filters['search'][Column::GLOBAL_ALIAS],
99
                ),
100
            );
101
        }, $this->columns);
102
103
        return $temp;
104
    }
105
106
    /**
107
     * @author Mathieu Petrini <[email protected]>
108
     *
109
     * @param QueryBuilder $query
110
     * @param mixed[]      $filters
111
     *
112
     * @return string[]
113
     *
114
     * @throws Exception\ResolveColumnNotHandle
115
     * @throws Exception\UnfilterableColumn
116
     * @throws Exception\WhereColumnNotHandle
117
     */
118
    private function createCondition(QueryBuilder &$query, array $filters): array
119
    {
120
        $where = '';
121
        $having = '';
122
123
        foreach ($filters['columns'] as $index => $filter) {
124
            if (isset($this->columns[$index]) && !empty($filter['search']['value'])) {
125
                $temp = '('.$this->columns[$index]->where($query, $filter['search']['value']).')';
126
127
                $this->columns[$index]->isHaving() ?
128
                    $having .= (!empty($having) ? ' AND ' : '').$temp :
129
                    $where .= (!empty($where) ? ' '.($this->globalSearch ? 'OR' : 'AND').' ' : '').$temp;
130
            }
131
        }
132
133
        return array(
134
            'where' => $where,
135
            'having' => $having,
136
        );
137
    }
138
139
    /**
140
     * @author Mathieu Petrini <[email protected]>
141
     *
142
     * @param QueryBuilder $query
143
     * @param bool         $withAlias (optional) (default=true)
144
     *
145
     * @return string
146
     */
147
    private function processColumnIdentifier(QueryBuilder &$query, bool $withAlias = true): string
148
    {
149
        return $query->getRootAliases()[0].'.'.$this->identifier.($withAlias ? ' AS '.$this->nameIdentifier : '');
150
    }
151
152
    /**
153
     * @author Mathieu Petrini <[email protected]>
154
     *
155
     * @param QueryBuilder $query
156
     * @param Column       $column
157
     */
158
    private function processColumnSelect(QueryBuilder &$query, Column $column): void
159
    {
160
        $query->addSelect($column->getName().' AS '.$column->getAlias());
161
    }
162
163
    /**
164
     * @author Mathieu Petrini <[email protected]>
165
     *
166
     * @param QueryBuilder $query
167
     * @param int          $index
168
     * @param string       $direction
169
     *
170
     * @return Datatable
171
     */
172
    private function orderBy(QueryBuilder &$query, int $index, string $direction): self
173
    {
174
        $query->orderBy(
175
            \array_slice($this->columns, $index, 1)[0]->getAlias(),
176
            $direction
177
        );
178
179
        return $this;
180
    }
181
182
    /**
183
     * @param QueryBuilder $query
184
     * @param mixed[]      $filters
185
     *
186
     * @return Datatable
187
     */
188
    private function limit(QueryBuilder &$query, array $filters): self
189
    {
190
        $query->setFirstResult($filters['start'] ?? 0)
191
            ->setMaxResults($filters['length'] ?? $this->resultPerPage);
192
193
        return $this;
194
    }
195
196
    /**
197
     * @author Mathieu Petrini <[email protected]>
198
     *
199
     * @param QueryBuilder $query
200
     *
201
     * @return int
202
     */
203
    private function count(QueryBuilder $query): int
204
    {
205
        $query = clone $query;
206
        $result = $query->select('COUNT(DISTINCT '.$this->processColumnIdentifier($query, false).') as count')
207
            ->resetDQLPart('orderBy')
208
            ->setFirstResult(0)
209
            ->setMaxResults(null)
210
            ->getQuery()
211
            ->getScalarResult();
212
213
        return !empty($result) ?
214
            (int) $result[0]['count'] :
215
            0;
216
    }
217
218
    /**
219
     * PROTECTED METHODS.
220
     */
221
222
    /**
223
     * @author Mathieu Petrini <[email protected]>
224
     *
225
     * @param QueryBuilder $query
226
     * @param mixed[]      $filters
227
     *
228
     * @return QueryBuilder
229
     *
230
     * @throws Exception\ResolveColumnNotHandle
231
     * @throws Exception\UnfilterableColumn
232
     * @throws Exception\WhereColumnNotHandle
233
     */
234
    protected function createFoundationQuery(QueryBuilder &$query, array $filters): QueryBuilder
235
    {
236
        // If global search we erase all specific where and only keep the unified filter
237
        if ($this->globalSearch && isset($filters['search']) && !empty($filters['search'][Column::GLOBAL_ALIAS])) {
238
            $filters = $this->createGlobalFilters($filters);
239
        }
240
241
        $conditions = isset($filters['columns']) ?
242
            $this->createCondition($query, $filters) :
243
            array();
244
245
        if (isset($conditions['where']) && !empty($conditions['where'])) {
246
            $query->andWhere($conditions['where']);
247
        }
248
249
        if (isset($conditions['having']) && !empty($conditions['having'])) {
250
            $query->andHaving($conditions['having']);
251
        }
252
253
        return $query;
254
    }
255
256
    /**
257
     * @return QueryBuilder
258
     */
259
    protected function createQueryResult(): QueryBuilder
260
    {
261
        $query = clone $this->query;
262
        $query->select($this->processColumnIdentifier($query));
263
        foreach ($this->columns as $column) {
264
            $this->processColumnSelect($query, $column);
265
        }
266
267
        return $query;
268
    }
269
270
    /**
271
     * @author Mathieu Petrini <[email protected]>
272
     *
273
     * @param QueryBuilder $query
274
     * @param int          $index
275
     * @param string       $direction
276
     *
277
     * @return mixed[]
278
     */
279
    protected function result(
280
        QueryBuilder &$query,
281
        int $index,
282
        string $direction
283
    ): array {
284
        $this->orderBy($query, $index, $direction);
285
286
        return $query->getQuery()
287
            ->getResult();
288
    }
289
290
    /**
291
     * PUBLIC METHODS.
292
     */
293
294
    /**
295
     * @author Mathieu Petrini <[email protected]>
296
     *
297
     * @param mixed[] $filters
298
     *
299
     * @return mixed[]
300
     *
301
     * @throws Exception\ResolveColumnNotHandle
302
     * @throws Exception\UnfilterableColumn
303
     * @throws Exception\WhereColumnNotHandle
304
     */
305
    public function get(array $filters): array
306
    {
307
        $query = $this->createQueryResult();
308
        $this->createFoundationQuery($query, $filters);
309
310
        $data = $this->limit($query, $filters)->result(
311
            $query,
312
            isset($filters['order']) ?
313
                $filters['order'][0]['column'] :
314
                0,
315
            isset($filters['order']) ?
316
                $filters['order'][0]['dir'] :
317
                'ASC'
318
        );
319
320
        return array(
321
            'recordsTotal' => $this->count($query),
322
            'recordsFiltered' => \count($data),
323
            'data' => $data,
324
        );
325
    }
326
327
    /**
328
     * GETTERS / SETTERS.
329
     */
330
331
    /**
332
     * @return string
333
     */
334
    public function getNameIdentifier(): string
335
    {
336
        return $this->nameIdentifier;
337
    }
338
339
    /**
340
     * @param string $nameIdentifier
341
     *
342
     * @return Datatable
343
     */
344
    public function setNameIdentifier(string $nameIdentifier): self
345
    {
346
        $this->nameIdentifier = $nameIdentifier;
347
348
        return $this;
349
    }
350
351
    /**
352
     * @param bool $globalSearch
353
     *
354
     * @return Datatable
355
     *
356
     * @throws GlobalFilterWithHavingColumn
357
     */
358
    public function setGlobalSearch(bool $globalSearch): self
359
    {
360
        $this->globalSearch = $globalSearch;
361
        if ($this->globalSearch) {
362
            foreach ($this->columns as $column) {
363
                if ($column->isHaving()) {
364
                    throw new GlobalFilterWithHavingColumn();
365
                }
366
            }
367
        }
368
369
        return $this;
370
    }
371
}
372