Passed
Push — master ( 8bd912...d93388 )
by Alan
06:58 queued 02:20
created

Bridge/NelmioApiDoc/Parser/ApiPlatformParser.php (1 issue)

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
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'] = $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'] = $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
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_subclass_of($className, \DateTimeInterface::class)) {
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);
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);
267
268
            return $data;
269
        }
270
271
        $data['dataType'] = self::TYPE_MAP[$builtinType] ?? DataTypes::STRING;
272
273
        return $data;
274
    }
275
}
276