Completed
Push — master ( aab2b5...386069 )
by Arjay
01:50
created

EloquentEngine::getEagerLoads()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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