Total Complexity | 48 |
Total Lines | 356 |
Duplicated Lines | 0 % |
Changes | 1 | ||
Bugs | 0 | Features | 0 |
Complex classes like SchemaGenerator 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 SchemaGenerator, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
17 | class SchemaGenerator |
||
18 | { |
||
19 | private const MAX_RECURSION = 2; |
||
20 | |||
21 | /** |
||
22 | * @var ClassMetadataFactory |
||
23 | */ |
||
24 | private $classMetadataFactory; |
||
25 | |||
26 | /** |
||
27 | * @var PropertyInfoExtractor |
||
28 | */ |
||
29 | private $propertyInfoExtractor; |
||
30 | |||
31 | /** |
||
32 | * @var ClassResourceConverter |
||
33 | */ |
||
34 | private $converter; |
||
35 | |||
36 | /** |
||
37 | * @var NameConverterInterface |
||
38 | */ |
||
39 | private $nameConverter; |
||
40 | |||
41 | /** |
||
42 | * @var Schema[] |
||
43 | */ |
||
44 | private $alreadyDefined = []; |
||
45 | |||
46 | /** |
||
47 | * @var Schema[] |
||
48 | */ |
||
49 | private $predefined = []; |
||
50 | |||
51 | /** |
||
52 | * @var callable[] |
||
53 | */ |
||
54 | private $schemaCallbacks = []; |
||
55 | |||
56 | /** |
||
57 | * @var bool[] |
||
58 | */ |
||
59 | private $building = []; |
||
60 | |||
61 | /** |
||
62 | * @param ClassMetadataFactory $classMetadataFactory |
||
63 | * @param PropertyInfoExtractor $propertyInfoExtractor |
||
64 | * @param ClassResourceConverter $converter |
||
65 | * @param NameConverterInterface $nameConverter |
||
66 | * @param callable[] $schemaCallbacks |
||
67 | */ |
||
68 | public function __construct( |
||
69 | ClassMetadataFactory $classMetadataFactory, |
||
70 | PropertyInfoExtractor $propertyInfoExtractor, |
||
71 | ClassResourceConverter $converter, |
||
72 | NameConverterInterface $nameConverter, |
||
73 | array $schemaCallbacks = [] |
||
74 | ) { |
||
75 | $this->classMetadataFactory = $classMetadataFactory; |
||
76 | $this->propertyInfoExtractor = $propertyInfoExtractor; |
||
77 | $this->converter = $converter; |
||
78 | $this->nameConverter = $nameConverter; |
||
79 | $this->schemaCallbacks = $schemaCallbacks; |
||
80 | } |
||
81 | |||
82 | /** |
||
83 | * Define a resource class and Schema manually. |
||
84 | * @param string $resourceClass |
||
85 | * @param Schema $schema |
||
86 | * @return SchemaGenerator |
||
87 | */ |
||
88 | public function defineSchemaForResource(string $resourceClass, Schema $schema): self |
||
89 | { |
||
90 | $this->predefined[$resourceClass] = $schema; |
||
91 | $this->alreadyDefined = []; |
||
92 | |||
93 | return $this; |
||
94 | } |
||
95 | |||
96 | /** |
||
97 | * Define an OpenAPI discriminator spec for an interface or base class that have a discriminator column. |
||
98 | * |
||
99 | * @param string $resourceInterface |
||
100 | * @param string $discriminatorColumn |
||
101 | * @param array $subclasses |
||
102 | * @param string $operation |
||
103 | * @param string[] $groups |
||
104 | * @return Schema |
||
105 | */ |
||
106 | public function defineSchemaForPolymorphicObject( |
||
107 | string $resourceInterface, |
||
108 | string $discriminatorColumn, |
||
109 | array $subclasses, |
||
110 | string $operation = 'get', |
||
111 | array $groups = ['get', 'read'] |
||
112 | ): Schema { |
||
113 | $cacheKey = $this->getCacheKey($resourceInterface, $operation, $groups); |
||
114 | /** @var Schema[] $subschemas */ |
||
115 | $subschemas = []; |
||
116 | $discriminatorMapping = []; |
||
117 | foreach ($subclasses as $keyValue => $subclass) { |
||
118 | $subschemas[$subclass] = $discriminatorMapping[$keyValue] = $this->createSchema($subclass, $operation, $groups); |
||
119 | $properties = $subschemas[$subclass]->properties; |
||
120 | if (isset($properties[$discriminatorColumn])) { |
||
121 | $properties[$discriminatorColumn]->default = $keyValue; |
||
122 | $properties[$discriminatorColumn]->example = $keyValue; |
||
123 | } else { |
||
124 | $properties[$discriminatorColumn] = new Schema([ |
||
125 | 'type' => 'string', |
||
126 | 'default' => $keyValue, |
||
127 | 'example' => $keyValue |
||
128 | ]); |
||
129 | } |
||
130 | $subschemas[$subclass]->properties = $properties; |
||
131 | } |
||
132 | $this->alreadyDefined[$cacheKey . ',0'] = new Schema([ |
||
133 | 'type' => 'object', |
||
134 | 'properties' => [ |
||
135 | $discriminatorColumn => new Schema(['type' => 'string']), |
||
136 | ], |
||
137 | 'oneOf' => array_values($subschemas), |
||
138 | 'discriminator' => new Discriminator($discriminatorColumn, $discriminatorMapping) |
||
139 | ]); |
||
140 | for ($i = 1; $i < self::MAX_RECURSION; $i++) { |
||
141 | $this->alreadyDefined[$cacheKey . ',' . $i] = $this->alreadyDefined[$cacheKey . ',0']; |
||
142 | } |
||
143 | return $this->alreadyDefined[$cacheKey . ',0']; |
||
144 | } |
||
145 | |||
146 | /** |
||
147 | * Creates a schema recursively. |
||
148 | * |
||
149 | * @param string $resourceClass |
||
150 | * @param string $operation |
||
151 | * @param string[] $groups |
||
152 | * @param int $recursion |
||
153 | * @return Schema |
||
154 | */ |
||
155 | private function createSchemaRecursive(string $resourceClass, string $operation, array $groups, int $recursion = 0): Schema |
||
156 | { |
||
157 | $metaData = $this->classMetadataFactory->getMetadataFor($resourceClass); |
||
158 | $cacheKey = $this->getCacheKey($resourceClass, $operation, $groups) . ',' . $recursion; |
||
159 | if (isset($this->alreadyDefined[$cacheKey])) { |
||
160 | return $this->alreadyDefined[$cacheKey]; |
||
161 | } |
||
162 | |||
163 | foreach ($this->predefined as $className => $schema) { |
||
164 | if (is_a($resourceClass, $className, true)) { |
||
165 | $this->alreadyDefined[$cacheKey] = $schema; |
||
166 | |||
167 | return $this->alreadyDefined[$cacheKey]; |
||
168 | } |
||
169 | } |
||
170 | |||
171 | if ($predefinedSchema = $this->runCallbacks($cacheKey, $resourceClass, $operation, $groups, $recursion)) { |
||
172 | return $this->alreadyDefined[$cacheKey] = $predefinedSchema; |
||
173 | } |
||
174 | |||
175 | $name = $this->converter->normalize($resourceClass); |
||
176 | $this->alreadyDefined[$cacheKey] = $schema = new Schema([ |
||
177 | 'title' => $name, |
||
178 | 'description' => $name . ' ' . $operation, |
||
179 | 'type' => 'object', |
||
180 | ]); |
||
181 | if ($groups) { |
||
|
|||
182 | $schema->description .= ' for groups ' . implode(', ', $groups); |
||
183 | } |
||
184 | $properties = []; |
||
185 | foreach ($metaData->getAttributesMetadata() as $attributeMetadata) { |
||
186 | $name = $attributeMetadata->getSerializedName() ?? $this->nameConverter->normalize($attributeMetadata->getName()); |
||
187 | if (!$this->isPropertyApplicable($resourceClass, $attributeMetadata, $operation, $groups)) { |
||
188 | continue; |
||
189 | } |
||
190 | $properties[$name] = new Schema([ |
||
191 | 'type' => 'string', |
||
192 | 'nullable' => true, |
||
193 | ]); |
||
194 | $types = $this->propertyInfoExtractor->getTypes($resourceClass, $attributeMetadata->getName()) ?? []; |
||
195 | $type = reset($types); |
||
196 | if ($type instanceof Type && ($recursion < (1 + self::MAX_RECURSION))) { |
||
197 | $properties[$name] = $this->convertTypeToSchema($type, $operation, $groups, $recursion); |
||
198 | } |
||
199 | if (!$properties[$name]->description) { |
||
200 | $properties[$name]->description = $this->propertyInfoExtractor->getShortDescription( |
||
201 | $resourceClass, |
||
202 | $attributeMetadata->getName() |
||
203 | ); |
||
204 | } |
||
205 | } |
||
206 | $schema->properties = $properties; |
||
207 | $this->alreadyDefined[$cacheKey] = $schema; |
||
208 | |||
209 | return $schema; |
||
210 | } |
||
211 | |||
212 | /** |
||
213 | * Iterate over a list of callbacks to see if they provide a schema for this resource class. |
||
214 | * |
||
215 | * @param string $cacheKey |
||
216 | * @param string $resourceClass |
||
217 | * @param string $operation |
||
218 | * @param array $groups |
||
219 | * @param int $recursion |
||
220 | * |
||
221 | * @return Schema|null |
||
222 | */ |
||
223 | private function runCallbacks(string $cacheKey, string $resourceClass, string $operation, array $groups, int $recursion): ?Schema |
||
224 | { |
||
225 | if (!empty($this->building[$cacheKey])) { |
||
226 | return null; |
||
227 | } |
||
228 | $this->building[$cacheKey] = true; |
||
229 | try { |
||
230 | // specifically defined: just call it. |
||
231 | if (isset($this->schemaCallbacks[$resourceClass])) { |
||
232 | return $this->schemaCallbacks[$resourceClass]($resourceClass, $operation, $groups, $recursion, $this); |
||
233 | } |
||
234 | foreach ($this->schemaCallbacks as $classDeclaration => $callable) { |
||
235 | if (is_a($resourceClass, $classDeclaration, true)) { |
||
236 | $res = $callable($resourceClass, $operation, $groups, $recursion, $this); |
||
237 | if ($res instanceof Schema) { |
||
238 | return $res; |
||
239 | } |
||
240 | } |
||
241 | } |
||
242 | return null; |
||
243 | } finally { |
||
244 | unset($this->building[$cacheKey]); |
||
245 | } |
||
246 | } |
||
247 | |||
248 | /** |
||
249 | * Convert Type into Schema. |
||
250 | * |
||
251 | * @param Type $type |
||
252 | * @param string $operation |
||
253 | * @param string[] $groups |
||
254 | * @param int $recursion |
||
255 | * |
||
256 | * @return Schema |
||
257 | */ |
||
258 | private function convertTypeToSchema(Type $type, string $operation, array $groups, int $recursion): Schema |
||
259 | { |
||
260 | $propertySchema = new Schema([ |
||
261 | 'type' => 'string', |
||
262 | 'nullable' => true, |
||
263 | ]); |
||
264 | $propertySchema->type = $this->translateType($type->getBuiltinType()); |
||
265 | if (!$type->isNullable()) { |
||
266 | $propertySchema->nullable = false; |
||
267 | } |
||
268 | if ($type->isCollection()) { |
||
269 | $propertySchema->type = 'array'; |
||
270 | $propertySchema->items = new Schema([ |
||
271 | 'oneOf' => [ |
||
272 | new Schema(['type' => 'string', 'nullable' => true]), |
||
273 | new Schema(['type' => 'integer']), |
||
274 | new Schema(['type' => 'boolean']), |
||
275 | ], |
||
276 | ]); |
||
277 | $arrayType = $type->getCollectionValueType(); |
||
278 | if ($arrayType) { |
||
279 | if ($arrayType->getClassName()) { |
||
280 | $propertySchema->items = $this->createSchemaRecursive($arrayType->getClassName(), $operation, $groups, $recursion + 1); |
||
281 | } elseif ($arrayType->getBuiltinType()) { |
||
282 | $type = $this->translateType($arrayType->getBuiltinType()); |
||
283 | $propertySchema->items = new Schema([ |
||
284 | 'type' => $type, |
||
285 | 'format' => ($type === 'number') ? $arrayType->getBuiltinType() : null, |
||
286 | ]); |
||
287 | } |
||
288 | } |
||
289 | return $propertySchema; |
||
290 | } |
||
291 | if ($propertySchema->type === 'number') { |
||
292 | $propertySchema->format = $type->getBuiltinType(); |
||
293 | } |
||
294 | $className = $type->getClassName(); |
||
295 | if ('object' === $type->getBuiltinType() && $recursion < self::MAX_RECURSION && !is_null($className)) { |
||
296 | return $this->createSchemaRecursive($className, $operation, $groups, $recursion + 1); |
||
297 | } |
||
298 | return $propertySchema; |
||
299 | } |
||
300 | |||
301 | /** |
||
302 | * Returns true if a property is applicable for a specific operation and a specific serialization group. |
||
303 | * |
||
304 | * @param string $resourceClass |
||
305 | * @param AttributeMetadataInterface $attributeMetadata |
||
306 | * @param string $operation |
||
307 | * @param string[] $groups |
||
308 | * @return bool |
||
309 | */ |
||
310 | private function isPropertyApplicable(string $resourceClass, AttributeMetadataInterface $attributeMetadata, string $operation, array $groups): bool |
||
311 | { |
||
312 | if (!array_intersect($attributeMetadata->getGroups(), $groups)) { |
||
313 | return false; |
||
314 | } |
||
315 | switch ($operation) { |
||
316 | case 'put': |
||
317 | return $this->propertyInfoExtractor->isReadable($resourceClass, $attributeMetadata->getName()) |
||
318 | && $this->propertyInfoExtractor->isWritable($resourceClass, $attributeMetadata->getName()); |
||
319 | case 'get': |
||
320 | return (bool) $this->propertyInfoExtractor->isReadable($resourceClass, $attributeMetadata->getName()); |
||
321 | case 'post': |
||
322 | return $this->propertyInfoExtractor->isWritable($resourceClass, $attributeMetadata->getName()) |
||
323 | || $this->propertyInfoExtractor->isInitializable($resourceClass, $attributeMetadata->getName()); |
||
324 | } |
||
325 | |||
326 | // @codeCoverageIgnoreStart |
||
327 | return true; |
||
328 | // @codeCoverageIgnoreEnd |
||
329 | } |
||
330 | |||
331 | /** |
||
332 | * Returns a Schema for a resource class, operation and serialization group tuple. |
||
333 | * |
||
334 | * @param string $resourceClass |
||
335 | * @param string $operation |
||
336 | * @param string[] $groups |
||
337 | * @return Schema |
||
338 | */ |
||
339 | public function createSchema(string $resourceClass, string $operation, array $groups): Schema |
||
340 | { |
||
341 | return $this->createSchemaRecursive($resourceClass, $operation, $groups); |
||
342 | } |
||
343 | |||
344 | /** |
||
345 | * Creates a unique cache key to be used for already defined schemas for performance reasons. |
||
346 | * |
||
347 | * @param string $resourceClass |
||
348 | * @param string $operation |
||
349 | * @param string[] $groups |
||
350 | * @return string |
||
351 | */ |
||
352 | private function getCacheKey(string $resourceClass, string $operation, array $groups) |
||
355 | } |
||
356 | |||
357 | /** |
||
358 | * Returns OpenApi property type for scalars. |
||
359 | * |
||
360 | * @param string $type |
||
361 | * @return string |
||
362 | */ |
||
363 | private function translateType(string $type): string |
||
373 | } |
||
374 | } |
||
375 |
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.