Completed
Push — master ( f63084...bb5576 )
by Michael
05:17
created

RelatedPlusTrait::scopeRelationJoin()   B

Complexity

Conditions 5
Paths 2

Size

Total Lines 31
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 31
rs 8.439
c 0
b 0
f 0
cc 5
eloc 24
nc 2
nop 8

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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;
30
31
    /**
32
     * Get the table associated with the model.
33
     *
34
     * @return string
35
     */
36
    abstract public function getTable();
37
38
    /**
39
     * Boot method for trait
40
     *
41
     */
42
    public static function bootRelatedPlusTrait()
43
    {
44
        static::saving(function ($model) {
45
            if (!empty($model->nullable)) {
46
                foreach ($model->attributes as $key => $value) {
47
                    if (isset($model->nullable[$key])) {
48
                        $model->{$key} = empty(trim($value)) ? null : $value;
49
                    }
50
                }
51
            }
52
        });
53
    }
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
        $connection = $this->connection;
82
83
        foreach ($this->parseRelationNames($relationName) as $relation) {
84
            $tableName = $relation->getRelated()->getTable();
85
            // if using a 'table' AS 'tableAlias' in a from statement, otherwise alias will be the table name
86
            $from = explode(' ', $relation->getQuery()->getQuery()->from);
87
            $tableAlias = array_pop($from);
88
89
            /** @var Model $query */
90
            if (empty($query->getQuery()->columns)) {
91
                $query->select($this->getTable() . ".*");
92
            }
93
            if ($relatedSelect) {
94
                foreach (Schema::connection($connection)->getColumnListing($tableName) as $relatedColumn) {
95
                    $query->addSelect(
96
                        new Expression("`$tableAlias`.`$relatedColumn` AS `$tableAlias.$relatedColumn`")
97
                    );
98
                }
99
            }
100
            $query->relationJoin($tableName, $tableAlias, $relation, $operator, $type, $where, $direction);
101
        }
102
103
        return $query;
104
    }
105
106
    /**
107
     * Join a model
108
     *
109
     * @param Builder $query
110
     * @param string $tableName
111
     * @param string $tableAlias
112
     * @param Relation $relation
113
     * @param string $operator
114
     * @param string $type
115
     * @param boolean $where
116
     * @param null $direction
117
     * @return Builder
118
     */
119
    public function scopeRelationJoin(
120
        Builder $query,
121
        $tableName,
122
        $tableAlias,
123
        $relation,
124
        $operator,
125
        $type,
126
        $where,
127
        $direction = null
128
    ) {
129
        if ($tableAlias !== '' && $tableName !== $tableAlias) {
130
            $fullTableName = $tableName . ' AS ' . $tableAlias;
131
        } else {
132
            $fullTableName = $tableName;
133
        }
134
135
        return $query->join($fullTableName, function (JoinClause $join) use (
136
            $tableName,
137
            $tableAlias,
138
            $relation,
139
            $operator,
140
            $direction
141
        ) {
142
            // If a HasOne relation and ordered - ie join to the latest/earliest
143
            if (class_basename($relation) === 'HasOne' && !empty($relation->toBase()->orders)) {
144
                return $this->hasOneJoin($relation, $join);
145
            } else {
146
                return $this->hasManyJoin($relation, $join, $tableName, $tableAlias, $operator, $direction);
147
            }
148
        }, null, null, $type, $where);
149
    }
150
151
    /**
152
     * Join a HasOne relation which is ordered
153
     *
154
     * @param Relation $relation
155
     * @param JoinClause $join
156
     * @return JoinClause
157
     */
158
    private function hasOneJoin($relation, $join)
159
    {
160
        // Get first relation order (should only be one)
161
        $order = $relation->toBase()->orders[0];
162
163
        return $join->on($order['column'], $this->hasOneJoinSql($relation, $order));
164
    }
165
166
    /**
167
     * Get join sql for a HasOne relation
168
     *
169
     * @param Relation $relation
170
     * @param array $order
171
     * @return Expression
172
     */
173
    public function hasOneJoinSql($relation, $order)
174
    {
175
        // Build subquery for getting first/last record in related table
176
        $subQuery = $this
177
            ->joinOne(
178
                $relation->getRelated()->newQuery(),
179
                $relation,
180
                $order['column'],
181
                $order['direction']
182
            )
183
            ->setBindings($relation->getBindings());
184
185
        return DB::raw('(' . $this->toSqlWithBindings($subQuery) . ')');
186
    }
187
188
    /**
189
     * Adds a where for a relation's join columns and and min/max for a given column
190
     *
191
     * @param Builder $query
192
     * @param Relation $relation
193
     * @param string $column
194
     * @param string $direction
195
     * @return Builder
196
     */
197
    public function joinOne($query, $relation, $column, $direction)
198
    {
199
        // Get join fields
200
        $joinColumns = $this->getJoinColumns($relation);
201
202
        return $this->selectMinMax(
203
            $query->whereColumn($joinColumns->first, '=', $joinColumns->second),
204
            $column,
205
            $direction
206
        );
207
    }
208
209
    /**
210
     * Get the join columns for a relation
211
     *
212
     * @param Relation|BelongsTo|HasOneOrMany $relation
213
     * @return \stdClass
214
     */
215
    protected function getJoinColumns($relation)
216
    {
217
        // Get keys with table names
218
        if ($relation instanceof BelongsTo) {
219
            $first = $relation->getOwnerKey();
220
            $second = $relation->getForeignKey();
221
        } else {
222
            $first = $relation->getQualifiedParentKeyName();
223
            $second = $relation->getQualifiedForeignKeyName();
224
        }
225
226
        return (object)['first' => $first, 'second' => $second];
227
    }
228
229
    /**
230
     * Adds a select for a min or max on the given column, depending on direction given
231
     *
232
     * @param Builder $query
233
     * @param string $column
234
     * @param string $direction
235
     * @return Builder
236
     */
237
    public function selectMinMax($query, $column, $direction)
238
    {
239
        $column = $this->addBackticks($column);
240
241
        /** @var Model $query */
242
        if ($direction == 'asc') {
243
            return $query->select(DB::raw('MIN(' . $column . ')'));
244
        } else {
245
            return $query->select(DB::raw('MAX(' . $column . ')'));
246
        }
247
    }
248
249
    /**
250
     * Join a HasMany Relation
251
     *
252
     * @param Relation $relation
253
     * @param JoinClause $join
254
     * @param string $tableName
255
     * @param string $tableAlias
256
     * @param string $operator
257
     * @param string $direction
258
     * @return Builder|JoinClause
259
     */
260
    private function hasManyJoin($relation, $join, $tableName, $tableAlias, $operator, $direction)
261
    {
262
        // Get relation join columns
263
        $joinColumns = $this->getJoinColumns($relation);
264
265
        $first = $joinColumns->first;
266
        $second = $joinColumns->second;
267
        if ($tableName !== $tableAlias) {
268
            $first = str_replace($tableName, $tableAlias, $first);
269
            $second = str_replace($tableName, $tableAlias, $second);
270
        }
271
272
        $join->on($first, $operator, $second);
273
274
        // Add any where clauses from the relationship
275
        $join = $this->addWhereConstraints($join, $relation, $tableAlias);
276
277
        if (!is_null($direction) && get_class($relation) === HasMany::class) {
278
            $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...
279
        }
280
281
        return $join;
282
    }
283
284
    /**
285
     * Add wheres if they exist for a relation
286
     *
287
     * @param Builder|JoinClause $builder
288
     * @param Relation|BelongsTo|HasOneOrMany $relation
289
     * @param string $table
290
     * @return Builder|JoinClause $builder
291
     */
292
    protected function addWhereConstraints($builder, $relation, $table)
293
    {
294
        // Get where clauses from the relationship
295
        $wheres = collect($relation->toBase()->wheres)
296
            ->where('type', 'Basic')
297
            ->map(function ($where) use ($table) {
298
                // Add table name to column if it is absent
299
                return [$this->columnWithTableName($table, $where['column']), $where['operator'], $where['value']];
300
            })->toArray();
301
302
        if (!empty($wheres)) {
303
            $builder->where($wheres);
304
        }
305
306
        return $builder;
307
    }
308
309
    /**
310
     * If the relation is one-to-many, just get the first related record
311
     *
312
     * @param JoinClause $joinClause
313
     * @param string $column
314
     * @param HasMany|Relation $relation
315
     * @param string $table
316
     * @param string $direction
317
     *
318
     * @return JoinClause
319
     */
320
    public function hasManyJoinWhere(JoinClause $joinClause, $column, $relation, $table, $direction)
321
    {
322
        return $joinClause->where(
323
            $column,
324
            function ($subQuery) use ($table, $direction, $relation, $column) {
325
                $subQuery = $this->joinOne(
326
                    $subQuery->from($table),
327
                    $relation,
328
                    $column,
329
                    $direction
330
                );
331
332
                // Add any where statements with the relationship
333
                $subQuery = $this->addWhereConstraints($subQuery, $relation, $table);
334
335
                // Add any order statements with the relationship
336
                return $this->addOrder($subQuery, $relation, $table);
337
            }
338
        );
339
    }
340
341
    /**
342
     * Add orderBy if orders exist for a relation
343
     *
344
     * @param Builder|JoinClause $builder
345
     * @param Relation|BelongsTo|HasOneOrMany $relation
346
     * @param string $table
347
     * @return Builder
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use Model.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
348
     */
349
    protected function addOrder($builder, $relation, $table)
350
    {
351
        /** @var Model $builder */
352
        if (!empty($relation->toBase()->orders)) {
353
            // Get where clauses from the relationship
354
            foreach ($relation->toBase()->orders as $order) {
355
                $builder->orderBy($this->columnWithTableName($table, $order['column']), $order['direction']);
356
            }
357
        }
358
359
        return $builder;
360
    }
361
362
    /**
363
     * Set the order of a model
364
     *
365
     * @param Builder $query
366
     * @param string $orderField
367
     * @param string $direction
368
     * @return Builder
369
     */
370
    public function scopeOrderByCustom(Builder $query, $orderField, $direction)
371
    {
372
        if ($this->hasFieldsAndDefaults($orderField, $direction)) {
373
            $query = $this->removeGlobalScope($query, 'order');
374
        }
375
376
        return $query->setCustomOrder($orderField, $direction);
377
    }
378
379
    /**
380
     * Check $order_fields and $order_defaults are set
381
     *
382
     * @param string $orderField
383
     * @param string $direction
384
     * @return bool
385
     */
386
    private function hasFieldsAndDefaults($orderField, $direction)
387
    {
388
        if (!isset($this->order_fields) || !is_array($this->order_fields)) {
389
            throw new InvalidArgumentException(get_class($this) . ' order fields not set correctly.');
390
        } else {
391
            if (($orderField === '' || $direction === '')
392
                && (!isset($this->order_defaults) || !is_array($this->order_defaults))) {
393
                throw new InvalidArgumentException(get_class($this) . ' order defaults not set and not overriden.');
394
            } else {
395
                return true;
396
            }
397
        }
398
    }
399
400
    /**
401
     * Join a related table if not already joined
402
     *
403
     * @param Builder $query
404
     * @param string $table
405
     * @return Builder
406
     */
407
    private function joinRelatedTable($query, $table)
408
    {
409
        if (isset($this->order_relations[$table]) &&
410
            !$this->hasJoin($query, $table, $this->order_relations[$table])) {
411
            $columnRelations = $this->order_relations[$table];
412
413
            $query->modelJoin(
414
                $columnRelations,
415
                '=',
416
                'left',
417
                false,
418
                false
419
            );
420
        }
421
422
        return $query;
423
    }
424
425
    /**
426
     * Switch a query to be a subquery of a model
427
     *
428
     * @param Builder $query
429
     * @param Builder $model
430
     * @return Builder
431
     */
432
    public function scopeSetSubquery(Builder $query, $model)
433
    {
434
        $sql = $this->toSqlWithBindings($model);
435
        $table = $model->getQuery()->from;
436
437
        return $query
438
            ->from(DB::raw("({$sql}) as " . $table))
439
            ->select($table . '.*');
440
    }
441
442
    /**
443
     * Use a model method to add columns or joins if in the order options
444
     *
445
     * @param Builder $query
446
     * @param string $order
447
     * @return Builder
448
     */
449
    public function scopeOrderByWith(Builder $query, $order)
450
    {
451
        if (isset($this->order_with[$order])) {
452
            $with = 'with' . $this->order_with[$order];
453
454
            $query->$with();
455
        }
456
457
        if (isset($this->order_fields[$order])) {
458
            $orderOption = (explode('.', $this->order_fields[$order]))[0];
459
460
            if (isset($this->order_relations[$orderOption])) {
461
                $query->modelJoin(
462
                    $this->order_relations[$orderOption],
463
                    '=',
464
                    'left',
465
                    false,
466
                    false
467
                );
468
            }
469
        }
470
471
        return $query;
472
    }
473
474
    /**
475
     * Add where statements for the model search fields
476
     *
477
     * @param Builder $query
478
     * @param string $searchText
479
     * @return Builder
480
     */
481
    public function scopeSearch(Builder $query, $searchText = '')
482
    {
483
        $searchText = trim($searchText);
484
485
        // If search is set
486
        if ($searchText != "") {
487
            if (!isset($this->search_fields) || !is_array($this->search_fields) || empty($this->search_fields)) {
488
                throw new InvalidArgumentException(get_class($this) . ' search properties not set correctly.');
489
            } else {
490
                $query = $this->checkSearchFields($query, $searchText);
491
            }
492
        }
493
494
        return $query;
495
    }
496
497
    /**
498
     * Add where statements for search fields to search for searchText
499
     *
500
     * @param Builder $query
501
     * @param string $searchText
502
     * @return Builder
503
     */
504
    private function checkSearchFields($query, $searchText)
505
    {
506
        return $query->where(function (Builder $query) use ($searchText) {
507
            if (isset($this->search_fields) && !empty($this->search_fields)) {
508
                /** @var Model $this */
509
                $table = $this->getTable();
510
                foreach ($this->search_fields as $searchField => $searchFieldParameters) {
511
                    $query = $this->checkSearchField($query, $table, $searchField, $searchFieldParameters, $searchText);
512
                }
513
            }
514
515
            return $query;
516
        });
517
    }
518
519
    /**
520
     * Add where statement for a search field
521
     *
522
     * @param Builder $query
523
     * @param string $table
524
     * @param string $searchField
525
     * @param array $searchFieldParameters
526
     * @param string $searchText
527
     * @return Builder
528
     */
529
    private function checkSearchField($query, $table, $searchField, $searchFieldParameters, $searchText)
530
    {
531
        if (!isset($searchFieldParameters['regex']) || preg_match($searchFieldParameters['regex'], $searchText)) {
532
            $searchColumn = is_array($searchFieldParameters) ? $searchField : $searchFieldParameters;
533
534
            if (isset($searchFieldParameters['relation'])) {
535
                return $this->searchRelation($query, $searchFieldParameters, $searchColumn, $searchText);
536
            } else {
537
                return $this->searchThis($query, $searchFieldParameters, $table, $searchColumn, $searchText);
538
            }
539
        } else {
540
            return $query;
541
        }
542
    }
543
544
    /**
545
     * Add where condition to search a relation
546
     *
547
     * @param Builder $query
548
     * @param array $searchFieldParameters
549
     * @param string $searchColumn
550
     * @param string $searchText
551
     * @return Builder
552
     */
553
    private function searchRelation(Builder $query, $searchFieldParameters, $searchColumn, $searchText)
554
    {
555
        $relation = $searchFieldParameters['relation'];
556
        $relatedTable = $this->$relation()->getRelated()->getTable();
557
558
        return $query->orWhere(function (Builder $query) use (
559
            $searchText,
560
            $searchColumn,
561
            $searchFieldParameters,
562
            $relation,
563
            $relatedTable
564
        ) {
565
            return $query->orWhereHas($relation, function (Builder $query2) use (
566
                $searchText,
567
                $searchColumn,
568
                $searchFieldParameters,
569
                $relatedTable
570
            ) {
571
                return $query2->where($relatedTable . '.' . $searchColumn, 'like', $searchText . '%');
572
            });
573
        });
574
    }
575
576
    /**
577
     * Add where condition to search current model
578
     *
579
     * @param Builder $query
580
     * @param array $searchFieldParameters
581
     * @param string $table
582
     * @param string $searchColumn
583
     * @param string $searchText
584
     * @return Builder
585
     */
586
    public function searchThis(Builder $query, $searchFieldParameters, $table, $searchColumn, $searchText)
587
    {
588
        $searchOperator = $searchFieldParameters['operator'] ?? 'like';
589
        $searchValue = $searchFieldParameters['value'] ?? '%{{search}}%';
590
591
        return $query->orWhere(
592
            $table . '.' . $searchColumn,
593
            $searchOperator,
594
            str_replace('{{search}}', $searchText, $searchValue)
595
        );
596
    }
597
}
598