Passed
Push — master ( 9ba3b5...c4bcc6 )
by Michael
02:46
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
     * Add orderBy if orders exist for a relation
33
     *
34
     * @param Builder|JoinClause $builder
35
     * @param Relation|BelongsTo|HasOneOrMany $relation
36
     * @param string $table
37
     * @return Builder|JoinClause $builder
38
     */
39
    protected function addOrder($builder, $relation, $table)
40
    {
41
        /** @var Model $builder */
42
        if (!empty($relation->toBase()->orders)) {
43
            // Get where clauses from the relationship
44
            foreach ($relation->toBase()->orders as $order) {
45
                $builder->orderBy($this->columnWithTableName($table, $order['column']), $order['direction']);
46
            }
47
        }
48
49
        return $builder;
50
    }
51
52
    /**
53
     * Add wheres if they exist for a relation
54
     *
55
     * @param Builder|JoinClause $builder
56
     * @param Relation|BelongsTo|HasOneOrMany $relation
57
     * @param string $table
58
     * @return Builder|JoinClause $builder
59
     */
60
    protected function addWhereConstraints($builder, $relation, $table)
61
    {
62
        // Get where clauses from the relationship
63
        $wheres = collect($relation->toBase()->wheres)
64
            ->where('type', 'Basic')
65
            ->map(function ($where) use ($table) {
66
                // Add table name to column if it is absent
67
                return [$this->columnWithTableName($table, $where['column']), $where['operator'], $where['value']];
68
            })->toArray();
69
70
        if (!empty($wheres)) {
71
            $builder->where($wheres);
72
        }
73
74
        return $builder;
75
    }
76
77
    /**
78
     * Boot method for trait
79
     *
80
     */
81
    public static function bootRelatedPlusTrait()
82
    {
83
        static::saving(function ($model) {
84
            if (!empty($model->nullable)) {
85
                foreach ($model->attributes as $key => $value) {
86
                    if (isset($model->nullable[$key])) {
87
                        $model->{$key} = empty(trim($value)) ? null : $value;
88
                    }
89
                }
90
            }
91
        });
92
    }
93
94
    /**
95
     * Get the table associated with the model.
96
     *
97
     * @return string
98
     */
99
    abstract public function getTable();
100
101
    /**
102
     * If the relation is one-to-many, just get the first related record
103
     *
104
     * @param JoinClause $joinClause
105
     * @param string $column
106
     * @param HasMany|Relation $relation
107
     * @param string $table
108
     * @param string $direction
109
     *
110
     * @return JoinClause
111
     */
112
    public function hasManyJoinWhere(JoinClause $joinClause, $column, $relation, $table, $direction)
113
    {
114
        return $joinClause->where(
115
            $column,
116
            function ($subQuery) use ($table, $direction, $relation, $column) {
117
                $subQuery = $this->joinOne(
118
                    $subQuery->from($table),
119
                    $relation,
120
                    $column,
121
                    $direction
122
                );
123
124
                // Add any where statements with the relationship
125
                $subQuery = $this->addWhereConstraints($subQuery, $relation, $table);
126
127
                // Add any order statements with the relationship
128
                return $this->addOrder($subQuery, $relation, $table);
129
            }
130
        );
131
    }
132
133
    /**
134
     * Get join sql for a HasOne relation
135
     *
136
     * @param Relation $relation
137
     * @param array $order
138
     * @return Expression
139
     */
140
    public function hasOneJoinSql($relation, $order)
141
    {
142
        // Build subquery for getting first/last record in related table
143
        $subQuery = $this
144
            ->joinOne(
145
                $relation->getRelated()->newQuery(),
146
                $relation,
147
                $order['column'],
148
                $order['direction']
149
            )
150
            ->setBindings($relation->getBindings());
151
152
        return DB::raw('(' . $this->toSqlWithBindings($subQuery) . ')');
153
    }
154
155
    /**
156
     * Adds a where for a relation's join columns and and min/max for a given column
157
     *
158
     * @param Builder $query
159
     * @param Relation $relation
160
     * @param string $column
161
     * @param string $direction
162
     * @return Builder
163
     */
164
    public function joinOne($query, $relation, $column, $direction)
165
    {
166
        // Get join fields
167
        $joinColumns = $this->getJoinColumns($relation);
168
169
        return $this->selectMinMax(
170
            $query->whereColumn($joinColumns->first, '=', $joinColumns->second),
171
            $column,
172
            $direction
173
        );
174
    }
175
176
    /**
177
     * Add joins for one or more relations
178
     * This determines the foreign key relations automatically to prevent the need to figure out the columns.
179
     * Usages:
180
     * $query->modelJoin('customers')
181
     * $query->modelJoin('customer.client')
182
     *
183
     * @param Builder $query
184
     * @param string $relationName
185
     * @param string $operator
186
     * @param string $type
187
     * @param bool $where
188
     * @param bool $relatedSelect
189
     * @param string|null $direction
190
     *
191
     * @return Builder
192
     */
193
    public function scopeModelJoin(
194
        Builder $query,
195
        $relationName,
196
        $operator = '=',
197
        $type = 'left',
198
        $where = false,
199
        $relatedSelect = true,
200
        $direction = null
201
    ) {
202
        $connection = $this->connection;
203
204
        foreach ($this->parseRelationNames($relationName) as $relation) {
205
            $tableName = $relation->getRelated()->getTable();
206
            // if using a 'table' AS 'tableAlias' in a from statement, otherwise alias will be the table name
207
            $from = explode(' ', $relation->getQuery()->getQuery()->from);
208
            $tableAlias = array_pop($from);
209
210
            /** @var Model $query */
211
            if (empty($query->getQuery()->columns)) {
212
                $query->select($this->getTable() . ".*");
213
            }
214
            if ($relatedSelect) {
215
                foreach (Schema::connection($connection)->getColumnListing($tableName) as $relatedColumn) {
216
                    $query->addSelect(
217
                        new Expression("`$tableAlias`.`$relatedColumn` AS `$tableAlias.$relatedColumn`")
218
                    );
219
                }
220
            }
221
            $query->relationJoin($tableName, $tableAlias, $relation, $operator, $type, $where, $direction);
222
        }
223
224
        return $query;
225
    }
226
227
    /**
228
     * Set the order of a model
229
     *
230
     * @param Builder $query
231
     * @param string $orderField
232
     * @param string $direction
233
     * @return Builder
234
     */
235
    public function scopeOrderByCustom(Builder $query, $orderField, $direction)
236
    {
237
        if ($this->hasFieldsAndDefaults($orderField, $direction)) {
238
            $query = $this->removeGlobalScope($query, 'order');
239
        }
240
241
        return $query->setCustomOrder($orderField, $direction);
242
    }
243
244
    /**
245
     * Use a model method to add columns or joins if in the order options
246
     *
247
     * @param Builder $query
248
     * @param string $order
249
     * @return Builder
250
     */
251
    public function scopeOrderByWith(Builder $query, $order)
252
    {
253
        if (isset($this->order_with[$order])) {
254
            $with = 'with' . $this->order_with[$order];
255
256
            $query->$with();
257
        }
258
259
        if (isset($this->order_fields[$order])) {
260
            $orderOption = (explode('.', $this->order_fields[$order]))[0];
261
262
            if (isset($this->order_relations[$orderOption])) {
263
                $query->modelJoin(
264
                    $this->order_relations[$orderOption],
265
                    '=',
266
                    'left',
267
                    false,
268
                    false
269
                );
270
            }
271
        }
272
273
        return $query;
274
    }
275
276
    /**
277
     * Join a model
278
     *
279
     * @param Builder $query
280
     * @param string $tableName
281
     * @param string $tableAlias
282
     * @param Relation $relation
283
     * @param string $operator
284
     * @param string $type
285
     * @param boolean $where
286
     * @param null $direction
287
     * @return Builder
288
     */
289
    public function scopeRelationJoin(
290
        Builder $query,
291
        $tableName,
292
        $tableAlias,
293
        $relation,
294
        $operator,
295
        $type,
296
        $where,
297
        $direction = null
298
    ) {
299
        if ($tableAlias !== '' && $tableName !== $tableAlias) {
300
            $fullTableName = $tableName . ' AS ' . $tableAlias;
301
        } else {
302
            $fullTableName = $tableName;
303
        }
304
305
        return $query->join($fullTableName, function (JoinClause $join) use (
306
            $tableName,
307
            $tableAlias,
308
            $relation,
309
            $operator,
310
            $direction
311
        ) {
312
            // If a HasOne relation and ordered - ie join to the latest/earliest
313
            if (class_basename($relation) === 'HasOne' && !empty($relation->toBase()->orders)) {
314
                return $this->hasOneJoin($relation, $join);
315
            } else {
316
                return $this->hasManyJoin($relation, $join, $tableName, $tableAlias, $operator, $direction);
317
            }
318
        }, null, null, $type, $where);
319
    }
320
321
    /**
322
     * Add where statements for the model search fields
323
     *
324
     * @param Builder $query
325
     * @param string $searchText
326
     * @return Builder
327
     */
328
    public function scopeSearch(Builder $query, $searchText = '')
329
    {
330
        $searchText = trim($searchText);
331
332
        // If search is set
333
        if ($searchText != "") {
334
            if (!isset($this->search_fields) || !is_array($this->search_fields) || empty($this->search_fields)) {
335
                throw new InvalidArgumentException(get_class($this) . ' search properties not set correctly.');
336
            } else {
337
                $query = $this->checkSearchFields($query, $searchText);
338
            }
339
        }
340
341
        return $query;
342
    }
343
344
    /**
345
     * Switch a query to be a subquery of a model
346
     *
347
     * @param Builder $query
348
     * @param Builder $model
349
     * @return Builder
350
     */
351
    public function scopeSetSubquery(Builder $query, $model)
352
    {
353
        $sql = $this->toSqlWithBindings($model);
354
        $table = $model->getQuery()->from;
355
356
        return $query
357
            ->from(DB::raw("({$sql}) as " . $table))
358
            ->select($table . '.*');
359
    }
360
361
    /**
362
     * Add where condition to search current model
363
     *
364
     * @param Builder $query
365
     * @param array $searchFieldParameters
366
     * @param string $table
367
     * @param string $searchColumn
368
     * @param string $searchText
369
     * @return Builder
370
     */
371
    public function searchThis(Builder $query, $searchFieldParameters, $table, $searchColumn, $searchText)
372
    {
373
        $searchOperator = $searchFieldParameters['operator'] ?? 'like';
374
        $searchValue = $searchFieldParameters['value'] ?? '%{{search}}%';
375
376
        return $query->orWhere(
377
            $table . '.' . $searchColumn,
378
            $searchOperator,
379
            str_replace('{{search}}', $searchText, $searchValue)
380
        );
381
    }
382
383
    /**
384
     * Adds a select for a min or max on the given column, depending on direction given
385
     *
386
     * @param Builder $query
387
     * @param string $column
388
     * @param string $direction
389
     * @return Builder
390
     */
391
    public function selectMinMax($query, $column, $direction)
392
    {
393
        $column = $this->addBackticks($column);
394
395
        /** @var Model $query */
396
        if ($direction == 'asc') {
397
            return $query->select(DB::raw('MIN(' . $column . ')'));
398
        } else {
399
            return $query->select(DB::raw('MAX(' . $column . ')'));
400
        }
401
    }
402
403
    /**
404
     * Join a HasOne relation which is ordered
405
     *
406
     * @param Relation $relation
407
     * @param JoinClause $join
408
     * @return JoinClause
409
     */
410
    private function hasOneJoin($relation, $join)
411
    {
412
        // Get first relation order (should only be one)
413
        $order = $relation->toBase()->orders[0];
414
415
        return $join->on($order['column'], $this->hasOneJoinSql($relation, $order));
416
    }
417
418
    /**
419
     * Join a HasMany Relation
420
     *
421
     * @param Relation $relation
422
     * @param JoinClause $join
423
     * @param string $tableName
424
     * @param string $tableAlias
425
     * @param string $operator
426
     * @param string $direction
427
     * @return Builder|JoinClause
428
     */
429
    private function hasManyJoin($relation, $join, $tableName, $tableAlias, $operator, $direction)
430
    {
431
        // Get relation join columns
432
        $joinColumns = $this->getJoinColumns($relation);
433
434
        $first = $joinColumns->first;
435
        $second = $joinColumns->second;
436
        if ($tableName !== $tableAlias) {
437
            $first = str_replace($tableName, $tableAlias, $first);
438
            $second = str_replace($tableName, $tableAlias, $second);
439
        }
440
441
        $join->on($first, $operator, $second);
442
443
        // Add any where clauses from the relationship
444
        $join = $this->addWhereConstraints($join, $relation, $tableAlias);
445
446
        if (!is_null($direction) && get_class($relation) === HasMany::class) {
447
            $join = $this->hasManyJoinWhere($join, $first, $relation, $tableAlias, $direction);
448
        }
449
450
        return $join;
451
    }
452
453
    /**
454
     * Check $order_fields and $order_defaults are set
455
     *
456
     * @param string $orderField
457
     * @param string $direction
458
     * @return bool
459
     */
460
    private function hasFieldsAndDefaults($orderField, $direction)
461
    {
462
        if (!isset($this->order_fields) || !is_array($this->order_fields)) {
463
            throw new InvalidArgumentException(get_class($this) . ' order fields not set correctly.');
464
        } else {
465
            if (($orderField === '' || $direction === '')
466
                && (!isset($this->order_defaults) || !is_array($this->order_defaults))) {
467
                throw new InvalidArgumentException(get_class($this) . ' order defaults not set and not overriden.');
468
            } else {
469
                return true;
470
            }
471
        }
472
    }
473
474
    /**
475
     * Add where statements for search fields to search for searchText
476
     *
477
     * @param Builder $query
478
     * @param string $searchText
479
     * @return Builder
480
     */
481
    private function checkSearchFields($query, $searchText)
482
    {
483
        return $query->where(function (Builder $query) use ($searchText) {
484
            if (isset($this->search_fields) && !empty($this->search_fields)) {
485
                /** @var Model $this */
486
                $table = $this->getTable();
487
                foreach ($this->search_fields as $searchField => $searchFieldParameters) {
488
                    $query = $this->checkSearchField($query, $table, $searchField, $searchFieldParameters, $searchText);
489
                }
490
            }
491
492
            return $query;
493
        });
494
    }
495
496
    /**
497
     * Add where statement for a search field
498
     *
499
     * @param Builder $query
500
     * @param string $table
501
     * @param string $searchField
502
     * @param array $searchFieldParameters
503
     * @param string $searchText
504
     * @return Builder
505
     */
506
    private function checkSearchField($query, $table, $searchField, $searchFieldParameters, $searchText)
507
    {
508
        if (!isset($searchFieldParameters['regex']) || preg_match($searchFieldParameters['regex'], $searchText)) {
509
            $searchColumn = is_array($searchFieldParameters) ? $searchField : $searchFieldParameters;
510
511
            if (isset($searchFieldParameters['relation'])) {
512
                return $this->searchRelation($query, $searchFieldParameters, $searchColumn, $searchText);
513
            } else {
514
                return $this->searchThis($query, $searchFieldParameters, $table, $searchColumn, $searchText);
515
            }
516
        } else {
517
            return $query;
518
        }
519
    }
520
521
    /**
522
     * Add where condition to search a relation
523
     *
524
     * @param Builder $query
525
     * @param array $searchFieldParameters
526
     * @param string $searchColumn
527
     * @param string $searchText
528
     * @return Builder
529
     */
530
    private function searchRelation(Builder $query, $searchFieldParameters, $searchColumn, $searchText)
531
    {
532
        $relation = $searchFieldParameters['relation'];
533
        $relatedTable = $this->$relation()->getRelated()->getTable();
534
535
        return $query->orWhere(function (Builder $query) use (
536
            $searchText,
537
            $searchColumn,
538
            $searchFieldParameters,
539
            $relation,
540
            $relatedTable
541
        ) {
542
            return $query->orWhereHas($relation, function (Builder $query2) use (
543
                $searchText,
544
                $searchColumn,
545
                $searchFieldParameters,
546
                $relatedTable
547
            ) {
548
                return $query2->where($relatedTable . '.' . $searchColumn, 'like', $searchText . '%');
549
            });
550
        });
551
    }
552
}
553