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 DocumentGenerator 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 DocumentGenerator, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 45 | class DocumentGenerator |
||
| 46 | { |
||
| 47 | /** |
||
| 48 | * @var bool |
||
| 49 | */ |
||
| 50 | private $backupExisting = true; |
||
| 51 | |||
| 52 | /** The extension to use for written php files */ |
||
| 53 | private $extension = '.php'; |
||
| 54 | |||
| 55 | /** Whether or not the current ClassMetadataInfo instance is new or old */ |
||
| 56 | private $isNew = true; |
||
| 57 | |||
| 58 | private $staticReflection = array(); |
||
| 59 | |||
| 60 | /** Number of spaces to use for indention in generated code */ |
||
| 61 | private $numSpaces = 4; |
||
| 62 | |||
| 63 | /** The actual spaces to use for indention */ |
||
| 64 | private $spaces = ' '; |
||
| 65 | |||
| 66 | /** The class all generated documents should extend */ |
||
| 67 | private $classToExtend; |
||
| 68 | |||
| 69 | /** Whether or not to generate annotations */ |
||
| 70 | private $generateAnnotations = false; |
||
| 71 | |||
| 72 | /** Whether or not to generate stub methods */ |
||
| 73 | private $generateDocumentStubMethods = false; |
||
| 74 | |||
| 75 | /** Whether or not to update the document class if it exists already */ |
||
| 76 | private $updateDocumentIfExists = false; |
||
| 77 | |||
| 78 | /** Whether or not to re-generate document class if it exists already */ |
||
| 79 | private $regenerateDocumentIfExists = false; |
||
| 80 | |||
| 81 | private static $classTemplate = |
||
| 82 | '<?php |
||
| 83 | |||
| 84 | <namespace> |
||
| 85 | |||
| 86 | <imports> |
||
| 87 | |||
| 88 | <documentAnnotation> |
||
| 89 | <documentClassName> |
||
| 90 | { |
||
| 91 | <documentBody> |
||
| 92 | }'; |
||
| 93 | |||
| 94 | private static $getMethodTemplate = |
||
| 95 | '/** |
||
| 96 | * <description> |
||
| 97 | * |
||
| 98 | * @return <variableType>$<variableName> |
||
| 99 | */ |
||
| 100 | public function <methodName>() |
||
| 101 | { |
||
| 102 | <spaces>return $this-><fieldName>; |
||
| 103 | }'; |
||
| 104 | |||
| 105 | private static $setMethodTemplate = |
||
| 106 | '/** |
||
| 107 | * <description> |
||
| 108 | * |
||
| 109 | * @param <variableType>$<variableName> |
||
| 110 | * @return self |
||
| 111 | */ |
||
| 112 | public function <methodName>(<methodTypeHint>$<variableName><variableDefault>) |
||
| 113 | { |
||
| 114 | <spaces>$this-><fieldName> = $<variableName>; |
||
| 115 | <spaces>return $this; |
||
| 116 | }'; |
||
| 117 | |||
| 118 | private static $addMethodTemplate = |
||
| 119 | '/** |
||
| 120 | * <description> |
||
| 121 | * |
||
| 122 | * @param <variableType>$<variableName> |
||
| 123 | */ |
||
| 124 | public function <methodName>(<methodTypeHint>$<variableName>) |
||
| 125 | { |
||
| 126 | <spaces>$this-><fieldName>[] = $<variableName>; |
||
| 127 | }'; |
||
| 128 | |||
| 129 | private static $removeMethodTemplate = |
||
| 130 | '/** |
||
| 131 | * <description> |
||
| 132 | * |
||
| 133 | * @param <variableType>$<variableName> |
||
| 134 | */ |
||
| 135 | public function <methodName>(<methodTypeHint>$<variableName>) |
||
| 136 | { |
||
| 137 | <spaces>$this-><fieldName>->removeElement($<variableName>); |
||
| 138 | }'; |
||
| 139 | |||
| 140 | private static $lifecycleCallbackMethodTemplate = |
||
| 141 | '<comment> |
||
| 142 | public function <methodName>() |
||
| 143 | { |
||
| 144 | <spaces>// Add your code here |
||
| 145 | }'; |
||
| 146 | |||
| 147 | private static $constructorMethodTemplate = |
||
| 148 | 'public function __construct() |
||
| 149 | { |
||
| 150 | <collections> |
||
| 151 | } |
||
| 152 | '; |
||
| 153 | |||
| 154 | /** |
||
| 155 | * Generate and write document classes for the given array of ClassMetadataInfo instances |
||
| 156 | * |
||
| 157 | * @param array $metadatas |
||
| 158 | * @param string $outputDirectory |
||
| 159 | * @return void |
||
| 160 | */ |
||
| 161 | public function generate(array $metadatas, $outputDirectory) |
||
| 167 | |||
| 168 | /** |
||
| 169 | * Generated and write document class to disk for the given ClassMetadataInfo instance |
||
| 170 | * |
||
| 171 | * @param ClassMetadataInfo $metadata |
||
| 172 | * @param string $outputDirectory |
||
| 173 | * @throws \RuntimeException |
||
| 174 | * @return void |
||
| 175 | */ |
||
| 176 | 6 | public function writeDocumentClass(ClassMetadataInfo $metadata, $outputDirectory) |
|
| 208 | |||
| 209 | /** |
||
| 210 | * Generate a PHP5 Doctrine 2 document class from the given ClassMetadataInfo instance |
||
| 211 | * |
||
| 212 | * @param ClassMetadataInfo $metadata |
||
| 213 | * @return string $code |
||
| 214 | */ |
||
| 215 | 6 | public function generateDocumentClass(ClassMetadataInfo $metadata) |
|
| 236 | |||
| 237 | /** |
||
| 238 | * Generate the updated code for the given ClassMetadataInfo and document at path |
||
| 239 | * |
||
| 240 | * @param ClassMetadataInfo $metadata |
||
| 241 | * @param string $path |
||
| 242 | * @return string $code; |
||
| 243 | */ |
||
| 244 | 1 | public function generateUpdatedDocumentClass(ClassMetadataInfo $metadata, $path) |
|
| 254 | |||
| 255 | /** |
||
| 256 | * Set the number of spaces the exported class should have |
||
| 257 | * |
||
| 258 | * @param integer $numSpaces |
||
| 259 | * @return void |
||
| 260 | */ |
||
| 261 | public function setNumSpaces($numSpaces) |
||
| 266 | |||
| 267 | /** |
||
| 268 | * Set the extension to use when writing php files to disk |
||
| 269 | * |
||
| 270 | * @param string $extension |
||
| 271 | * @return void |
||
| 272 | */ |
||
| 273 | public function setExtension($extension) |
||
| 277 | |||
| 278 | /** |
||
| 279 | * Set the name of the class the generated classes should extend from |
||
| 280 | * |
||
| 281 | * @return void |
||
| 282 | */ |
||
| 283 | 1 | public function setClassToExtend($classToExtend) |
|
| 287 | |||
| 288 | /** |
||
| 289 | * Set whether or not to generate annotations for the document |
||
| 290 | * |
||
| 291 | * @param bool $bool |
||
| 292 | * @return void |
||
| 293 | */ |
||
| 294 | 6 | public function setGenerateAnnotations($bool) |
|
| 298 | |||
| 299 | /** |
||
| 300 | * Set whether or not to try and update the document if it already exists |
||
| 301 | * |
||
| 302 | * @param bool $bool |
||
| 303 | * @return void |
||
| 304 | */ |
||
| 305 | 6 | public function setUpdateDocumentIfExists($bool) |
|
| 309 | |||
| 310 | /** |
||
| 311 | * Set whether or not to regenerate the document if it exists |
||
| 312 | * |
||
| 313 | * @param bool $bool |
||
| 314 | * @return void |
||
| 315 | */ |
||
| 316 | 6 | public function setRegenerateDocumentIfExists($bool) |
|
| 320 | |||
| 321 | /** |
||
| 322 | * Set whether or not to generate stub methods for the document |
||
| 323 | * |
||
| 324 | * @param bool $bool |
||
| 325 | * @return void |
||
| 326 | */ |
||
| 327 | 6 | public function setGenerateStubMethods($bool) |
|
| 331 | |||
| 332 | /** |
||
| 333 | * Should an existing document be backed up if it already exists? |
||
| 334 | */ |
||
| 335 | public function setBackupExisting($bool) |
||
| 339 | |||
| 340 | 6 | private function generateDocumentNamespace(ClassMetadataInfo $metadata) |
|
| 346 | |||
| 347 | 6 | private function generateDocumentClassName(ClassMetadataInfo $metadata) |
|
| 352 | |||
| 353 | 6 | private function generateDocumentBody(ClassMetadataInfo $metadata) |
|
| 382 | |||
| 383 | 6 | private function generateDocumentConstructor(ClassMetadataInfo $metadata) |
|
| 384 | { |
||
| 385 | 6 | if ($this->hasMethod('__construct', $metadata)) { |
|
| 386 | 1 | return ''; |
|
| 387 | } |
||
| 388 | |||
| 389 | 6 | $collections = array(); |
|
| 390 | 6 | foreach ($metadata->fieldMappings AS $mapping) { |
|
| 391 | 6 | if ($mapping['type'] === ClassMetadataInfo::MANY) { |
|
| 392 | 6 | $collections[] = '$this->' . $mapping['fieldName'] . ' = new \Doctrine\Common\Collections\ArrayCollection();'; |
|
| 393 | 6 | } |
|
| 394 | 6 | } |
|
| 395 | 6 | if ($collections) { |
|
| 396 | 6 | return $this->prefixCodeWithSpaces(str_replace("<collections>", $this->spaces . implode("\n" . $this->spaces, $collections), self::$constructorMethodTemplate)); |
|
| 397 | } |
||
| 398 | return ''; |
||
| 399 | } |
||
| 400 | |||
| 401 | /** |
||
| 402 | * @todo this won't work if there is a namespace in brackets and a class outside of it. |
||
| 403 | * @param string $path |
||
| 404 | */ |
||
| 405 | 1 | private function parseTokensInDocumentFile($path) |
|
| 438 | |||
| 439 | 6 | View Code Duplication | private function hasProperty($property, ClassMetadataInfo $metadata) |
| 440 | { |
||
| 441 | 6 | if ($this->extendsClass()) { |
|
| 442 | // don't generate property if its already on the base class. |
||
| 443 | 1 | $reflClass = new \ReflectionClass($this->getClassToExtend()); |
|
| 444 | 1 | if ($reflClass->hasProperty($property)) { |
|
| 445 | return true; |
||
| 446 | } |
||
| 447 | 1 | } |
|
| 448 | |||
| 449 | 6 | foreach ($this->getTraits($metadata) as $trait) { |
|
| 450 | if ($trait->hasProperty($property)) { |
||
| 451 | return true; |
||
| 452 | } |
||
| 453 | 6 | } |
|
| 454 | |||
| 455 | return ( |
||
| 456 | 6 | isset($this->staticReflection[$metadata->name]) && |
|
| 457 | 1 | in_array($property, $this->staticReflection[$metadata->name]['properties']) |
|
| 458 | 6 | ); |
|
| 459 | } |
||
| 460 | |||
| 461 | 6 | View Code Duplication | private function hasMethod($method, ClassMetadataInfo $metadata) |
| 462 | { |
||
| 463 | 6 | if ($this->extendsClass()) { |
|
| 464 | // don't generate method if its already on the base class. |
||
| 465 | 1 | $reflClass = new \ReflectionClass($this->getClassToExtend()); |
|
| 466 | 1 | if ($reflClass->hasMethod($method)) { |
|
| 467 | return true; |
||
| 468 | } |
||
| 469 | 1 | } |
|
| 470 | |||
| 471 | 6 | foreach ($this->getTraits($metadata) as $trait) { |
|
| 472 | if ($trait->hasMethod($method)) { |
||
| 473 | return true; |
||
| 474 | } |
||
| 475 | 6 | } |
|
| 476 | |||
| 477 | return ( |
||
| 478 | 6 | isset($this->staticReflection[$metadata->name]) && |
|
| 479 | 1 | in_array($method, $this->staticReflection[$metadata->name]['methods']) |
|
| 480 | 6 | ); |
|
| 481 | } |
||
| 482 | |||
| 483 | 6 | private function hasNamespace(ClassMetadataInfo $metadata) |
|
| 487 | |||
| 488 | 6 | private function extendsClass() |
|
| 492 | |||
| 493 | 1 | private function getClassToExtend() |
|
| 497 | |||
| 498 | 1 | private function getClassToExtendName() |
|
| 504 | |||
| 505 | 6 | private function getClassName(ClassMetadataInfo $metadata) |
|
| 510 | |||
| 511 | 6 | private function getNamespace(ClassMetadataInfo $metadata) |
|
| 515 | |||
| 516 | /** |
||
| 517 | * @param ClassMetadataInfo $metadata |
||
| 518 | * |
||
| 519 | * @return array |
||
| 520 | */ |
||
| 521 | 6 | protected function getTraits(ClassMetadataInfo $metadata) |
|
| 522 | { |
||
| 523 | 6 | if (PHP_VERSION_ID >= 50400 && ($metadata->reflClass !== null || class_exists($metadata->name))) { |
|
| 524 | $reflClass = $metadata->reflClass === null ? new \ReflectionClass($metadata->name) : $metadata->reflClass; |
||
| 525 | $traits = array(); |
||
| 526 | while ($reflClass !== false) { |
||
| 527 | $traits = array_merge($traits, $reflClass->getTraits()); |
||
| 528 | $reflClass = $reflClass->getParentClass(); |
||
| 529 | } |
||
| 530 | return $traits; |
||
| 531 | } |
||
| 532 | 6 | return array(); |
|
| 533 | } |
||
| 534 | |||
| 535 | 6 | private function generateDocumentImports(ClassMetadataInfo $metadata) |
|
| 541 | |||
| 542 | 6 | private function generateDocumentDocBlock(ClassMetadataInfo $metadata) |
|
| 617 | |||
| 618 | 6 | private function generateInheritanceAnnotation(ClassMetadataInfo $metadata) |
|
| 624 | |||
| 625 | 6 | private function generateDiscriminatorFieldAnnotation(ClassMetadataInfo $metadata) |
|
| 631 | |||
| 632 | 6 | private function generateDiscriminatorMapAnnotation(ClassMetadataInfo $metadata) |
|
| 644 | |||
| 645 | 6 | private function generateDefaultDiscriminatorValueAnnotation(ClassMetadataInfo $metadata) |
|
| 651 | |||
| 652 | 6 | private function generateChangeTrackingPolicyAnnotation(ClassMetadataInfo $metadata) |
|
| 656 | |||
| 657 | 6 | private function generateDocumentStubMethods(ClassMetadataInfo $metadata) |
|
| 701 | |||
| 702 | /** |
||
| 703 | * @param array $fieldMapping |
||
| 704 | * |
||
| 705 | * @return bool |
||
| 706 | */ |
||
| 707 | 6 | protected function isAssociationNullable($fieldMapping) |
|
| 711 | |||
| 712 | 6 | private function generateDocumentLifecycleCallbackMethods(ClassMetadataInfo $metadata) |
|
| 713 | { |
||
| 714 | 6 | if (empty($metadata->lifecycleCallbacks)) { |
|
| 715 | return ''; |
||
| 716 | } |
||
| 717 | |||
| 718 | 6 | $methods = array(); |
|
| 719 | |||
| 720 | 6 | foreach ($metadata->lifecycleCallbacks as $event => $callbacks) { |
|
| 721 | 6 | foreach ($callbacks as $callback) { |
|
| 722 | 6 | if ($code = $this->generateLifecycleCallbackMethod($event, $callback, $metadata)) { |
|
| 723 | 6 | $methods[] = $code; |
|
| 724 | 6 | } |
|
| 725 | 6 | } |
|
| 726 | 6 | } |
|
| 727 | |||
| 728 | 6 | return implode("\n\n", $methods); |
|
| 729 | } |
||
| 730 | |||
| 731 | 6 | private function generateDocumentAssociationMappingProperties(ClassMetadataInfo $metadata) |
|
| 751 | |||
| 752 | 6 | private function generateDocumentFieldMappingProperties(ClassMetadataInfo $metadata) |
|
| 772 | |||
| 773 | 6 | private function generateDocumentStubMethod(ClassMetadataInfo $metadata, $type, $fieldName, $typeHint = null, $defaultValue = null) |
|
| 813 | |||
| 814 | 6 | private function generateLifecycleCallbackMethod($name, $methodName, ClassMetadataInfo $metadata) |
|
| 833 | |||
| 834 | 6 | private function generateAssociationMappingPropertyDocBlock(array $fieldMapping, ClassMetadataInfo $metadata) |
|
| 883 | |||
| 884 | 6 | private function generateFieldMappingPropertyDocBlock(array $fieldMapping, ClassMetadataInfo $metadata) |
|
| 885 | { |
||
| 886 | 6 | $lines = array(); |
|
| 887 | 6 | $lines[] = $this->spaces . '/**'; |
|
| 888 | 6 | if (isset($fieldMapping['id']) && $fieldMapping['id']) { |
|
| 889 | 6 | $fieldMapping['strategy'] = isset($fieldMapping['strategy']) ? $fieldMapping['strategy'] : ClassMetadataInfo::GENERATOR_TYPE_AUTO; |
|
| 890 | 6 | if ($fieldMapping['strategy'] === ClassMetadataInfo::GENERATOR_TYPE_AUTO) { |
|
| 891 | 6 | $lines[] = $this->spaces . ' * @var MongoId $' . $fieldMapping['fieldName']; |
|
| 892 | 6 | View Code Duplication | } elseif ($fieldMapping['strategy'] === ClassMetadataInfo::GENERATOR_TYPE_INCREMENT) { |
| 893 | $lines[] = $this->spaces . ' * @var integer $' . $fieldMapping['fieldName']; |
||
| 894 | } elseif ($fieldMapping['strategy'] === ClassMetadataInfo::GENERATOR_TYPE_UUID) { |
||
| 895 | $lines[] = $this->spaces . ' * @var string $' . $fieldMapping['fieldName']; |
||
| 896 | View Code Duplication | } elseif ($fieldMapping['strategy'] === ClassMetadataInfo::GENERATOR_TYPE_NONE) { |
|
| 897 | $lines[] = $this->spaces . ' * @var $' . $fieldMapping['fieldName']; |
||
| 898 | } else { |
||
| 899 | $lines[] = $this->spaces . ' * @var $' . $fieldMapping['fieldName']; |
||
| 900 | } |
||
| 901 | 6 | } else { |
|
| 902 | 6 | $lines[] = $this->spaces . ' * @var ' . $fieldMapping['type'] . ' $' . $fieldMapping['fieldName']; |
|
| 903 | } |
||
| 904 | |||
| 905 | 6 | if ($this->generateAnnotations) { |
|
| 906 | 6 | $lines[] = $this->spaces . ' *'; |
|
| 907 | |||
| 908 | 6 | $field = array(); |
|
| 909 | 6 | if (isset($fieldMapping['id']) && $fieldMapping['id']) { |
|
| 910 | 6 | if (isset($fieldMapping['strategy'])) { |
|
| 911 | 6 | $field[] = 'strategy="' . $this->getIdGeneratorTypeString($metadata->generatorType) . '"'; |
|
| 912 | 6 | } |
|
| 913 | 6 | $lines[] = $this->spaces . ' * @ODM\\Id(' . implode(', ', $field) . ')'; |
|
| 914 | 6 | } else { |
|
| 915 | 6 | if (isset($fieldMapping['name'])) { |
|
| 916 | 6 | $field[] = 'name="' . $fieldMapping['name'] . '"'; |
|
| 917 | 6 | } |
|
| 918 | |||
| 919 | 6 | if (isset($fieldMapping['type'])) { |
|
| 920 | 6 | $field[] = 'type="' . $fieldMapping['type'] . '"'; |
|
| 921 | 6 | } |
|
| 922 | |||
| 923 | 6 | if (isset($fieldMapping['nullable']) && $fieldMapping['nullable'] === true) { |
|
| 924 | $field[] = 'nullable=' . var_export($fieldMapping['nullable'], true); |
||
| 925 | } |
||
| 926 | 6 | if (isset($fieldMapping['options'])) { |
|
| 927 | $options = array(); |
||
| 928 | foreach ($fieldMapping['options'] as $key => $value) { |
||
| 929 | $options[] = '"' . $key . '" = "' . $value . '"'; |
||
| 930 | } |
||
| 931 | $field[] = "options={" . implode(', ', $options) . "}"; |
||
| 932 | } |
||
| 933 | 6 | $lines[] = $this->spaces . ' * @ODM\\Field(' . implode(', ', $field) . ')'; |
|
| 934 | } |
||
| 935 | |||
| 936 | 6 | if (isset($fieldMapping['version']) && $fieldMapping['version']) { |
|
| 937 | $lines[] = $this->spaces . ' * @ODM\\Version'; |
||
| 938 | } |
||
| 939 | 6 | } |
|
| 940 | |||
| 941 | 6 | $lines[] = $this->spaces . ' */'; |
|
| 942 | |||
| 943 | 6 | return implode("\n", $lines); |
|
| 944 | } |
||
| 945 | |||
| 946 | 6 | private function prefixCodeWithSpaces($code, $num = 1) |
|
| 956 | |||
| 957 | private function getInheritanceTypeString($type) |
||
| 973 | |||
| 974 | 6 | private function getChangeTrackingPolicyString($policy) |
|
| 990 | |||
| 991 | 6 | private function getIdGeneratorTypeString($type) |
|
| 992 | { |
||
| 993 | switch ($type) { |
||
| 994 | 6 | case ClassMetadataInfo::GENERATOR_TYPE_AUTO: |
|
| 995 | return 'AUTO'; |
||
| 996 | |||
| 1016 | } |
||
| 1017 |
In PHP, under loose comparison (like
==, or!=, orswitchconditions), values of different types might be equal.For
stringvalues, the empty string''is a special case, in particular the following results might be unexpected: