Passed
Push — 6.5.0.0 ( 14f5ba...89c3a0 )
by Christian
17:46 queued 04:24
created

StructEncoder::encodeExtensions()   B

Complexity

Conditions 9
Paths 9

Size

Total Lines 53
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 26
nc 9
nop 3
dl 0
loc 53
rs 8.0555
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php declare(strict_types=1);
2
3
namespace Shopware\Core\System\SalesChannel\Api;
4
5
use Shopware\Core\Checkout\Cart\Error\Error;
6
use Shopware\Core\Checkout\Cart\Error\ErrorCollection;
7
use Shopware\Core\Framework\Api\Context\SalesChannelApiSource;
8
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\ApiAware;
9
use Shopware\Core\Framework\DataAbstractionLayer\Search\AggregationResult\AggregationResultCollection;
10
use Shopware\Core\Framework\DataAbstractionLayer\Search\EntitySearchResult;
11
use Shopware\Core\Framework\Log\Package;
12
use Shopware\Core\Framework\Struct\Collection;
13
use Shopware\Core\Framework\Struct\Struct;
14
use Shopware\Core\System\SalesChannel\Entity\DefinitionRegistryChain;
15
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
16
17
#[Package('core')]
18
class StructEncoder
19
{
20
    /**
21
     * @var array<string, bool>
22
     */
23
    private array $protections = [];
24
25
    /**
26
     * @internal
27
     */
28
    public function __construct(
29
        private readonly DefinitionRegistryChain $registry,
30
        private readonly NormalizerInterface $serializer
31
    ) {
32
    }
33
34
    /**
35
     * @return array<mixed>
36
     */
37
    public function encode(Struct $struct, ResponseFields $fields): array
38
    {
39
        $array = $this->serializer->normalize($struct);
40
41
        if (!\is_array($array)) {
42
            throw new \RuntimeException('Normalized struct must be an array');
43
        }
44
45
        return $this->loop($struct, $fields, $array);
46
    }
47
48
    /**
49
     * @param array<mixed> $array
50
     *
51
     * @return array<mixed>
52
     */
53
    private function loop(Struct $struct, ResponseFields $fields, array $array): array
54
    {
55
        $data = $array;
56
57
        if ($struct instanceof AggregationResultCollection) {
58
            $mapped = [];
59
            foreach (\array_keys($struct->getElements()) as $index => $key) {
60
                if (!isset($data[$index]) || !\is_array($data[$index])) {
61
                    throw new \RuntimeException(\sprintf('Can not find encoded aggregation %s for data index %s', $key, $index));
62
                }
63
64
                $entity = $struct->get($key);
65
                if (!$entity instanceof Struct) {
66
                    throw new \RuntimeException(\sprintf('Aggregation %s is not an struct', $key));
67
                }
68
69
                $mapped[$key] = $this->encodeStruct($entity, $fields, $data[$index]);
70
            }
71
72
            return $mapped;
73
        }
74
75
        if ($struct instanceof EntitySearchResult) {
76
            $data = $this->encodeStruct($struct, $fields, $data);
77
78
            if (isset($data['elements'])) {
79
                $entities = [];
80
81
                foreach (\array_values($data['elements']) as $index => $value) {
82
                    $entity = $struct->getAt($index);
83
                    if (!$entity instanceof Struct) {
84
                        throw new \RuntimeException(\sprintf('Entity %s is not an struct', $index));
85
                    }
86
87
                    $entities[] = $this->encodeStruct($entity, $fields, $value);
88
                }
89
                $data['elements'] = $entities;
90
            }
91
92
            return $data;
93
        }
94
95
        if ($struct instanceof ErrorCollection) {
96
            return array_map(static fn (Error $error) => $error->jsonSerialize(), $struct->getElements());
97
        }
98
99
        if ($struct instanceof Collection) {
100
            $new = [];
101
            foreach ($data as $index => $value) {
102
                $new[] = $this->encodeStruct($struct->getAt($index), $fields, $value);
0 ignored issues
show
Bug introduced by
It seems like $struct->getAt($index) can also be of type null; however, parameter $struct of Shopware\Core\System\Sal...Encoder::encodeStruct() does only seem to accept Shopware\Core\Framework\Struct\Struct, 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

102
                $new[] = $this->encodeStruct(/** @scrutinizer ignore-type */ $struct->getAt($index), $fields, $value);
Loading history...
103
            }
104
105
            return $new;
106
        }
107
108
        return $this->encodeStruct($struct, $fields, $data);
109
    }
110
111
    /**
112
     * @param array<mixed> $data
113
     *
114
     * @return array<mixed>
115
     */
116
    private function encodeStruct(Struct $struct, ResponseFields $fields, array $data, ?string $alias = null): array
117
    {
118
        $alias = $alias ?? $struct->getApiAlias();
119
120
        foreach ($data as $property => $value) {
121
            if ($property === 'customFields' && $value === []) {
122
                $data[$property] = $value = new \stdClass();
123
            }
124
125
            if ($property === 'extensions') {
126
                $data[$property] = $this->encodeExtensions($struct, $fields, $value);
0 ignored issues
show
Bug introduced by
It seems like $value can also be of type stdClass; however, parameter $value of Shopware\Core\System\Sal...der::encodeExtensions() does only seem to accept array, 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
                $data[$property] = $this->encodeExtensions($struct, $fields, /** @scrutinizer ignore-type */ $value);
Loading history...
127
128
                if (empty($data[$property])) {
129
                    unset($data[$property]);
130
                }
131
132
                continue;
133
            }
134
135
            if (!$this->isAllowed($alias, (string) $property, $fields) && !$fields->hasNested($alias, (string) $property)) {
136
                unset($data[$property]);
137
138
                continue;
139
            }
140
141
            if (!\is_array($value)) {
142
                continue;
143
            }
144
145
            $object = $value;
146
            if (\array_key_exists($property, $struct->getVars())) {
147
                $object = $struct->getVars()[$property];
148
            }
149
150
            if ($object instanceof Struct) {
151
                $data[$property] = $this->loop($object, $fields, $value);
152
153
                continue;
154
            }
155
156
            // simple array of structs case
157
            if ($this->isStructArray($object)) {
158
                $array = [];
159
                foreach ($object as $key => $item) {
160
                    $array[$key] = $this->encodeStruct($item, $fields, $value[$key]);
161
                }
162
163
                $data[$property] = $array;
164
165
                continue;
166
            }
167
168
            $data[$property] = $this->encodeNestedArray($struct->getApiAlias(), (string) $property, $value, $fields);
169
        }
170
171
        $data['apiAlias'] = $struct->getApiAlias();
172
173
        return $data;
174
    }
175
176
    /**
177
     * @param array<mixed> $data
178
     *
179
     * @return array<mixed>
180
     */
181
    private function encodeNestedArray(string $alias, string $prefix, array $data, ResponseFields $fields): array
182
    {
183
        if ($prefix !== 'translated' && !$fields->hasNested($alias, $prefix)) {
184
            return $data;
185
        }
186
187
        foreach ($data as $property => &$value) {
188
            if ($property === 'customFields' && $value === []) {
189
                $value = new \stdClass();
190
            }
191
192
            $accessor = $prefix . '.' . $property;
193
            if ($prefix === 'translated') {
194
                $accessor = $property;
195
            }
196
197
            if (!$this->isAllowed($alias, $accessor, $fields)) {
198
                unset($data[$property]);
199
200
                continue;
201
            }
202
203
            if (!\is_array($value)) {
204
                continue;
205
            }
206
207
            $data[$property] = $this->encodeNestedArray($alias, $accessor, $value, $fields);
208
        }
209
210
        unset($value);
211
212
        return $data;
213
    }
214
215
    private function isAllowed(string $type, string $property, ResponseFields $fields): bool
216
    {
217
        if ($this->isProtected($type, $property)) {
218
            return false;
219
        }
220
221
        return $fields->isAllowed($type, $property);
222
    }
223
224
    private function isProtected(string $type, string $property): bool
225
    {
226
        $key = $type . '.' . $property;
227
        if (isset($this->protections[$key])) {
228
            return $this->protections[$key];
229
        }
230
231
        if (!$this->registry->has($type)) {
232
            return $this->protections[$key] = false;
233
        }
234
235
        $definition = $this->registry->getByEntityName($type);
236
237
        $field = $definition->getField($property);
238
239
        if ($property === 'translated') {
240
            return $this->protections[$key] = false;
241
        }
242
243
        if (!$field) {
244
            return $this->protections[$key] = true;
245
        }
246
247
        /** @var ApiAware|null $flag */
248
        $flag = $field->getFlag(ApiAware::class);
249
250
        if ($flag === null) {
251
            return $this->protections[$key] = true;
252
        }
253
254
        if (!$flag->isSourceAllowed(SalesChannelApiSource::class)) {
255
            return $this->protections[$key] = true;
256
        }
257
258
        return $this->protections[$key] = false;
259
    }
260
261
    /**
262
     * @param array<string, mixed> $value
263
     *
264
     * @return array<string, mixed>
265
     */
266
    private function encodeExtensions(Struct $struct, ResponseFields $fields, array $value): array
267
    {
268
        $alias = $struct->getApiAlias();
269
270
        $extensions = array_keys($value);
271
272
        foreach ($extensions as $name) {
273
            if ($name === 'search') {
274
                if (!$fields->isAllowed($alias, $name)) {
275
                    unset($value[$name]);
276
277
                    continue;
278
                }
279
280
                $value[$name] = $this->encodeNestedArray($alias, 'search', $value[$name], $fields);
281
282
                continue;
283
            }
284
            if ($name === 'foreignKeys') {
285
                // loop the foreign keys array with the api alias of the original struct to scope the values within the same entity definition
286
                $extension = $struct->getExtension('foreignKeys');
287
288
                if (!$extension instanceof Struct) {
289
                    unset($value[$name]);
290
291
                    continue;
292
                }
293
294
                $value[$name] = $this->encodeStruct($extension, $fields, $value['foreignKeys'], $alias);
295
296
                // only api alias inside, remove it
297
                if (\count($value[$name]) === 1) {
298
                    unset($value[$name]);
299
                }
300
301
                continue;
302
            }
303
304
            if (!$this->isAllowed($alias, $name, $fields)) {
305
                unset($value[$name]);
306
307
                continue;
308
            }
309
310
            $extension = $struct->getExtension($name);
311
            if ($extension === null) {
312
                continue;
313
            }
314
315
            $value[$name] = $this->loop($extension, $fields, $value[$name]);
316
        }
317
318
        return $value;
319
    }
320
321
    /**
322
     * @param array|mixed $object
323
     */
324
    private function isStructArray($object): bool
325
    {
326
        if (!\is_array($object)) {
327
            return false;
328
        }
329
330
        $values = array_values($object);
331
        if (!isset($values[0])) {
332
            return false;
333
        }
334
335
        return $values[0] instanceof Struct;
336
    }
337
}
338