Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like BindCommandHandler 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 BindCommandHandler, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
38 | class BindCommandHandler |
||
39 | { |
||
40 | /** |
||
41 | * @var DiscoveryManager |
||
42 | */ |
||
43 | private $discoveryManager; |
||
44 | |||
45 | /** |
||
46 | * @var PackageCollection |
||
47 | */ |
||
48 | private $packages; |
||
49 | |||
50 | /** |
||
51 | * @var string |
||
52 | */ |
||
53 | private $currentPath = '/'; |
||
54 | |||
55 | /** |
||
56 | * Creates the handler. |
||
57 | * |
||
58 | * @param DiscoveryManager $discoveryManager The discovery manager. |
||
59 | * @param PackageCollection $packages The loaded packages. |
||
60 | */ |
||
61 | 39 | public function __construct(DiscoveryManager $discoveryManager, PackageCollection $packages) |
|
66 | |||
67 | /** |
||
68 | * Handles the "bind --list" command. |
||
69 | * |
||
70 | * @param Args $args The console arguments. |
||
71 | * @param IO $io The I/O. |
||
72 | * |
||
73 | * @return int The status code. |
||
74 | */ |
||
75 | 14 | public function handleList(Args $args, IO $io) |
|
76 | { |
||
77 | 14 | $packageNames = ArgsUtil::getPackageNames($args, $this->packages); |
|
78 | 14 | $bindingStates = $this->getBindingStates($args); |
|
79 | |||
80 | 14 | $printBindingState = count($bindingStates) > 1; |
|
81 | 14 | $printPackageName = count($packageNames) > 1; |
|
82 | 14 | $printHeaders = $printBindingState || $printPackageName; |
|
83 | 14 | $indentation = $printBindingState && $printPackageName ? 8 |
|
84 | 14 | : ($printBindingState || $printPackageName ? 4 : 0); |
|
85 | |||
86 | 14 | foreach ($bindingStates as $bindingState) { |
|
87 | 14 | $bindingStatePrinted = !$printBindingState; |
|
88 | |||
89 | 14 | foreach ($packageNames as $packageName) { |
|
90 | 14 | $expr = Expr::method('getContainingPackage', Expr::method('getName', Expr::same($packageName))) |
|
91 | 14 | ->andMethod('getState', Expr::same($bindingState)); |
|
92 | |||
93 | 14 | $descriptors = $this->discoveryManager->findBindingDescriptors($expr); |
|
94 | |||
95 | 14 | if (empty($descriptors)) { |
|
96 | 1 | continue; |
|
97 | } |
||
98 | |||
99 | 14 | if (!$bindingStatePrinted) { |
|
100 | 7 | $this->printBindingStateHeader($io, $bindingState); |
|
101 | 7 | $bindingStatePrinted = true; |
|
102 | } |
||
103 | |||
104 | 14 | View Code Duplication | if ($printPackageName) { |
|
|||
105 | 10 | $prefix = $printBindingState ? ' ' : ''; |
|
106 | 10 | $io->writeLine(sprintf('%sPackage: %s', $prefix, $packageName)); |
|
107 | 10 | $io->writeLine(''); |
|
108 | } |
||
109 | |||
110 | 14 | $this->printBindingTable($io, $descriptors, $indentation, BindingState::ENABLED === $bindingState); |
|
111 | |||
112 | 14 | if ($printHeaders) { |
|
113 | 14 | $io->writeLine(''); |
|
114 | } |
||
115 | } |
||
116 | } |
||
117 | |||
118 | 14 | return 0; |
|
119 | } |
||
120 | |||
121 | /** |
||
122 | * Handles the "bind <query> <type>" command. |
||
123 | * |
||
124 | * @param Args $args The console arguments. |
||
125 | * |
||
126 | * @return int The status code. |
||
127 | */ |
||
128 | 8 | public function handleAdd(Args $args) |
|
129 | { |
||
130 | 8 | $flags = $args->isOptionSet('force') |
|
131 | 1 | ? DiscoveryManager::OVERRIDE | DiscoveryManager::IGNORE_TYPE_NOT_FOUND |
|
132 | 1 | | DiscoveryManager::IGNORE_TYPE_NOT_ENABLED |
|
133 | 8 | : 0; |
|
134 | |||
135 | 8 | $bindingParams = array(); |
|
136 | 8 | $artifact = $args->getArgument('artifact'); |
|
137 | |||
138 | 8 | $this->parseParams($args, $bindingParams); |
|
139 | |||
140 | 7 | if (false !== strpos($artifact, '\\') || $args->isOptionSet('class')) { |
|
141 | 2 | $binding = new ClassBinding( |
|
142 | $artifact, |
||
143 | 2 | $args->getArgument('type'), |
|
144 | $bindingParams |
||
145 | ); |
||
146 | } else { |
||
147 | 5 | $binding = new ResourceBinding( |
|
148 | 5 | Path::makeAbsolute($artifact, $this->currentPath), |
|
149 | 5 | $args->getArgument('type'), |
|
150 | $bindingParams, |
||
151 | 5 | $args->getOption('language') |
|
152 | ); |
||
153 | } |
||
154 | |||
155 | 7 | $this->discoveryManager->addRootBindingDescriptor(new BindingDescriptor($binding), $flags); |
|
156 | |||
157 | 7 | return 0; |
|
158 | } |
||
159 | |||
160 | /** |
||
161 | * Handles the "bind --update <uuid>" command. |
||
162 | * |
||
163 | * @param Args $args The console arguments. |
||
164 | * |
||
165 | * @return int The status code. |
||
166 | */ |
||
167 | 7 | public function handleUpdate(Args $args) |
|
168 | { |
||
169 | 7 | $flags = $args->isOptionSet('force') |
|
170 | 1 | ? DiscoveryManager::OVERRIDE | DiscoveryManager::IGNORE_TYPE_NOT_FOUND |
|
171 | 1 | | DiscoveryManager::IGNORE_TYPE_NOT_ENABLED |
|
172 | 7 | : DiscoveryManager::OVERRIDE; |
|
173 | |||
174 | 7 | $descriptorToUpdate = $this->getBindingByUuidPrefix($args->getArgument('uuid')); |
|
175 | 7 | $bindingToUpdate = $descriptorToUpdate->getBinding(); |
|
176 | |||
177 | 7 | if (!$descriptorToUpdate->getContainingPackage() instanceof RootPackage) { |
|
178 | 1 | throw new RuntimeException(sprintf( |
|
179 | 1 | 'Can only update bindings in the package "%s".', |
|
180 | 1 | $this->packages->getRootPackageName() |
|
181 | )); |
||
182 | } |
||
183 | |||
184 | 6 | if ($bindingToUpdate instanceof ResourceBinding) { |
|
185 | 5 | $updatedBinding = $this->getUpdatedResourceBinding($args, $bindingToUpdate); |
|
186 | } elseif ($bindingToUpdate instanceof ClassBinding) { |
||
187 | 1 | $updatedBinding = $this->getUpdatedClassBinding($args, $bindingToUpdate); |
|
188 | } else { |
||
189 | throw new RuntimeException(sprintf( |
||
190 | 'Cannot update bindings of type %s.', |
||
191 | get_class($bindingToUpdate) |
||
192 | )); |
||
193 | } |
||
194 | |||
195 | 6 | $updatedDescriptor = new BindingDescriptor($updatedBinding); |
|
196 | |||
197 | 6 | if ($this->bindingsEqual($descriptorToUpdate, $updatedDescriptor)) { |
|
198 | 1 | throw new RuntimeException('Nothing to update.'); |
|
199 | } |
||
200 | |||
201 | 5 | $this->discoveryManager->addRootBindingDescriptor($updatedDescriptor, $flags); |
|
202 | |||
203 | 5 | return 0; |
|
204 | } |
||
205 | |||
206 | /** |
||
207 | * Handles the "bind --delete" command. |
||
208 | * |
||
209 | * @param Args $args The console arguments. |
||
210 | * |
||
211 | * @return int The status code. |
||
212 | */ |
||
213 | 4 | View Code Duplication | public function handleDelete(Args $args) |
214 | { |
||
215 | 4 | $bindingToRemove = $this->getBindingByUuidPrefix($args->getArgument('uuid')); |
|
216 | |||
217 | 2 | if (!$bindingToRemove->getContainingPackage() instanceof RootPackage) { |
|
218 | 1 | throw new RuntimeException(sprintf( |
|
219 | 1 | 'Can only delete bindings from the package "%s".', |
|
220 | 1 | $this->packages->getRootPackageName() |
|
221 | )); |
||
222 | } |
||
223 | |||
224 | 1 | $this->discoveryManager->removeRootBindingDescriptor($bindingToRemove->getUuid()); |
|
225 | |||
226 | 1 | return 0; |
|
227 | } |
||
228 | |||
229 | /** |
||
230 | * Handles the "bind --enable" command. |
||
231 | * |
||
232 | * @param Args $args The console arguments. |
||
233 | * |
||
234 | * @return int The status code. |
||
235 | */ |
||
236 | 3 | View Code Duplication | public function handleEnable(Args $args) |
237 | { |
||
238 | 3 | $bindingToEnable = $this->getBindingByUuidPrefix($args->getArgument('uuid')); |
|
239 | |||
240 | 2 | if ($bindingToEnable->getContainingPackage() instanceof RootPackage) { |
|
241 | 1 | throw new RuntimeException(sprintf( |
|
242 | 1 | 'Cannot enable bindings in the package "%s".', |
|
243 | 1 | $bindingToEnable->getContainingPackage()->getName() |
|
244 | )); |
||
245 | } |
||
246 | |||
247 | 1 | $this->discoveryManager->enableBindingDescriptor($bindingToEnable->getUuid()); |
|
248 | |||
249 | 1 | return 0; |
|
250 | } |
||
251 | |||
252 | /** |
||
253 | * Handles the "bind --disable" command. |
||
254 | * |
||
255 | * @param Args $args The console arguments. |
||
256 | * |
||
257 | * @return int The status code. |
||
258 | */ |
||
259 | 3 | View Code Duplication | public function handleDisable(Args $args) |
260 | { |
||
261 | 3 | $bindingToDisable = $this->getBindingByUuidPrefix($args->getArgument('uuid')); |
|
262 | |||
263 | 2 | if ($bindingToDisable->getContainingPackage() instanceof RootPackage) { |
|
264 | 1 | throw new RuntimeException(sprintf( |
|
265 | 1 | 'Cannot disable bindings in the package "%s".', |
|
266 | 1 | $bindingToDisable->getContainingPackage()->getName() |
|
267 | )); |
||
268 | } |
||
269 | |||
270 | 1 | $this->discoveryManager->disableBindingDescriptor($bindingToDisable->getUuid()); |
|
271 | |||
272 | 1 | return 0; |
|
273 | } |
||
274 | |||
275 | /** |
||
276 | * Returns the binding states selected in the console arguments. |
||
277 | * |
||
278 | * @param Args $args The console arguments. |
||
279 | * |
||
280 | * @return int[] The selected {@link BindingState} constants. |
||
281 | */ |
||
282 | 14 | private function getBindingStates(Args $args) |
|
283 | { |
||
284 | $states = array( |
||
285 | 14 | BindingState::ENABLED => 'enabled', |
|
286 | 14 | BindingState::DISABLED => 'disabled', |
|
287 | 14 | BindingState::TYPE_NOT_FOUND => 'type-not-found', |
|
288 | 14 | BindingState::TYPE_NOT_ENABLED => 'type-not-enabled', |
|
289 | 14 | BindingState::INVALID => 'invalid', |
|
290 | ); |
||
291 | |||
292 | 14 | $states = array_filter($states, function ($option) use ($args) { |
|
293 | 14 | return $args->isOptionSet($option); |
|
294 | 14 | }); |
|
295 | |||
296 | 14 | return array_keys($states) ?: BindingState::all(); |
|
297 | } |
||
298 | |||
299 | /** |
||
300 | * Prints a list of binding descriptors. |
||
301 | * |
||
302 | * @param IO $io The I/O. |
||
303 | * @param BindingDescriptor[] $descriptors The binding descriptors. |
||
304 | * @param int $indentation The number of spaces to indent. |
||
305 | * @param bool $enabled Whether the binding descriptors |
||
306 | * are enabled. If not, the output |
||
307 | * is printed in red. |
||
308 | */ |
||
309 | 14 | private function printBindingTable(IO $io, array $descriptors, $indentation = 0, $enabled = true) |
|
310 | { |
||
311 | 14 | $table = new Table(PuliTableStyle::borderless()); |
|
312 | |||
313 | 14 | $table->setHeaderRow(array('UUID', 'Glob', 'Type')); |
|
314 | |||
315 | 14 | $paramTag = $enabled ? 'c1' : 'bad'; |
|
316 | 14 | $artifactTag = $enabled ? 'c1' : 'bad'; |
|
317 | 14 | $typeTag = $enabled ? 'u' : 'bad'; |
|
318 | |||
319 | 14 | foreach ($descriptors as $descriptor) { |
|
320 | 14 | $parameters = array(); |
|
321 | 14 | $binding = $descriptor->getBinding(); |
|
322 | |||
323 | 14 | foreach ($binding->getParameterValues() as $parameterName => $parameterValue) { |
|
324 | 1 | $parameters[] = $parameterName.'='.StringUtil::formatValue($parameterValue); |
|
325 | } |
||
326 | |||
327 | 14 | $uuid = substr($descriptor->getUuid(), 0, 6); |
|
328 | |||
329 | 14 | if (!$enabled) { |
|
330 | 10 | $uuid = sprintf('<bad>%s</bad>', $uuid); |
|
331 | } |
||
332 | |||
333 | 14 | $paramString = ''; |
|
334 | |||
335 | 14 | if (!empty($parameters)) { |
|
336 | // \xc2\xa0 is a non-breaking space |
||
337 | 1 | $paramString = sprintf( |
|
338 | 1 | ' <%s>(%s)</%s>', |
|
339 | $paramTag, |
||
340 | 1 | implode(",\xc2\xa0", $parameters), |
|
341 | $paramTag |
||
342 | ); |
||
343 | } |
||
344 | |||
345 | 14 | if ($binding instanceof ResourceBinding) { |
|
346 | 14 | $artifact = $binding->getQuery(); |
|
347 | } elseif ($binding instanceof ClassBinding) { |
||
348 | 6 | $artifact = StringUtil::getShortClassName($binding->getClassName()); |
|
349 | } else { |
||
350 | continue; |
||
351 | } |
||
352 | |||
353 | 14 | $typeString = StringUtil::getShortClassName($binding->getTypeName()); |
|
354 | |||
355 | 14 | $table->addRow(array( |
|
356 | 14 | $uuid, |
|
357 | 14 | sprintf('<%s>%s</%s>', $artifactTag, $artifact, $artifactTag), |
|
358 | 14 | sprintf('<%s>%s</%s>%s', $typeTag, $typeString, $typeTag, $paramString), |
|
359 | )); |
||
360 | } |
||
361 | |||
362 | 14 | $table->render($io, $indentation); |
|
363 | 14 | } |
|
364 | |||
365 | /** |
||
366 | * Prints the header for a binding state. |
||
367 | * |
||
368 | * @param IO $io The I/O. |
||
369 | * @param int $bindingState The {@link BindingState} constant. |
||
370 | */ |
||
371 | 7 | private function printBindingStateHeader(IO $io, $bindingState) |
|
405 | |||
406 | 14 | private function parseParams(Args $args, array &$bindingParams) |
|
407 | { |
||
408 | 14 | foreach ($args->getOption('param') as $parameter) { |
|
409 | 4 | $pos = strpos($parameter, '='); |
|
410 | |||
411 | 4 | if (false === $pos) { |
|
412 | 1 | throw new RuntimeException(sprintf( |
|
413 | 'The "--param" option expects a parameter in the form '. |
||
414 | 1 | '"key=value". Got: "%s"', |
|
415 | $parameter |
||
416 | )); |
||
417 | } |
||
418 | |||
419 | 3 | $key = substr($parameter, 0, $pos); |
|
420 | 3 | $value = StringUtil::parseValue(substr($parameter, $pos + 1)); |
|
421 | |||
422 | 3 | $bindingParams[$key] = $value; |
|
423 | } |
||
424 | 13 | } |
|
425 | |||
426 | 6 | private function unsetParams(Args $args, array &$bindingParams) |
|
427 | { |
||
428 | 6 | foreach ($args->getOption('unset-param') as $parameter) { |
|
429 | 1 | unset($bindingParams[$parameter]); |
|
430 | } |
||
431 | 6 | } |
|
432 | |||
433 | /** |
||
434 | * @param string $uuidPrefix |
||
435 | * |
||
436 | * @return BindingDescriptor |
||
437 | */ |
||
438 | 17 | View Code Duplication | private function getBindingByUuidPrefix($uuidPrefix) |
439 | { |
||
440 | 17 | $expr = Expr::method('getUuid', Expr::startsWith($uuidPrefix)); |
|
441 | 17 | $descriptors = $this->discoveryManager->findBindingDescriptors($expr); |
|
442 | |||
443 | 17 | if (0 === count($descriptors)) { |
|
444 | 3 | throw new RuntimeException(sprintf('The binding "%s" does not exist.', $uuidPrefix)); |
|
445 | } |
||
446 | |||
447 | 14 | if (count($descriptors) > 1) { |
|
448 | 1 | throw new RuntimeException(sprintf( |
|
449 | 1 | 'More than one binding matches the UUID prefix "%s".', |
|
450 | $uuidPrefix |
||
451 | )); |
||
452 | } |
||
453 | |||
454 | 13 | return reset($descriptors); |
|
455 | } |
||
456 | |||
457 | 6 | private function bindingsEqual(BindingDescriptor $descriptor1, BindingDescriptor $descriptor2) |
|
461 | |||
462 | /** |
||
463 | * @param Args $args |
||
464 | * @param ResourceBinding $bindingToUpdate |
||
465 | * |
||
466 | * @return ResourceBinding |
||
467 | */ |
||
468 | 5 | private function getUpdatedResourceBinding(Args $args, ResourceBinding $bindingToUpdate) |
|
469 | { |
||
470 | 5 | $query = $bindingToUpdate->getQuery(); |
|
471 | 5 | $typeName = $bindingToUpdate->getTypeName(); |
|
472 | 5 | $language = $bindingToUpdate->getLanguage(); |
|
498 | |||
499 | /** |
||
500 | * @param Args $args |
||
501 | * @param ClassBinding $bindingToUpdate |
||
502 | * |
||
503 | * @return ClassBinding |
||
504 | */ |
||
505 | 1 | private function getUpdatedClassBinding(Args $args, ClassBinding $bindingToUpdate) |
|
529 | } |
||
530 |
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.