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 |
||
22 | final class TypeResolver |
||
23 | { |
||
24 | /** @var string Definition of the ARRAY operator for types */ |
||
25 | const OPERATOR_ARRAY = '[]'; |
||
26 | |||
27 | /** @var string Definition of the NAMESPACE operator in PHP */ |
||
28 | const OPERATOR_NAMESPACE = '\\'; |
||
29 | |||
30 | /** @var integer the iterator parser is inside a compound context */ |
||
31 | const PARSER_IN_COMPOUND = 0; |
||
32 | |||
33 | /** @var integer the iterator parser is inside a nullable expression context */ |
||
34 | const PARSER_IN_NULLABLE = 1; |
||
35 | |||
36 | /** @var integer the iterator parser is inside an array expression context */ |
||
37 | const PARSER_IN_ARRAY_EXPRESSION = 2; |
||
38 | |||
39 | /** @var string[] List of recognized keywords and unto which Value Object they map */ |
||
40 | private $keywords = array( |
||
41 | 'string' => Types\String_::class, |
||
42 | 'int' => Types\Integer::class, |
||
43 | 'integer' => Types\Integer::class, |
||
44 | 'bool' => Types\Boolean::class, |
||
45 | 'boolean' => Types\Boolean::class, |
||
46 | 'float' => Types\Float_::class, |
||
47 | 'double' => Types\Float_::class, |
||
48 | 'object' => Object_::class, |
||
49 | 'mixed' => Types\Mixed_::class, |
||
50 | 'array' => Array_::class, |
||
51 | 'resource' => Types\Resource_::class, |
||
52 | 'void' => Types\Void_::class, |
||
53 | 'null' => Types\Null_::class, |
||
54 | 'scalar' => Types\Scalar::class, |
||
55 | 'callback' => Types\Callable_::class, |
||
56 | 'callable' => Types\Callable_::class, |
||
57 | 'false' => Types\Boolean::class, |
||
58 | 'true' => Types\Boolean::class, |
||
59 | 'self' => Types\Self_::class, |
||
60 | '$this' => Types\This::class, |
||
61 | 'static' => Types\Static_::class, |
||
62 | 'parent' => Types\Parent_::class, |
||
63 | 'iterable' => Iterable_::class, |
||
64 | ); |
||
65 | |||
66 | /** @var FqsenResolver */ |
||
67 | private $fqsenResolver; |
||
68 | |||
69 | /** |
||
70 | * Initializes this TypeResolver with the means to create and resolve Fqsen objects. |
||
71 | * |
||
72 | * @param FqsenResolver $fqsenResolver |
||
73 | */ |
||
74 | 34 | public function __construct(FqsenResolver $fqsenResolver = null) |
|
78 | |||
79 | /** |
||
80 | * Analyzes the given type and returns the FQCN variant. |
||
81 | * |
||
82 | * When a type is provided this method checks whether it is not a keyword or |
||
83 | * Fully Qualified Class Name. If so it will use the given namespace and |
||
84 | * aliases to expand the type to a FQCN representation. |
||
85 | * |
||
86 | * This method only works as expected if the namespace and aliases are set; |
||
87 | * no dynamic reflection is being performed here. |
||
88 | * |
||
89 | * @param string $type The relative or absolute type. |
||
90 | * @param Context $context |
||
91 | * |
||
92 | * @uses Context::getNamespace() to determine with what to prefix the type name. |
||
93 | * @uses Context::getNamespaceAliases() to check whether the first part of the relative type name should not be |
||
94 | * replaced with another namespace. |
||
95 | * |
||
96 | * @return Type |
||
97 | */ |
||
98 | 31 | public function resolve($type, Context $context = null) |
|
99 | { |
||
100 | 31 | if (!is_string($type)) { |
|
101 | 1 | throw new \InvalidArgumentException( |
|
102 | 1 | 'Attempted to resolve type but it appeared not to be a string, received: ' . var_export($type, true) |
|
103 | 1 | ); |
|
104 | } |
||
105 | |||
106 | 30 | $type = trim($type); |
|
107 | 30 | if (!$type) { |
|
108 | 1 | throw new \InvalidArgumentException('Attempted to resolve "' . $type . '" but it appears to be empty'); |
|
109 | } |
||
110 | |||
111 | 29 | if ($context === null) { |
|
112 | $context = new Context(''); |
||
113 | } |
||
114 | |||
115 | // split the type string into tokens `|`, `?`, `(`, `)[]` and type names |
||
116 | 29 | $tokens = preg_split('/(\||\?|\(|\)(?:\[\])+)/', $type, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE); |
|
117 | 29 | $tokenIterator = new \ArrayIterator($tokens); |
|
118 | |||
119 | 29 | return $this->parseTypes($tokenIterator, $context, self::PARSER_IN_COMPOUND); |
|
120 | } |
||
121 | |||
122 | /** |
||
123 | * Analyse each tokens and creates types |
||
124 | * |
||
125 | * @param \ArrayIterator $tokens the iterator on tokens |
||
126 | * @param Context $context |
||
127 | * @param integer $parserContext on of self::PARSER_* constants, indicating |
||
128 | * the context where we are in the parsing |
||
129 | * |
||
130 | * @return Type |
||
131 | */ |
||
132 | 29 | private function parseTypes(\ArrayIterator $tokens, Context $context, $parserContext) |
|
133 | { |
||
134 | 29 | $types = array(); |
|
135 | 29 | $token = ''; |
|
136 | 29 | while ($tokens->valid()) { |
|
137 | 29 | $token = $tokens->current(); |
|
138 | |||
139 | 29 | if ($token == '|') { |
|
140 | 4 | if (count($types) == 0) { |
|
141 | throw new \RuntimeException( |
||
142 | 'A type is missing before a type separator' |
||
143 | ); |
||
144 | } |
||
145 | if ($parserContext !== self::PARSER_IN_COMPOUND |
||
146 | 4 | && $parserContext !== self::PARSER_IN_ARRAY_EXPRESSION) { |
|
147 | throw new \RuntimeException( |
||
148 | 'Unexpected type separator' |
||
149 | ); |
||
150 | } |
||
151 | 4 | $tokens->next(); |
|
152 | |||
|
|||
153 | 29 | } else if ($token == '?') { |
|
154 | if ($parserContext !== self::PARSER_IN_COMPOUND |
||
155 | 1 | && $parserContext !== self::PARSER_IN_ARRAY_EXPRESSION) { |
|
156 | throw new \RuntimeException( |
||
157 | 'Unexpected nullable character' |
||
158 | ); |
||
159 | } |
||
160 | |||
161 | 1 | $tokens->next(); |
|
162 | 1 | $type = $this->parseTypes($tokens, $context, self::PARSER_IN_NULLABLE); |
|
163 | 1 | $types[] = new Nullable($type); |
|
164 | |||
165 | 29 | } else if ($token === '(') { |
|
166 | 1 | $tokens->next(); |
|
167 | 1 | $type = $this->parseTypes($tokens, $context, self::PARSER_IN_ARRAY_EXPRESSION); |
|
168 | |||
169 | 1 | $resolvedType = new Array_($type); |
|
170 | |||
171 | // we generates arrays corresponding to the number of '[]' |
||
172 | // after the ')' |
||
173 | 1 | $numberOfArrays = (strlen($tokens->current()) -1) / 2; |
|
174 | 1 | for ($i = 0; $i < $numberOfArrays - 1; $i++) { |
|
175 | $resolvedType = new Array_($resolvedType); |
||
176 | } |
||
177 | 1 | $types[] = $resolvedType; |
|
178 | 1 | $tokens->next(); |
|
179 | |||
180 | 1 | } else if ($parserContext === self::PARSER_IN_ARRAY_EXPRESSION |
|
181 | 29 | && $token[0] === ')' |
|
182 | 29 | ) { |
|
183 | 1 | break; |
|
184 | |||
185 | } else { |
||
186 | 29 | $type = $this->resolveSingleType($token, $context); |
|
187 | 29 | $tokens->next(); |
|
188 | 29 | if ($parserContext === self::PARSER_IN_NULLABLE) { |
|
189 | 1 | return $type; |
|
190 | } |
||
191 | 28 | $types[] = $type; |
|
192 | } |
||
193 | 29 | } |
|
194 | |||
195 | 29 | if ($token == '|') { |
|
196 | throw new \RuntimeException( |
||
197 | 'A type is missing after a type separator' |
||
198 | ); |
||
199 | } |
||
200 | 29 | if (count($types) == 0) { |
|
201 | if ($parserContext == self::PARSER_IN_NULLABLE) { |
||
202 | throw new \RuntimeException( |
||
203 | 'A type is missing after a nullable character' |
||
204 | ); |
||
205 | } |
||
206 | if ($parserContext == self::PARSER_IN_ARRAY_EXPRESSION) { |
||
207 | throw new \RuntimeException( |
||
208 | 'A type is missing in an array expression' |
||
209 | ); |
||
210 | } |
||
211 | throw new \RuntimeException( |
||
212 | 'No types in a compound list' |
||
213 | ); |
||
214 | 29 | } else if (count($types) == 1) { |
|
215 | 26 | return $types[0]; |
|
216 | } |
||
217 | 4 | return new Compound($types); |
|
218 | } |
||
219 | |||
220 | /** |
||
221 | * resolve the given type into a type object |
||
222 | * |
||
223 | * @param string $type the type string, representing a single type |
||
224 | * @param Context $context |
||
225 | * @return Type|Array_|Object_ |
||
226 | */ |
||
227 | 29 | private function resolveSingleType($type, Context $context) |
|
228 | { |
||
229 | 29 | switch (true) { |
|
230 | 29 | case $this->isKeyword($type): |
|
231 | 23 | return $this->resolveKeyword($type); |
|
232 | 8 | case $this->isTypedArray($type): |
|
233 | 2 | return $this->resolveTypedArray($type, $context); |
|
234 | 7 | case $this->isFqsen($type): |
|
235 | 4 | return $this->resolveTypedObject($type); |
|
236 | 5 | case $this->isPartialStructuralElementName($type): |
|
237 | 5 | return $this->resolveTypedObject($type, $context); |
|
238 | // @codeCoverageIgnoreStart |
||
239 | default: |
||
240 | // I haven't got the foggiest how the logic would come here but added this as a defense. |
||
241 | throw new \RuntimeException( |
||
242 | 'Unable to resolve type "' . $type . '", there is no known method to resolve it' |
||
243 | ); |
||
244 | } |
||
245 | // @codeCoverageIgnoreEnd |
||
246 | } |
||
247 | |||
248 | /** |
||
249 | * Adds a keyword to the list of Keywords and associates it with a specific Value Object. |
||
250 | * |
||
251 | * @param string $keyword |
||
252 | * @param string $typeClassName |
||
253 | * |
||
254 | * @return void |
||
255 | */ |
||
256 | 3 | public function addKeyword($keyword, $typeClassName) |
|
257 | { |
||
258 | 3 | if (!class_exists($typeClassName)) { |
|
259 | 1 | throw new \InvalidArgumentException( |
|
260 | 1 | 'The Value Object that needs to be created with a keyword "' . $keyword . '" must be an existing class' |
|
261 | 1 | . ' but we could not find the class ' . $typeClassName |
|
262 | 1 | ); |
|
263 | } |
||
264 | |||
265 | 2 | if (!in_array(Type::class, class_implements($typeClassName))) { |
|
266 | 1 | throw new \InvalidArgumentException( |
|
267 | 1 | 'The class "' . $typeClassName . '" must implement the interface "phpDocumentor\Reflection\Type"' |
|
268 | 1 | ); |
|
269 | } |
||
270 | |||
271 | 1 | $this->keywords[$keyword] = $typeClassName; |
|
272 | 1 | } |
|
273 | |||
274 | /** |
||
275 | * Detects whether the given type represents an array. |
||
276 | * |
||
277 | * @param string $type A relative or absolute type as defined in the phpDocumentor documentation. |
||
278 | * |
||
279 | * @return bool |
||
280 | */ |
||
281 | 8 | private function isTypedArray($type) |
|
285 | |||
286 | /** |
||
287 | * Detects whether the given type represents a PHPDoc keyword. |
||
288 | * |
||
289 | * @param string $type A relative or absolute type as defined in the phpDocumentor documentation. |
||
290 | * |
||
291 | * @return bool |
||
292 | */ |
||
293 | 29 | private function isKeyword($type) |
|
297 | |||
298 | /** |
||
299 | * Detects whether the given type represents a relative structural element name. |
||
300 | * |
||
301 | * @param string $type A relative or absolute type as defined in the phpDocumentor documentation. |
||
302 | * |
||
303 | * @return bool |
||
304 | */ |
||
305 | 5 | private function isPartialStructuralElementName($type) |
|
309 | |||
310 | /** |
||
311 | * Tests whether the given type is a Fully Qualified Structural Element Name. |
||
312 | * |
||
313 | * @param string $type |
||
314 | * |
||
315 | * @return bool |
||
316 | */ |
||
317 | 7 | private function isFqsen($type) |
|
318 | { |
||
319 | 7 | return strpos($type, self::OPERATOR_NAMESPACE) === 0; |
|
320 | } |
||
321 | |||
322 | /** |
||
323 | * Resolves the given typed array string (i.e. `string[]`) into an Array object with the right types set. |
||
324 | * |
||
325 | * @param string $type |
||
326 | * @param Context $context |
||
327 | * |
||
328 | * @return Array_ |
||
329 | */ |
||
330 | 2 | private function resolveTypedArray($type, Context $context) |
|
331 | { |
||
332 | 2 | return new Array_($this->resolveSingleType(substr($type, 0, -2), $context)); |
|
333 | } |
||
334 | |||
335 | /** |
||
336 | * Resolves the given keyword (such as `string`) into a Type object representing that keyword. |
||
337 | * |
||
338 | * @param string $type |
||
339 | * |
||
340 | * @return Type |
||
341 | */ |
||
342 | 23 | private function resolveKeyword($type) |
|
348 | |||
349 | /** |
||
350 | * Resolves the given FQSEN string into an FQSEN object. |
||
351 | * |
||
352 | * @param string $type |
||
353 | * @param Context|null $context |
||
354 | * |
||
355 | * @return Object_ |
||
356 | */ |
||
357 | 7 | private function resolveTypedObject($type, Context $context = null) |
|
361 | } |
||
362 |