Passed
Push — master ( 9ba3b5...c4bcc6 )
by Michael
02:46
created

RelatedPlusTrait::selectMinMax()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 11
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 6
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;
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