Passed
Push — master ( 5dcb31...b3f3ee )
by Michael
02:32
created

JoinsTrait::addBackticks()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 2
nc 2
nop 1
dl 0
loc 4
rs 10
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
    /**
27
     * Check relation type and get join
28
     *
29
     * @param Relation $relation
30
     * @param JoinClause $join
31
     * @param \stdClass $table
32
     * @param string $operator
33
     * @param string|null $direction
34
     * @return Builder|JoinClause
35
     */
36
    protected function getRelationJoin($relation, $join, $table, $operator, $direction = null)
37
    {
38
        // If a HasOne relation and ordered - ie join to the latest/earliest
39
        if (class_basename($relation) === 'HasOne' && !empty($relation->toBase()->orders)) {
40
            return $this->hasOneJoin($relation, $join);
41
        }
42
43
        return $this->hasManyJoin($relation, $join, $table, $operator, $direction);
44
    }
45
46
    /**
47
     * Join a HasOne relation which is ordered
48
     *
49
     * @param Relation $relation
50
     * @param JoinClause $join
51
     * @return JoinClause
52
     */
53
    protected function hasOneJoin($relation, $join)
54
    {
55
        // Get first relation order (should only be one)
56
        $order = $relation->toBase()->orders[0];
57
58
        return $join->on($order['column'], $this->hasOneJoinSql($relation, $order));
59
    }
60
61
    /**
62
     * Get join sql for a HasOne relation
63
     *
64
     * @param Relation $relation
65
     * @param array $order
66
     * @return Expression
67
     */
68
    protected function hasOneJoinSql($relation, $order)
69
    {
70
        // Build subquery for getting first/last record in related table
71
        $subQuery = $this
72
            ->joinOne(
73
                $relation->getRelated()->newQuery(),
74
                $relation,
75
                $order['column'],
76
                $order['direction']
77
            )
78
            ->setBindings($relation->getBindings());
79
80
        return DB::raw('(' . $this->toSqlWithBindings($subQuery) . ')');
0 ignored issues
show
Bug introduced by
It seems like toSqlWithBindings() 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

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

98
            /** @scrutinizer ignore-type */ $query->whereColumn($joinColumns->first, '=', $joinColumns->second),
Loading history...
99
            $column,
100
            $direction
101
        );
102
    }
103
104
    /**
105
     * Get the join columns for a relation
106
     *
107
     * @param Relation|BelongsTo|HasOneOrMany $relation
108
     * @return \stdClass
109
     */
110
    protected function getJoinColumns($relation)
111
    {
112
        // Get keys with table names
113
        if ($relation instanceof BelongsTo) {
114
            $first = $relation->getOwnerKey();
115
            $second = $relation->getForeignKey();
116
        } else {
117
            $first = $relation->getQualifiedParentKeyName();
118
            $second = $relation->getQualifiedForeignKeyName();
119
        }
120
121
        return (object)['first' => $first, 'second' => $second];
122
    }
123
124
    /**
125
     * Adds a select for a min or max on the given column, depending on direction given
126
     *
127
     * @param Builder $query
128
     * @param string $column
129
     * @param string $direction
130
     * @return Builder
131
     */
132
    protected function selectMinMax($query, $column, $direction)
133
    {
134
        $sql_direction = ($direction == 'asc' ? 'MIN' : 'MAX');
135
136
        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...
137
    }
138
139
    /**
140
     * Add backticks to a table/column
141
     *
142
     * @param string $column
143
     * @return string
144
     */
145
    protected function addBackticks($column)
146
    {
147
        return preg_match('/^[0-9a-zA-Z\.]*$/', $column) ?
148
            '`' . str_replace(['`', '.'], ['', '`.`'], $column) . '`' : $column;
149
    }
150
151
    /**
152
     * Join a HasMany Relation
153
     *
154
     * @param Relation $relation
155
     * @param JoinClause $join
156
     * @param \stdClass $table
157
     * @param string $operator
158
     * @param string $direction
159
     * @return Builder|JoinClause
160
     */
161
    protected function hasManyJoin($relation, $join, $table, $operator, $direction)
162
    {
163
        // Get relation join columns
164
        $joinColumns = $this->getJoinColumns($relation);
165
        $joinColumns = $this->replaceColumnTables($joinColumns, $table);
166
167
        $join->on($joinColumns->first, $operator, $joinColumns->second);
168
169
        // Add any where clauses from the relationship
170
        $join = $this->addRelatedWhereConstraints($join, $relation, $table->alias);
171
172
        if (!is_null($direction) && get_class($relation) === HasMany::class) {
173
            $join = $this->hasManyJoinWhere($join, $joinColumns->first, $relation, $table->alias, $direction);
174
        }
175
176
        return $join;
177
    }
178
179
    /**
180
     * Replace column table names with aliases
181
     *
182
     * @param \stdClass $joinColumns
183
     * @param \stdClass $table
184
     * @return \stdClass
185
     */
186
    protected function replaceColumnTables($joinColumns, $table)
187
    {
188
        if ($table->name !== $table->alias) {
189
            $joinColumns->first = str_replace($table->name, $table->alias, $joinColumns->first);
190
            $joinColumns->second = str_replace($table->name, $table->alias, $joinColumns->second);
191
        }
192
193
        return $joinColumns;
194
    }
195
196
    /**
197
     * Add wheres if they exist for a relation
198
     *
199
     * @param Builder|JoinClause $builder
200
     * @param Relation|BelongsTo|HasOneOrMany $relation
201
     * @param string $table
202
     * @return Builder|JoinClause $builder
203
     */
204
    protected function addRelatedWhereConstraints($builder, $relation, $table)
205
    {
206
        // Get where clauses from the relationship
207
        $wheres = collect($relation->toBase()->wheres)
208
            ->where('type', 'Basic')
209
            ->map(function ($where) use ($table) {
210
                // Add table name to column if it is absent
211
                return [$this->columnWithTableName($table, $where['column']), $where['operator'], $where['value']];
0 ignored issues
show
Bug introduced by
It seems like columnWithTableName() 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

211
                return [$this->/** @scrutinizer ignore-call */ columnWithTableName($table, $where['column']), $where['operator'], $where['value']];
Loading history...
212
            })->toArray();
213
214
        if (!empty($wheres)) {
215
            $builder->where($wheres);
216
        }
217
218
        return $builder;
219
    }
220
221
    /**
222
     * If the relation is one-to-many, just get the first related record
223
     *
224
     * @param JoinClause $joinClause
225
     * @param string $column
226
     * @param HasMany|Relation $relation
227
     * @param string $table
228
     * @param string $direction
229
     *
230
     * @return JoinClause
231
     */
232
    protected function hasManyJoinWhere(JoinClause $joinClause, $column, $relation, $table, $direction)
233
    {
234
        return $joinClause->where(
235
            $column,
236
            function ($subQuery) use ($table, $direction, $relation, $column) {
237
                $subQuery = $this->joinOne(
238
                    $subQuery->from($table),
239
                    $relation,
240
                    $column,
241
                    $direction
242
                );
243
244
                // Add any where statements with the relationship
245
                $subQuery = $this->addRelatedWhereConstraints($subQuery, $relation, $table);
246
247
                // Add any order statements with the relationship
248
                return $this->addOrder($subQuery, $relation, $table);
249
            }
250
        );
251
    }
252
253
    /**
254
     * Add orderBy if orders exist for a relation
255
     *
256
     * @param Builder|JoinClause $builder
257
     * @param Relation|BelongsTo|HasOneOrMany $relation
258
     * @param string $table
259
     * @return Builder|JoinClause $builder
260
     */
261
    protected function addOrder($builder, $relation, $table)
262
    {
263
        if (!empty($relation->toBase()->orders)) {
264
            // Get where clauses from the relationship
265
            foreach ($relation->toBase()->orders as $order) {
266
                $builder->orderBy($this->columnWithTableName($table, $order['column']), $order['direction']);
267
            }
268
        }
269
270
        return $builder;
271
    }
272
}
273