Passed
Push — master ( 4f50ad...2698b3 )
by Michael
02:21
created

RelationPlus::hasOneJoinSql()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 12
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 7
nc 1
nop 1
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\Query\Expression;
11
use Illuminate\Database\Query\JoinClause;
12
13
/**
14
 * Class RelationPlus
15
 *
16
 * @package Blasttech\EloquentRelatedPlus\Relations
17
 */
18
class RelationPlus
19
{
20
    /**
21
     * @var string $tableName
22
     */
23
    public $tableName;
24
25
    /**
26
     * @var string $tableAlias
27
     */
28
    public $tableAlias;
29
30
    /**
31
     * @var BelongsTo|HasOneOrMany $relation
32
     */
33
    private $relation;
34
35
    public function __construct($relation)
36
    {
37
        $this->setRelation($relation);
38
        $this->tableName = $this->relation->getRelated()->getTable();
39
        $from = explode(' ', $this->relation->getQuery()->getQuery()->from);
40
        // if using a 'table' AS 'tableAlias' in a from statement, otherwise alias will be the table name
41
        $this->tableAlias = array_pop($from);
42
    }
43
44
    /**
45
     * @return BelongsTo|HasOneOrMany
46
     */
47
    public function getRelation()
48
    {
49
        return $this->relation;
50
    }
51
52
    /**
53
     * @param BelongsTo|HasOneOrMany $relation
54
     */
55
    public function setRelation($relation)
56
    {
57
        $this->relation = $relation;
58
    }
59
60
    /**
61
     * Check relation type and get join
62
     *
63
     * @param JoinClause $join
64
     * @param string $operator
65
     * @param string|null $direction
66
     * @return Builder|JoinClause
67
     */
68
    public function getRelationJoin($join, $operator, $direction = null)
69
    {
70
        // If a HasOne relation and ordered - ie join to the latest/earliest
71
        if (class_basename($this->relation) === 'HasOne') {
72
            $relation = RelatedPlusHelpers::removeGlobalScopes($this->relation->getRelated(), $this->relation, 'order');
0 ignored issues
show
Bug introduced by
$this->relation of type Illuminate\Database\Eloq...\Relations\HasOneOrMany is incompatible with the type Illuminate\Database\Eloquent\Builder expected by parameter $query of Blasttech\EloquentRelate...s::removeGlobalScopes(). ( Ignorable by Annotation )

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

72
            $relation = RelatedPlusHelpers::removeGlobalScopes($this->relation->getRelated(), /** @scrutinizer ignore-type */ $this->relation, 'order');
Loading history...
73
74
            if (!empty($relation->toBase()->orders)) {
75
                return $this->hasOneJoin($join);
76
            }
77
        }
78
79
        return $this->hasManyJoin($join, $operator, $direction);
80
    }
81
82
    /**
83
     * Join a HasOne relation which is ordered
84
     *
85
     * @param JoinClause $join
86
     * @return JoinClause
87
     */
88
    private function hasOneJoin($join)
89
    {
90
        // Get first relation order (should only be one)
91
        $order = $this->relation->toBase()->orders[0];
92
93
        return $join->on($order['column'], $this->hasOneJoinSql($order));
94
    }
95
96
    /**
97
     * Get join sql for a HasOne relation
98
     *
99
     * @param array $order
100
     * @return Expression
101
     */
102
    private function hasOneJoinSql($order)
103
    {
104
        // Build subquery for getting first/last record in related table
105
        $subQuery = $this
106
            ->joinOne(
107
                $this->relation->getRelated()->newQuery(),
108
                $order['column'],
109
                $order['direction']
110
            )
111
            ->setBindings($this->relation->getBindings());
112
113
        return DB::raw('(' . RelatedPlusHelpers::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...rs::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

113
        return DB::raw('(' . RelatedPlusHelpers::toSqlWithBindings(/** @scrutinizer ignore-type */ $subQuery) . ')');
Loading history...
114
    }
115
116
    /**
117
     * Adds a where for a relation's join columns and and min/max for a given column
118
     *
119
     * @param Builder $query
120
     * @param string $column
121
     * @param string $direction
122
     * @return Builder
123
     */
124
    private function joinOne($query, $column, $direction)
125
    {
126
        // Get join fields
127
        $joinColumns = $this->getJoinColumns();
128
129
        return $this->selectMinMax(
130
            $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...ionPlus::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

130
            /** @scrutinizer ignore-type */ $query->whereColumn($joinColumns->first, '=', $joinColumns->second),
Loading history...
131
            $column,
132
            $direction
133
        );
134
    }
135
136
    /**
137
     * Get the join columns for a relation
138
     *
139
     * @return \stdClass
140
     */
141
    private function getJoinColumns()
142
    {
143
        // Get keys with table names
144
        if ($this->relation instanceof BelongsTo) {
145
            return $this->getBelongsToColumns();
146
        }
147
148
        return $this->getHasOneOrManyColumns();
149
    }
150
151
    /**
152
     * Get the join columns for a BelongsTo relation
153
     *
154
     * @return object
155
     */
156
    private function getBelongsToColumns()
157
    {
158
        $first = $this->relation->getOwnerKey();
159
        $second = $this->relation->getForeignKey();
160
161
        return (object)['first' => $first, 'second' => $second];
162
    }
163
164
    /**
165
     * Get the join columns for a HasOneOrMany relation
166
     *
167
     * @return object
168
     */
169
    private function getHasOneOrManyColumns()
170
    {
171
        $first = $this->relation->getQualifiedParentKeyName();
172
        $second = $this->relation->getQualifiedForeignKeyName();
173
174
        return (object)['first' => $first, 'second' => $second];
175
    }
176
177
    /**
178
     * Adds a select for a min or max on the given column, depending on direction given
179
     *
180
     * @param Builder $query
181
     * @param string $column
182
     * @param string $direction
183
     * @return Builder
184
     */
185
    private function selectMinMax($query, $column, $direction)
186
    {
187
        $sql_direction = ($direction == 'asc' ? 'MIN' : 'MAX');
188
189
        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...
190
    }
191
192
    /**
193
     * Add backticks to a table/column
194
     *
195
     * @param string $column
196
     * @return string
197
     */
198
    private function addBackticks($column)
199
    {
200
        return preg_match('/^[0-9a-zA-Z\.]*$/', $column) ?
201
            '`' . str_replace(['`', '.'], ['', '`.`'], $column) . '`' : $column;
202
    }
203
204
    /**
205
     * Join a HasMany Relation
206
     *
207
     * @param JoinClause $join
208
     * @param string $operator
209
     * @param string $direction
210
     * @return Builder|JoinClause
211
     */
212
    private function hasManyJoin($join, $operator, $direction)
213
    {
214
        // Get relation join columns
215
        $joinColumns = $this->getJoinColumns();
216
        $joinColumns = $this->replaceColumnTables($joinColumns);
217
218
        $join->on($joinColumns->first, $operator, $joinColumns->second);
219
220
        // Add any where clauses from the relationship
221
        $join = $this->addRelatedWhereConstraints($join); // $table->alias
222
223
        if (!is_null($direction) && get_class($this) === HasMany::class) {
224
            $join = $this->hasManyJoinWhere($join, $joinColumns->first, $direction); // $table->alias,
225
        }
226
227
        return $join;
228
    }
229
230
    /**
231
     * Replace column table names with aliases
232
     *
233
     * @param \stdClass $joinColumns
234
     * @return \stdClass
235
     */
236
    private function replaceColumnTables($joinColumns)
237
    {
238
        if ($this->tableName !== $this->tableAlias) {
239
            $joinColumns->first = str_replace($this->tableName, $this->tableAlias, $joinColumns->first);
240
            $joinColumns->second = str_replace($this->tableName, $this->tableAlias, $joinColumns->second);
241
        }
242
243
        return $joinColumns;
244
    }
245
246
    /**
247
     * Add wheres if they exist for a relation
248
     *
249
     * @param Builder|JoinClause $builder
250
     * @return Builder|JoinClause $builder
251
     */
252
    private function addRelatedWhereConstraints($builder)
253
    {
254
        // Get where clauses from the relationship
255
        $wheres = collect($this->relation->toBase()->wheres)
256
            ->where('type', 'Basic')
257
            ->map(function ($where) {
258
                // Add table name to column if it is absent
259
                return [
260
                    $this->columnWithTableName($where['column']),
261
                    $where['operator'],
262
                    $where['value']
263
                ];
264
            })->toArray();
265
266
        if (!empty($wheres)) {
267
            $builder->where($wheres);
268
        }
269
270
        return $builder;
271
    }
272
273
    /**
274
     * Add table name to column name if table name not already included in column name
275
     *
276
     * @param string $column
277
     * @return string
278
     */
279
    private function columnWithTableName($column)
280
    {
281
        return (preg_match('/(' . $this->tableAlias . '\.|`' . $this->tableAlias . '`)/i',
282
                $column) > 0 ? '' : $this->tableAlias . '.') . $column;
283
    }
284
285
    /**
286
     * If the relation is one-to-many, just get the first related record
287
     *
288
     * @param JoinClause $joinClause
289
     * @param string $column
290
     * @param string $direction
291
     *
292
     * @return JoinClause
293
     */
294
    private function hasManyJoinWhere(JoinClause $joinClause, $column, $direction)
295
    {
296
        return $joinClause->where(
297
            $column,
298
            function ($subQuery) use ($direction, $column) {
299
                $subQuery = $this->joinOne(
300
                    $subQuery->from($this->tableAlias),
301
                    $column,
302
                    $direction
303
                );
304
305
                // Add any where statements with the relationship
306
                $subQuery = $this->addRelatedWhereConstraints($subQuery); // $this->tableAlias
307
308
                // Add any order statements with the relationship
309
                return $this->addOrder($subQuery); // $this->tableAlias
310
            }
311
        );
312
    }
313
314
    /**
315
     * Add orderBy if orders exist for a relation
316
     *
317
     * @param Builder|JoinClause $builder
318
     * @return Builder|JoinClause $builder
319
     */
320
    private function addOrder($builder)
321
    {
322
        if (!empty($this->relation->toBase()->orders)) {
323
            // Get where clauses from the relationship
324
            foreach ($this->relation->toBase()->orders as $order) {
325
                $builder->orderBy($this->columnWithTableName($order['column']),
326
                    $order['direction']);
327
            }
328
        }
329
330
        return $builder;
331
    }
332
333
    /**
334
     * Get table name with alias if different to table name
335
     *
336
     * @return string
337
     */
338
    public function getTableWithAlias()
339
    {
340
        if ($this->tableAlias !== '' && $this->tableName !== $this->tableAlias) {
341
            return $this->tableName . ' AS ' . $this->tableAlias;
342
        }
343
344
        return $this->tableName;
345
    }
346
}