Total Complexity | 57 |
Total Lines | 331 |
Duplicated Lines | 0 % |
Changes | 0 |
Complex classes like AbstractDocumentationNormalizer 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 AbstractDocumentationNormalizer, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
36 | abstract class AbstractDocumentationNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface |
||
37 | { |
||
38 | const FORMAT = 'json'; |
||
39 | const ATTRIBUTE_NAME = 'openapi_context'; |
||
40 | const BASE_URL = 'base_url'; |
||
41 | |||
42 | protected $resourceMetadataFactory; |
||
43 | protected $propertyNameCollectionFactory; |
||
44 | protected $propertyMetadataFactory; |
||
45 | protected $resourceClassResolver; |
||
46 | protected $operationMethodResolver; |
||
47 | protected $operationPathResolver; |
||
48 | protected $nameConverter; |
||
49 | protected $oauthEnabled; |
||
50 | protected $oauthType; |
||
51 | protected $oauthFlow; |
||
52 | protected $oauthTokenUrl; |
||
53 | protected $oauthAuthorizationUrl; |
||
54 | protected $oauthScopes; |
||
55 | protected $apiKeys; |
||
56 | protected $subresourceOperationFactory; |
||
57 | protected $paginationEnabled; |
||
58 | protected $paginationPageParameterName; |
||
59 | protected $clientItemsPerPage; |
||
60 | protected $itemsPerPageParameterName; |
||
61 | protected $paginationClientEnabled; |
||
62 | protected $paginationClientEnabledParameterName; |
||
63 | protected $formatsProvider; |
||
64 | protected $defaultContext = [self::BASE_URL => '/']; |
||
65 | |||
66 | /** |
||
67 | * {@inheritdoc} |
||
68 | */ |
||
69 | public function supportsNormalization($data, $format = null) |
||
70 | { |
||
71 | return self::FORMAT === $format && $data instanceof Documentation; |
||
72 | } |
||
73 | |||
74 | /** |
||
75 | * Gets the path for an operation. |
||
76 | * |
||
77 | * If the path ends with the optional _format parameter, it is removed |
||
78 | * as optional path parameters are not yet supported. |
||
79 | * |
||
80 | * @see https://github.com/OAI/OpenAPI-Specification/issues/93 |
||
81 | */ |
||
82 | protected function getPath(string $resourceShortName, string $operationName, array $operation, string $operationType): string |
||
83 | { |
||
84 | $path = $this->operationPathResolver->resolveOperationPath($resourceShortName, $operation, $operationType, $operationName); |
||
85 | if ('.{_format}' === substr($path, -10)) { |
||
86 | $path = substr($path, 0, -10); |
||
87 | } |
||
88 | |||
89 | return $path; |
||
90 | } |
||
91 | |||
92 | /** |
||
93 | * Gets the Swagger's type corresponding to the given PHP's type. |
||
94 | * |
||
95 | * @param string $className |
||
96 | * @param bool $readableLink |
||
97 | */ |
||
98 | protected function getType(string $type, bool $isCollection, string $className = null, bool $readableLink = null, \ArrayObject $definitions, array $serializerContext = null): array |
||
99 | { |
||
100 | if ($isCollection) { |
||
101 | return ['type' => 'array', 'items' => $this->getType($type, false, $className, $readableLink, $definitions, $serializerContext)]; |
||
102 | } |
||
103 | |||
104 | if (Type::BUILTIN_TYPE_STRING === $type) { |
||
105 | return ['type' => 'string']; |
||
106 | } |
||
107 | |||
108 | if (Type::BUILTIN_TYPE_INT === $type) { |
||
109 | return ['type' => 'integer']; |
||
110 | } |
||
111 | |||
112 | if (Type::BUILTIN_TYPE_FLOAT === $type) { |
||
113 | return ['type' => 'number']; |
||
114 | } |
||
115 | |||
116 | if (Type::BUILTIN_TYPE_BOOL === $type) { |
||
117 | return ['type' => 'boolean']; |
||
118 | } |
||
119 | |||
120 | if (Type::BUILTIN_TYPE_OBJECT === $type) { |
||
121 | if (null === $className) { |
||
122 | return ['type' => 'string']; |
||
123 | } |
||
124 | |||
125 | if (is_subclass_of($className, \DateTimeInterface::class)) { |
||
126 | return ['type' => 'string', 'format' => 'date-time']; |
||
127 | } |
||
128 | |||
129 | if (!$this->resourceClassResolver->isResourceClass($className)) { |
||
130 | return ['type' => 'string']; |
||
131 | } |
||
132 | |||
133 | if (true === $readableLink) { |
||
134 | return ['$ref' => sprintf('#/definitions/%s', $this->getDefinition($definitions, |
||
135 | $this->resourceMetadataFactory->create($className), |
||
136 | $className, $serializerContext) |
||
137 | )]; |
||
138 | } |
||
139 | } |
||
140 | |||
141 | return ['type' => 'string']; |
||
142 | } |
||
143 | |||
144 | /** |
||
145 | * @return array|null |
||
146 | */ |
||
147 | protected function getSerializerContext(string $operationType, bool $denormalization, ResourceMetadata $resourceMetadata, string $operationName) |
||
156 | } |
||
157 | |||
158 | protected function getDefinition(\ArrayObject $definitions, ResourceMetadata $resourceMetadata, string $resourceClass, array $serializerContext = null): string |
||
159 | { |
||
160 | if (isset($serializerContext[DocumentationNormalizer::SWAGGER_DEFINITION_NAME])) { |
||
161 | $definitionKey = sprintf('%s-%s', $resourceMetadata->getShortName(), $serializerContext[DocumentationNormalizer::SWAGGER_DEFINITION_NAME]); |
||
162 | } else { |
||
163 | $definitionKey = $this->getDefinitionKey($resourceMetadata->getShortName(), (array) ($serializerContext[AbstractNormalizer::GROUPS] ?? [])); |
||
164 | } |
||
165 | |||
166 | if (!isset($definitions[$definitionKey])) { |
||
167 | $definitions[$definitionKey] = []; // Initialize first to prevent infinite loop |
||
168 | $definitions[$definitionKey] = $this->getDefinitionSchema($resourceClass, $resourceMetadata, $definitions, $serializerContext); |
||
169 | } |
||
170 | |||
171 | return $definitionKey; |
||
172 | } |
||
173 | |||
174 | protected function getDefinitionKey(string $resourceShortName, array $groups): string |
||
175 | { |
||
176 | return $groups ? sprintf('%s-%s', $resourceShortName, implode('_', $groups)) : $resourceShortName; |
||
177 | } |
||
178 | |||
179 | /** |
||
180 | * Gets a definition Schema Object. |
||
181 | * |
||
182 | * @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#schemaObject |
||
183 | */ |
||
184 | protected function getDefinitionSchema(string $resourceClass, ResourceMetadata $resourceMetadata, \ArrayObject $definitions, array $serializerContext = null): \ArrayObject |
||
185 | { |
||
186 | $definitionSchema = new \ArrayObject(['type' => 'object']); |
||
187 | |||
188 | if (null !== $description = $resourceMetadata->getDescription()) { |
||
189 | $definitionSchema['description'] = $description; |
||
190 | } |
||
191 | |||
192 | if (null !== $iri = $resourceMetadata->getIri()) { |
||
193 | $definitionSchema['externalDocs'] = ['url' => $iri]; |
||
194 | } |
||
195 | |||
196 | $options = isset($serializerContext[AbstractNormalizer::GROUPS]) ? ['serializer_groups' => $serializerContext[AbstractNormalizer::GROUPS]] : []; |
||
197 | foreach ($this->propertyNameCollectionFactory->create($resourceClass, $options) as $propertyName) { |
||
198 | $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName); |
||
199 | $normalizedPropertyName = $this->nameConverter ? $this->nameConverter->normalize($propertyName, $resourceClass, self::FORMAT, $serializerContext ?? []) : $propertyName; |
||
200 | if ($propertyMetadata->isRequired()) { |
||
201 | $definitionSchema['required'][] = $normalizedPropertyName; |
||
202 | } |
||
203 | |||
204 | $definitionSchema['properties'][$normalizedPropertyName] = $this->getPropertySchema($propertyMetadata, $definitions, $serializerContext); |
||
205 | } |
||
206 | |||
207 | return $definitionSchema; |
||
208 | } |
||
209 | |||
210 | /** |
||
211 | * Gets a property Schema Object. |
||
212 | * |
||
213 | * @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#schemaObject |
||
214 | */ |
||
215 | protected function getPropertySchema(PropertyMetadata $propertyMetadata, \ArrayObject $definitions, array $serializerContext = null): \ArrayObject |
||
216 | { |
||
217 | $propertySchema = new \ArrayObject($propertyMetadata->getAttributes()[static::ATTRIBUTE_NAME] ?? []); |
||
218 | |||
219 | if (false === $propertyMetadata->isWritable() && !$propertyMetadata->isInitializable()) { |
||
220 | $propertySchema['readOnly'] = true; |
||
221 | } |
||
222 | |||
223 | if (null !== $description = $propertyMetadata->getDescription()) { |
||
224 | $propertySchema['description'] = $description; |
||
225 | } |
||
226 | |||
227 | if (null === $type = $propertyMetadata->getType()) { |
||
228 | return $propertySchema; |
||
229 | } |
||
230 | |||
231 | $isCollection = $type->isCollection(); |
||
232 | if (null === $valueType = $isCollection ? $type->getCollectionValueType() : $type) { |
||
233 | $builtinType = 'string'; |
||
234 | $className = null; |
||
235 | } else { |
||
236 | $builtinType = $valueType->getBuiltinType(); |
||
237 | $className = $valueType->getClassName(); |
||
238 | } |
||
239 | |||
240 | $valueSchema = $this->getType($builtinType, $isCollection, $className, $propertyMetadata->isReadableLink(), $definitions, $serializerContext); |
||
241 | |||
242 | return new \ArrayObject((array) $propertySchema + $valueSchema); |
||
243 | } |
||
244 | |||
245 | /** |
||
246 | * Gets a path Operation Object. |
||
247 | * |
||
248 | * @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#operation-object |
||
249 | * |
||
250 | * @param string[] $mimeTypes |
||
251 | */ |
||
252 | protected function getPathOperation(string $operationName, array $operation, string $method, string $operationType, string $resourceClass, ResourceMetadata $resourceMetadata, array $mimeTypes, \ArrayObject $definitions): \ArrayObject |
||
280 | } |
||
281 | |||
282 | abstract protected function updateGetOperation(\ArrayObject $pathOperation, array $mimeTypes, string $operationType, ResourceMetadata $resourceMetadata, string $resourceClass, string $resourceShortName, string $operationName, \ArrayObject $definitions); |
||
283 | |||
284 | abstract protected function updatePostOperation(\ArrayObject $pathOperation, array $mimeTypes, string $operationType, ResourceMetadata $resourceMetadata, string $resourceClass, string $resourceShortName, string $operationName, \ArrayObject $definitions); |
||
285 | |||
286 | abstract protected function updatePutOperation(\ArrayObject $pathOperation, array $mimeTypes, string $operationType, ResourceMetadata $resourceMetadata, string $resourceClass, string $resourceShortName, string $operationName, \ArrayObject $definitions); |
||
287 | |||
288 | abstract protected function updateDeleteOperation(\ArrayObject $pathOperation, string $resourceShortName); |
||
289 | |||
290 | /** |
||
291 | * Updates the list of entries in the paths collection. |
||
292 | */ |
||
293 | protected function addPaths(\ArrayObject $paths, \ArrayObject $definitions, string $resourceClass, string $resourceShortName, ResourceMetadata $resourceMetadata, array $mimeTypes, string $operationType) |
||
294 | { |
||
295 | if (null === $operations = OperationType::COLLECTION === $operationType ? $resourceMetadata->getCollectionOperations() : $resourceMetadata->getItemOperations()) { |
||
296 | return; |
||
297 | } |
||
298 | |||
299 | foreach ($operations as $operationName => $operation) { |
||
300 | $path = $this->getPath($resourceShortName, $operationName, $operation, $operationType); |
||
301 | $method = OperationType::ITEM === $operationType ? $this->operationMethodResolver->getItemOperationMethod($resourceClass, $operationName) : $this->operationMethodResolver->getCollectionOperationMethod($resourceClass, $operationName); |
||
302 | |||
303 | $paths[$path][strtolower($method)] = $this->getPathOperation($operationName, $operation, $method, $operationType, $resourceClass, $resourceMetadata, $mimeTypes, $definitions); |
||
304 | } |
||
305 | } |
||
306 | |||
307 | /** |
||
308 | * Returns pagination parameters for the "get" collection operation. |
||
309 | */ |
||
310 | protected function getPaginationParameters(): array |
||
311 | { |
||
312 | return [ |
||
313 | 'name' => $this->paginationPageParameterName, |
||
314 | 'in' => 'query', |
||
315 | 'required' => false, |
||
316 | 'type' => 'integer', |
||
317 | 'description' => 'The collection page number', |
||
318 | ]; |
||
319 | } |
||
320 | |||
321 | /** |
||
322 | * Returns enable pagination parameter for the "get" collection operation. |
||
323 | */ |
||
324 | protected function getPaginationClientEnabledParameters(): array |
||
325 | { |
||
326 | return [ |
||
327 | 'name' => $this->paginationClientEnabledParameterName, |
||
328 | 'in' => 'query', |
||
329 | 'required' => false, |
||
330 | 'type' => 'boolean', |
||
331 | 'description' => 'Enable or disable pagination', |
||
332 | ]; |
||
333 | } |
||
334 | |||
335 | /** |
||
336 | * Returns items per page parameters for the "get" collection operation. |
||
337 | */ |
||
338 | protected function getItemsPerPageParameters(): array |
||
339 | { |
||
340 | return [ |
||
341 | 'name' => $this->itemsPerPageParameterName, |
||
342 | 'in' => 'query', |
||
343 | 'required' => false, |
||
344 | 'type' => 'integer', |
||
345 | 'description' => 'The number of items per page', |
||
346 | ]; |
||
347 | } |
||
348 | |||
349 | /** |
||
350 | * {@inheritdoc} |
||
351 | */ |
||
352 | public function hasCacheableSupportsMethod(): bool |
||
355 | } |
||
356 | |||
357 | protected function extractMimeTypes(array $responseFormats): array |
||
367 | } |
||
368 | } |
||
369 |
Let?s assume that you have a directory layout like this:
and let?s assume the following content of
Bar.php
:If both files
OtherDir/Foo.php
andSomeDir/Foo.php
are loaded in the same runtime, you will see a PHP error such as the following:PHP Fatal error: Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php
However, as
OtherDir/Foo.php
does not necessarily have to be loaded and the error is only triggered if it is loaded beforeOtherDir/Bar.php
, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias: