Complex classes like TypeResolver 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
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 TypeResolver, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
47 | final class TypeResolver |
||
48 | { |
||
49 | /** @var string Definition of the ARRAY operator for types */ |
||
50 | private const OPERATOR_ARRAY = '[]'; |
||
51 | |||
52 | /** @var string Definition of the NAMESPACE operator in PHP */ |
||
53 | private const OPERATOR_NAMESPACE = '\\'; |
||
54 | |||
55 | /** @var int the iterator parser is inside a compound context */ |
||
56 | private const PARSER_IN_COMPOUND = 0; |
||
57 | |||
58 | /** @var int the iterator parser is inside a nullable expression context */ |
||
59 | private const PARSER_IN_NULLABLE = 1; |
||
60 | |||
61 | /** @var int the iterator parser is inside an array expression context */ |
||
62 | private const PARSER_IN_ARRAY_EXPRESSION = 2; |
||
63 | |||
64 | /** @var int the iterator parser is inside a collection expression context */ |
||
65 | private const PARSER_IN_COLLECTION_EXPRESSION = 3; |
||
66 | |||
67 | /** |
||
68 | * @var array<string, string> List of recognized keywords and unto which Value Object they map |
||
69 | * @psalm-var array<string, class-string<Type>> |
||
70 | */ |
||
71 | private $keywords = [ |
||
72 | 'string' => Types\String_::class, |
||
73 | 'class-string' => Types\ClassString::class, |
||
74 | 'int' => Types\Integer::class, |
||
75 | 'integer' => Types\Integer::class, |
||
76 | 'bool' => Types\Boolean::class, |
||
77 | 'boolean' => Types\Boolean::class, |
||
78 | 'real' => Types\Float_::class, |
||
79 | 'float' => Types\Float_::class, |
||
80 | 'double' => Types\Float_::class, |
||
81 | 'object' => Object_::class, |
||
82 | 'mixed' => Types\Mixed_::class, |
||
83 | 'array' => Array_::class, |
||
84 | 'resource' => Types\Resource_::class, |
||
85 | 'void' => Types\Void_::class, |
||
86 | 'null' => Types\Null_::class, |
||
87 | 'scalar' => Types\Scalar::class, |
||
88 | 'callback' => Types\Callable_::class, |
||
89 | 'callable' => Types\Callable_::class, |
||
90 | 'false' => Types\Boolean::class, |
||
91 | 'true' => Types\Boolean::class, |
||
92 | 'self' => Types\Self_::class, |
||
93 | '$this' => Types\This::class, |
||
94 | 'static' => Types\Static_::class, |
||
95 | 'parent' => Types\Parent_::class, |
||
96 | 'iterable' => Iterable_::class, |
||
97 | ]; |
||
98 | |||
99 | /** |
||
100 | 54 | * @var FqsenResolver |
|
101 | * @psalm-readonly |
||
102 | 54 | */ |
|
103 | 54 | private $fqsenResolver; |
|
104 | |||
105 | /** |
||
106 | * Initializes this TypeResolver with the means to create and resolve Fqsen objects. |
||
107 | */ |
||
108 | public function __construct(?FqsenResolver $fqsenResolver = null) |
||
109 | { |
||
110 | $this->fqsenResolver = $fqsenResolver ?: new FqsenResolver(); |
||
111 | } |
||
112 | |||
113 | /** |
||
114 | * Analyzes the given type and returns the FQCN variant. |
||
115 | * |
||
116 | * When a type is provided this method checks whether it is not a keyword or |
||
117 | * Fully Qualified Class Name. If so it will use the given namespace and |
||
118 | * aliases to expand the type to a FQCN representation. |
||
119 | * |
||
120 | * This method only works as expected if the namespace and aliases are set; |
||
121 | 51 | * no dynamic reflection is being performed here. |
|
122 | * |
||
123 | 51 | * @uses Context::getNamespaceAliases() to check whether the first part of the relative type name should not be |
|
124 | 51 | * replaced with another namespace. |
|
125 | 1 | * @uses Context::getNamespace() to determine with what to prefix the type name. |
|
126 | * |
||
127 | * @param string $type The relative or absolute type. |
||
128 | 50 | */ |
|
129 | 1 | public function resolve(string $type, ?Context $context = null) : Type |
|
157 | |||
158 | 50 | /** |
|
159 | 50 | * Analyse each tokens and creates types |
|
160 | 50 | * |
|
161 | 50 | * @param ArrayIterator<int, string|null> $tokens the iterator on tokens |
|
162 | * @param int $parserContext on of self::PARSER_* constants, indicating |
||
163 | 50 | * the context where we are in the parsing |
|
164 | 14 | */ |
|
165 | private function parseTypes(ArrayIterator $tokens, Context $context, int $parserContext) : Type |
||
166 | { |
||
167 | $types = []; |
||
168 | $token = ''; |
||
169 | $compoundToken = '|'; |
||
170 | 14 | while ($tokens->valid()) { |
|
171 | 14 | $token = $tokens->current(); |
|
172 | 14 | if ($token === null) { |
|
173 | throw new RuntimeException( |
||
174 | 'Unexpected nullable character' |
||
175 | ); |
||
176 | } |
||
177 | |||
178 | if ($token === '|' || $token === '&') { |
||
179 | 14 | if (count($types) === 0) { |
|
180 | 50 | throw new RuntimeException( |
|
181 | 2 | 'A type is missing before a type separator' |
|
182 | 2 | ); |
|
183 | 2 | } |
|
184 | |||
185 | if (!in_array($parserContext, [ |
||
186 | self::PARSER_IN_COMPOUND, |
||
187 | self::PARSER_IN_ARRAY_EXPRESSION, |
||
188 | self::PARSER_IN_COLLECTION_EXPRESSION, |
||
189 | ], true) |
||
190 | 2 | ) { |
|
191 | 2 | throw new RuntimeException( |
|
192 | 2 | 'Unexpected type separator' |
|
193 | 50 | ); |
|
194 | 5 | } |
|
195 | 5 | ||
196 | $compoundToken = $token; |
||
197 | 5 | $tokens->next(); |
|
198 | } elseif ($token === '?') { |
||
199 | 5 | if (!in_array($parserContext, [ |
|
200 | self::PARSER_IN_COMPOUND, |
||
201 | 5 | self::PARSER_IN_ARRAY_EXPRESSION, |
|
202 | 1 | self::PARSER_IN_COLLECTION_EXPRESSION, |
|
203 | ], true) |
||
204 | ) { |
||
205 | throw new RuntimeException( |
||
206 | 4 | 'Unexpected nullable character' |
|
207 | 4 | ); |
|
208 | 1 | } |
|
209 | |||
210 | $tokens->next(); |
||
211 | 4 | $type = $this->parseTypes($tokens, $context, self::PARSER_IN_NULLABLE); |
|
212 | 4 | $types[] = new Nullable($type); |
|
213 | 50 | } elseif ($token === '(') { |
|
214 | 4 | $tokens->next(); |
|
215 | 50 | $type = $this->parseTypes($tokens, $context, self::PARSER_IN_ARRAY_EXPRESSION); |
|
216 | 13 | ||
217 | 1 | $token = $tokens->current(); |
|
218 | 1 | if ($token === null) { // Someone did not properly close their array expression .. |
|
219 | break; |
||
220 | } |
||
221 | |||
222 | 12 | $tokens->next(); |
|
223 | 12 | ||
224 | 12 | $resolvedType = new Expression($type); |
|
225 | |||
226 | $types[] = $resolvedType; |
||
227 | 8 | } elseif ($parserContext === self::PARSER_IN_ARRAY_EXPRESSION && $token[0] === ')') { |
|
228 | 49 | break; |
|
229 | 49 | } elseif ($token === '<') { |
|
230 | if (count($types) === 0) { |
||
231 | 10 | throw new RuntimeException( |
|
232 | 'Unexpected collection operator "<", class name is missing' |
||
233 | 49 | ); |
|
234 | 49 | } |
|
235 | 49 | ||
236 | 2 | $classType = array_pop($types); |
|
237 | if ($classType !== null) { |
||
238 | if ((string) $classType === 'class-string') { |
||
239 | 48 | $types[] = $this->resolveClassString($tokens, $context); |
|
240 | } else { |
||
241 | $types[] = $this->resolveCollection($tokens, $classType, $context); |
||
242 | } |
||
243 | 48 | } |
|
244 | |||
245 | $tokens->next(); |
||
246 | } elseif ($parserContext === self::PARSER_IN_COLLECTION_EXPRESSION |
||
247 | && ($token === '>' || trim($token) === ',') |
||
248 | ) { |
||
249 | 48 | break; |
|
250 | 1 | } elseif ($token === self::OPERATOR_ARRAY) { |
|
251 | end($types); |
||
252 | $last = key($types); |
||
253 | $lastItem = $types[$last]; |
||
254 | if ($lastItem instanceof Expression) { |
||
255 | $lastItem = $lastItem->getValueType(); |
||
256 | 1 | } |
|
257 | |||
258 | $types[$last] = new Array_($lastItem); |
||
259 | |||
260 | $tokens->next(); |
||
261 | } else { |
||
262 | 1 | $type = $this->resolveSingleType($token, $context); |
|
263 | $tokens->next(); |
||
264 | 1 | if ($parserContext === self::PARSER_IN_NULLABLE) { |
|
265 | return $type; |
||
266 | } |
||
267 | 48 | ||
268 | 41 | $types[] = $type; |
|
269 | } |
||
270 | } |
||
271 | 14 | ||
272 | if ($token === '|' || $token === '&') { |
||
273 | throw new RuntimeException( |
||
274 | 'A type is missing after a type separator' |
||
275 | ); |
||
276 | } |
||
277 | |||
278 | if (count($types) === 0) { |
||
279 | if ($parserContext === self::PARSER_IN_NULLABLE) { |
||
280 | throw new RuntimeException( |
||
281 | 49 | 'A type is missing after a nullable character' |
|
282 | ); |
||
283 | } |
||
284 | 49 | ||
285 | 43 | if ($parserContext === self::PARSER_IN_ARRAY_EXPRESSION) { |
|
286 | 20 | throw new RuntimeException( |
|
287 | 5 | 'A type is missing in an array expression' |
|
288 | 17 | ); |
|
289 | 9 | } |
|
290 | 10 | ||
291 | 10 | if ($parserContext === self::PARSER_IN_COLLECTION_EXPRESSION) { |
|
292 | throw new RuntimeException( |
||
293 | 'A type is missing in a collection expression' |
||
294 | ); |
||
295 | } |
||
296 | } elseif (count($types) === 1) { |
||
297 | return $types[0]; |
||
298 | } |
||
299 | |||
300 | if ($compoundToken === '|') { |
||
301 | return new Compound(array_values($types)); |
||
302 | } |
||
303 | |||
304 | return new Intersection(array_values($types)); |
||
305 | } |
||
306 | 3 | ||
307 | /** |
||
308 | 3 | * resolve the given type into a type object |
|
309 | 1 | * |
|
310 | 1 | * @param string $type the type string, representing a single type |
|
311 | 1 | * |
|
312 | * @return Type|Array_|Object_ |
||
313 | * |
||
314 | * @psalm-pure |
||
315 | 2 | */ |
|
316 | 1 | private function resolveSingleType(string $type, Context $context) : object |
|
317 | 1 | { |
|
318 | switch (true) { |
||
319 | case $this->isKeyword($type): |
||
320 | return $this->resolveKeyword($type); |
||
321 | 1 | case $this->isFqsen($type): |
|
322 | 1 | return $this->resolveTypedObject($type); |
|
323 | case $this->isPartialStructuralElementName($type): |
||
324 | return $this->resolveTypedObject($type, $context); |
||
325 | |||
326 | // @codeCoverageIgnoreStart |
||
327 | default: |
||
328 | // I haven't got the foggiest how the logic would come here but added this as a defense. |
||
329 | 20 | throw new RuntimeException( |
|
330 | 'Unable to resolve type "' . $type . '", there is no known method to resolve it' |
||
331 | 20 | ); |
|
332 | } |
||
333 | |||
334 | // @codeCoverageIgnoreEnd |
||
335 | } |
||
336 | |||
337 | /** |
||
338 | * Adds a keyword to the list of Keywords and associates it with a specific Value Object. |
||
339 | 49 | * |
|
340 | * @psalm-param class-string<Type> $typeClassName |
||
341 | 49 | */ |
|
342 | public function addKeyword(string $keyword, string $typeClassName) : void |
||
343 | { |
||
344 | if (!class_exists($typeClassName)) { |
||
345 | throw new InvalidArgumentException( |
||
346 | 'The Value Object that needs to be created with a keyword "' . $keyword . '" must be an existing class' |
||
347 | . ' but we could not find the class ' . $typeClassName |
||
348 | ); |
||
349 | 10 | } |
|
350 | |||
351 | 10 | if (!in_array(Type::class, class_implements($typeClassName), true)) { |
|
352 | throw new InvalidArgumentException( |
||
353 | 'The class "' . $typeClassName . '" must implement the interface "phpDocumentor\Reflection\Type"' |
||
354 | ); |
||
355 | } |
||
356 | |||
357 | 17 | $this->keywords[$keyword] = $typeClassName; |
|
358 | } |
||
359 | 17 | ||
360 | /** |
||
361 | * Detects whether the given type represents a PHPDoc keyword. |
||
362 | * |
||
363 | * @param string $type A relative or absolute type as defined in the phpDocumentor documentation. |
||
364 | * |
||
365 | 5 | * @psalm-pure |
|
366 | */ |
||
367 | 5 | private function isKeyword(string $type) : bool |
|
371 | |||
372 | /** |
||
373 | 43 | * Detects whether the given type represents a relative structural element name. |
|
374 | * |
||
375 | 43 | * @param string $type A relative or absolute type as defined in the phpDocumentor documentation. |
|
376 | 43 | * |
|
377 | * @psalm-pure |
||
378 | */ |
||
379 | private function isPartialStructuralElementName(string $type) : bool |
||
383 | |||
384 | 17 | /** |
|
385 | * Tests whether the given type is a Fully Qualified Structural Element Name. |
||
386 | * |
||
387 | * @psalm-pure |
||
388 | */ |
||
389 | private function isFqsen(string $type) : bool |
||
393 | |||
394 | 12 | /** |
|
395 | 12 | * Resolves the given keyword (such as `string`) into a Type object representing that keyword. |
|
396 | * |
||
397 | * @psalm-pure |
||
398 | 12 | */ |
|
399 | 12 | private function resolveKeyword(string $type) : Type |
|
405 | 11 | ||
406 | /** |
||
407 | 11 | * Resolves the given FQSEN string into an FQSEN object. |
|
408 | 11 | * |
|
409 | * @psalm-pure |
||
410 | 11 | */ |
|
411 | private function resolveTypedObject(string $type, ?Context $context = null) : Object_ |
||
415 | |||
416 | 5 | /** |
|
417 | 5 | * Resolves class string |
|
418 | 5 | * |
|
419 | * @param ArrayIterator<int, (string|null)> $tokens |
||
420 | 1 | */ |
|
421 | 1 | private function resolveClassString(ArrayIterator $tokens, Context $context) : Type |
|
448 | |||
449 | /** |
||
450 | * Resolves the collection values and keys |
||
451 | * |
||
452 | * @param ArrayIterator<int, (string|null)> $tokens |
||
453 | * |
||
454 | * @return Array_|Iterable_|Collection |
||
455 | 8 | */ |
|
456 | 4 | private function resolveCollection(ArrayIterator $tokens, Type $classType, Context $context) : Type |
|
535 | |||
536 | /** |
||
537 | * @psalm-pure |
||
538 | */ |
||
539 | private function makeCollectionFromObject(Object_ $object, Type $valueType, ?Type $keyType = null) : Collection |
||
543 | } |
||
544 |