Passed
Push — master ( b30c75...6d865c )
by Michael
02:36
created

RelatedPlusTrait::scopeSearch()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 15
rs 8.8571
c 0
b 0
f 0
cc 5
eloc 8
nc 3
nop 2
1
<?php
2
3
namespace Blasttech\EloquentRelatedPlus;
4
5
use Illuminate\Database\Eloquent\Builder;
6
use Illuminate\Database\Eloquent\Model;
7
use Illuminate\Database\Eloquent\Relations\BelongsTo;
8
use Illuminate\Database\Eloquent\Relations\HasMany;
9
use Illuminate\Database\Eloquent\Relations\HasOneOrMany;
10
use Illuminate\Database\Eloquent\Relations\Relation;
11
use Illuminate\Database\Query\Expression;
12
use Illuminate\Database\Query\JoinClause;
13
use Illuminate\Support\Facades\DB;
14
use Illuminate\Support\Facades\Schema;
15
use InvalidArgumentException;
16
17
/**
18
 * Trait RelatedPlusTrait
19
 *
20
 * @property array order_fields
21
 * @property array order_defaults
22
 * @property array order_relations
23
 * @property array order_with
24
 * @property array search_fields
25
 * @property string connection
26
 */
27
trait RelatedPlusTrait
28
{
29
    use CustomOrderTrait, HelperMethodTrait, SearchTrait;
30
31
    /**
32
     * Boot method for trait
33
     *
34
     */
35
    public static function bootRelatedPlusTrait()
36
    {
37
        static::saving(function ($model) {
38
            if (!empty($model->nullable)) {
39
                foreach ($model->attributes as $key => $value) {
40
                    if (isset($model->nullable[$key])) {
41
                        $model->{$key} = empty(trim($value)) ? null : $value;
42
                    }
43
                }
44
            }
45
        });
46
    }
47
48
    /**
49
     * Get the table associated with the model.
50
     *
51
     * @return string
52
     */
53
    abstract public function getTable();
54
55
    /**
56
     * Add joins for one or more relations
57
     * This determines the foreign key relations automatically to prevent the need to figure out the columns.
58
     * Usages:
59
     * $query->modelJoin('customers')
60
     * $query->modelJoin('customer.client')
61
     *
62
     * @param Builder $query
63
     * @param string $relationName
64
     * @param string $operator
65
     * @param string $type
66
     * @param bool $where
67
     * @param bool $relatedSelect
68
     * @param string|null $direction
69
     *
70
     * @return Builder
71
     */
72
    public function scopeModelJoin(
73
        Builder $query,
74
        $relationName,
75
        $operator = '=',
76
        $type = 'left',
77
        $where = false,
78
        $relatedSelect = true,
79
        $direction = null
80
    ) {
81
        foreach ($this->parseRelationNames($relationName) as $relation) {
82
            $table = $this->getRelationTables($relation);
83
84
            /** @var Model $query */
85
            if (empty($query->getQuery()->columns)) {
86
                $query->select($this->getTable() . ".*");
87
            }
88
            if ($relatedSelect) {
89
                $query = $this->selectRelated($query, $table);
90
            }
91
            $query->relationJoin($table, $relation, $operator, $type, $where, $direction);
92
        }
93
94
        return $query;
95
    }
96
97
    /**
98
     * Add select for related table fields
99
     *
100
     * @param Builder $query
101
     * @param \stdClass $table
102
     * @return Builder
103
     */
104
    public function selectRelated(Builder $query, $table)
105
    {
106
        $connection = $this->connection;
107
108
        foreach (Schema::connection($connection)->getColumnListing($table->name) as $relatedColumn) {
109
            $query->addSelect(
110
                new Expression("`$table->alias`.`$relatedColumn` AS `$table->alias.$relatedColumn`")
111
            );
112
        }
113
114
        return $query;
115
    }
116
117
    /**
118
     * Set the order of a model
119
     *
120
     * @param Builder $query
121
     * @param string $orderField
122
     * @param string $direction
123
     * @return Builder
124
     */
125
    public function scopeOrderByCustom(Builder $query, $orderField, $direction)
126
    {
127
        if ($this->hasOrderFieldsAndDefaults($orderField, $direction)) {
128
            $query = $this->removeGlobalScope($query, 'order');
129
        }
130
131
        return $query->setCustomOrder($orderField, $direction);
132
    }
133
134
    /**
135
     * Use a model method to add columns or joins if in the order options
136
     *
137
     * @param Builder $query
138
     * @param string $order
139
     * @return Builder
140
     */
141
    public function scopeOrderByWith(Builder $query, $order)
142
    {
143
        if (isset($this->order_with[$order])) {
144
            $with = 'with' . $this->order_with[$order];
145
146
            $query->$with();
147
        }
148
149
        if (isset($this->order_fields[$order])) {
150
            $orderOption = (explode('.', $this->order_fields[$order]))[0];
151
152
            if (isset($this->order_relations[$orderOption])) {
153
                $query->modelJoin(
154
                    $this->order_relations[$orderOption],
155
                    '=',
156
                    'left',
157
                    false,
158
                    false
159
                );
160
            }
161
        }
162
163
        return $query;
164
    }
165
166
    /**
167
     * Join a model
168
     *
169
     * @param Builder $query
170
     * @param \stdClass $table
171
     * @param Relation $relation
172
     * @param string $operator
173
     * @param string $type
174
     * @param boolean $where
175
     * @param null $direction
176
     * @return Builder
177
     */
178
    public function scopeRelationJoin(
179
        Builder $query,
180
        $table,
181
        $relation,
182
        $operator,
183
        $type,
184
        $where,
185
        $direction = null
186
    ) {
187
        $fullTableName = $this->getTableWithAlias($table);
188
189
        return $query->join($fullTableName, function (JoinClause $join) use (
190
            $table,
191
            $relation,
192
            $operator,
193
            $direction
194
        ) {
195
            // If a HasOne relation and ordered - ie join to the latest/earliest
196
            if (class_basename($relation) === 'HasOne' && !empty($relation->toBase()->orders)) {
197
                return $this->hasOneJoin($relation, $join);
198
            } else {
199
                return $this->hasManyJoin($relation, $join, $table, $operator, $direction);
200
            }
201
        }, null, null, $type, $where);
202
    }
203
204
    /**
205
     * Join a HasOne relation which is ordered
206
     *
207
     * @param Relation $relation
208
     * @param JoinClause $join
209
     * @return JoinClause
210
     */
211
    private function hasOneJoin($relation, $join)
212
    {
213
        // Get first relation order (should only be one)
214
        $order = $relation->toBase()->orders[0];
215
216
        return $join->on($order['column'], $this->hasOneJoinSql($relation, $order));
217
    }
218
219
    /**
220
     * Get join sql for a HasOne relation
221
     *
222
     * @param Relation $relation
223
     * @param array $order
224
     * @return Expression
225
     */
226
    public function hasOneJoinSql($relation, $order)
227
    {
228
        // Build subquery for getting first/last record in related table
229
        $subQuery = $this
230
            ->joinOne(
231
                $relation->getRelated()->newQuery(),
232
                $relation,
233
                $order['column'],
234
                $order['direction']
235
            )
236
            ->setBindings($relation->getBindings());
237
238
        return DB::raw('(' . $this->toSqlWithBindings($subQuery) . ')');
239
    }
240
241
    /**
242
     * Adds a where for a relation's join columns and and min/max for a given column
243
     *
244
     * @param Builder $query
245
     * @param Relation $relation
246
     * @param string $column
247
     * @param string $direction
248
     * @return Builder
249
     */
250
    public function joinOne($query, $relation, $column, $direction)
251
    {
252
        // Get join fields
253
        $joinColumns = $this->getJoinColumns($relation);
254
255
        return $this->selectMinMax(
256
            $query->whereColumn($joinColumns->first, '=', $joinColumns->second),
257
            $column,
258
            $direction
259
        );
260
    }
261
262
    /**
263
     * Adds a select for a min or max on the given column, depending on direction given
264
     *
265
     * @param Builder $query
266
     * @param string $column
267
     * @param string $direction
268
     * @return Builder
269
     */
270
    public function selectMinMax($query, $column, $direction)
271
    {
272
        $column = $this->addBackticks($column);
273
274
        /** @var Model $query */
275
        if ($direction == 'asc') {
276
            return $query->select(DB::raw('MIN(' . $column . ')'));
277
        } else {
278
            return $query->select(DB::raw('MAX(' . $column . ')'));
279
        }
280
    }
281
282
    /**
283
     * Join a HasMany Relation
284
     *
285
     * @param Relation $relation
286
     * @param JoinClause $join
287
     * @param \stdClass $table
288
     * @param string $operator
289
     * @param string $direction
290
     * @return Builder|JoinClause
291
     */
292
    protected function hasManyJoin($relation, $join, $table, $operator, $direction)
293
    {
294
        // Get relation join columns
295
        $joinColumns = $this->getJoinColumns($relation);
296
297
        $first = $joinColumns->first;
298
        $second = $joinColumns->second;
299
        if ($table->name !== $table->alias) {
300
            $first = str_replace($table->name, $table->alias, $first);
301
            $second = str_replace($table->name, $table->alias, $second);
302
        }
303
304
        $join->on($first, $operator, $second);
305
306
        // Add any where clauses from the relationship
307
        $join = $this->addRelatedWhereConstraints($join, $relation, $table->alias);
308
309
        if (!is_null($direction) && get_class($relation) === HasMany::class) {
310
            $join = $this->hasManyJoinWhere($join, $first, $relation, $table->alias, $direction);
0 ignored issues
show
Bug introduced by
It seems like $join can also be of type object<Illuminate\Database\Eloquent\Builder>; however, Blasttech\EloquentRelate...ait::hasManyJoinWhere() does only seem to accept object<Illuminate\Database\Query\JoinClause>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
311
        }
312
313
        return $join;
314
    }
315
316
    /**
317
     * Add wheres if they exist for a relation
318
     *
319
     * @param Builder|JoinClause $builder
320
     * @param Relation|BelongsTo|HasOneOrMany $relation
321
     * @param string $table
322
     * @return Builder|JoinClause $builder
323
     */
324
    protected function addRelatedWhereConstraints($builder, $relation, $table)
325
    {
326
        // Get where clauses from the relationship
327
        $wheres = collect($relation->toBase()->wheres)
328
            ->where('type', 'Basic')
329
            ->map(function ($where) use ($table) {
330
                // Add table name to column if it is absent
331
                return [$this->columnWithTableName($table, $where['column']), $where['operator'], $where['value']];
332
            })->toArray();
333
334
        if (!empty($wheres)) {
335
            $builder->where($wheres);
336
        }
337
338
        return $builder;
339
    }
340
341
    /**
342
     * If the relation is one-to-many, just get the first related record
343
     *
344
     * @param JoinClause $joinClause
345
     * @param string $column
346
     * @param HasMany|Relation $relation
347
     * @param string $table
348
     * @param string $direction
349
     *
350
     * @return JoinClause
351
     */
352
    public function hasManyJoinWhere(JoinClause $joinClause, $column, $relation, $table, $direction)
353
    {
354
        return $joinClause->where(
355
            $column,
356
            function ($subQuery) use ($table, $direction, $relation, $column) {
357
                $subQuery = $this->joinOne(
358
                    $subQuery->from($table),
359
                    $relation,
360
                    $column,
361
                    $direction
362
                );
363
364
                // Add any where statements with the relationship
365
                $subQuery = $this->addRelatedWhereConstraints($subQuery, $relation, $table);
366
367
                // Add any order statements with the relationship
368
                return $this->addOrder($subQuery, $relation, $table);
369
            }
370
        );
371
    }
372
373
    /**
374
     * Add orderBy if orders exist for a relation
375
     *
376
     * @param Builder|JoinClause $builder
377
     * @param Relation|BelongsTo|HasOneOrMany $relation
378
     * @param string $table
379
     * @return Builder|JoinClause $builder
380
     */
381
    protected function addOrder($builder, $relation, $table)
382
    {
383
        /** @var Model $builder */
384
        if (!empty($relation->toBase()->orders)) {
385
            // Get where clauses from the relationship
386
            foreach ($relation->toBase()->orders as $order) {
387
                $builder->orderBy($this->columnWithTableName($table, $order['column']), $order['direction']);
388
            }
389
        }
390
391
        return $builder;
392
    }
393
394
    /**
395
     * Add where statements for the model search fields
396
     *
397
     * @param Builder $query
398
     * @param string $searchText
399
     * @return Builder
400
     */
401
    public function scopeSearch(Builder $query, $searchText = '')
402
    {
403
        $searchText = trim($searchText);
404
405
        // If search is set
406
        if ($searchText != "") {
407
            if (!isset($this->search_fields) || !is_array($this->search_fields) || empty($this->search_fields)) {
408
                throw new InvalidArgumentException(get_class($this) . ' search properties not set correctly.');
409
            } else {
410
                $query = $this->checkSearchFields($query, $searchText);
411
            }
412
        }
413
414
        return $query;
415
    }
416
417
    /**
418
     * Switch a query to be a subquery of a model
419
     *
420
     * @param Builder $query
421
     * @param Builder $model
422
     * @return Builder
423
     */
424
    public function scopeSetSubquery(Builder $query, $model)
425
    {
426
        $sql = $this->toSqlWithBindings($model);
427
        $table = $model->getQuery()->from;
428
429
        return $query
430
            ->from(DB::raw("({$sql}) as " . $table))
431
            ->select($table . '.*');
432
    }
433
434
    /**
435
     * Set the model order
436
     *
437
     * @param Builder $query
438
     * @param string $column
439
     * @param string $direction
440
     * @return Builder
441
     */
442
    public function scopeSetCustomOrder(Builder $query, $column, $direction)
443
    {
444
        if (isset($this->order_defaults)) {
445
            $column = $this->setOrderColumn($column);
446
            $direction = $this->setOrderDirection($direction);
447
        }
448
449
        return $this->setOrder($query, $column, $direction);
450
    }
451
452
    /**
453
     * Check if column being sorted by is from a related model
454
     *
455
     * @param Builder $query
456
     * @param string $column
457
     * @param string $direction
458
     * @return Builder
459
     */
460
    public function scopeOrderByCheckModel(Builder $query, $column, $direction)
461
    {
462
        /** @var Model $query */
463
        $query->orderBy(DB::raw($column), $direction);
464
465
        if (isset($this->order_relations) && (strpos($column,
466
                    '.') !== false || isset($this->order_relations[$column]))) {
467
            $query = $this->joinRelatedTable($query, $this->getTableFromColumn($column));
468
        }
469
470
        return $query;
471
    }
472
}
473