Total Complexity | 84 |
Total Lines | 618 |
Duplicated Lines | 0 % |
Changes | 0 |
Complex classes like IntrospectionHelper 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.
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 IntrospectionHelper, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
49 | class IntrospectionHelper |
||
50 | { |
||
51 | /** |
||
52 | * Holds the attribute setter methods. |
||
53 | * |
||
54 | * @var array string[] |
||
55 | */ |
||
56 | private $attributeSetters = []; |
||
57 | |||
58 | /** |
||
59 | * Holds methods to create nested elements. |
||
60 | * |
||
61 | * @var array string[] |
||
62 | */ |
||
63 | private $nestedCreators = []; |
||
64 | |||
65 | /** |
||
66 | * Holds methods to store configured nested elements. |
||
67 | * |
||
68 | * @var array string[] |
||
69 | */ |
||
70 | private $nestedStorers = []; |
||
71 | |||
72 | /** |
||
73 | * Map from attribute names to nested types. |
||
74 | */ |
||
75 | private $nestedTypes = []; |
||
76 | |||
77 | /** |
||
78 | * New idea in phing: any class can register certain |
||
79 | * keys -- e.g. "task.current_file" -- which can be used in |
||
80 | * task attributes, if supported. In the build XML these |
||
81 | * are referred to like this: |
||
82 | * <regexp pattern="\n" replace="%{task.current_file}"/> |
||
83 | * In the type/task a listener method must be defined: |
||
84 | * function setListeningReplace($slot) {}. |
||
85 | * |
||
86 | * @var array string[] |
||
87 | */ |
||
88 | private $slotListeners = []; |
||
89 | |||
90 | /** |
||
91 | * The method to add PCDATA stuff. |
||
92 | * |
||
93 | * @var string Method name of the addText (redundant?) method, if class supports it :) |
||
94 | */ |
||
95 | private $methodAddText; |
||
96 | |||
97 | /** |
||
98 | * The Class that's been introspected. |
||
99 | * |
||
100 | * @var object |
||
101 | */ |
||
102 | private $bean; |
||
103 | |||
104 | /** |
||
105 | * The cache of IntrospectionHelper classes instantiated by getHelper(). |
||
106 | * |
||
107 | * @var array IntrospectionHelpers[] |
||
108 | */ |
||
109 | private static $helpers = []; |
||
110 | |||
111 | /** |
||
112 | * This function constructs a new introspection helper for a specific class. |
||
113 | * |
||
114 | * This method loads all methods for the specified class and categorizes them |
||
115 | * as setters, creators, slot listeners, etc. This way, the setAttribue() doesn't |
||
116 | * need to perform any introspection -- either the requested attribute setter/creator |
||
117 | * exists or it does not & a BuildException is thrown. |
||
118 | * |
||
119 | * @param string $class the classname for this IH |
||
120 | * |
||
121 | * @throws BuildException |
||
122 | */ |
||
123 | public function __construct($class) |
||
248 | } |
||
249 | } // if $method->isPublic() |
||
250 | } // foreach |
||
251 | } |
||
252 | |||
253 | /** |
||
254 | * Factory method for helper objects. |
||
255 | * |
||
256 | * @param string $class The class to create a Helper for |
||
257 | * |
||
258 | * @return IntrospectionHelper |
||
259 | */ |
||
260 | public static function getHelper($class) |
||
261 | { |
||
262 | if (!isset(self::$helpers[$class])) { |
||
263 | self::$helpers[$class] = new IntrospectionHelper($class); |
||
264 | } |
||
265 | |||
266 | return self::$helpers[$class]; |
||
267 | } |
||
268 | |||
269 | /** |
||
270 | * Indicates whether the introspected class is a task container, supporting arbitrary nested tasks/types. |
||
271 | * |
||
272 | * @return bool true if the introspected class is a container; false otherwise |
||
273 | */ |
||
274 | public function isContainer() |
||
275 | { |
||
276 | return $this->bean->implementsInterface(TaskContainer::class); |
||
277 | } |
||
278 | |||
279 | /** |
||
280 | * Sets the named attribute. |
||
281 | * |
||
282 | * @param object $element |
||
283 | * @param string $attributeName |
||
284 | * @param mixed $value |
||
285 | * |
||
286 | * @throws BuildException |
||
287 | */ |
||
288 | public function setAttribute(Project $project, $element, $attributeName, &$value) |
||
289 | { |
||
290 | // we want to check whether the value we are setting looks like |
||
291 | // a slot-listener variable: %{task.current_file} |
||
292 | // |
||
293 | // slot-listener variables are not like properties, in that they cannot be mixed with |
||
294 | // other text values. The reason for this disparity is that properties are only |
||
295 | // set when first constructing objects from XML, whereas slot-listeners are always dynamic. |
||
296 | // |
||
297 | // This is made possible by PHP5 (objects automatically passed by reference) and PHP's loose |
||
298 | // typing. |
||
299 | if (StringHelper::isSlotVar($value)) { |
||
|
|||
300 | $as = 'setlistening' . strtolower($attributeName); |
||
301 | |||
302 | if (!isset($this->slotListeners[$as])) { |
||
303 | $msg = $this->getElementName( |
||
304 | $project, |
||
305 | $element |
||
306 | ) . " doesn't support a slot-listening '{$attributeName}' attribute."; |
||
307 | |||
308 | throw new BuildException($msg); |
||
309 | } |
||
310 | |||
311 | $method = $this->slotListeners[$as]; |
||
312 | |||
313 | $key = StringHelper::slotVar($value); |
||
314 | $value = Register::getSlot( |
||
315 | $key |
||
316 | ); // returns a RegisterSlot object which will hold current value of that register (accessible using getValue()) |
||
317 | } else { |
||
318 | // Traditional value options |
||
319 | |||
320 | $as = 'set' . strtolower($attributeName); |
||
321 | |||
322 | if (!isset($this->attributeSetters[$as])) { |
||
323 | if ($element instanceof DynamicAttribute) { |
||
324 | $element->setDynamicAttribute($attributeName, (string) $value); |
||
325 | |||
326 | return; |
||
327 | } |
||
328 | $msg = $this->getElementName($project, $element) . " doesn't support the '{$attributeName}' attribute."; |
||
329 | |||
330 | throw new BuildException($msg); |
||
331 | } |
||
332 | |||
333 | $method = $this->attributeSetters[$as]; |
||
334 | |||
335 | if ('setrefid' == $as) { |
||
336 | $value = new Reference($project, $value); |
||
337 | } else { |
||
338 | $params = $method->getParameters(); |
||
339 | |||
340 | /** @var ReflectionType $hint */ |
||
341 | $reflectedAttr = ($hint = $params[0]->getType()) ? $hint->getName() : null; |
||
342 | |||
343 | // value is a string representation of a bool type, |
||
344 | // convert it to primitive |
||
345 | if ('bool' === $reflectedAttr || ('string' !== $reflectedAttr && StringHelper::isBoolean($value))) { |
||
346 | $value = StringHelper::booleanValue($value); |
||
347 | } |
||
348 | |||
349 | // there should only be one param; we'll just assume .... |
||
350 | if (null !== $reflectedAttr) { |
||
351 | switch ($reflectedAttr) { |
||
352 | case File::class: |
||
353 | $value = $project->resolveFile($value); |
||
354 | |||
355 | break; |
||
356 | |||
357 | case Path::class: |
||
358 | $value = new Path($project, $value); |
||
359 | |||
360 | break; |
||
361 | |||
362 | case Reference::class: |
||
363 | $value = new Reference($project, $value); |
||
364 | |||
365 | break; |
||
366 | // any other object params we want to support should go here ... |
||
367 | } |
||
368 | } // if hint !== null |
||
369 | } // if not setrefid |
||
370 | } // if is slot-listener |
||
371 | |||
372 | try { |
||
373 | $project->log( |
||
374 | ' -calling setter ' . $method->getDeclaringClass()->getName() . '::' . $method->getName() . '()', |
||
375 | Project::MSG_DEBUG |
||
376 | ); |
||
377 | $method->invoke($element, $value); |
||
378 | } catch (Exception $exc) { |
||
379 | throw new BuildException($exc->getMessage(), $exc); |
||
380 | } |
||
381 | } |
||
382 | |||
383 | /** |
||
384 | * Adds PCDATA areas. |
||
385 | * |
||
386 | * @param string $element |
||
387 | * @param string $text |
||
388 | * |
||
389 | * @throws BuildException |
||
390 | */ |
||
391 | public function addText(Project $project, $element, $text) |
||
392 | { |
||
393 | if (null === $this->methodAddText) { |
||
394 | $msg = $this->getElementName($project, $element) . " doesn't support nested text data."; |
||
395 | |||
396 | throw new BuildException($msg); |
||
397 | } |
||
398 | |||
399 | try { |
||
400 | $method = $this->methodAddText; |
||
401 | $method->invoke($element, $text); |
||
402 | } catch (Exception $exc) { |
||
403 | throw new BuildException($exc->getMessage(), $exc); |
||
404 | } |
||
405 | } |
||
406 | |||
407 | /** |
||
408 | * Creates a named nested element. |
||
409 | * |
||
410 | * Valid creators can be in the form createFoo() or addFoo(Bar). |
||
411 | * |
||
412 | * @param object $element Object the XML tag is child of. |
||
413 | * Often a task object. |
||
414 | * @param string $elementName XML tag name |
||
415 | * |
||
416 | * @throws BuildException |
||
417 | * |
||
418 | * @return object returns the nested element |
||
419 | */ |
||
420 | public function createElement(Project $project, $element, $elementName) |
||
514 | } |
||
515 | |||
516 | /** |
||
517 | * Creates a named nested element. |
||
518 | * |
||
519 | * @param Project $project |
||
520 | * @param string $element |
||
521 | * @param string $child |
||
522 | * @param null|string $elementName |
||
523 | * |
||
524 | * @throws BuildException |
||
525 | */ |
||
526 | public function storeElement($project, $element, $child, $elementName = null) |
||
527 | { |
||
528 | if (null === $elementName) { |
||
529 | return; |
||
530 | } |
||
531 | |||
532 | $storer = 'addconfigured' . strtolower($elementName); |
||
533 | |||
534 | if (isset($this->nestedStorers[$storer])) { |
||
535 | $method = $this->nestedStorers[$storer]; |
||
536 | |||
537 | try { |
||
538 | $project->log( |
||
539 | ' -calling storer ' . $method->getDeclaringClass()->getName() . '::' . $method->getName() . '()', |
||
540 | Project::MSG_DEBUG |
||
541 | ); |
||
542 | $method->invoke($element, $child); |
||
543 | } catch (Exception $exc) { |
||
544 | throw new BuildException($exc->getMessage(), $exc); |
||
545 | } |
||
546 | } |
||
547 | } |
||
548 | |||
549 | /** |
||
550 | * Does the introspected class support PCDATA? |
||
551 | * |
||
552 | * @return bool |
||
553 | */ |
||
554 | public function supportsCharacters() |
||
555 | { |
||
556 | return null !== $this->methodAddText; |
||
557 | } |
||
558 | |||
559 | /** |
||
560 | * Return all attribues supported by the introspected class. |
||
561 | * |
||
562 | * @return string[] |
||
563 | */ |
||
564 | public function getAttributes() |
||
565 | { |
||
566 | $attribs = []; |
||
567 | foreach (array_keys($this->attributeSetters) as $setter) { |
||
568 | $attribs[] = $this->getPropertyName($setter, 'set'); |
||
569 | } |
||
570 | |||
571 | return $attribs; |
||
572 | } |
||
573 | |||
574 | /** |
||
575 | * Return all nested elements supported by the introspected class. |
||
576 | * |
||
577 | * @return string[] |
||
578 | */ |
||
579 | public function getNestedElements() |
||
580 | { |
||
581 | return $this->nestedTypes; |
||
582 | } |
||
583 | |||
584 | /** |
||
585 | * Get the name for an element. |
||
586 | * When possible the full classnam (phing.tasks.system.PropertyTask) will |
||
587 | * be returned. If not available (loaded in taskdefs or typedefs) then the |
||
588 | * XML element name will be returned. |
||
589 | * |
||
590 | * @param object $element the Task or type element |
||
591 | * |
||
592 | * @return string fully qualified class name of element when possible |
||
593 | */ |
||
594 | public function getElementName(Project $project, $element) |
||
595 | { |
||
596 | $taskdefs = $project->getTaskDefinitions(); |
||
597 | $typedefs = $project->getDataTypeDefinitions(); |
||
598 | |||
599 | // check if class of element is registered with project (tasks & types) |
||
600 | // most element types don't have a getTag() method |
||
601 | $elClass = get_class($element); |
||
602 | |||
603 | if (!in_array('getTag', get_class_methods($elClass))) { |
||
604 | // loop through taskdefs and typesdefs and see if the class name |
||
605 | // matches (case-insensitive) any of the classes in there |
||
606 | foreach (array_merge($taskdefs, $typedefs) as $elName => $class) { |
||
607 | if (0 === strcasecmp($elClass, $class)) { |
||
608 | return $class; |
||
609 | } |
||
610 | } |
||
611 | |||
612 | return "{$elClass} (unknown)"; |
||
613 | } |
||
614 | |||
615 | // ->getTag() method does exist, so use it |
||
616 | $elName = $element->getTag(); |
||
617 | if (isset($taskdefs[$elName])) { |
||
618 | return $taskdefs[$elName]; |
||
619 | } |
||
620 | |||
621 | if (isset($typedefs[$elName])) { |
||
622 | return $typedefs[$elName]; |
||
623 | } |
||
624 | |||
625 | return "{$elName} (unknown)"; |
||
626 | } |
||
627 | |||
628 | /** |
||
629 | * Extract the name of a property from a method name - subtracting a given prefix. |
||
630 | * |
||
631 | * @param string $methodName |
||
632 | * @param string $prefix |
||
633 | * |
||
634 | * @return string |
||
635 | */ |
||
636 | public function getPropertyName($methodName, $prefix) |
||
641 | } |
||
642 | |||
643 | /** |
||
644 | * Prints warning message to screen if -debug was used. |
||
645 | * |
||
646 | * @param string $msg |
||
647 | */ |
||
648 | public function warn($msg) |
||
649 | { |
||
652 | } |
||
653 | } |
||
654 | |||
655 | /** |
||
656 | * @param \ReflectionParameter $parameter |
||
657 | * @return mixed|null |
||
658 | */ |
||
659 | private function getClassnameFromParameter(\ReflectionParameter $parameter) |
||
667 | } |
||
668 | } |
||
669 |
In PHP, under loose comparison (like
==
, or!=
, orswitch
conditions), values of different types might be equal.For
integer
values, zero is a special case, in particular the following results might be unexpected: