ApiPlatformParser   A
last analyzed

Complexity

Total Complexity 40

Size/Duplication

Total Lines 238
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 114
dl 0
loc 238
rs 9.2
c 0
b 0
f 0
wmc 40

8 Methods

Rating   Name   Duplication   Size   Complexity  
A parse() 0 12 2
A supports() 0 19 4
A __construct() 0 8 1
A getGroupsForItemAndCollectionOperation() 0 18 5
A parseResource() 0 20 4
A getGroupsContext() 0 9 2
B getPropertyMetadata() 0 16 7
C parseProperty() 0 77 15

How to fix   Complexity   

Complex Class

Complex classes like ApiPlatformParser often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ApiPlatformParser, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/*
4
 * This file is part of the API Platform project.
5
 *
6
 * (c) Kévin Dunglas <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
13
14
namespace ApiPlatform\Core\Bridge\NelmioApiDoc\Parser;
15
16
use ApiPlatform\Core\Exception\ResourceClassNotFoundException;
17
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
18
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
19
use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
20
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
21
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
22
use Nelmio\ApiDocBundle\DataTypes;
23
use Nelmio\ApiDocBundle\Parser\ParserInterface;
24
use Symfony\Component\PropertyInfo\Type;
25
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
26
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
27
28
/**
29
 * Extract input and output information for the NelmioApiDocBundle.
30
 *
31
 * @author Kévin Dunglas <[email protected]>
32
 * @author Teoh Han Hui <[email protected]>
33
 *
34
 * @deprecated since version 2.2, to be removed in 3.0. NelmioApiDocBundle 3 has native support for API Platform.
35
 */
36
final class ApiPlatformParser implements ParserInterface
37
{
38
    public const IN_PREFIX = 'api_platform_in';
39
    public const OUT_PREFIX = 'api_platform_out';
40
    public const TYPE_IRI = 'IRI';
41
    public const TYPE_MAP = [
42
        Type::BUILTIN_TYPE_BOOL => DataTypes::BOOLEAN,
43
        Type::BUILTIN_TYPE_FLOAT => DataTypes::FLOAT,
44
        Type::BUILTIN_TYPE_INT => DataTypes::INTEGER,
45
        Type::BUILTIN_TYPE_STRING => DataTypes::STRING,
46
    ];
47
48
    private $resourceMetadataFactory;
49
    private $propertyNameCollectionFactory;
50
    private $propertyMetadataFactory;
51
    private $nameConverter;
52
53
    public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, NameConverterInterface $nameConverter = null)
54
    {
55
        @trigger_error('The '.__CLASS__.' class is deprecated since version 2.2 and will be removed in 3.0. NelmioApiDocBundle 3 has native support for API Platform.', E_USER_DEPRECATED);
56
57
        $this->resourceMetadataFactory = $resourceMetadataFactory;
58
        $this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
59
        $this->propertyMetadataFactory = $propertyMetadataFactory;
60
        $this->nameConverter = $nameConverter;
61
    }
62
63
    /**
64
     * {@inheritdoc}
65
     */
66
    public function supports(array $item)
67
    {
68
        $data = explode(':', $item['class'], 3);
69
        if (!\in_array($data[0], [self::IN_PREFIX, self::OUT_PREFIX], true)) {
70
            return false;
71
        }
72
        if (!isset($data[1])) {
73
            return false;
74
        }
75
76
        try {
77
            $this->resourceMetadataFactory->create($data[1]);
78
79
            return true;
80
        } catch (ResourceClassNotFoundException $e) {
81
            // return false
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
82
        }
83
84
        return false;
85
    }
86
87
    /**
88
     * {@inheritdoc}
89
     */
90
    public function parse(array $item): array
91
    {
92
        [$io, $resourceClass, $operationName] = explode(':', $item['class'], 3);
93
        $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
94
95
        $classOperations = $this->getGroupsForItemAndCollectionOperation($resourceMetadata, $operationName, $io);
96
97
        if (!empty($classOperations['serializer_groups'])) {
98
            return $this->getPropertyMetadata($resourceMetadata, $resourceClass, $io, [], $classOperations);
99
        }
100
101
        return $this->parseResource($resourceMetadata, $resourceClass, $io);
102
    }
103
104
    /**
105
     * Parses a class.
106
     *
107
     * @param string[] $visited
108
     */
109
    private function parseResource(ResourceMetadata $resourceMetadata, string $resourceClass, string $io, array $visited = []): array
110
    {
111
        $visited[] = $resourceClass;
112
113
        $options = [];
114
        $attributes = $resourceMetadata->getAttributes();
115
116
        if (isset($attributes['normalization_context'][AbstractNormalizer::GROUPS])) {
117
            $options['serializer_groups'] = (array) $attributes['normalization_context'][AbstractNormalizer::GROUPS];
118
        }
119
120
        if (isset($attributes['denormalization_context'][AbstractNormalizer::GROUPS])) {
121
            if (isset($options['serializer_groups'])) {
122
                $options['serializer_groups'] += $attributes['denormalization_context'][AbstractNormalizer::GROUPS];
123
            } else {
124
                $options['serializer_groups'] = (array) $attributes['denormalization_context'][AbstractNormalizer::GROUPS];
125
            }
126
        }
127
128
        return $this->getPropertyMetadata($resourceMetadata, $resourceClass, $io, $visited, $options);
129
    }
130
131
    private function getGroupsContext(ResourceMetadata $resourceMetadata, string $operationName, bool $isNormalization): array
132
    {
133
        $groupsContext = $isNormalization ? 'normalization_context' : 'denormalization_context';
134
        $itemOperationAttribute = $resourceMetadata->getItemOperationAttribute($operationName, $groupsContext, [AbstractNormalizer::GROUPS => []], true)[AbstractNormalizer::GROUPS];
135
        $collectionOperationAttribute = $resourceMetadata->getCollectionOperationAttribute($operationName, $groupsContext, [AbstractNormalizer::GROUPS => []], true)[AbstractNormalizer::GROUPS];
136
137
        return [
138
            $groupsContext => [
139
                AbstractNormalizer::GROUPS => array_merge((array) ($itemOperationAttribute ?? []), (array) ($collectionOperationAttribute ?? [])),
140
            ],
141
        ];
142
    }
143
144
    /**
145
     * Returns groups of item & collection.
146
     */
147
    private function getGroupsForItemAndCollectionOperation(ResourceMetadata $resourceMetadata, string $operationName, string $io): array
148
    {
149
        $operation = $this->getGroupsContext($resourceMetadata, $operationName, true);
150
        $operation += $this->getGroupsContext($resourceMetadata, $operationName, false);
151
152
        if (self::OUT_PREFIX === $io) {
153
            return [
154
                'serializer_groups' => !empty($operation['normalization_context']) ? $operation['normalization_context'][AbstractNormalizer::GROUPS] : [],
155
            ];
156
        }
157
158
        if (self::IN_PREFIX === $io) {
159
            return [
160
                'serializer_groups' => !empty($operation['denormalization_context']) ? $operation['denormalization_context'][AbstractNormalizer::GROUPS] : [],
161
            ];
162
        }
163
164
        return [];
165
    }
166
167
    /**
168
     * Returns a property metadata.
169
     *
170
     * @param string[] $visited
171
     * @param string[] $options
172
     */
173
    private function getPropertyMetadata(ResourceMetadata $resourceMetadata, string $resourceClass, string $io, array $visited, array $options): array
174
    {
175
        $data = [];
176
177
        foreach ($this->propertyNameCollectionFactory->create($resourceClass, $options) as $propertyName) {
178
            $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName);
179
            if (
180
                ($propertyMetadata->isReadable() && self::OUT_PREFIX === $io) ||
181
                ($propertyMetadata->isWritable() && self::IN_PREFIX === $io)
182
            ) {
183
                $normalizedPropertyName = $this->nameConverter ? $this->nameConverter->normalize($propertyName, $resourceClass) : $propertyName;
0 ignored issues
show
Unused Code introduced by
The call to Symfony\Component\Serial...rInterface::normalize() has too many arguments starting with $resourceClass. ( Ignorable by Annotation )

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

183
                $normalizedPropertyName = $this->nameConverter ? $this->nameConverter->/** @scrutinizer ignore-call */ normalize($propertyName, $resourceClass) : $propertyName;

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
184
                $data[$normalizedPropertyName] = $this->parseProperty($resourceMetadata, $propertyMetadata, $io, null, $visited);
185
            }
186
        }
187
188
        return $data;
189
    }
190
191
    /**
192
     * Parses a property.
193
     *
194
     * @param string   $io
195
     * @param string[] $visited
196
     */
197
    private function parseProperty(ResourceMetadata $resourceMetadata, PropertyMetadata $propertyMetadata, $io, Type $type = null, array $visited = []): array
198
    {
199
        $data = [
200
            'dataType' => null,
201
            'required' => $propertyMetadata->isRequired(),
202
            'description' => $propertyMetadata->getDescription(),
203
            'readonly' => !$propertyMetadata->isWritable(),
204
        ];
205
206
        if (null === $type && null === $type = $propertyMetadata->getType()) {
207
            // Default to string
208
            $data['dataType'] = DataTypes::STRING;
209
210
            return $data;
211
        }
212
213
        if ($type->isCollection()) {
214
            $data['actualType'] = DataTypes::COLLECTION;
215
216
            if ($collectionType = $type->getCollectionValueType()) {
217
                $subProperty = $this->parseProperty($resourceMetadata, $propertyMetadata, $io, $collectionType, $visited);
218
                if (self::TYPE_IRI === $subProperty['dataType']) {
219
                    $data['dataType'] = 'array of IRIs';
220
                    $data['subType'] = DataTypes::STRING;
221
222
                    return $data;
223
                }
224
225
                $data['subType'] = $subProperty['subType'] ?? null;
226
                if (isset($subProperty['children'])) {
227
                    $data['children'] = $subProperty['children'];
228
                }
229
            }
230
231
            return $data;
232
        }
233
234
        $builtinType = $type->getBuiltinType();
235
        if ('object' === $builtinType) {
236
            $className = $type->getClassName();
237
238
            if (is_a($className, \DateTimeInterface::class, true)) {
239
                $data['dataType'] = DataTypes::DATETIME;
240
                $data['format'] = sprintf('{DateTime %s}', \DateTime::RFC3339);
241
242
                return $data;
243
            }
244
245
            try {
246
                $this->resourceMetadataFactory->create($className);
0 ignored issues
show
Bug introduced by
It seems like $className can also be of type null; however, parameter $resourceClass of ApiPlatform\Core\Metadat...toryInterface::create() does only seem to accept string, 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

246
                $this->resourceMetadataFactory->create(/** @scrutinizer ignore-type */ $className);
Loading history...
247
            } catch (ResourceClassNotFoundException $e) {
248
                $data['actualType'] = DataTypes::MODEL;
249
                $data['subType'] = $className;
250
251
                return $data;
252
            }
253
254
            if (
255
                (self::OUT_PREFIX === $io && true !== $propertyMetadata->isReadableLink()) ||
256
                (self::IN_PREFIX === $io && true !== $propertyMetadata->isWritableLink())
257
            ) {
258
                $data['dataType'] = self::TYPE_IRI;
259
                $data['actualType'] = DataTypes::STRING;
260
261
                return $data;
262
            }
263
264
            $data['actualType'] = DataTypes::MODEL;
265
            $data['subType'] = $className;
266
            $data['children'] = \in_array($className, $visited, true) ? [] : $this->parseResource($resourceMetadata, $className, $io, $visited);
0 ignored issues
show
Bug introduced by
It seems like $className can also be of type null; however, parameter $resourceClass of ApiPlatform\Core\Bridge\...Parser::parseResource() does only seem to accept string, 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

266
            $data['children'] = \in_array($className, $visited, true) ? [] : $this->parseResource($resourceMetadata, /** @scrutinizer ignore-type */ $className, $io, $visited);
Loading history...
267
268
            return $data;
269
        }
270
271
        $data['dataType'] = self::TYPE_MAP[$builtinType] ?? DataTypes::STRING;
272
273
        return $data;
274
    }
275
}
276