Completed
Push — master ( 7d93ac...5e5621 )
by Arjay
01:41
created

EloquentEngine::eagerLoadSearch()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 17
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 13
nc 2
nop 3
dl 0
loc 17
rs 9.4285
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
     * @var \Illuminate\Database\Eloquent\Builder
21
     */
22
    protected $query;
23
24
    /**
25
     * EloquentEngine constructor.
26
     *
27
     * @param mixed $model
28
     */
29
    public function __construct($model)
30
    {
31
        $builder = $model instanceof Builder ? $model : $model->getQuery();
32
        parent::__construct($builder->getQuery());
33
34
        $this->query = $builder;
35
    }
36
37
    /**
38
     * If column name could not be resolved then use primary key.
39
     *
40
     * @return string
41
     */
42
    protected function getPrimaryKeyName()
43
    {
44
        return $this->query->getModel()->getKeyName();
45
    }
46
47
    /**
48
     * Perform global search for the given keyword.
49
     *
50
     * @param string $keyword
51
     */
52
    protected function globalSearch($keyword)
53
    {
54
        $this->query->where(function ($query) use ($keyword) {
55
            $query = $this->getBaseQueryBuilder($query);
56
57
            collect($this->request->searchableColumnIndex())
58
                ->map(function ($index) {
59
                    return $this->getColumnName($index);
60
                })
61
                ->reject(function ($column) {
62
                    return $this->isBlacklisted($column) && !$this->hasFilterColumn($column);
63
                })
64
                ->each(function ($column) use ($keyword, $query) {
65
                    if ($this->hasFilterColumn($column)) {
66
                        $this->applyFilterColumn($query, $column, $keyword);
67
                    } else {
68
                        if (count(explode('.', $column)) > 1) {
69
                            $this->eagerLoadSearch($query, $column, $keyword);
70
                        } else {
71
                            $this->compileQuerySearch($query, $column, $keyword);
72
                        }
73
                    }
74
75
                    $this->isFilterApplied = true;
76
                });
77
        });
78
    }
79
80
    /**
81
     * Perform search on eager loaded relation column.
82
     *
83
     * @param mixed  $query
84
     * @param string $columnName
85
     * @param string $keyword
86
     */
87
    private function eagerLoadSearch($query, $columnName, $keyword)
88
    {
89
        $eagerLoads     = $this->getEagerLoads();
90
        $parts          = explode('.', $columnName);
91
        $relationColumn = array_pop($parts);
92
        $relation       = implode('.', $parts);
93
        if (in_array($relation, $eagerLoads)) {
94
            $this->compileRelationSearch(
95
                $query,
96
                $relation,
97
                $relationColumn,
98
                $keyword
99
            );
100
        } else {
101
            $this->compileQuerySearch($query, $columnName, $keyword);
102
        }
103
    }
104
105
    /**
106
     * Get eager loads keys if eloquent.
107
     *
108
     * @return array
109
     */
110
    protected function getEagerLoads()
111
    {
112
        return array_keys($this->query->getEagerLoads());
113
    }
114
115
    /**
116
     * Add relation query on global search.
117
     *
118
     * @param Builder $query
119
     * @param string  $relation
120
     * @param string  $column
121
     * @param string  $keyword
122
     */
123
    private function compileRelationSearch($query, $relation, $column, $keyword)
124
    {
125
        $myQuery = clone $this->query;
126
127
        /**
128
         * For compile nested relation, we need store all nested relation as array
129
         * and reverse order to apply where query.
130
         * With this method we can create nested sub query with properly relation.
131
         */
132
133
        /**
134
         * Store all relation data that require in next step
135
         */
136
        $relationChunk = [];
137
138
        /**
139
         * Store last eloquent query builder for get next relation.
140
         */
141
        $lastQuery = $query;
142
143
        $relations    = explode('.', $relation);
144
        $lastRelation = end($relations);
145
        foreach ($relations as $relation) {
146
            $relationType = $myQuery->getModel()->{$relation}();
147
            $myQuery->orWhereHas($relation, function ($builder) use (
148
                $column,
149
                $keyword,
150
                $query,
151
                $relationType,
152
                $relation,
153
                $lastRelation,
154
                &$relationChunk,
155
                &$lastQuery
156
            ) {
157
                $builder->select($this->connection->raw('count(1)'));
158
159
                // We will perform search on last relation only.
160
                if ($relation == $lastRelation) {
161
                    $this->compileQuerySearch($builder, $column, $keyword, '');
162
                }
163
164
                // Put require object to next step!!
165
                $relationChunk[$relation] = [
166
                    'builder'      => $builder,
167
                    'relationType' => $relationType,
168
                    'query'        => $lastQuery,
169
                ];
170
171
                // This is trick make sub query.
172
                $lastQuery = $builder;
173
            });
174
175
            // This is trick to make nested relation by pass previous relation to be next query eloquent builder
176
            $myQuery = $relationType;
177
        }
178
179
        /**
180
         * Reverse them all
181
         */
182
        $relationChunk = array_reverse($relationChunk, true);
183
184
        /**
185
         * Create valuable for use in check last relation
186
         */
187
        end($relationChunk);
188
        $lastRelation = key($relationChunk);
189
        reset($relationChunk);
190
191
        /**
192
         * Walking ...
193
         */
194
        foreach ($relationChunk as $relation => $chunk) {
195
            /** @var Builder $builder */
196
            $builder  = $chunk['builder'];
197
            $query    = $chunk['query'];
198
            $bindings = $builder->getBindings();
199
            $builder  = "({$builder->toSql()}) >= 1";
200
201
            // Check if it last relation we will use orWhereRaw
202
            if ($lastRelation == $relation) {
203
                $relationMethod = "orWhereRaw";
204
            } else {
205
                // For case parent relation of nested relation.
206
                // We must use and for properly query and get correct result
207
                $relationMethod = "whereRaw";
208
            }
209
210
            $query->{$relationMethod}($builder, $bindings);
211
        }
212
    }
213
214
    /**
215
     * Resolve the proper column name be used.
216
     *
217
     * @param string $column
218
     * @return string
219
     */
220
    protected function resolveRelationColumn($column)
221
    {
222
        if (count(explode('.', $column)) > 1) {
223
            $eagerLoads     = $this->getEagerLoads();
224
            $parts          = explode('.', $column);
225
            $relationColumn = array_pop($parts);
226
            $relation       = implode('.', $parts);
227
228
            if (in_array($relation, $eagerLoads)) {
229
                $column = $this->joinEagerLoadedColumn($relation, $relationColumn);
230
            }
231
        }
232
233
        return $column;
234
    }
235
236
    /**
237
     * Join eager loaded relation and get the related column name.
238
     *
239
     * @param string $relation
240
     * @param string $relationColumn
241
     * @return string
242
     * @throws \Yajra\Datatables\Exception
243
     */
244
    protected function joinEagerLoadedColumn($relation, $relationColumn)
245
    {
246
        $table     = '';
247
        $lastQuery = $this->query;
248
        foreach (explode('.', $relation) as $eachRelation) {
249
            $model = $lastQuery->getRelation($eachRelation);
250
            switch (true) {
251
                case $model instanceof BelongsToMany:
252
                    $pivot   = $model->getTable();
253
                    $pivotPK = $model->getExistenceCompareKey();
254
                    $pivotFK = $model->getQualifiedParentKeyName();
255
                    $this->performJoin($pivot, $pivotPK, $pivotFK);
256
257
                    $related = $model->getRelated();
258
                    $table   = $related->getTable();
259
                    $tablePK = $related->getForeignKey();
260
                    $foreign = $pivot . '.' . $tablePK;
261
                    $other   = $related->getQualifiedKeyName();
262
263
                    $lastQuery->addSelect($table . '.' . $relationColumn);
264
                    $this->performJoin($table, $foreign, $other);
265
266
                    break;
267
268
                case $model instanceof HasOneOrMany:
269
                    $table   = $model->getRelated()->getTable();
270
                    $foreign = $model->getQualifiedForeignKeyName();
271
                    $other   = $model->getQualifiedParentKeyName();
272
                    break;
273
274
                case $model instanceof BelongsTo:
275
                    $table   = $model->getRelated()->getTable();
276
                    $foreign = $model->getQualifiedForeignKey();
277
                    $other   = $model->getQualifiedOwnerKeyName();
278
                    break;
279
280
                default:
281
                    throw new Exception('Relation ' . get_class($model) . ' is not yet supported.');
282
            }
283
            $this->performJoin($table, $foreign, $other);
284
            $lastQuery = $model->getQuery();
285
        }
286
287
        return $table . '.' . $relationColumn;
288
    }
289
290
    /**
291
     * Perform join query.
292
     *
293
     * @param string $table
294
     * @param string $foreign
295
     * @param string $other
296
     * @param string $type
297
     */
298
    protected function performJoin($table, $foreign, $other, $type = 'left')
299
    {
300
        $joins = [];
301
        foreach ((array) $this->getBaseQueryBuilder()->joins as $key => $join) {
302
            $joins[] = $join->table;
303
        }
304
305
        if (!in_array($table, $joins)) {
306
            $this->getBaseQueryBuilder()->join($table, $foreign, '=', $other, $type);
307
        }
308
    }
309
}
310