Passed
Push — master ( 2438e7...304e51 )
by Michael
02:19
created

RelationPlus::getOrders()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
eloc 1
nc 1
nop 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\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
     * Initialise $relation, $tableName and $tableAlias
32
     * If using a 'table' AS 'tableAlias' in a from statement, otherwise alias will be the table name
33
     *
34
     * @var BelongsTo|HasOneOrMany $relation
35
     */
36
    private $relation;
37
38
    public function __construct($relation)
39
    {
40
        $this->setRelation($relation);
41
        $this->tableName = $this->relation->getRelated()->getTable();
42
        $from = explode(' ', $this->relation->getQuery()->getQuery()->from);
43
        $this->tableAlias = array_pop($from);
44
    }
45
46
    /**
47
     * @return BelongsTo|HasOneOrMany
48
     */
49
    public function getRelation()
50
    {
51
        return $this->relation;
52
    }
53
54
    /**
55
     * @param BelongsTo|HasOneOrMany $relation
56
     */
57
    public function setRelation($relation)
58
    {
59
        $this->relation = $relation;
60
    }
61
62
    /**
63
     * Check relation type and get join
64
     *
65
     * @param JoinClause $join
66
     * @param string $operator
67
     * @param string|null $direction
68
     * @return Builder|JoinClause
69
     */
70
    public function getRelationJoin($join, $operator, $direction = null)
71
    {
72
        // If a HasOne relation and ordered - ie join to the latest/earliest
73
        if (class_basename($this->relation) === 'HasOne') {
74
            $this->relation = RelatedPlusHelpers::removeGlobalScopes($this->relation->getRelated(), $this->relation,
0 ignored issues
show
Documentation Bug introduced by
It seems like Blasttech\EloquentRelate...his->relation, 'order') of type Illuminate\Database\Eloquent\Builder is incompatible with the declared type Illuminate\Database\Eloq...\Relations\HasOneOrMany of property $relation.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

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

126
        return DB::raw('(' . RelatedPlusHelpers::toSqlWithBindings(/** @scrutinizer ignore-type */ $subQuery) . ')');
Loading history...
127
    }
128
129
    /**
130
     * Adds a where for a relation's join columns and and min/max for a given column
131
     *
132
     * @param Builder $query
133
     * @param string $column
134
     * @param string $direction
135
     * @return Builder
136
     */
137
    private function joinOne($query, $column, $direction)
138
    {
139
        // Get join fields
140
        $joinColumns = $this->getJoinColumns();
141
142
        return $this->selectMinMax(
143
            $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

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