CreatesMergeViews   A
last analyzed

Complexity

Total Complexity 38

Size/Duplication

Total Lines 304
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 108
c 3
b 0
f 0
dl 0
loc 304
rs 9.36
wmc 38

12 Methods

Rating   Name   Duplication   Size   Complexity  
A createOrReplaceMergeViewWithoutDuplicates() 0 3 1
A getMergedForeignKey() 0 11 3
A isHasManyDeepRelationWithLeadingBelongsTo() 0 4 2
A getOriginalForeignKey() 0 19 5
B getPivotTables() 0 42 8
A createMergeViewWithoutDuplicates() 0 3 1
A getRelationshipColumns() 0 15 3
A createMergeView() 0 9 2
B getQuery() 0 60 7
A createOrReplaceMergeView() 0 3 1
A removeConstraints() 0 9 2
A addRelationQueryConstraints() 0 19 3
1
<?php
2
3
namespace Staudenmeir\LaravelMergedRelations\Schema\Builders;
4
5
use Illuminate\Database\Eloquent\Relations\BelongsTo;
6
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
7
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
8
use Illuminate\Database\Eloquent\Relations\HasOneOrMany;
9
use Illuminate\Database\Eloquent\Relations\Relation;
10
use RuntimeException;
11
use Staudenmeir\EloquentHasManyDeep\HasManyDeep;
12
13
trait CreatesMergeViews
14
{
15
    /**
16
     * Create a view that merges relationships.
17
     *
18
     * @param string $name
19
     * @param \Illuminate\Database\Eloquent\Relations\Relation[] $relations
20
     * @param bool $duplicates
21
     * @param bool $orReplace
22
     * @return void
23
     */
24
    public function createMergeView($name, array $relations, $duplicates = true, $orReplace = false)
25
    {
26
        $this->removeConstraints($relations);
27
28
        $union = $duplicates ? 'unionAll' : 'union';
29
30
        $query = $this->getQuery($relations, $union);
31
32
        $this->createView($name, $query, null, $orReplace);
0 ignored issues
show
Bug introduced by
The method createView() does not exist on Staudenmeir\LaravelMerge...lders\CreatesMergeViews. Did you maybe mean createMergeView()? ( Ignorable by Annotation )

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

32
        $this->/** @scrutinizer ignore-call */ 
33
               createView($name, $query, null, $orReplace);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
33
    }
34
35
    /**
36
     * Create a view that merges relationships without duplicates.
37
     *
38
     * @param string $name
39
     * @param \Illuminate\Database\Eloquent\Relations\Relation[] $relations
40
     * @return void
41
     */
42
    public function createMergeViewWithoutDuplicates($name, array $relations)
43
    {
44
        $this->createMergeView($name, $relations, false);
45
    }
46
47
    /**
48
     * Create a view that merges relationships or replace an existing one.
49
     *
50
     * @param string $name
51
     * @param array $relations
52
     * @param bool $duplicates
53
     * @return void
54
     */
55
    public function createOrReplaceMergeView($name, array $relations, $duplicates = true)
56
    {
57
        $this->createMergeView($name, $relations, $duplicates, true);
58
    }
59
60
    /**
61
     * Create a view that merges relationships or replace an existing one without duplicates.
62
     *
63
     * @param string $name
64
     * @param array $relations
65
     * @return void
66
     */
67
    public function createOrReplaceMergeViewWithoutDuplicates($name, array $relations)
68
    {
69
        $this->createOrReplaceMergeView($name, $relations, false);
70
    }
71
72
    /**
73
     * Remove the foreign key constraints from the relationships.
74
     *
75
     * @param \Illuminate\Database\Eloquent\Relations\Relation[] $relations
76
     * @return void
77
     */
78
    protected function removeConstraints(array $relations)
79
    {
80
        foreach ($relations as $relation) {
81
            $foreignKey = $this->getOriginalForeignKey($relation);
82
83
            $relation->getQuery()->getQuery()->wheres = collect($relation->getQuery()->getQuery()->wheres)
84
                ->reject(function ($where) use ($foreignKey) {
85
                    return $where['column'] === $foreignKey;
86
                })->values()->all();
87
        }
88
    }
89
90
    /**
91
     * Get the foreign key of the original relationship.
92
     *
93
     * @param \Illuminate\Database\Eloquent\Relations\Relation $relation
94
     * @return string
95
     */
96
    protected function getOriginalForeignKey(Relation $relation)
97
    {
98
        if ($relation instanceof BelongsTo) {
99
            return $relation->getQualifiedOwnerKeyName();
100
        }
101
102
        if ($relation instanceof BelongsToMany) {
103
            return $relation->getQualifiedForeignPivotKeyName();
104
        }
105
106
        if ($relation instanceof HasManyThrough) {
107
            return $relation->getQualifiedFirstKeyName();
108
        }
109
110
        if ($relation instanceof HasOneOrMany) {
111
            return $relation->getQualifiedForeignKeyName();
112
        }
113
114
        throw new RuntimeException('This type of relationship is not supported.'); // @codeCoverageIgnore
115
    }
116
117
    /**
118
     * Get the merge query.
119
     *
120
     * @param \Illuminate\Database\Eloquent\Relations\Relation[] $relations
121
     * @param string $union
122
     * @return \Illuminate\Database\Eloquent\Builder
123
     */
124
    protected function getQuery(array $relations, $union)
125
    {
126
        $grammar = $this->connection->getQueryGrammar();
127
128
        $pdo = $this->connection->getPdo();
129
130
        $columns = $this->getRelationshipColumns($relations);
131
132
        $pivotTables = $this->getPivotTables($relations);
133
134
        $allColumns = array_unique(array_merge(...array_values($columns)));
135
136
        $query = null;
137
138
        foreach ($relations as $relation) {
139
            $relationQuery = $relation->getQuery();
140
141
            $from = $relationQuery->getQuery()->from;
142
143
            $foreignKey = $this->getMergedForeignKey($relation);
144
145
            $model = $relation->getRelated()->getMorphClass();
146
147
            $placeholders = [];
148
149
            foreach ($allColumns as $column) {
150
                if (in_array($column, $columns[$from])) {
151
                    $relationQuery->addSelect($from.'.'.$column);
152
                } else {
153
                    $relationQuery->selectRaw('null as '.$grammar->wrap($column));
154
155
                    $placeholders[] = $column;
156
                }
157
            }
158
159
            foreach ($pivotTables as $pivotTable) {
160
                foreach ($pivotTable['columns'] as $column) {
161
                    $alias = "__{$pivotTable['table']}__{$pivotTable['accessor']}__$column";
162
163
                    $relationQuery->addSelect("{$pivotTable['table']}.$column as $alias");
164
                }
165
            }
166
167
            $with = array_keys($relationQuery->getEagerLoads());
168
169
            $relationQuery->selectRaw($grammar->wrap($foreignKey).' as laravel_foreign_key')
170
                ->selectRaw($pdo->quote($model).' as laravel_model')
171
                ->selectRaw($pdo->quote(implode(',', $placeholders)).' as laravel_placeholders')
172
                ->selectRaw($pdo->quote(implode(',', $with)).' as laravel_with');
173
174
            $this->addRelationQueryConstraints($relation);
175
176
            if (!$query) {
177
                $query = $relationQuery;
178
            } else {
179
                $query->$union($relationQuery);
180
            }
181
        }
182
183
        return $query;
184
    }
185
186
    /**
187
     * Get the columns of all relationship tables.
188
     *
189
     * @param \Illuminate\Database\Eloquent\Relations\Relation[] $relations
190
     * @return array
191
     */
192
    protected function getRelationshipColumns(array $relations)
193
    {
194
        $columns = [];
195
196
        foreach ($relations as $relation) {
197
            $table = $relation->getQuery()->getQuery()->from;
198
199
            if (!isset($columns[$table])) {
200
                $listing = $relation->getRelated()->getConnection()->getSchemaBuilder()->getColumnListing($table);
201
202
                $columns[$table] = $listing;
203
            }
204
        }
205
206
        return $columns;
207
    }
208
209
    /**
210
     * Get the pivot tables that are requested by all relationships.
211
     *
212
     * @param array $relations
213
     * @return array
214
     */
215
    protected function getPivotTables(array $relations): array
216
    {
217
        $tables = [];
218
219
        foreach ($relations as $i => $relation) {
220
            if ($relation instanceof BelongsToMany) {
221
                $pivotColumns = $relation->getPivotColumns();
222
223
                if ($pivotColumns) {
224
                    $tables[$i][] = [
225
                        'accessor' => $relation->getPivotAccessor(),
226
                        'columns' => $pivotColumns,
227
                        'table' => $relation->getTable(),
228
                    ];
229
                }
230
            } elseif($relation instanceof HasManyDeep) {
231
                $intermediateTables = $relation->getIntermediateTables();
232
233
                foreach ($intermediateTables as $accessor => $table) {
234
                    $tables[$i][] = [
235
                        'accessor' => $accessor,
236
                        'columns' => $table['columns'],
237
                        'table' => $table['table'],
238
                    ];
239
                }
240
            }
241
        }
242
243
        if (count($tables) === count($relations)) {
244
            $hashes = array_map(
245
                fn (array $table) => serialize($table),
246
                $tables
247
            );
248
249
            $uniqueHashes = array_unique($hashes);
250
251
            if (count($uniqueHashes) === 1) {
252
                return $tables[0];
253
            }
254
        }
255
256
        return [];
257
    }
258
259
    /**
260
     * Get the foreign key for the merged relationship.
261
     *
262
     * @param \Illuminate\Database\Eloquent\Relations\Relation $relation
263
     * @return string
264
     */
265
    protected function getMergedForeignKey(Relation $relation)
266
    {
267
        if ($relation instanceof BelongsTo) {
268
            return $relation->getQualifiedParentKeyName();
269
        }
270
271
        if ($this->isHasManyDeepRelationWithLeadingBelongsTo($relation)) {
272
            return $relation->getFarParent()->getQualifiedKeyName();
273
        }
274
275
        return $this->getOriginalForeignKey($relation);
276
    }
277
278
    /**
279
     * Add relation-specific constraints to the query.
280
     *
281
     * @param \Illuminate\Database\Eloquent\Relations\Relation $relation
282
     * @return void
283
     */
284
    protected function addRelationQueryConstraints(Relation $relation)
285
    {
286
        if ($relation instanceof BelongsTo) {
287
            $relation->getQuery()->distinct()
288
                          ->join(
289
                              $relation->getParent()->getTable(),
290
                              $relation->getQualifiedForeignKeyName(),
291
                              '=',
292
                              $relation->getQualifiedOwnerKeyName()
293
                          );
294
        }
295
296
        if ($this->isHasManyDeepRelationWithLeadingBelongsTo($relation)) {
297
            $relation->getQuery()
298
                     ->join(
299
                         $relation->getFarParent()->getTable(),
300
                         $relation->getQualifiedLocalKeyName(),
301
                         '=',
302
                         $relation->getQualifiedFirstKeyName()
303
                     );
304
        }
305
    }
306
307
    /**
308
     * Determine if the relationship is a HasManyDeep relationship that starts with a BelongsTo relationship.
309
     *
310
     * @param \Illuminate\Database\Eloquent\Relations\Relation $relation
311
     * @return bool
312
     */
313
    protected function isHasManyDeepRelationWithLeadingBelongsTo(Relation $relation): bool
314
    {
315
        return $relation instanceof HasManyDeep
316
            && $relation->getFirstKeyName() === $relation->getParent()->getKeyName();
317
    }
318
}
319