Passed
Push — master ( ff949a...a6258d )
by Michael
02:30
created

RelatedPlusTrait::scopeOrderByWith()   B

Complexity

Conditions 4
Paths 6

Size

Total Lines 24
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 24
c 0
b 0
f 0
rs 8.6845
cc 4
eloc 14
nc 6
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
 *
26
 * @package Blasttech\WherePlus
27
 */
28
trait RelatedPlusTrait
29
{
30
    /**
31
     * Boot method for trait
32
     *
33
     */
34
    public static function bootRelatedPlusTrait()
35
    {
36
        static::saving(function ($model) {
37
            if (!empty($model->nullable)) {
38
                foreach ($model->attributes as $key => $value) {
39
                    if (isset($model->nullable[$key])) {
40
                        $model->{$key} = empty(trim($value)) ? null : $value;
41
                    }
42
                }
43
            }
44
        });
45
    }
46
47
    /**
48
     * Add joins for one or more relations
49
     * This determines the foreign key relations automatically to prevent the need to figure out the columns.
50
     * Usages:
51
     * $query->modelJoin('customers')
52
     * $query->modelJoin('customer.client')
53
     *
54
     * @param Builder $query
55
     * @param string $relationName
56
     * @param string $operator
57
     * @param string $type
58
     * @param bool $where
59
     * @param bool $relatedSelect
60
     * @param string|null $direction
61
     *
62
     * @return Builder
63
     */
64
    public function scopeModelJoin(
65
        Builder $query,
66
        $relationName,
67
        $operator = '=',
68
        $type = 'left',
69
        $where = false,
70
        $relatedSelect = true,
71
        $direction = null
72
    ) {
73
        $connection = $this->connection;
0 ignored issues
show
Bug introduced by
The property connection does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
74
75
        foreach ($this->parseRelationNames($relationName) as $relation) {
76
            $tableName = $relation->getRelated()->getTable();
77
            // if using a 'table' AS 'tableAlias' in a from statement, otherwise alias will be the table name
78
            $from = explode(' ', $relation->getQuery()->getQuery()->from);
79
            $tableAlias = array_pop($from);
80
81
            if (empty($query->getQuery()->columns)) {
82
                $query->select($this->getTable() . ".*");
0 ignored issues
show
Bug introduced by
It seems like getTable() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
Bug introduced by
The method select() does not exist on Illuminate\Database\Eloquent\Builder. Did you maybe mean createSelectWithConstraint()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
83
            }
84
            if ($relatedSelect) {
85
                foreach (Schema::connection($connection)->getColumnListing($tableName) as $relatedColumn) {
86
                    $query->addSelect(
87
                        new Expression("`$tableAlias`.`$relatedColumn` AS `$tableAlias.$relatedColumn`")
88
                    );
89
                }
90
            }
91
            $query->relationJoin($tableName, $tableAlias, $relation, $operator, $type, $where, $direction);
92
        }
93
94
        return $query;
95
    }
96
97
    /**
98
     * Get the relations from a relation name
99
     * $relationName can be a single relation
100
     * Usage for User model:
101
     * parseRelationNames('customer') returns [$user->customer()]
102
     * parseRelationNames('customer.contact') returns [$user->customer(), $user->customer->contact()]
103
     *
104
     * @param string $relationName
105
     * @return Relation[]
106
     */
107
    protected function parseRelationNames($relationName)
108
    {
109
        $relationNames = explode('.', $relationName);
110
        $parentRelationName = null;
111
        $relations = [];
112
113
        foreach ($relationNames as $relationName) {
114
            if (is_null($parentRelationName)) {
115
                $relations[] = $this->$relationName();
116
                $parentRelationName = $this->$relationName()->getRelated();
117
            } else {
118
                $relations[] = $parentRelationName->$relationName();
119
            }
120
        }
121
122
        return $relations;
123
    }
124
125
    /**
126
     * Join a model
127
     *
128
     * @param Builder $query
129
     * @param string $tableName
130
     * @param string $tableAlias
131
     * @param Relation $relation
132
     * @param string $operator
133
     * @param string $type
134
     * @param boolean $where
135
     * @param null $direction
136
     * @return Builder
137
     */
138
    public function scopeRelationJoin(
139
        Builder $query,
140
        $tableName,
141
        $tableAlias,
142
        $relation,
143
        $operator,
144
        $type,
145
        $where,
146
        $direction = null
147
    ) {
148
        if ($tableAlias !== '' && $tableName !== $tableAlias) {
149
            $fullTableName = $tableName . ' AS ' . $tableAlias;
150
        } else {
151
            $fullTableName = $tableName;
152
        }
153
154
        return $query->join($fullTableName, function (JoinClause $join) use (
155
            $tableName,
156
            $tableAlias,
157
            $relation,
158
            $operator,
159
            $direction
160
        ) {
161
            // If a HasOne relation and ordered - ie join to the latest/earliest
162
            if (class_basename($relation) === 'HasOne' && !empty($relation->toBase()->orders)) {
163
                return $this->hasOneJoin($relation, $join);
164
            } else {
165
                return $this->hasManyJoin($relation, $join, $tableName, $tableAlias, $operator, $direction);
166
            }
167
        }, null, null, $type, $where);
168
    }
169
170
    /**
171
     * Join a HasOne relation which is ordered
172
     *
173
     * @param Relation $relation
174
     * @param JoinClause $join
175
     * @return JoinClause
176
     */
177
    private function hasOneJoin($relation, $join)
178
    {
179
        // Get first relation order (should only be one)
180
        $order = $relation->toBase()->orders[0];
181
182
        return $join->on($order['column'], $this->hasOneJoinSql($relation, $order));
183
    }
184
185
    /**
186
     * Get join sql for a HasOne relation
187
     *
188
     * @param Relation $relation
189
     * @param array $order
190
     * @return Expression
191
     */
192
    public function hasOneJoinSql($relation, $order)
193
    {
194
        // Build subquery for getting first/last record in related table
195
        $subQuery = $this
196
            ->joinOne(
197
                $relation->getRelated()->newQuery(),
198
                $relation,
199
                $order['column'],
200
                $order['direction']
201
            )
202
            ->setBindings($relation->getBindings());
203
204
        return DB::raw('(' . $this->toSqlWithBindings($subQuery) . ')');
205
    }
206
207
    /**
208
     * Adds a where for a relation's join columns and and min/max for a given column
209
     *
210
     * @param Builder $query
211
     * @param Relation $relation
212
     * @param string $column
213
     * @param string $direction
214
     * @return Builder
215
     */
216
    public function joinOne($query, $relation, $column, $direction)
217
    {
218
        // Get join fields
219
        $joinColumns = $this->getJoinColumns($relation);
220
221
        return $this->selectMinMax(
222
            $query->whereColumn($joinColumns->first, '=', $joinColumns->second),
223
            $column,
224
            $direction
225
        );
226
    }
227
228
    /**
229
     * Get the join columns for a relation
230
     *
231
     * @param Relation|BelongsTo|HasOneOrMany $relation
232
     * @return \stdClass
233
     */
234
    protected function getJoinColumns($relation)
235
    {
236
        // Get keys with table names
237
        if ($relation instanceof BelongsTo) {
238
            $first = $relation->getOwnerKey();
239
            $second = $relation->getForeignKey();
240
        } else {
241
            $first = $relation->getQualifiedParentKeyName();
242
            $second = $relation->getQualifiedForeignKeyName();
243
        }
244
245
        return (object)['first' => $first, 'second' => $second];
246
    }
247
248
    /**
249
     * Adds a select for a min or max on the given column, depending on direction given
250
     *
251
     * @param Builder $query
252
     * @param string $column
253
     * @param string $direction
254
     * @return Builder
255
     */
256
    public function selectMinMax($query, $column, $direction)
257
    {
258
        $column = $this->addBackticks($column);
259
260
        if ($direction == 'asc') {
261
            return $query->select(DB::raw('MIN(' . $column . ')'));
0 ignored issues
show
Bug introduced by
The method select() does not exist on Illuminate\Database\Eloquent\Builder. Did you maybe mean createSelectWithConstraint()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
262
        } else {
263
            return $query->select(DB::raw('MAX(' . $column . ')'));
0 ignored issues
show
Bug introduced by
The method select() does not exist on Illuminate\Database\Eloquent\Builder. Did you maybe mean createSelectWithConstraint()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
264
        }
265
    }
266
267
    /**
268
     * Add backticks to a table/column
269
     *
270
     * @param string $column
271
     * @return string
272
     */
273
    private function addBackticks($column)
274
    {
275
        return preg_match('/^[0-9a-zA-Z\.]*$/', $column) ?
276
            '`' . str_replace(['`', '.'], ['', '`.`'], $column) . '`' : $column;
277
    }
278
279
    /**
280
     * Return the sql for a query with the bindings replaced with the binding values
281
     *
282
     * @param Builder $builder
283
     * @return string
284
     */
285
    private function toSqlWithBindings(Builder $builder)
286
    {
287
        $replacements = array_map('addslashes', $builder->getBindings());
288
        $sql = $builder->toSql();
289
290
        return preg_replace_callback(
291
            '/(\?)(?=(?:[^\'"]|["\'][^\'"]*["\'])*$)/',
292
            function ($matches) use (&$replacements) {
0 ignored issues
show
Unused Code introduced by
The parameter $matches is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
293
                return array_shift($replacements);
294
            },
295
            $sql
296
        );
297
    }
298
299
    /**
300
     * Join a HasMany Relation
301
     *
302
     * @param Relation $relation
303
     * @param JoinClause $join
304
     * @param string $tableName
305
     * @param string $tableAlias
306
     * @param string $operator
307
     * @param string $direction
308
     * @return Builder|JoinClause
309
     */
310
    private function hasManyJoin($relation, $join, $tableName, $tableAlias, $operator, $direction)
311
    {
312
        // Get relation join columns
313
        $joinColumns = $this->getJoinColumns($relation);
314
315
        $first = $joinColumns->first;
316
        $second = $joinColumns->second;
317
        if ($tableName !== $tableAlias) {
318
            $first = str_replace($tableName, $tableAlias, $first);
319
            $second = str_replace($tableName, $tableAlias, $second);
320
        }
321
322
        $join->on($first, $operator, $second);
323
324
        // Add any where clauses from the relationship
325
        $join = $this->addWhereConstraints($join, $relation, $tableAlias);
326
327
        if (!is_null($direction) && get_class($relation) === HasMany::class) {
328
            $join = $this->hasManyJoinWhere($join, $first, $relation, $tableAlias, $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...
329
        }
330
331
        return $join;
332
    }
333
334
    /**
335
     * Add wheres if they exist for a relation
336
     *
337
     * @param Builder|JoinClause $builder
338
     * @param Relation|BelongsTo|HasOneOrMany $relation
339
     * @param string $table
340
     * @return Builder|JoinClause
341
     */
342
    protected function addWhereConstraints($builder, $relation, $table)
343
    {
344
        // Get where clauses from the relationship
345
        $wheres = collect($relation->toBase()->wheres)
346
            ->where('type', 'Basic')
347
            ->map(function ($where) use ($table) {
348
                // Add table name to column if it is absent
349
                return [$this->columnWithTableName($table, $where['column']), $where['operator'], $where['value']];
350
            })->toArray();
351
352
        if (!empty($wheres)) {
353
            $builder->where($wheres);
354
        }
355
356
        return $builder;
357
    }
358
359
    /**
360
     * Add table name to column name if table name not already included in column name
361
     *
362
     * @param string $table
363
     * @param string $column
364
     * @return string
365
     */
366
    private function columnWithTableName($table, $column)
367
    {
368
        return (preg_match('/(' . $table . '\.|`' . $table . '`)/i', $column) > 0 ? '' : $table . '.') . $column;
369
    }
370
371
    /**
372
     * If the relation is one-to-many, just get the first related record
373
     *
374
     * @param Builder|JoinClause $joinClause
0 ignored issues
show
Documentation introduced by
Consider making the type for parameter $joinClause a bit more specific; maybe use JoinClause.
Loading history...
375
     * @param string $column
376
     * @param HasMany|Relation $relation
377
     * @param string $table
378
     * @param string $direction
379
     *
380
     * @return JoinClause
381
     */
382
    public function hasManyJoinWhere(JoinClause $joinClause, $column, $relation, $table, $direction)
383
    {
384
        return $joinClause->where(
385
            $column,
386
            function ($subQuery) use ($table, $direction, $relation, $column) {
387
                $subQuery = $this->joinOne(
388
                    $subQuery->from($table),
389
                    $relation,
390
                    $column,
391
                    $direction
392
                );
393
394
                // Add any where statements with the relationship
395
                $subQuery = $this->addWhereConstraints($subQuery, $relation, $table);
396
397
                // Add any order statements with the relationship
398
                return $this->addOrder($subQuery, $relation, $table);
399
            }
400
        );
401
    }
402
403
    /**
404
     * Add orderBy if orders exist for a relation
405
     *
406
     * @param Builder|JoinClause $builder
407
     * @param Relation|BelongsTo|HasOneOrMany $relation
408
     * @param string $table
409
     * @return Builder
410
     */
411
    protected function addOrder($builder, $relation, $table)
412
    {
413
        if (!empty($relation->toBase()->orders)) {
414
            // Get where clauses from the relationship
415
            foreach ($relation->toBase()->orders as $order) {
416
                $builder->orderBy($this->columnWithTableName($table, $order['column']), $order['direction']);
0 ignored issues
show
Bug introduced by
The method orderBy does only exist in Illuminate\Database\Query\JoinClause, but not in Illuminate\Database\Eloquent\Builder.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
417
            }
418
        }
419
420
        return $builder;
421
    }
422
423
    /**
424
     * Set the order of a model
425
     *
426
     * @param Builder $query
427
     * @param string $orderField
428
     * @param string $direction
429
     * @return Builder
430
     */
431
    public function scopeOrderByCustom(Builder $query, $orderField, $direction)
432
    {
433
        if ($this->fieldsCheck($orderField, $direction)) {
434
            $query = $this->removeOrderGlobalScope($query);
435
        }
436
437
        return $query->setCustomOrder($orderField, $direction);
438
    }
439
440
    /**
441
     * Check $order_fields and $order_defaults are set
442
     *
443
     * @param $orderField
444
     * @param $direction
445
     * @return bool
446
     */
447
    private function fieldsCheck($orderField, $direction)
0 ignored issues
show
Coding Style introduced by
function fieldsCheck() does not seem to conform to the naming convention (^(?:is|has|should|may|supports)).

This check examines a number of code elements and verifies that they conform to the given naming conventions.

You can set conventions for local variables, abstract classes, utility classes, constant, properties, methods, parameters, interfaces, classes, exceptions and special methods.

Loading history...
448
    {
449
        if (!isset($this->order_fields) || !is_array($this->order_fields)) {
450
            throw new InvalidArgumentException(get_class($this) . ' order fields not set correctly.');
451
        } else {
452
            if (($orderField === '' || $direction === '')
453
                && (!isset($this->order_defaults) || !is_array($this->order_defaults))) {
454
                throw new InvalidArgumentException(get_class($this) . ' order defaults not set and not overriden.');
455
            } else {
456
                return true;
457
            }
458
        }
459
    }
460
461
    /**
462
     * Remove order global scope if it exists
463
     *
464
     * @param Builder $query
465
     * @return Builder
466
     */
467
    private function removeOrderGlobalScope($query)
468
    {
469
        /** @var Model $this */
470
        $globalScopes = $this->getGlobalScopes();
471
        if (isset($globalScopes['order'])) {
472
            $query->withoutGlobalScope('order');
473
        }
474
475
        return $query;
476
    }
477
478
    /**
479
     * Check if column being sorted by is from a related model
480
     *
481
     * @param Builder $query
482
     * @param string $column
483
     * @param string $direction
484
     * @return Builder
485
     */
486
    public function scopeOrderByCheckModel(Builder $query, $column, $direction)
487
    {
488
        $query->orderBy(DB::raw($column), $direction);
0 ignored issues
show
Bug introduced by
The method orderBy() does not exist on Illuminate\Database\Eloquent\Builder. Did you maybe mean enforceOrderBy()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
489
490
        $periodPos = strpos($column, '.');
491
        if (isset($this->order_relations) && ($periodPos !== false || isset($this->order_relations[$column]))) {
492
            $table = ($periodPos !== false ? substr($column, 0, $periodPos) : $column);
493
            $query = $this->joinRelatedTable($query, $table);
494
        }
495
496
        return $query;
497
    }
498
499
    /**
500
     * Join a related table if not already joined
501
     *
502
     * @param Builder $query
503
     * @param string $table
504
     * @return Builder
505
     */
506
    private function joinRelatedTable($query, $table)
507
    {
508
        if (isset($this->order_relations[$table]) &&
509
            !$this->hasJoin($query, $table, $this->order_relations[$table])) {
510
            $columnRelations = $this->order_relations[$table];
511
512
            $query->modelJoin(
513
                $columnRelations,
514
                '=',
515
                'left',
516
                false,
517
                false
518
            );
519
        }
520
521
        return $query;
522
    }
523
524
    /**
525
     * Check if this model has already been joined to a table or relation
526
     *
527
     * @param Builder $builder
528
     * @param string $table
529
     * @param \Illuminate\Database\Eloquent\Relations\Relation $relation
530
     * @return bool
531
     */
532
    protected function hasJoin(Builder $builder, $table, $relation)
533
    {
534
        if (!$this->isJoined($builder, $table)) {
535
            return $this->isEagerLoaded($builder, $relation);
536
        } else {
537
            return true;
538
        }
539
    }
540
541
    /**
542
     * Check if model is currently joined to $table
543
     *
544
     * @param Builder $builder
545
     * @param string $table
546
     * @return bool
547
     */
548
    private function isJoined(Builder $builder, $table)
549
    {
550
        $joins = $builder->getQuery()->joins;
551
        if (!is_null($joins)) {
552
            foreach ($joins as $joinClause) {
553
                if ($joinClause->table == $table) {
554
                    return true;
555
                }
556
            }
557
        }
558
559
        return false;
560
    }
561
562
    /**
563
     * Check if relation exists in eager loads
564
     *
565
     * @param Builder $builder
566
     * @param \Illuminate\Database\Eloquent\Relations\Relation $relation
567
     * @return bool
568
     */
569
    private function isEagerLoaded(Builder $builder, $relation)
570
    {
571
        $eagerLoads = $builder->getEagerLoads();
572
573
        return !is_null($eagerLoads) && in_array($relation, $eagerLoads);
574
    }
575
576
    /**
577
     * Set the model order
578
     *
579
     * @param Builder $query
580
     * @param string $column
581
     * @param string $direction
582
     * @return Builder
583
     */
584
    public function scopeSetCustomOrder(Builder $query, $column, $direction)
585
    {
586
        if (isset($this->order_defaults)) {
587
            $column = $this->setColumn($column);
588
            $direction = $this->setDirection($direction);
589
        }
590
591
        return $this->setOrder($query, $column, $direction);
592
    }
593
594
    /**
595
     * Override column if provided column not valid
596
     *
597
     * @param $column
598
     * @return string
599
     */
600
    private function setColumn($column)
601
    {
602
        // If $column not in order_fields list, use default
603
        if ($column == '' || !isset($this->order_fields[$column])) {
604
            $column = $this->order_defaults['field'];
605
        }
606
607
        return $column;
608
    }
609
610
    /**
611
     * Override direction if provided direction not valid
612
     *
613
     * @param string $direction
614
     * @return string
615
     */
616
    private function setDirection($direction)
617
    {
618
        // If $direction not asc or desc, use default
619
        if ($direction == '' || !in_array(strtoupper($direction), ['ASC', 'DESC'])) {
620
            $direction = $this->order_defaults['dir'];
621
        }
622
623
        return $direction;
624
    }
625
626
    /**
627
     * Set order based on order_fields
628
     *
629
     * @param Builder $query
630
     * @param string $column
631
     * @param string $direction
632
     * @return Builder
633
     */
634
    private function setOrder($query, $column, $direction)
635
    {
636
        if (!is_array($this->order_fields[$column])) {
637
            $query->orderByCheckModel($this->order_fields[$column], $direction);
638
        } else {
639
            foreach ($this->order_fields[$column] as $dbField) {
640
                $query->orderByCheckModel($dbField, $direction);
641
            }
642
        }
643
644
        return $query;
645
    }
646
647
    /**
648
     * Switch a query to be a subquery of a model
649
     *
650
     * @param Builder $query
651
     * @param Builder $model
652
     * @return Builder
653
     */
654
    public function scopeSetSubquery(Builder $query, $model)
655
    {
656
        $sql = $this->toSqlWithBindings($model);
657
        $table = $model->getQuery()->from;
658
659
        return $query
660
            ->from(DB::raw("({$sql}) as " . $table))
661
            ->select($table . '.*');
662
    }
663
664
    /**
665
     * Use a model method to add columns or joins if in the order options
666
     *
667
     * @param Builder $query
668
     * @param string $order
669
     * @return Builder
670
     */
671
    public function scopeOrderByWith(Builder $query, $order)
672
    {
673
        if (isset($this->order_with[$order])) {
674
            $with = 'with' . $this->order_with[$order];
675
676
            $query->$with();
677
        }
678
679
        if (isset($this->order_fields[$order])) {
680
            $orderOption = (explode('.', $this->order_fields[$order]))[0];
681
682
            if (isset($this->order_relations[$orderOption])) {
683
                $query->modelJoin(
684
                    $this->order_relations[$orderOption],
685
                    '=',
686
                    'left',
687
                    false,
688
                    false
689
                );
690
            }
691
        }
692
693
        return $query;
694
    }
695
696
    /**
697
     * Add where statements for the model search fields
698
     *
699
     * @param Builder $query
700
     * @param string $searchText
701
     * @return Builder
702
     */
703
    public function scopeSearch(Builder $query, $searchText = '')
704
    {
705
        $searchText = trim($searchText);
706
707
        // If search is set
708
        if ($searchText != "") {
709
            if (!isset($this->search_fields) || !is_array($this->search_fields) || empty($this->search_fields)) {
710
                throw new InvalidArgumentException(get_class($this) . ' search properties not set correctly.');
711
            } else {
712
                $query = $this->checkSearchFields($query, $searchText);
713
            }
714
        }
715
716
        return $query;
717
    }
718
719
    /**
720
     * Add where statements for search fields to search for searchText
721
     *
722
     * @param Builder $query
723
     * @param string $searchText
724
     * @return Builder
725
     */
726
    private function checkSearchFields($query, $searchText)
727
    {
728
        return $query->where(function (Builder $query) use ($searchText) {
729
            if (isset($this->search_fields) && !empty($this->search_fields)) {
730
                /** @var Model $this */
731
                $table = $this->getTable();
732
                foreach ($this->search_fields as $searchField => $searchFieldParameters) {
733
                    $query = $this->checkSearchField($query, $table, $searchField, $searchFieldParameters, $searchText);
734
                }
735
            }
736
737
            return $query;
738
        });
739
    }
740
741
    /**
742
     * Add where statement for a search field
743
     *
744
     * @param Builder $query
745
     * @param string $table
746
     * @param string $searchField
747
     * @param array $searchFieldParameters
748
     * @param string $searchText
749
     * @return Builder
750
     */
751
    private function checkSearchField($query, $table, $searchField, $searchFieldParameters, $searchText)
752
    {
753
        if (!isset($searchFieldParameters['regex']) || preg_match($searchFieldParameters['regex'], $searchText)) {
754
            $searchColumn = is_array($searchFieldParameters) ? $searchField : $searchFieldParameters;
755
756
            if (isset($searchFieldParameters['relation'])) {
757
                return $this->searchRelation($query, $searchFieldParameters, $searchColumn, $searchText);
758
            } else {
759
                return $this->searchThis($query, $searchFieldParameters, $table, $searchColumn, $searchText);
760
            }
761
        } else {
762
            return $query;
763
        }
764
    }
765
766
    /**
767
     * Add where condition to search a relation
768
     *
769
     * @param Builder $query
770
     * @param array $searchFieldParameters
771
     * @param string $searchColumn
772
     * @param string $searchText
773
     * @return Builder
774
     */
775
    private function searchRelation(Builder $query, $searchFieldParameters, $searchColumn, $searchText)
776
    {
777
        $relation = $searchFieldParameters['relation'];
778
        $relatedTable = $this->$relation()->getRelated()->getTable();
779
780
        return $query->orWhere(function (Builder $query) use (
781
            $searchText,
782
            $searchColumn,
783
            $searchFieldParameters,
784
            $relation,
785
            $relatedTable
786
        ) {
787
            return $query->orWhereHas($relation, function (Builder $query2) use (
788
                $searchText,
789
                $searchColumn,
790
                $searchFieldParameters,
791
                $relatedTable
792
            ) {
793
                return $query2->where($relatedTable . '.' . $searchColumn, 'like', $searchText . '%');
794
            });
795
        });
796
    }
797
798
    /**
799
     * Add where condition to search current model
800
     *
801
     * @param Builder $query
802
     * @param array $searchFieldParameters
803
     * @param string $table
804
     * @param string $searchColumn
805
     * @param string $searchText
806
     * @return Builder
807
     */
808
    public function searchThis(Builder $query, $searchFieldParameters, $table, $searchColumn, $searchText)
809
    {
810
        $searchOperator = $searchFieldParameters['operator'] ?? 'like';
811
        $searchValue = $searchFieldParameters['value'] ?? '%{{search}}%';
812
813
        return $query->orWhere(
814
            $table . '.' . $searchColumn,
815
            $searchOperator,
816
            str_replace('{{search}}', $searchText, $searchValue)
817
        );
818
    }
819
}
820