These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
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 | |||
27 | /** |
||
28 | * Extract input and output information for the NelmioApiDocBundle. |
||
29 | * |
||
30 | * @author Kévin Dunglas <[email protected]> |
||
31 | * @author Teoh Han Hui <[email protected]> |
||
32 | */ |
||
33 | final class ApiPlatformParser implements ParserInterface |
||
34 | { |
||
35 | const IN_PREFIX = 'api_platform_in'; |
||
36 | const OUT_PREFIX = 'api_platform_out'; |
||
37 | const TYPE_IRI = 'IRI'; |
||
38 | const TYPE_MAP = [ |
||
39 | Type::BUILTIN_TYPE_BOOL => DataTypes::BOOLEAN, |
||
40 | Type::BUILTIN_TYPE_FLOAT => DataTypes::FLOAT, |
||
41 | Type::BUILTIN_TYPE_INT => DataTypes::INTEGER, |
||
42 | Type::BUILTIN_TYPE_STRING => DataTypes::STRING, |
||
43 | ]; |
||
44 | |||
45 | private $resourceMetadataFactory; |
||
46 | private $propertyNameCollectionFactory; |
||
47 | private $propertyMetadataFactory; |
||
48 | private $nameConverter; |
||
49 | |||
50 | public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, NameConverterInterface $nameConverter = null) |
||
51 | { |
||
52 | $this->resourceMetadataFactory = $resourceMetadataFactory; |
||
53 | $this->propertyNameCollectionFactory = $propertyNameCollectionFactory; |
||
54 | $this->propertyMetadataFactory = $propertyMetadataFactory; |
||
55 | $this->nameConverter = $nameConverter; |
||
56 | } |
||
57 | |||
58 | /** |
||
59 | * {@inheritdoc} |
||
60 | */ |
||
61 | public function supports(array $item) |
||
62 | { |
||
63 | $data = explode(':', $item['class'], 3); |
||
64 | if (!in_array($data[0], [self::IN_PREFIX, self::OUT_PREFIX], true)) { |
||
65 | return false; |
||
66 | } |
||
67 | if (!isset($data[1])) { |
||
68 | return false; |
||
69 | } |
||
70 | |||
71 | try { |
||
72 | $this->resourceMetadataFactory->create($data[1]); |
||
73 | |||
74 | return true; |
||
75 | } catch (ResourceClassNotFoundException $e) { |
||
76 | // return false |
||
77 | } |
||
78 | |||
79 | return false; |
||
80 | } |
||
81 | |||
82 | /** |
||
83 | * {@inheritdoc} |
||
84 | */ |
||
85 | public function parse(array $item): array |
||
86 | { |
||
87 | list($io, $resourceClass, $operationName) = explode(':', $item['class'], 3); |
||
88 | $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); |
||
89 | |||
90 | $classOperations = $this->getGroupsForItemAndCollectionOperation($resourceMetadata, $operationName, $io); |
||
91 | |||
92 | if (!empty($classOperations['serializer_groups'])) { |
||
93 | return $this->getPropertyMetadata($resourceMetadata, $resourceClass, $io, [], $classOperations); |
||
94 | } |
||
95 | |||
96 | return $this->parseResource($resourceMetadata, $resourceClass, $io); |
||
97 | } |
||
98 | |||
99 | /** |
||
100 | * Parses a class. |
||
101 | * |
||
102 | * @param ResourceMetadata $resourceMetadata |
||
103 | * @param string $resourceClass |
||
104 | * @param string $io |
||
105 | * @param string[] $visited |
||
106 | * |
||
107 | * @return array |
||
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 | View Code Duplication | if (isset($attributes['normalization_context']['groups'])) { |
|
117 | $options['serializer_groups'] = $attributes['normalization_context']['groups']; |
||
118 | } |
||
119 | |||
120 | if (isset($attributes['denormalization_context']['groups'])) { |
||
121 | View Code Duplication | if (isset($options['serializer_groups'])) { |
|
0 ignored issues
–
show
|
|||
122 | $options['serializer_groups'] += $attributes['denormalization_context']['groups']; |
||
123 | } else { |
||
124 | $options['serializer_groups'] = $attributes['denormalization_context']['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) |
||
132 | { |
||
133 | $groupsContext = $isNormalization ? 'normalization_context' : 'denormalization_context'; |
||
134 | $itemOperationAttribute = $resourceMetadata->getItemOperationAttribute($operationName, $groupsContext, ['groups' => []], true)['groups']; |
||
135 | $collectionOperationAttribute = $resourceMetadata->getCollectionOperationAttribute($operationName, $groupsContext, ['groups' => []], true)['groups']; |
||
136 | |||
137 | return [ |
||
138 | $groupsContext => [ |
||
139 | 'groups' => array_merge($itemOperationAttribute ?? [], $collectionOperationAttribute ?? []), |
||
140 | ], |
||
141 | ]; |
||
142 | } |
||
143 | |||
144 | /** |
||
145 | * Returns groups of item & collection. |
||
146 | * |
||
147 | * @param ResourceMetadata $resourceMetadata |
||
148 | * @param string $operationName |
||
149 | * @param string $io |
||
150 | * |
||
151 | * @return array |
||
152 | */ |
||
153 | private function getGroupsForItemAndCollectionOperation(ResourceMetadata $resourceMetadata, string $operationName, string $io): array |
||
154 | { |
||
155 | $operation = $this->getGroupsContext($resourceMetadata, $operationName, true); |
||
156 | $operation += $this->getGroupsContext($resourceMetadata, $operationName, false); |
||
157 | |||
158 | View Code Duplication | if (self::OUT_PREFIX === $io) { |
|
159 | return [ |
||
160 | 'serializer_groups' => !empty($operation['normalization_context']) ? $operation['normalization_context']['groups'] : [], |
||
161 | ]; |
||
162 | } |
||
163 | |||
164 | View Code Duplication | if (self::IN_PREFIX === $io) { |
|
165 | return [ |
||
166 | 'serializer_groups' => !empty($operation['denormalization_context']) ? $operation['denormalization_context']['groups'] : [], |
||
167 | ]; |
||
168 | } |
||
169 | |||
170 | return []; |
||
171 | } |
||
172 | |||
173 | /** |
||
174 | * Returns a property metadata. |
||
175 | * |
||
176 | * @param ResourceMetadata $resourceMetadata |
||
177 | * @param string $resourceClass |
||
178 | * @param string $io |
||
179 | * @param string[] $visited |
||
180 | * @param string[] $options |
||
181 | * |
||
182 | * @return array |
||
183 | */ |
||
184 | private function getPropertyMetadata(ResourceMetadata $resourceMetadata, string $resourceClass, string $io, array $visited, array $options): array |
||
185 | { |
||
186 | $data = []; |
||
187 | |||
188 | foreach ($this->propertyNameCollectionFactory->create($resourceClass, $options) as $propertyName) { |
||
189 | $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName); |
||
190 | if ( |
||
191 | ($propertyMetadata->isReadable() && self::OUT_PREFIX === $io) || |
||
192 | ($propertyMetadata->isWritable() && self::IN_PREFIX === $io) |
||
193 | ) { |
||
194 | $normalizedPropertyName = $this->nameConverter ? $this->nameConverter->normalize($propertyName) : $propertyName; |
||
195 | $data[$normalizedPropertyName] = $this->parseProperty($resourceMetadata, $propertyMetadata, $io, null, $visited); |
||
196 | } |
||
197 | } |
||
198 | |||
199 | return $data; |
||
200 | } |
||
201 | |||
202 | /** |
||
203 | * Parses a property. |
||
204 | * |
||
205 | * @param ResourceMetadata $resourceMetadata |
||
206 | * @param PropertyMetadata $propertyMetadata |
||
207 | * @param string $io |
||
208 | * @param Type|null $type |
||
209 | * @param string[] $visited |
||
210 | * |
||
211 | * @return array |
||
212 | */ |
||
213 | private function parseProperty(ResourceMetadata $resourceMetadata, PropertyMetadata $propertyMetadata, $io, Type $type = null, array $visited = []) |
||
214 | { |
||
215 | $data = [ |
||
216 | 'dataType' => null, |
||
217 | 'required' => $propertyMetadata->isRequired(), |
||
218 | 'description' => $propertyMetadata->getDescription(), |
||
219 | 'readonly' => !$propertyMetadata->isWritable(), |
||
220 | ]; |
||
221 | |||
222 | if (null === $type && null === $type = $propertyMetadata->getType()) { |
||
223 | // Default to string |
||
224 | $data['dataType'] = DataTypes::STRING; |
||
225 | |||
226 | return $data; |
||
227 | } |
||
228 | |||
229 | if ($type->isCollection()) { |
||
230 | $data['actualType'] = DataTypes::COLLECTION; |
||
231 | |||
232 | if ($collectionType = $type->getCollectionValueType()) { |
||
233 | $subProperty = $this->parseProperty($resourceMetadata, $propertyMetadata, $io, $collectionType, $visited); |
||
234 | if (self::TYPE_IRI === $subProperty['dataType']) { |
||
235 | $data['dataType'] = 'array of IRIs'; |
||
236 | $data['subType'] = DataTypes::STRING; |
||
237 | |||
238 | return $data; |
||
239 | } |
||
240 | |||
241 | $data['subType'] = $subProperty['subType'] ?? null; |
||
242 | if (isset($subProperty['children'])) { |
||
243 | $data['children'] = $subProperty['children']; |
||
244 | } |
||
245 | } |
||
246 | |||
247 | return $data; |
||
248 | } |
||
249 | |||
250 | $builtinType = $type->getBuiltinType(); |
||
251 | if ('object' === $builtinType) { |
||
252 | $className = $type->getClassName(); |
||
253 | |||
254 | if (is_subclass_of($className, \DateTimeInterface::class)) { |
||
255 | $data['dataType'] = DataTypes::DATETIME; |
||
256 | $data['format'] = sprintf('{DateTime %s}', \DateTime::RFC3339); |
||
257 | |||
258 | return $data; |
||
259 | } |
||
260 | |||
261 | try { |
||
262 | $this->resourceMetadataFactory->create($className); |
||
263 | } catch (ResourceClassNotFoundException $e) { |
||
264 | $data['actualType'] = DataTypes::MODEL; |
||
265 | $data['subType'] = $className; |
||
266 | |||
267 | return $data; |
||
268 | } |
||
269 | |||
270 | if ( |
||
271 | (self::OUT_PREFIX === $io && true !== $propertyMetadata->isReadableLink()) || |
||
272 | (self::IN_PREFIX === $io && true !== $propertyMetadata->isWritableLink()) |
||
273 | ) { |
||
274 | $data['dataType'] = self::TYPE_IRI; |
||
275 | $data['actualType'] = DataTypes::STRING; |
||
276 | |||
277 | return $data; |
||
278 | } |
||
279 | |||
280 | $data['actualType'] = DataTypes::MODEL; |
||
281 | $data['subType'] = $className; |
||
282 | $data['children'] = in_array($className, $visited, true) ? [] : $this->parseResource($resourceMetadata, $className, $io, $visited); |
||
283 | |||
284 | return $data; |
||
285 | } |
||
286 | |||
287 | $data['dataType'] = self::TYPE_MAP[$builtinType] ?? DataTypes::STRING; |
||
288 | |||
289 | return $data; |
||
290 | } |
||
291 | } |
||
292 |
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.