Passed
Push — master ( 7994a9...5d160b )
by Alexander
06:35
created

HasRelationships::extractChildRelations()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 7
nc 1
nop 2
dl 0
loc 13
ccs 7
cts 7
cp 1
crap 2
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace Flugg\Responder\Transformers\Concerns;
4
5
use Illuminate\Database\Eloquent\Model;
6
7
/**
8
 * A trait to be used by a transformer to handle relations
9
 *
10
 * @package flugger/laravel-responder
11
 * @author  Alexander Tømmerås <[email protected]>
12
 * @license The MIT License
13
 */
14
trait HasRelationships
15
{
16
    /**
17
     * List of available relations.
18
     *
19
     * @var string[]
20
     */
21
    protected $relations = [];
22
23
    /**
24
     * A list of autoloaded default relations.
25
     *
26
     * @var array
27
     */
28
    protected $load = [];
29
30
    /**
31
     * Get a list of whitelisted relations that are requested, including nested relations.
32
     *
33
     * @param  array $requested
34
     * @return array
35
     */
36 41 View Code Duplication
    public function relations(array $requested = []): array
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
37
    {
38 41
        $requested = $this->normalizeRelations($requested);
39 41
        $relations = $this->applyQueryConstraints($this->extractRelations($requested));
40 41
        $nestedRelations = $this->nestedRelations($requested, $relations, 'relations');
41
42 41
        return array_merge($relations, $nestedRelations);
43
    }
44
45
    /**
46
     * Get a list of default relations including nested relations.
47
     *
48
     * @param  array $requested
49
     * @return array
50
     */
51 41 View Code Duplication
    public function defaultRelations(array $requested = []): array
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
52
    {
53 41
        $requested = $this->normalizeRelations($requested);
54 41
        $relations = $this->applyQueryConstraints($this->normalizeRelations($this->load));
55 41
        $nestedRelations = $this->nestedRelations($relations, array_merge($relations, $requested), 'defaultRelations');
56
57 41
        return array_merge($relations, $nestedRelations);
58
    }
59
60
    /**
61
     * Get a list of available relations from the transformer with a normalized structure.
62
     *
63
     * @return array
64
     */
65 44
    protected function availableRelations(): array
66
    {
67 44
        return $this->normalizeRelations(array_merge($this->relations, $this->load));
68
    }
69
70
    /**
71
     * Get nested relations from transformers resolved from the $available parameter that
72
     * also occur in the $requested parameter.
73
     *
74
     * @param  array  $requested
75
     * @param  array  $available
76
     * @param  string $method
77
     * @return array
78
     */
79 41
    protected function nestedRelations(array $requested, array $available, string $method): array
80
    {
81 41
        $transformers = $this->mappedTransformers($available);
82
83 41
        return collect(array_keys($transformers))->reduce(function ($nestedRelations, $relation) use ($requested, $method, $transformers) {
84 12
            $transformer = $transformers[$relation];
85 12
            $children = $this->extractChildRelations($requested, $relation);
86 12
            $childRelations = $this->wrapChildRelations($transformer->$method($children), $relation);
87
88 12
            return array_merge($nestedRelations, $childRelations);
89 41
        }, []);
90
    }
91
92
    /**
93
     * Extract available root relations from the given list of relations.
94
     *
95
     * @param  array $relations
96
     * @return array
97
     */
98 41
    protected function extractRelations(array $relations): array
99
    {
100 41
        $available = $this->availableRelations();
101
102 18
        return array_filter($this->mapRelations($relations, function ($relation, $constraint) {
103 18
            $identifier = explode('.', $relation)[0];
104 18
            $constraint = $identifier === $relation ? $constraint : null;
105
106 18
            return [$identifier => $constraint ?: $this->resolveQueryConstraint($identifier)];
107
        }), function ($relation) use ($available) {
108 18
            return array_has($available, explode(':', $relation)[0]);
109 41
        }, ARRAY_FILTER_USE_KEY);
110
    }
111
112
    /**
113
     * Extract all nested relations under a given identifier.
114
     *
115
     * @param  array  $relations
116
     * @param  string $identifier
117
     * @return array
118
     */
119
    protected function extractChildRelations(array $relations, string $identifier): array
120
    {
121 12
        return array_reduce(array_keys($relations), function ($nested, $relation) use ($relations, $identifier) {
122 12
            if (! starts_with($relation, "$identifier.")) {
123 12
                return $nested;
124
            }
125
126 6
            $nestedIdentifier = explode('.', $relation);
127 6
            array_shift($nestedIdentifier);
128
129 6
            return array_merge($nested, [implode('.', $nestedIdentifier) => $relations[$relation]]);
130 12
        }, []);
131
    }
132
133
    /**
134
     * Wrap the identifier of each relation of the given list of nested relations with
135
     * the parent relation identifier using dot notation.
136
     *
137
     * @param  array  $nestedRelations
138
     * @param  string $relation
139
     * @return array
140
     */
141
    protected function wrapChildRelations(array $nestedRelations, string $relation): array
142
    {
143 12
        return $this->mapRelations($nestedRelations, function ($nestedRelation, $constraint) use ($relation) {
144 9
            return ["$relation.$nestedRelation" => $constraint];
145 12
        });
146
    }
147
148
    /**
149
     * Normalize relations to force an [identifier => constraint/transformer] structure.
150
     *
151
     * @param  array $relations
152
     * @return array
153
     */
154
    protected function normalizeRelations(array $relations): array
155
    {
156 47
        return array_reduce(array_keys($relations), function ($normalized, $relation) use ($relations) {
157 26
            if (is_numeric($relation)) {
158 16
                return array_merge($normalized, [$relations[$relation] => null]);
159
            }
160
161 22
            return array_merge($normalized, [$relation => $relations[$relation]]);
162 47
        }, []);
163
    }
164
165
    /**
166
     * Map over a list of relations with the [identifier => constraint/transformer] structure.
167
     *
168
     * @param  array    $relations
169
     * @param  callable $callback
170
     * @return array
171
     */
172 41
    protected function mapRelations(array $relations, callable $callback): array
173
    {
174 41
        $mapped = [];
175
176 41
        foreach ($relations as $identifier => $value) {
177 20
            $mapped = array_merge($mapped, $callback($identifier, $value));
178
        }
179
180 41
        return $mapped;
181
    }
182
183
    /**
184
     * Applies any query constraints defined in the transformer to the list of relaations.
185
     *
186
     * @param  array $relations
187
     * @return array
188
     */
189
    protected function applyQueryConstraints(array $relations): array
190
    {
191 41
        return $this->mapRelations($relations, function ($relation, $constraint) {
192 19
            return [$relation => is_callable($constraint) ? $constraint : $this->resolveQueryConstraint($relation)];
193 41
        });
194
    }
195
196
    /**
197
     * Resolve a query constraint for a given relation identifier.
198
     *
199
     * @param  string $identifier
200
     * @return \Closure|null
201
     */
202 19
    protected function resolveQueryConstraint(string $identifier)
203
    {
204 19
        if (! method_exists($this, $method = 'load' . ucfirst(camel_case($identifier)))) {
205 18
            return null;
206
        }
207
208 1
        return function ($query) use ($method) {
209 1
            return $this->$method($query);
210 1
        };
211
    }
212
213
    /**
214
     * Resolve a relation from a model instance and an identifier.
215
     *
216
     * @param  \Illuminate\Database\Eloquent\Model $model
217
     * @param  string                              $identifier
218
     * @return mixed
219
     */
220 18
    protected function resolveRelation(Model $model, string $identifier)
221
    {
222 18
        $identifier = camel_case($identifier);
223 18
        $relation = $model->$identifier;
224
225 18
        if (method_exists($this, $method = 'filter' . ucfirst($identifier))) {
226 1
            return $this->$method($relation);
227
        }
228
229 17
        return $relation;
230
    }
231
232
    /**
233
     * Resolve a list of transformers from a list of relations mapped to transformers.
234
     *
235
     * @param  array $relations
236
     * @return array
237
     */
238
    protected function mappedTransformers(array $relations): array
239
    {
240 19
        return collect($this->availableRelations())->filter(function ($transformer) {
241 19
            return ! is_null($transformer);
242
        })->map(function ($transformer) {
243 12
            return $this->resolveTransformer($transformer);
244 41
        })->intersectKey($relations)->all();
245
    }
246
247
    /**
248
     * Get a related transformer class mapped to a relation identifier.
249
     *
250
     * @param  string $identifier
251
     * @return string|null
252
     */
253 22
    protected function mappedTransformerClass(string $identifier)
254
    {
255 22
        return $this->availableRelations()[$identifier] ?? null;
256
    }
257
258
    /**
259
     * Resolve a transformer from a class name string.
260
     *
261
     * @param  string $transformer
262
     * @return mixed
263
     */
264
    protected abstract function resolveTransformer(string $transformer);
265
}