Passed
Push — master ( 5b5c8d...b30c75 )
by Michael
02:46
created

RelatedPlusTrait::addRelatedWhereConstraints()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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