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
|
|||||
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; |
||||
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); |
||||
0 ignored issues
–
show
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
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); |
||||
267 | |||||
268 | return $data; |
||||
269 | } |
||||
270 | |||||
271 | $data['dataType'] = self::TYPE_MAP[$builtinType] ?? DataTypes::STRING; |
||||
272 | |||||
273 | return $data; |
||||
274 | } |
||||
275 | } |
||||
276 |
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.