Passed
Push — master ( 1543af...f8a677 )
by Jonas
04:29 queued 16s
created

IsConcatenableRelation::replacePathSeparator()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 5
c 1
b 0
f 0
dl 0
loc 7
rs 10
cc 2
nc 2
nop 3
1
<?php
2
3
namespace Staudenmeir\LaravelAdjacencyList\Eloquent\Relations\Graph\Traits\Concatenation;
4
5
use Illuminate\Database\Eloquent\Builder;
6
use Illuminate\Database\Eloquent\Collection;
7
use Illuminate\Database\Eloquent\Model;
8
use Illuminate\Database\PostgresConnection;
9
use RuntimeException;
10
11
trait IsConcatenableRelation
12
{
13
    /**
14
     * Append the relation's through parents, foreign and local keys to a deep relationship.
15
     *
16
     * @param \Illuminate\Database\Eloquent\Model[] $through
17
     * @param array $foreignKeys
18
     * @param array $localKeys
19
     * @param int $position
20
     * @return array
21
     */
22
    public function appendToDeepRelationship(array $through, array $foreignKeys, array $localKeys, int $position): array
23
    {
24
        if ($position === 0) {
25
            $foreignKeys[] = function (Builder $query, Builder $parentQuery = null) {
26
                if ($parentQuery) {
27
                    $this->getRelationExistenceQuery($this->query, $parentQuery);
0 ignored issues
show
Bug introduced by
It seems like getRelationExistenceQuery() 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

27
                    $this->/** @scrutinizer ignore-call */ 
28
                           getRelationExistenceQuery($this->query, $parentQuery);
Loading history...
28
                }
29
30
                $this->mergeExpressions($query, $this->query);
31
            };
32
33
            $localKeys[] = null;
34
        } else {
35
            throw new RuntimeException(
36
                sprintf(
37
                    '%s can only be at the beginning of deep relationships at the moment.',
38
                    class_basename($this)
39
                )
40
            );
41
        }
42
43
        return [$through, $foreignKeys, $localKeys];
44
    }
45
46
    /**
47
     * Get the related table name for a deep relationship.
48
     *
49
     * @return string
50
     */
51
    public function getTableForDeepRelationship(): string
52
    {
53
        return $this->related->getExpressionName();
54
    }
55
56
    /**
57
     * The custom callback to run at the end of the get() method.
58
     *
59
     * @param \Illuminate\Database\Eloquent\Collection $models
60
     * @return void
61
     */
62
    public function postGetCallback(Collection $models): void
63
    {
64
        if (!$this->query->getConnection() instanceof PostgresConnection) {
65
            return;
66
        }
67
68
        if (!isset($models[0]->laravel_through_key)) {
69
            return;
70
        }
71
72
        $this->replacePathSeparator(
73
            $models,
74
            'laravel_through_key',
75
            $this->related->getPathSeparator()
76
        );
77
    }
78
79
    /**
80
     * Replace the separator in a PostgreSQL path column.
81
     *
82
     * @param \Illuminate\Database\Eloquent\Collection $models
83
     * @param string $column
84
     * @param string $separator
85
     * @return void
86
     */
87
    protected function replacePathSeparator(Collection $models, string $column, string $separator): void
88
    {
89
        foreach ($models as $model) {
90
            $model->$column = str_replace(
91
                ',',
92
                $separator,
93
                substr($model->$column, 1, -1)
94
            );
95
        }
96
    }
97
98
    /**
99
     * Set the constraints for an eager load of the deep relation.
100
     *
101
     * @param \Illuminate\Database\Eloquent\Builder $query
102
     * @param array $models
103
     * @return void
104
     */
105
    public function addEagerConstraintsToDeepRelationship(Builder $query, array $models): void
106
    {
107
        $this->addEagerConstraints($models);
0 ignored issues
show
Bug introduced by
The method addEagerConstraints() does not exist on Staudenmeir\LaravelAdjac...\IsConcatenableRelation. Did you maybe mean addEagerConstraintsToDeepRelationship()? ( Ignorable by Annotation )

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

107
        $this->/** @scrutinizer ignore-call */ 
108
               addEagerConstraints($models);

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...
108
109
        $this->mergeExpressions($query, $this->query);
110
111
        $query->getQuery()->distinct = $this->query->getQuery()->distinct;
112
    }
113
114
    /**
115
     * Merge the common table expressions from one query into another.
116
     *
117
     * @param \Illuminate\Database\Eloquent\Builder $query
118
     * @param \Illuminate\Database\Eloquent\Builder $from
119
     * @return \Illuminate\Database\Eloquent\Builder
120
     */
121
    protected function mergeExpressions(Builder $query, Builder $from): Builder
122
    {
123
        $query->getQuery()->expressions = array_merge(
0 ignored issues
show
Bug introduced by
The property expressions does not seem to exist on Illuminate\Database\Query\Builder.
Loading history...
124
            $query->getQuery()->expressions,
125
            $from->getQuery()->expressions
126
        );
127
128
        return $query->addBinding(
0 ignored issues
show
Bug Best Practice introduced by
The expression return $query->addBindin...sions'], 'expressions') could return the type Illuminate\Database\Query\Builder which is incompatible with the type-hinted return Illuminate\Database\Eloquent\Builder. Consider adding an additional type-check to rule them out.
Loading history...
129
            $from->getQuery()->getRawBindings()['expressions'],
130
            'expressions'
131
        );
132
    }
133
134
    /**
135
     * Match the eagerly loaded results for a deep relationship to their parents.
136
     *
137
     * @param array $models
138
     * @param \Illuminate\Database\Eloquent\Collection $results
139
     * @param string $relation
140
     * @param string $type
141
     * @return array
142
     */
143
    public function matchResultsForDeepRelationship(
144
        array $models,
145
        Collection $results,
146
        string $relation,
147
        string $type = 'many'
148
    ): array {
149
        $dictionary = $this->buildDictionaryForDeepRelationship($results);
150
151
        $attribute = $this->parentKey;
152
153
        foreach ($models as $model) {
154
            $key = $model->$attribute;
155
156
            if (isset($dictionary[$key])) {
157
                $value = $dictionary[$key];
158
159
                $value = $type === 'one' ? reset($value) : $this->related->newCollection($value);
160
161
                $model->setRelation($relation, $value);
162
            }
163
        }
164
165
        return $models;
166
    }
167
168
    /**
169
     * Build the model dictionary for a deep relation.
170
     *
171
     * @param \Illuminate\Database\Eloquent\Collection $results
172
     * @return array
173
     */
174
    protected function buildDictionaryForDeepRelationship(Collection $results): array
175
    {
176
        $pathSeparator = $this->related->getPathSeparator();
177
178
        if ($this->andSelf) {
179
            return $results->mapToDictionary(function (Model $result) use ($pathSeparator) {
180
                return [strtok($result->laravel_through_key, $pathSeparator) => $result];
181
            })->all();
182
        }
183
184
        $dictionary = [];
185
186
        $firstLevelResults = $results->filter(
187
            fn (Model $result) => !str_contains($result->laravel_through_key, $pathSeparator)
188
        )->groupBy('laravel_through_key');
189
190
        foreach ($results as $result) {
191
            $keys = [];
192
193
            if (str_contains($result->laravel_through_key, $pathSeparator)) {
194
                $firstPathSegment = strtok($result->laravel_through_key, $pathSeparator);
195
196
                foreach ($firstLevelResults[$firstPathSegment] as $model) {
197
                    $keys[] = $model->laravel_through_key_pivot_id;
198
                }
199
            } else {
200
                $keys[] = $result->laravel_through_key_pivot_id;
201
            }
202
203
            foreach ($keys as $key) {
204
                $dictionary[$key][] = $result;
205
            }
206
        }
207
208
        return $dictionary;
209
    }
210
}
211