Complex classes like CommandInfo 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 CommandInfo, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
9 | class CommandInfo |
||
10 | { |
||
11 | /** |
||
12 | * @var \ReflectionParameter |
||
13 | */ |
||
14 | protected $params; |
||
15 | |||
16 | /** |
||
17 | * @var string |
||
18 | */ |
||
19 | protected $name; |
||
20 | |||
21 | /** |
||
22 | * @var string |
||
23 | */ |
||
24 | protected $description = ''; |
||
25 | |||
26 | /** |
||
27 | * @var string |
||
28 | */ |
||
29 | protected $help = ''; |
||
30 | |||
31 | /** |
||
32 | * @var array |
||
33 | */ |
||
34 | protected $options = []; |
||
35 | |||
36 | /** |
||
37 | * @var array |
||
38 | */ |
||
39 | protected $arguments = []; |
||
40 | |||
41 | /** |
||
42 | * @var array |
||
43 | */ |
||
44 | protected $argumentDescriptions = []; |
||
45 | |||
46 | /** |
||
47 | * @var array |
||
48 | */ |
||
49 | protected $optionDescriptions = []; |
||
50 | |||
51 | /** |
||
52 | * @var array |
||
53 | */ |
||
54 | protected $exampleUsage = []; |
||
55 | |||
56 | /** |
||
57 | * @var array |
||
58 | */ |
||
59 | protected $otherAnnotations = []; |
||
60 | |||
61 | /** |
||
62 | * @var array |
||
63 | */ |
||
64 | protected $aliases = []; |
||
65 | |||
66 | /** |
||
67 | * @var string |
||
68 | */ |
||
69 | protected $methodName; |
||
70 | |||
71 | public function __construct($classNameOrInstance, $methodName) |
||
72 | { |
||
73 | $reflectionMethod = new \ReflectionMethod($classNameOrInstance, $methodName); |
||
74 | $this->methodName = $methodName; |
||
75 | $this->setDefaultName($methodName); |
||
76 | $this->initializeFromParameters($reflectionMethod->getParameters()); |
||
77 | $this->parseDocBlock($reflectionMethod->getDocComment()); |
||
78 | } |
||
79 | |||
80 | public function getMethodName() |
||
81 | { |
||
82 | return $this->methodName; |
||
83 | } |
||
84 | |||
85 | public function getParameters() |
||
86 | { |
||
87 | return $this->params; |
||
88 | } |
||
89 | |||
90 | /** |
||
91 | * Get the synopsis of the command (~first line). |
||
92 | */ |
||
93 | public function getDescription() |
||
94 | { |
||
95 | return $this->description; |
||
96 | } |
||
97 | |||
98 | public function setDescription($description) |
||
99 | { |
||
100 | $this->description = $description; |
||
101 | } |
||
102 | |||
103 | /** |
||
104 | * Get the help text of the command (the description) |
||
105 | */ |
||
106 | public function getHelp() |
||
107 | { |
||
108 | return $this->help; |
||
109 | } |
||
110 | |||
111 | public function setHelp($help) |
||
112 | { |
||
113 | $this->help = $help; |
||
114 | } |
||
115 | |||
116 | public function getAliases() |
||
117 | { |
||
118 | return $this->aliases; |
||
119 | } |
||
120 | |||
121 | public function setAliases($aliases) |
||
122 | { |
||
123 | if (is_string($aliases)) { |
||
124 | $aliases = explode(',', static::convertListToCommaSeparated($aliases)); |
||
125 | } |
||
126 | $this->aliases = array_filter($aliases); |
||
127 | } |
||
128 | |||
129 | public function getExampleUsages() |
||
130 | { |
||
131 | return $this->exampleUsage; |
||
132 | } |
||
133 | |||
134 | public function getName() |
||
135 | { |
||
136 | return $this->name; |
||
137 | } |
||
138 | |||
139 | public function setName($name) |
||
140 | { |
||
141 | $this->name = $name; |
||
142 | } |
||
143 | |||
144 | public function getArguments() |
||
145 | { |
||
146 | return $this->arguments; |
||
147 | } |
||
148 | |||
149 | public function hasArgument($name) |
||
150 | { |
||
151 | return array_key_exists($name, $this->arguments); |
||
152 | } |
||
153 | |||
154 | public function setArgumentDefaultValue($name, $defaultValue) |
||
155 | { |
||
156 | $this->arguments[$name] = $defaultValue; |
||
157 | } |
||
158 | |||
159 | public function addArgument($name, $description, $defaultValue = null) |
||
160 | { |
||
161 | if (!$this->hasArgument($name) || isset($defaultValue)) { |
||
162 | $this->arguments[$name] = $defaultValue; |
||
163 | } |
||
164 | unset($this->argumentDescriptions[$name]); |
||
165 | if (isset($description)) { |
||
166 | $this->argumentDescriptions[$name] = $description; |
||
167 | } |
||
168 | } |
||
169 | |||
170 | public function getOptions() |
||
171 | { |
||
172 | return $this->options; |
||
173 | } |
||
174 | |||
175 | public function hasOption($name) |
||
176 | { |
||
177 | return array_key_exists($name, $this->options); |
||
178 | } |
||
179 | |||
180 | public function setOptionDefaultValue($name, $defaultValue) |
||
181 | { |
||
182 | $this->options[$name] = $defaultValue; |
||
183 | } |
||
184 | |||
185 | public function addOption($name, $description, $defaultValue = false) |
||
186 | { |
||
187 | if (!$this->hasOption($name) || $defaultValue) { |
||
188 | $this->options[$name] = $defaultValue; |
||
189 | } |
||
190 | unset($this->optionDescriptions[$name]); |
||
191 | if (isset($description)) { |
||
192 | $this->optionDescriptions[$name] = $description; |
||
193 | } |
||
194 | } |
||
195 | |||
196 | public function getArgumentDescription($name) |
||
197 | { |
||
198 | if (array_key_exists($name, $this->argumentDescriptions)) { |
||
199 | return $this->argumentDescriptions[$name]; |
||
200 | } |
||
201 | |||
202 | return ''; |
||
203 | } |
||
204 | |||
205 | public function getOptionDescription($name) |
||
206 | { |
||
207 | if (array_key_exists($name, $this->optionDescriptions)) { |
||
208 | return $this->optionDescriptions[$name]; |
||
209 | } |
||
210 | |||
211 | return ''; |
||
212 | } |
||
213 | |||
214 | public function getAnnotations() |
||
215 | { |
||
216 | return $this->otherAnnotations; |
||
217 | } |
||
218 | |||
219 | public function getAnnotation($annotation) |
||
220 | { |
||
221 | // hasAnnotation parses the docblock |
||
222 | if (!$this->hasAnnotation($annotation)) { |
||
223 | return null; |
||
224 | } |
||
225 | return $this->otherAnnotations[$annotation]; |
||
226 | } |
||
227 | |||
228 | public function hasAnnotation($annotation) |
||
229 | { |
||
230 | return array_key_exists($annotation, $this->otherAnnotations); |
||
231 | } |
||
232 | |||
233 | public function setExampleUsage($usage, $description) |
||
234 | { |
||
235 | $this->exampleUsage[$usage] = $description; |
||
236 | } |
||
237 | |||
238 | /** |
||
239 | * Save any tag that we do not explicitly recognize in the |
||
240 | * 'otherAnnotations' map. |
||
241 | */ |
||
242 | public function addOtherAnnotation($name, $content) |
||
243 | { |
||
244 | $this->otherAnnotations[$name] = $content; |
||
245 | } |
||
246 | |||
247 | /** |
||
248 | * An option might have a name such as 'silent|s'. In this |
||
249 | * instance, we will allow the @option or @default tag to |
||
250 | * reference the option only by name (e.g. 'silent' or 's' |
||
251 | * instead of 'silent|s'). |
||
252 | */ |
||
253 | public function findMatchingOption($optionName) |
||
254 | { |
||
255 | // Exit fast if there's an exact match |
||
256 | if (isset($this->options[$optionName])) { |
||
257 | return $optionName; |
||
258 | } |
||
259 | $existingOptionName = $this->findExistingOption($optionName); |
||
260 | if (isset($existingOptionName)) { |
||
261 | return $existingOptionName; |
||
262 | } |
||
263 | return $this->findOptionAmongAlternatives($optionName); |
||
264 | } |
||
265 | |||
266 | protected function findOptionAmongAlternatives($optionName) |
||
267 | { |
||
268 | // Check the other direction: if the annotation contains @silent|s |
||
269 | // and the options array has 'silent|s'. |
||
270 | $checkMatching = explode('|', $optionName); |
||
271 | if (count($checkMatching) > 1) { |
||
272 | foreach ($checkMatching as $checkName) { |
||
273 | if (isset($this->options[$checkName])) { |
||
274 | $this->options[$optionName] = $this->options[$checkName]; |
||
275 | unset($this->options[$checkName]); |
||
276 | return $optionName; |
||
277 | } |
||
278 | } |
||
279 | } |
||
280 | return $optionName; |
||
281 | } |
||
282 | |||
283 | protected function findExistingOption($optionName) |
||
284 | { |
||
285 | // Check to see if we can find the option name in an existing option, |
||
286 | // e.g. if the options array has 'silent|s' => false, and the annotation |
||
287 | // is @silent. |
||
288 | foreach ($this->options as $name => $default) { |
||
289 | if (in_array($optionName, explode('|', $name))) { |
||
290 | return $name; |
||
291 | } |
||
292 | } |
||
293 | } |
||
294 | |||
295 | protected function initializeFromParameters($params) |
||
296 | { |
||
297 | // Set up a default name for the command from the method name. |
||
298 | // This can be overridden via @command or @name annotations. |
||
299 | $this->params = $params; |
||
300 | $this->options = $this->determineOptionsFromParameters($this->params); |
||
301 | $this->arguments = $this->determineAgumentClassifications($this->params); |
||
302 | } |
||
303 | |||
304 | /** |
||
305 | * Parse the docBlock comment for this command, and set the |
||
306 | * fields of this class with the data thereby obtained. |
||
307 | */ |
||
308 | protected function parseDocBlock($docblock) |
||
309 | { |
||
310 | $parser = new CommandDocBlockParser($this); |
||
311 | $parser->parse($docblock); |
||
312 | } |
||
313 | |||
314 | protected function determineAgumentClassifications($params) |
||
315 | { |
||
316 | $args = []; |
||
317 | if (!empty($this->determineOptionsFromParameters($params))) { |
||
318 | array_pop($params); |
||
319 | } |
||
320 | foreach ($params as $param) { |
||
321 | $defaultValue = $this->getArgumentClassification($param); |
||
322 | if ($defaultValue !== false) { |
||
323 | $args[$param->name] = $defaultValue; |
||
324 | } |
||
325 | } |
||
326 | return $args; |
||
327 | } |
||
328 | |||
329 | protected function determineOptionsFromParameters($params) |
||
330 | { |
||
331 | if (empty($params)) { |
||
332 | return []; |
||
333 | } |
||
334 | $param = end($params); |
||
335 | if (!$param->isDefaultValueAvailable()) { |
||
336 | return []; |
||
337 | } |
||
338 | if (!$this->isAssoc($param->getDefaultValue())) { |
||
339 | return []; |
||
340 | } |
||
341 | return $param->getDefaultValue(); |
||
342 | } |
||
343 | |||
344 | /** |
||
345 | * Examine the provided parameter, and determine whether it |
||
346 | * is a parameter that will be filled in with a positional |
||
347 | * commandline argument. |
||
348 | * |
||
349 | * @return false|null|string|array |
||
350 | */ |
||
351 | protected function getArgumentClassification($param) |
||
352 | { |
||
353 | $defaultValue = null; |
||
354 | if ($param->isDefaultValueAvailable()) { |
||
355 | $defaultValue = $param->getDefaultValue(); |
||
356 | if ($this->isAssoc($defaultValue)) { |
||
357 | return false; |
||
358 | } |
||
359 | } |
||
360 | if ($param->isArray()) { |
||
361 | return []; |
||
362 | } |
||
363 | // Commandline arguments must be strings, so ignore |
||
364 | // any parameter that is typehinted to anything else. |
||
365 | if (($param->getClass() != null) && ($param->getClass() != 'string')) { |
||
366 | return false; |
||
367 | } |
||
368 | return $defaultValue; |
||
369 | } |
||
370 | |||
371 | protected function setDefaultName($methodName) |
||
372 | { |
||
373 | $this->name = $this->convertName($methodName); |
||
374 | } |
||
375 | |||
376 | protected function convertName($camel) |
||
377 | { |
||
378 | $splitter="-"; |
||
379 | $camel=preg_replace('/(?!^)[[:upper:]][[:lower:]]/', '$0', preg_replace('/(?!^)[[:upper:]]+/', $splitter.'$0', $camel)); |
||
380 | $camel = preg_replace("/$splitter/", ':', $camel, 1); |
||
381 | return strtolower($camel); |
||
382 | } |
||
383 | |||
384 | protected function isAssoc($arr) |
||
385 | { |
||
386 | if (!is_array($arr)) { |
||
387 | return false; |
||
388 | } |
||
389 | return array_keys($arr) !== range(0, count($arr) - 1); |
||
390 | } |
||
391 | |||
392 | /** |
||
393 | * Given a list that might be 'a b c' or 'a, b, c' or 'a,b,c', |
||
394 | * convert the data into the last of these forms. |
||
395 | */ |
||
396 | protected static function convertListToCommaSeparated($text) |
||
400 | } |
||
401 |