Complex classes like StringScalarPrefixer 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 StringScalarPrefixer, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
56 | final class StringScalarPrefixer extends NodeVisitorAbstract |
||
57 | { |
||
58 | private const SPECIAL_FUNCTION_NAMES = [ |
||
59 | 'is_a', |
||
60 | 'is_subclass_of', |
||
61 | 'interface_exists', |
||
62 | 'class_exists', |
||
63 | 'trait_exists', |
||
64 | 'function_exists', |
||
65 | 'class_alias', |
||
66 | 'define', |
||
67 | ]; |
||
68 | |||
69 | private $prefix; |
||
70 | private $whitelist; |
||
71 | private $reflector; |
||
72 | |||
73 | 10 | public function __construct(string $prefix, Whitelist $whitelist, Reflector $reflector) |
|
74 | { |
||
75 | 10 | $this->prefix = $prefix; |
|
76 | 10 | $this->whitelist = $whitelist; |
|
77 | 10 | $this->reflector = $reflector; |
|
78 | } |
||
79 | |||
80 | /** |
||
81 | * @inheritdoc |
||
82 | */ |
||
83 | 10 | public function enterNode(Node $node): Node |
|
90 | |||
91 | 9 | private function prefixStringScalar(String_ $string): String_ |
|
92 | { |
||
93 | 9 | if (false === (ParentNodeAppender::hasParent($string) && is_string($string->value)) |
|
94 | 9 | || 1 !== preg_match('/^((\\\\)?[\p{L}_\d]+)$|((\\\\)?(?:[\p{L}_\d]+\\\\+)+[\p{L}_\d]+)$/u', $string->value) |
|
95 | ) { |
||
96 | 9 | return $string; |
|
97 | } |
||
98 | |||
99 | 6 | if ($this->whitelist->belongsToWhitelistedNamespace($string->value)) { |
|
100 | 2 | return $string; |
|
101 | } |
||
102 | |||
103 | // From this point either the symbol belongs to the global namespace or the symbol belongs to the symbol |
||
104 | // namespace is whitelisted |
||
105 | |||
106 | 5 | $parentNode = ParentNodeAppender::getParent($string); |
|
107 | |||
108 | // The string scalar either has a class form or a simple string which can either be a symbol from the global |
||
109 | // namespace or a completely unrelated string. |
||
110 | |||
111 | 5 | if ($parentNode instanceof Arg) { |
|
112 | 5 | return $this->prefixStringArg($string, $parentNode); |
|
113 | } |
||
114 | |||
115 | 5 | if ($parentNode instanceof ArrayItem) { |
|
116 | return $this->prefixArrayItemString($string, $parentNode); |
||
117 | } |
||
118 | |||
119 | if (false === ( |
||
120 | 5 | $parentNode instanceof Assign |
|
121 | 5 | || $parentNode instanceof Param |
|
122 | 5 | || $parentNode instanceof Const_ |
|
123 | 5 | || $parentNode instanceof PropertyProperty |
|
124 | ) |
||
125 | ) { |
||
126 | return $string; |
||
127 | } |
||
128 | |||
129 | // If belongs to the global namespace then we cannot differentiate the value from a symbol and a regular string |
||
130 | 5 | return $this->belongsToTheGlobalNamespace($string) |
|
131 | 5 | ? $string |
|
132 | 5 | : $this->createPrefixedString($string) |
|
133 | ; |
||
134 | } |
||
135 | |||
136 | 5 | private function prefixStringArg(String_ $string, Arg $parentNode): String_ |
|
137 | { |
||
138 | 5 | $functionNode = ParentNodeAppender::getParent($parentNode); |
|
139 | |||
140 | 5 | if (false === ($functionNode instanceof FuncCall)) { |
|
141 | // If belongs to the global namespace then we cannot differentiate the value from a symbol and a regular string |
||
142 | return $this->belongsToTheGlobalNamespace($string) |
||
143 | ? $string |
||
144 | : $this->createPrefixedString($string) |
||
145 | ; |
||
146 | } |
||
147 | /** @var FuncCall $functionNode */ |
||
148 | |||
149 | // In the case of a function call, we allow to prefix strings which could be classes belonging to the global |
||
150 | // namespace in some cases |
||
151 | 5 | $functionName = $functionNode->name instanceof Name ? (string) $functionNode->name : null; |
|
152 | |||
153 | 5 | if (false === in_array($functionName, self::SPECIAL_FUNCTION_NAMES, true)) { |
|
154 | return $this->belongsToTheGlobalNamespace($string) |
||
155 | ? $string |
||
156 | : $this->createPrefixedString($string) |
||
157 | ; |
||
158 | } |
||
159 | |||
160 | 5 | if ('function_exists' === $functionName) { |
|
161 | return $this->reflector->isFunctionInternal($string->value) |
||
162 | ? $string |
||
163 | : $this->createPrefixedString($string) |
||
164 | ; |
||
165 | } |
||
166 | |||
167 | 5 | $isConstantNode = $this->isConstantNode($string); |
|
168 | |||
169 | 5 | if (false === $isConstantNode) { |
|
170 | 1 | if ('define' === $functionName |
|
171 | 1 | && $this->belongsToTheGlobalNamespace($string) |
|
172 | ) { |
||
173 | 1 | return $string; |
|
174 | } |
||
175 | |||
176 | return $this->reflector->isClassInternal($string->value) |
||
177 | ? $string |
||
178 | : $this->createPrefixedString($string) |
||
179 | ; |
||
180 | } |
||
181 | |||
182 | 5 | return ($this->whitelist->isSymbolWhitelisted($string->value, true) |
|
183 | 5 | || $this->whitelist->isGlobalWhitelistedConstant($string->value) |
|
184 | ) |
||
185 | 2 | ? $string |
|
186 | 5 | : $this->createPrefixedString($string) |
|
187 | ; |
||
188 | } |
||
189 | |||
190 | private function prefixArrayItemString(String_ $string, ArrayItem $parentNode): String_ |
||
191 | { |
||
192 | // ArrayItem can lead to two results: either the string is used for `spl_autoload_register()`, e.g. |
||
193 | // `spl_autoload_register(['Swift', 'autoload'])` in which case the string `'Swift'` is guaranteed to be class |
||
194 | // name, or something else in which case a string like `'Swift'` can be anything and cannot be prefixed. |
||
195 | |||
196 | $arrayItemNode = $parentNode; |
||
197 | |||
198 | $parentNode = ParentNodeAppender::getParent($parentNode); |
||
199 | |||
200 | /** @var Array_ $arrayNode */ |
||
201 | $arrayNode = $parentNode; |
||
202 | $parentNode = ParentNodeAppender::getParent($parentNode); |
||
203 | |||
204 | if (false === ($parentNode instanceof Arg) |
||
205 | || null === $functionNode = ParentNodeAppender::findParent($parentNode) |
||
206 | ) { |
||
207 | // If belongs to the global namespace then we cannot differentiate the value from a symbol and a regular string |
||
208 | return $this->belongsToTheGlobalNamespace($string) |
||
209 | ? $string |
||
210 | : $this->createPrefixedString($string) |
||
211 | ; |
||
212 | } |
||
213 | |||
214 | $functionNode = ParentNodeAppender::getParent($parentNode); |
||
215 | |||
216 | if (false === ($functionNode instanceof FuncCall)) { |
||
217 | // If belongs to the global namespace then we cannot differentiate the value from a symbol and a regular string |
||
218 | return $this->belongsToTheGlobalNamespace($string) |
||
219 | ? $string |
||
220 | : $this->createPrefixedString($string) |
||
221 | ; |
||
222 | } |
||
223 | |||
224 | /** @var FuncCall $functionNode */ |
||
225 | if (false === ($functionNode->name instanceof Name)) { |
||
226 | return $string; |
||
227 | } |
||
228 | |||
229 | $functionName = (string) $functionNode->name; |
||
230 | |||
231 | return ('spl_autoload_register' === $functionName |
||
232 | && array_key_exists(0, $arrayNode->items) |
||
233 | && $arrayItemNode === $arrayNode->items[0] |
||
234 | && false === $this->reflector->isClassInternal($string->value) |
||
235 | ) |
||
236 | ? $this->createPrefixedString($string) |
||
237 | : $string |
||
238 | ; |
||
239 | } |
||
240 | |||
241 | 5 | private function isConstantNode(String_ $node): bool |
|
242 | { |
||
243 | 5 | $parent = ParentNodeAppender::getParent($node); |
|
244 | |||
245 | 5 | if (false === ($parent instanceof Arg)) { |
|
246 | return false; |
||
247 | } |
||
248 | |||
249 | /** @var Arg $parent */ |
||
250 | 5 | $argParent = ParentNodeAppender::getParent($parent); |
|
251 | |||
252 | 5 | if (false === ($argParent instanceof FuncCall)) { |
|
253 | return false; |
||
254 | } |
||
255 | |||
256 | /* @var FuncCall $argParent */ |
||
257 | 5 | if (false === ($argParent->name instanceof Name) || 'define' !== (string) $argParent->name) { |
|
258 | return false; |
||
259 | } |
||
260 | |||
261 | 5 | return $parent === $argParent->args[0]; |
|
262 | } |
||
263 | |||
264 | 5 | private function createPrefixedString(String_ $previous): String_ |
|
265 | { |
||
266 | 5 | $previousValueParts = array_values( |
|
267 | 5 | array_filter( |
|
268 | 5 | explode('\\', $previous->value) |
|
269 | ) |
||
270 | ); |
||
271 | |||
272 | 5 | if ($this->prefix === $previousValueParts[0]) { |
|
273 | array_shift($previousValueParts); |
||
274 | } |
||
275 | |||
276 | 5 | $previousValue = implode('\\', $previousValueParts); |
|
277 | |||
278 | 5 | $string = new String_( |
|
279 | 5 | (string) FullyQualified::concat($this->prefix, $previousValue), |
|
280 | 5 | $previous->getAttributes() |
|
281 | ); |
||
282 | |||
283 | 5 | $string->setAttribute(ParentNodeAppender::PARENT_ATTRIBUTE, $string); |
|
284 | |||
285 | 5 | return $string; |
|
286 | } |
||
287 | |||
288 | 5 | private function belongsToTheGlobalNamespace(String_ $string): bool |
|
292 | } |
||
293 |