Passed
Push — master ( 4bd2dc...6390fb )
by Michael
02:32
created

RelatedPlusTrait   B

Complexity

Total Complexity 48

Size/Duplication

Total Lines 477
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 12

Importance

Changes 0
Metric Value
wmc 48
lcom 2
cbo 12
dl 0
loc 477
rs 8.4864
c 0
b 0
f 0

22 Methods

Rating   Name   Duplication   Size   Complexity  
A hasManyJoinWhere() 0 20 1
A bootRelatedPlusTrait() 0 9 2
A setAttributesNull() 0 8 4
getTable() 0 1 ?
B scopeModelJoin() 0 24 4
A selectRelated() 0 12 2
A scopeOrderByCustom() 0 8 2
A scopeOrderByWith() 0 12 3
A addOrderWith() 0 6 1
A addOrderJoin() 0 16 2
B scopeRelationJoin() 0 25 3
A hasOneJoin() 0 7 1
A hasOneJoinSql() 0 14 1
A joinOne() 0 11 1
A selectMinMax() 0 11 2
B hasManyJoin() 0 23 4
A addRelatedWhereConstraints() 0 16 2
A addOrder() 0 12 3
A scopeSearch() 0 11 3
A scopeSetSubquery() 0 9 1
A scopeSetCustomOrder() 0 9 2
A scopeOrderByCheckModel() 0 12 4

How to fix   Complexity   

Complex Class

Complex classes like RelatedPlusTrait often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use RelatedPlusTrait, and based on these observations, apply Extract Interface, too.

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
16
/**
17
 * Trait RelatedPlusTrait
18
 *
19
 * @property array order_fields
20
 * @property array order_defaults
21
 * @property array order_relations
22
 * @property array order_with
23
 * @property array search_fields
24
 * @property string connection
25
 */
26
trait RelatedPlusTrait
27
{
28
    use CustomOrderTrait, HelperMethodTrait, SearchTrait;
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
                /* @var \Illuminate\Database\Eloquent\Model|static $model */
39
                $model->setAttributesNull();
0 ignored issues
show
Bug introduced by
The method setAttributesNull does only exist in Blasttech\EloquentRelatedPlus\RelatedPlusTrait, but not in Illuminate\Database\Eloquent\Model.

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