Passed
Push — master ( 110432...a914c5 )
by Michael
02:54
created

JoinsTrait::addOrder()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 11
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 4
nc 2
nop 3
dl 0
loc 11
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\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(
0 ignored issues
show
Bug introduced by
It seems like selectMinMax() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

100
        return $this->/** @scrutinizer ignore-call */ selectMinMax(
Loading history...
101
            $query->whereColumn($joinColumns->first, '=', $joinColumns->second),
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
     * Join a HasMany Relation
129
     *
130
     * @param Relation $relation
131
     * @param JoinClause $join
132
     * @param \stdClass $table
133
     * @param string $operator
134
     * @param string $direction
135
     * @return Builder|JoinClause
136
     */
137
    protected function hasManyJoin($relation, $join, $table, $operator, $direction)
138
    {
139
        // Get relation join columns
140
        $joinColumns = $this->getJoinColumns($relation);
141
        $joinColumns = $this->replaceColumnTables($joinColumns, $table);
142
143
        $join->on($joinColumns->first, $operator, $joinColumns->second);
144
145
        // Add any where clauses from the relationship
146
        $join = $this->addRelatedWhereConstraints($join, $relation, $table->alias);
147
148
        if (!is_null($direction) && get_class($relation) === HasMany::class) {
149
            $join = $this->hasManyJoinWhere($join, $joinColumns->first, $relation, $table->alias, $direction);
150
        }
151
152
        return $join;
153
    }
154
155
    /**
156
     * Replace column table names with aliases
157
     *
158
     * @param \stdClass $joinColumns
159
     * @param \stdClass $table
160
     * @return \stdClass
161
     */
162
    protected function replaceColumnTables($joinColumns, $table)
163
    {
164
        if ($table->name !== $table->alias) {
165
            $joinColumns->first = str_replace($table->name, $table->alias, $joinColumns->first);
166
            $joinColumns->second = str_replace($table->name, $table->alias, $joinColumns->second);
167
        }
168
169
        return $joinColumns;
170
    }
171
172
    /**
173
     * Add wheres if they exist for a relation
174
     *
175
     * @param Builder|JoinClause $builder
176
     * @param Relation|BelongsTo|HasOneOrMany $relation
177
     * @param string $table
178
     * @return Builder|JoinClause $builder
179
     */
180
    protected function addRelatedWhereConstraints($builder, $relation, $table)
181
    {
182
        // Get where clauses from the relationship
183
        $wheres = collect($relation->toBase()->wheres)
184
            ->where('type', 'Basic')
185
            ->map(function ($where) use ($table) {
186
                // Add table name to column if it is absent
187
                return [$this->columnWithTableName($table, $where['column']), $where['operator'], $where['value']];
188
            })->toArray();
189
190
        if (!empty($wheres)) {
191
            $builder->where($wheres);
192
        }
193
194
        return $builder;
195
    }
196
197
    /**
198
     * If the relation is one-to-many, just get the first related record
199
     *
200
     * @param JoinClause $joinClause
201
     * @param string $column
202
     * @param HasMany|Relation $relation
203
     * @param string $table
204
     * @param string $direction
205
     *
206
     * @return JoinClause
207
     */
208
    public function hasManyJoinWhere(JoinClause $joinClause, $column, $relation, $table, $direction)
209
    {
210
        return $joinClause->where(
211
            $column,
212
            function ($subQuery) use ($table, $direction, $relation, $column) {
213
                $subQuery = $this->joinOne(
214
                    $subQuery->from($table),
215
                    $relation,
216
                    $column,
217
                    $direction
218
                );
219
220
                // Add any where statements with the relationship
221
                $subQuery = $this->addRelatedWhereConstraints($subQuery, $relation, $table);
222
223
                // Add any order statements with the relationship
224
                return $this->addOrder($subQuery, $relation, $table);
225
            }
226
        );
227
    }
228
229
    /**
230
     * Add orderBy if orders exist for a relation
231
     *
232
     * @param Builder|JoinClause $builder
233
     * @param Relation|BelongsTo|HasOneOrMany $relation
234
     * @param string $table
235
     * @return Builder|JoinClause $builder
236
     */
237
    protected function addOrder($builder, $relation, $table)
238
    {
239
        /** @var Model $builder */
240
        if (!empty($relation->toBase()->orders)) {
241
            // Get where clauses from the relationship
242
            foreach ($relation->toBase()->orders as $order) {
243
                $builder->orderBy($this->columnWithTableName($table, $order['column']), $order['direction']);
244
            }
245
        }
246
247
        return $builder;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $builder returns the type Illuminate\Database\Eloquent\Model which is incompatible with the documented return type Illuminate\Database\Eloq...tabase\Query\JoinClause.
Loading history...
248
    }
249
}
250