Test Failed
Push — master ( e74f61...2d0031 )
by Mathieu
07:10 queued 10s
created

Datatable::export()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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