Completed
Push — master ( f28daa...aab2b5 )
by Arjay
02:12
created

EloquentEngine::resolveOrderByColumn()   B

Complexity

Conditions 6
Paths 4

Size

Total Lines 31
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 17
nc 4
nop 1
dl 0
loc 31
rs 8.439
c 0
b 0
f 0
1
<?php
2
3
namespace Yajra\Datatables\Engines;
4
5
use Illuminate\Database\Eloquent\Builder;
6
use Illuminate\Database\Eloquent\Relations\BelongsTo;
7
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
8
use Illuminate\Database\Eloquent\Relations\HasOneOrMany;
9
use Illuminate\Database\Eloquent\Relations\MorphToMany;
10
use Yajra\Datatables\Exception;
11
12
/**
13
 * Class EloquentEngine.
14
 *
15
 * @package Yajra\Datatables\Engines
16
 * @author  Arjay Angeles <[email protected]>
17
 */
18
class EloquentEngine extends QueryBuilderEngine
19
{
20
    /**
21
     * Select trashed records in count function for models with soft deletes trait.
22
     * By default we do not select soft deleted records.
23
     *
24
     * @var bool
25
     */
26
    protected $withTrashed = false;
27
28
    /**
29
     * Select only trashed records in count function for models with soft deletes trait.
30
     * By default we do not select soft deleted records.
31
     *
32
     * @var bool
33
     */
34
    protected $onlyTrashed = false;
35
36
    /**
37
     * @var \Illuminate\Database\Eloquent\Builder
38
     */
39
    protected $query;
40
41
    /**
42
     * EloquentEngine constructor.
43
     *
44
     * @param mixed $model
45
     */
46
    public function __construct($model)
47
    {
48
        $builder = $model instanceof Builder ? $model : $model->getQuery();
49
        parent::__construct($builder->getQuery());
50
51
        $this->query = $builder;
52
    }
53
54
    /**
55
     * Counts current query.
56
     *
57
     * @return int
58
     */
59
    public function count()
60
    {
61
        $builder = $this->prepareCountQuery();
62
63
        if ($this->isSoftDeleting()) {
64
            $builder->whereNull($builder->getModel()->getQualifiedDeletedAtColumn());
65
        }
66
67
        if ($this->isOnlyTrashed()) {
68
            $builder->whereNotNull($builder->getModel()->getQualifiedDeletedAtColumn());
69
        }
70
71
        $table = $this->connection->raw('(' . $builder->toSql() . ') count_row_table');
72
73
        return $this->connection->table($table)
74
                                ->setBindings($builder->getBindings())
75
                                ->count();
76
    }
77
78
    /**
79
     * Check if engine uses soft deletes.
80
     *
81
     * @return bool
82
     */
83
    private function isSoftDeleting()
84
    {
85
        return !$this->withTrashed && !$this->onlyTrashed && $this->modelUseSoftDeletes();
86
    }
87
88
    /**
89
     * Check if model use SoftDeletes trait.
90
     *
91
     * @return boolean
92
     */
93
    private function modelUseSoftDeletes()
94
    {
95
        return in_array('Illuminate\Database\Eloquent\SoftDeletes', class_uses($this->query->getModel()));
96
    }
97
98
    /**
99
     * Check if engine uses only trashed.
100
     *
101
     * @return bool
102
     */
103
    private function isOnlyTrashed()
104
    {
105
        return $this->onlyTrashed && $this->modelUseSoftDeletes();
106
    }
107
108
    /**
109
     * Change withTrashed flag value.
110
     *
111
     * @param bool $withTrashed
112
     * @return $this
113
     */
114
    public function withTrashed($withTrashed = true)
115
    {
116
        $this->withTrashed = $withTrashed;
117
118
        return $this;
119
    }
120
121
    /**
122
     * Change onlyTrashed flag value.
123
     *
124
     * @param bool $onlyTrashed
125
     * @return $this
126
     */
127
    public function onlyTrashed($onlyTrashed = true)
128
    {
129
        $this->onlyTrashed = $onlyTrashed;
130
131
        return $this;
132
    }
133
134
    /**
135
     * If column name could not be resolved then use primary key.
136
     *
137
     * @return string
138
     */
139
    protected function getPrimaryKeyName()
140
    {
141
        return $this->query->getModel()->getKeyName();
142
    }
143
144
    /**
145
     * Perform global search for the given keyword.
146
     *
147
     * @param string $keyword
148
     */
149
    protected function globalSearch($keyword)
150
    {
151
        $this->query->where(function ($query) use ($keyword) {
152
            $query = $this->getBaseQueryBuilder($query);
153
154
            collect($this->request->searchableColumnIndex())
155
                ->map(function ($index) {
156
                    return $this->getColumnName($index);
157
                })
158
                ->reject(function ($column) {
159
                    return $this->isBlacklisted($column) && !$this->hasCustomFilter($column);
160
                })
161
                ->each(function ($column) use ($keyword, $query) {
162
                    if ($this->hasCustomFilter($column)) {
163
                        $this->applyFilterColumn($query, $column, $keyword);
164
                    } else {
165
                        if (count(explode('.', $column)) > 1) {
166
                            $this->eagerLoadSearch($query, $column, $keyword);
167
                        } else {
168
                            $this->compileQuerySearch($query, $column, $keyword);
169
                        }
170
                    }
171
172
                    $this->isFilterApplied = true;
173
                });
174
        });
175
    }
176
177
    /**
178
     * Perform search on eager loaded relation column.
179
     *
180
     * @param mixed  $query
181
     * @param string $columnName
182
     * @param string $keyword
183
     */
184
    private function eagerLoadSearch($query, $columnName, $keyword)
185
    {
186
        $eagerLoads     = $this->getEagerLoads();
187
        $parts          = explode('.', $columnName);
188
        $relationColumn = array_pop($parts);
189
        $relation       = implode('.', $parts);
190
        if (in_array($relation, $eagerLoads)) {
191
            $this->compileRelationSearch(
192
                $query,
193
                $relation,
194
                $relationColumn,
195
                $keyword
196
            );
197
        } else {
198
            $this->compileQuerySearch($query, $columnName, $keyword);
199
        }
200
    }
201
202
    /**
203
     * Add relation query on global search.
204
     *
205
     * @param Builder $query
206
     * @param string  $relation
207
     * @param string  $column
208
     * @param string  $keyword
209
     */
210
    private function compileRelationSearch($query, $relation, $column, $keyword)
211
    {
212
        $myQuery = clone $this->query;
213
214
        /**
215
         * For compile nested relation, we need store all nested relation as array
216
         * and reverse order to apply where query.
217
         * With this method we can create nested sub query with properly relation.
218
         */
219
220
        /**
221
         * Store all relation data that require in next step
222
         */
223
        $relationChunk = [];
224
225
        /**
226
         * Store last eloquent query builder for get next relation.
227
         */
228
        $lastQuery = $query;
229
230
        $relations    = explode('.', $relation);
231
        $lastRelation = end($relations);
232
        foreach ($relations as $relation) {
233
            $relationType = $myQuery->getModel()->{$relation}();
234
            $myQuery->orWhereHas($relation, function ($builder) use (
235
                $column,
236
                $keyword,
237
                $query,
238
                $relationType,
239
                $relation,
240
                $lastRelation,
241
                &$relationChunk,
242
                &$lastQuery
243
            ) {
244
                $builder->select($this->connection->raw('count(1)'));
245
246
                // We will perform search on last relation only.
247
                if ($relation == $lastRelation) {
248
                    $this->compileQuerySearch($builder, $column, $keyword, '');
249
                }
250
251
                // Put require object to next step!!
252
                $relationChunk[$relation] = [
253
                    'builder'      => $builder,
254
                    'relationType' => $relationType,
255
                    'query'        => $lastQuery,
256
                ];
257
258
                // This is trick make sub query.
259
                $lastQuery = $builder;
260
            });
261
262
            // This is trick to make nested relation by pass previous relation to be next query eloquent builder
263
            $myQuery = $relationType;
264
        }
265
266
        /**
267
         * Reverse them all
268
         */
269
        $relationChunk = array_reverse($relationChunk, true);
270
271
        /**
272
         * Create valuable for use in check last relation
273
         */
274
        end($relationChunk);
275
        $lastRelation = key($relationChunk);
276
        reset($relationChunk);
277
278
        /**
279
         * Walking ...
280
         */
281
        foreach ($relationChunk as $relation => $chunk) {
282
            /** @var Builder $builder */
283
            $builder  = $chunk['builder'];
284
            $query    = $chunk['query'];
285
            $bindings = $builder->getBindings();
286
            $builder  = "({$builder->toSql()}) >= 1";
287
288
            // Check if it last relation we will use orWhereRaw
289
            if ($lastRelation == $relation) {
290
                $relationMethod = "orWhereRaw";
291
            } else {
292
                // For case parent relation of nested relation.
293
                // We must use and for properly query and get correct result
294
                $relationMethod = "whereRaw";
295
            }
296
297
            $query->{$relationMethod}($builder, $bindings);
298
        }
299
    }
300
301
    /**
302
     * Resolve the proper column name be used.
303
     *
304
     * @param string $column
305
     * @return string
306
     */
307
    protected function resolveOrderByColumn($column)
308
    {
309
        if (count(explode('.', $column)) > 1) {
310
            $eagerLoads     = $this->getEagerLoads();
311
            $parts          = explode('.', $column);
312
            $relationColumn = array_pop($parts);
313
            $relation       = implode('.', $parts);
314
315
            if (in_array($relation, $eagerLoads)) {
316
                // Loop for nested relations
317
                // This code is check morph many or not.
318
                // If one of nested relation is MorphToMany
319
                // we will call joinEagerLoadedColumn.
320
                $lastQuery     = $this->query;
321
                $isMorphToMany = false;
322
                foreach (explode('.', $relation) as $eachRelation) {
323
                    $relationship = $lastQuery->getRelation($eachRelation);
324
                    if (!($relationship instanceof MorphToMany)) {
325
                        $isMorphToMany = true;
326
                    }
327
                    $lastQuery = $relationship;
328
                }
329
330
                if ($isMorphToMany) {
331
                    return $this->joinEagerLoadedColumn($relation, $relationColumn);
332
                }
333
            }
334
        }
335
336
        return $column;
337
    }
338
339
    /**
340
     * Join eager loaded relation and get the related column name.
341
     *
342
     * @param string $relation
343
     * @param string $relationColumn
344
     * @return string
345
     * @throws \Yajra\Datatables\Exception
346
     */
347
    protected function joinEagerLoadedColumn($relation, $relationColumn)
348
    {
349
        $table     = '';
350
        $lastQuery = $this->query;
351
        foreach (explode('.', $relation) as $eachRelation) {
352
            $model = $lastQuery->getRelation($eachRelation);
353
            switch (true) {
354
                case $model instanceof BelongsToMany:
355
                    $pivot   = $model->getTable();
356
                    $pivotPK = $model->getExistenceCompareKey();
357
                    $pivotFK = $model->getQualifiedParentKeyName();
358
                    $this->performJoin($pivot, $pivotPK, $pivotFK);
359
360
                    $related = $model->getRelated();
361
                    $table   = $related->getTable();
362
                    $tablePK = $related->getForeignKey();
363
                    $foreign = $pivot . '.' . $tablePK;
364
                    $other   = $related->getQualifiedKeyName();
365
366
                    $lastQuery->addSelect($table . '.' . $relationColumn);
367
                    $this->performJoin($table, $foreign, $other);
368
369
                    break;
370
371
                case $model instanceof HasOneOrMany:
372
                    $table   = $model->getRelated()->getTable();
373
                    $foreign = $model->getQualifiedForeignKeyName();
374
                    $other   = $model->getQualifiedParentKeyName();
375
                    break;
376
377
                case $model instanceof BelongsTo:
378
                    $table   = $model->getRelated()->getTable();
379
                    $foreign = $model->getQualifiedForeignKey();
380
                    $other   = $model->getQualifiedOwnerKeyName();
381
                    break;
382
383
                default:
384
                    throw new Exception('Relation ' . get_class($model) . ' is not yet supported.');
385
            }
386
            $this->performJoin($table, $foreign, $other);
387
            $lastQuery = $model->getQuery();
388
        }
389
390
        return $table . '.' . $relationColumn;
391
    }
392
393
    /**
394
     * Perform join query.
395
     *
396
     * @param string $table
397
     * @param string $foreign
398
     * @param string $other
399
     * @param string $type
400
     */
401
    protected function performJoin($table, $foreign, $other, $type = 'left')
402
    {
403
        $joins = [];
404
        foreach ((array) $this->getBaseQueryBuilder()->joins as $key => $join) {
405
            $joins[] = $join->table;
406
        }
407
408
        if (!in_array($table, $joins)) {
409
            $this->getBaseQueryBuilder()->join($table, $foreign, '=', $other, $type);
410
        }
411
    }
412
}
413