Passed
Push — master ( 6d865c...4bd2dc )
by Michael
02:34
created

RelatedPlusTrait::scopeModelJoin()   B

Complexity

Conditions 4
Paths 5

Size

Total Lines 24
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

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