Passed
Push — master ( 479ccf...5dcb31 )
by Michael
02:29
created

JoinsTrait::getRelationJoin()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 8
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 3
nc 2
nop 5
dl 0
loc 8
rs 9.4285
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\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
14
/**
15
 * Trait JoinsTrait
16
 *
17
 * @property array order_fields
18
 * @property array order_defaults
19
 * @property array order_relations
20
 * @property array order_with
21
 * @property array search_fields
22
 * @property string connection
23
 */
24
trait JoinsTrait
25
{
26
    use HelperMethodTrait;
27
28
    /**
29
     * Check relation type and get join
30
     *
31
     * @param Relation $relation
32
     * @param JoinClause $join
33
     * @param \stdClass $table
34
     * @param string $operator
35
     * @param string|null $direction
36
     * @return Builder|JoinClause
37
     */
38
    protected function getRelationJoin($relation, $join, $table, $operator, $direction = null)
39
    {
40
        // If a HasOne relation and ordered - ie join to the latest/earliest
41
        if (class_basename($relation) === 'HasOne' && !empty($relation->toBase()->orders)) {
42
            return $this->hasOneJoin($relation, $join);
43
        }
44
45
        return $this->hasManyJoin($relation, $join, $table, $operator, $direction);
46
    }
47
48
    /**
49
     * Join a HasOne relation which is ordered
50
     *
51
     * @param Relation $relation
52
     * @param JoinClause $join
53
     * @return JoinClause
54
     */
55
    protected function hasOneJoin($relation, $join)
56
    {
57
        // Get first relation order (should only be one)
58
        $order = $relation->toBase()->orders[0];
59
60
        return $join->on($order['column'], $this->hasOneJoinSql($relation, $order));
61
    }
62
63
    /**
64
     * Get join sql for a HasOne relation
65
     *
66
     * @param Relation $relation
67
     * @param array $order
68
     * @return Expression
69
     */
70
    protected function hasOneJoinSql($relation, $order)
71
    {
72
        // Build subquery for getting first/last record in related table
73
        $subQuery = $this
74
            ->joinOne(
75
                $relation->getRelated()->newQuery(),
76
                $relation,
77
                $order['column'],
78
                $order['direction']
79
            )
80
            ->setBindings($relation->getBindings());
81
82
        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

82
        return DB::raw('(' . $this->toSqlWithBindings(/** @scrutinizer ignore-type */ $subQuery) . ')');
Loading history...
83
    }
84
85
    /**
86
     * Adds a where for a relation's join columns and and min/max for a given column
87
     *
88
     * @param Builder $query
89
     * @param Relation $relation
90
     * @param string $column
91
     * @param string $direction
92
     * @return Builder
93
     */
94
    protected function joinOne($query, $relation, $column, $direction)
95
    {
96
        // Get join fields
97
        $joinColumns = $this->getJoinColumns($relation);
98
99
        return $this->selectMinMax(
100
            $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

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