Passed
Push — master ( a914c5...479ccf )
by Michael
02:27
created

JoinsTrait::selectMinMax()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 3
dl 0
loc 9
rs 9.6666
c 0
b 0
f 0
1
<?php
2
3
namespace Blasttech\EloquentRelatedPlus;
4
5
use DB;
6
use Illuminate\Database\Eloquent\Builder;
7
use Illuminate\Database\Eloquent\Model;
8
use Illuminate\Database\Eloquent\Relations\BelongsTo;
9
use Illuminate\Database\Eloquent\Relations\HasMany;
10
use Illuminate\Database\Eloquent\Relations\HasOneOrMany;
11
use Illuminate\Database\Eloquent\Relations\Relation;
12
use Illuminate\Database\Query\Expression;
13
use Illuminate\Database\Query\JoinClause;
14
15
/**
16
 * Trait JoinsTrait
17
 *
18
 * @property array order_fields
19
 * @property array order_defaults
20
 * @property array order_relations
21
 * @property array order_with
22
 * @property array search_fields
23
 * @property string connection
24
 */
25
trait JoinsTrait
26
{
27
    use HelperMethodTrait;
28
29
    /**
30
     * Check relation type and join
31
     *
32
     * @param Relation $relation
33
     * @param JoinClause $join
34
     * @param \stdClass $table
35
     * @param string $operator
36
     * @param string|null $direction
37
     * @return Builder|JoinClause
38
     */
39
    protected function relationJoinType($relation, $join, $table, $operator, $direction = null)
40
    {
41
        // If a HasOne relation and ordered - ie join to the latest/earliest
42
        if (class_basename($relation) === 'HasOne' && !empty($relation->toBase()->orders)) {
43
            return $this->hasOneJoin($relation, $join);
44
        } else {
45
            return $this->hasManyJoin($relation, $join, $table, $operator, $direction);
46
        }
47
    }
48
49
    /**
50
     * Join a HasOne relation which is ordered
51
     *
52
     * @param Relation $relation
53
     * @param JoinClause $join
54
     * @return JoinClause
55
     */
56
    protected function hasOneJoin($relation, $join)
57
    {
58
        // Get first relation order (should only be one)
59
        $order = $relation->toBase()->orders[0];
60
61
        return $join->on($order['column'], $this->hasOneJoinSql($relation, $order));
62
    }
63
64
    /**
65
     * Get join sql for a HasOne relation
66
     *
67
     * @param Relation $relation
68
     * @param array $order
69
     * @return Expression
70
     */
71
    protected function hasOneJoinSql($relation, $order)
72
    {
73
        // Build subquery for getting first/last record in related table
74
        $subQuery = $this
75
            ->joinOne(
76
                $relation->getRelated()->newQuery(),
77
                $relation,
78
                $order['column'],
79
                $order['direction']
80
            )
81
            ->setBindings($relation->getBindings());
82
83
        return DB::raw('(' . $this->toSqlWithBindings($subQuery) . ')');
0 ignored issues
show
Bug introduced by
It seems like $subQuery can also be of type Illuminate\Database\Query\Builder; however, parameter $builder of Blasttech\EloquentRelate...it::toSqlWithBindings() does only seem to accept Illuminate\Database\Eloquent\Builder, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

83
        return DB::raw('(' . $this->toSqlWithBindings(/** @scrutinizer ignore-type */ $subQuery) . ')');
Loading history...
84
    }
85
86
    /**
87
     * Adds a where for a relation's join columns and and min/max for a given column
88
     *
89
     * @param Builder $query
90
     * @param Relation $relation
91
     * @param string $column
92
     * @param string $direction
93
     * @return Builder
94
     */
95
    protected function joinOne($query, $relation, $column, $direction)
96
    {
97
        // Get join fields
98
        $joinColumns = $this->getJoinColumns($relation);
99
100
        return $this->selectMinMax(
101
            $query->whereColumn($joinColumns->first, '=', $joinColumns->second),
0 ignored issues
show
Bug introduced by
It seems like $query->whereColumn($joi..., $joinColumns->second) can also be of type Illuminate\Database\Query\Builder; however, parameter $query of Blasttech\EloquentRelate...nsTrait::selectMinMax() does only seem to accept Illuminate\Database\Eloquent\Builder, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

101
            /** @scrutinizer ignore-type */ $query->whereColumn($joinColumns->first, '=', $joinColumns->second),
Loading history...
102
            $column,
103
            $direction
104
        );
105
    }
106
107
    /**
108
     * Get the join columns for a relation
109
     *
110
     * @param Relation|BelongsTo|HasOneOrMany $relation
111
     * @return \stdClass
112
     */
113
    protected function getJoinColumns($relation)
114
    {
115
        // Get keys with table names
116
        if ($relation instanceof BelongsTo) {
117
            $first = $relation->getOwnerKey();
118
            $second = $relation->getForeignKey();
119
        } else {
120
            $first = $relation->getQualifiedParentKeyName();
121
            $second = $relation->getQualifiedForeignKeyName();
122
        }
123
124
        return (object)['first' => $first, 'second' => $second];
125
    }
126
127
    /**
128
     * Adds a select for a min or max on the given column, depending on direction given
129
     *
130
     * @param Builder $query
131
     * @param string $column
132
     * @param string $direction
133
     * @return Builder
134
     */
135
    protected function selectMinMax($query, $column, $direction)
136
    {
137
        $column = $this->addBackticks($column);
138
139
        /** @var Model $query */
140
        if ($direction == 'asc') {
141
            return $query->select(DB::raw('MIN(' . $column . ')'));
0 ignored issues
show
Bug Best Practice introduced by
The expression return $query->select(DB...MIN(' . $column . ')')) also could return the type Illuminate\Database\Query\Builder which is incompatible with the documented return type Illuminate\Database\Eloquent\Builder.
Loading history...
142
        } else {
143
            return $query->select(DB::raw('MAX(' . $column . ')'));
0 ignored issues
show
Bug Best Practice introduced by
The expression return $query->select(DB...MAX(' . $column . ')')) also could return the type Illuminate\Database\Query\Builder which is incompatible with the documented return type Illuminate\Database\Eloquent\Builder.
Loading history...
144
        }
145
    }
146
147
    /**
148
     * Join a HasMany Relation
149
     *
150
     * @param Relation $relation
151
     * @param JoinClause $join
152
     * @param \stdClass $table
153
     * @param string $operator
154
     * @param string $direction
155
     * @return Builder|JoinClause
156
     */
157
    protected function hasManyJoin($relation, $join, $table, $operator, $direction)
158
    {
159
        // Get relation join columns
160
        $joinColumns = $this->getJoinColumns($relation);
161
        $joinColumns = $this->replaceColumnTables($joinColumns, $table);
162
163
        $join->on($joinColumns->first, $operator, $joinColumns->second);
164
165
        // Add any where clauses from the relationship
166
        $join = $this->addRelatedWhereConstraints($join, $relation, $table->alias);
167
168
        if (!is_null($direction) && get_class($relation) === HasMany::class) {
169
            $join = $this->hasManyJoinWhere($join, $joinColumns->first, $relation, $table->alias, $direction);
170
        }
171
172
        return $join;
173
    }
174
175
    /**
176
     * Replace column table names with aliases
177
     *
178
     * @param \stdClass $joinColumns
179
     * @param \stdClass $table
180
     * @return \stdClass
181
     */
182
    protected function replaceColumnTables($joinColumns, $table)
183
    {
184
        if ($table->name !== $table->alias) {
185
            $joinColumns->first = str_replace($table->name, $table->alias, $joinColumns->first);
186
            $joinColumns->second = str_replace($table->name, $table->alias, $joinColumns->second);
187
        }
188
189
        return $joinColumns;
190
    }
191
192
    /**
193
     * Add wheres if they exist for a relation
194
     *
195
     * @param Builder|JoinClause $builder
196
     * @param Relation|BelongsTo|HasOneOrMany $relation
197
     * @param string $table
198
     * @return Builder|JoinClause $builder
199
     */
200
    protected function addRelatedWhereConstraints($builder, $relation, $table)
201
    {
202
        // Get where clauses from the relationship
203
        $wheres = collect($relation->toBase()->wheres)
204
            ->where('type', 'Basic')
205
            ->map(function ($where) use ($table) {
206
                // Add table name to column if it is absent
207
                return [$this->columnWithTableName($table, $where['column']), $where['operator'], $where['value']];
208
            })->toArray();
209
210
        if (!empty($wheres)) {
211
            $builder->where($wheres);
212
        }
213
214
        return $builder;
215
    }
216
217
    /**
218
     * If the relation is one-to-many, just get the first related record
219
     *
220
     * @param JoinClause $joinClause
221
     * @param string $column
222
     * @param HasMany|Relation $relation
223
     * @param string $table
224
     * @param string $direction
225
     *
226
     * @return JoinClause
227
     */
228
    public function hasManyJoinWhere(JoinClause $joinClause, $column, $relation, $table, $direction)
229
    {
230
        return $joinClause->where(
231
            $column,
232
            function ($subQuery) use ($table, $direction, $relation, $column) {
233
                $subQuery = $this->joinOne(
234
                    $subQuery->from($table),
235
                    $relation,
236
                    $column,
237
                    $direction
238
                );
239
240
                // Add any where statements with the relationship
241
                $subQuery = $this->addRelatedWhereConstraints($subQuery, $relation, $table);
242
243
                // Add any order statements with the relationship
244
                return $this->addOrder($subQuery, $relation, $table);
245
            }
246
        );
247
    }
248
249
    /**
250
     * Add orderBy if orders exist for a relation
251
     *
252
     * @param Builder|JoinClause $builder
253
     * @param Relation|BelongsTo|HasOneOrMany $relation
254
     * @param string $table
255
     * @return Builder|JoinClause $builder
256
     */
257
    protected function addOrder($builder, $relation, $table)
258
    {
259
        if (!empty($relation->toBase()->orders)) {
260
            // Get where clauses from the relationship
261
            foreach ($relation->toBase()->orders as $order) {
262
                $builder->orderBy($this->columnWithTableName($table, $order['column']), $order['direction']);
263
            }
264
        }
265
266
        return $builder;
267
    }
268
}
269