Passed
Push — master ( 86219f...fe7fb1 )
by Jonas
14:36
created

CreatesMergeViews   A

Complexity

Total Complexity 38

Size/Duplication

Total Lines 304
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 108
dl 0
loc 304
ccs 88
cts 88
cp 1
rs 9.36
c 3
b 0
f 0
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 52
     */
24
    public function createMergeView($name, array $relations, $duplicates = true, $orReplace = false)
25 52
    {
26
        $this->removeConstraints($relations);
27 52
28
        $union = $duplicates ? 'unionAll' : 'union';
29 52
30
        $query = $this->getQuery($relations, $union);
31 52
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 4
     */
42
    public function createMergeViewWithoutDuplicates($name, array $relations)
43 4
    {
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 8
     */
55
    public function createOrReplaceMergeView($name, array $relations, $duplicates = true)
56 8
    {
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 4
     */
67
    public function createOrReplaceMergeViewWithoutDuplicates($name, array $relations)
68 4
    {
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 52
     */
78
    protected function removeConstraints(array $relations)
79 52
    {
80 52
        foreach ($relations as $relation) {
81
            $foreignKey = $this->getOriginalForeignKey($relation);
82 52
83 52
            $relation->getQuery()->getQuery()->wheres = collect($relation->getQuery()->getQuery()->wheres)
84 52
                ->reject(function ($where) use ($foreignKey) {
85 52
                    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 52
     */
96
    protected function getOriginalForeignKey(Relation $relation)
97 52
    {
98 8
        if ($relation instanceof BelongsTo) {
99
            return $relation->getQualifiedOwnerKeyName();
100
        }
101 48
102 24
        if ($relation instanceof BelongsToMany) {
103
            return $relation->getQualifiedForeignPivotKeyName();
104
        }
105 24
106 24
        if ($relation instanceof HasManyThrough) {
107
            return $relation->getQualifiedFirstKeyName();
108
        }
109 20
110 20
        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 52
     */
124
    protected function getQuery(array $relations, $union)
125 52
    {
126
        $grammar = $this->connection->getQueryGrammar();
127 52
128
        $pdo = $this->connection->getPdo();
129 52
130
        $columns = $this->getRelationshipColumns($relations);
131 52
132
        $pivotTables = $this->getPivotTables($relations);
133 52
134
        $allColumns = array_unique(array_merge(...array_values($columns)));
135 52
136 52
        $query = null;
137
138 52
        foreach ($relations as $relation) {
139
            $relationQuery = $relation->getQuery();
140 52
141
            $from = $relationQuery->getQuery()->from;
142 52
143
            $foreignKey = $this->getMergedForeignKey($relation);
144 52
145
            $model = $relation->getRelated()->getMorphClass();
146 52
147 52
            $placeholders = [];
148 52
149
            foreach ($allColumns as $column) {
150 24
                if (in_array($column, $columns[$from])) {
151
                    $relationQuery->addSelect($from.'.'.$column);
152 24
                } else {
153
                    $relationQuery->selectRaw('null as '.$grammar->wrap($column));
154
155
                    $placeholders[] = $column;
156 52
                }
157
            }
158 52
159 52
            foreach ($pivotTables as $pivotTable) {
160 52
                foreach ($pivotTable['columns'] as $column) {
161 52
                    $alias = "__{$pivotTable['table']}__{$pivotTable['accessor']}__$column";
162
163 52
                    $relationQuery->addSelect("{$pivotTable['table']}.$column as $alias");
164
                }
165 52
            }
166 52
167
            $with = array_keys($relationQuery->getEagerLoads());
168 48
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 52
                ->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 52
        }
182
183 52
        return $query;
184
    }
185 52
186 52
    /**
187
     * Get the columns of all relationship tables.
188 52
     *
189 52
     * @param \Illuminate\Database\Eloquent\Relations\Relation[] $relations
190
     * @return array
191 52
     */
192
    protected function getRelationshipColumns(array $relations)
193
    {
194
        $columns = [];
195 52
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 52
        }
205
206 52
        return $columns;
207 8
    }
208
209
    /**
210 48
     * Get the pivot tables that are requested by all relationships.
211 4
     *
212
     * @param array $relations
213
     * @return array
214 44
     */
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 52
                if ($pivotColumns) {
224
                    $tables[$i][] = [
225 52
                        'accessor' => $relation->getPivotAccessor(),
226 8
                        'columns' => $pivotColumns,
227 8
                        'table' => $relation->getTable(),
228 8
                    ];
229 8
                }
230 8
            } elseif($relation instanceof HasManyDeep) {
231 8
                $intermediateTables = $relation->getIntermediateTables();
232 8
233
                foreach ($intermediateTables as $accessor => $table) {
234
                    $tables[$i][] = [
235 52
                        'accessor' => $accessor,
236 4
                        'columns' => $table['columns'],
237 4
                        'table' => $table['table'],
238 4
                    ];
239 4
                }
240 4
            }
241 4
        }
242 4
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 52
                return $tables[0];
253
            }
254 52
        }
255 52
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