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 |
||
| 24 | class DocumentGenerator |
||
| 25 | { |
||
| 26 | /** |
||
| 27 | * @var bool |
||
| 28 | */ |
||
| 29 | private $backupExisting = true; |
||
| 30 | |||
| 31 | /** The extension to use for written php files */ |
||
| 32 | private $extension = '.php'; |
||
| 33 | |||
| 34 | /** Whether or not the current ClassMetadataInfo instance is new or old */ |
||
| 35 | private $isNew = true; |
||
| 36 | |||
| 37 | private $staticReflection = array(); |
||
| 38 | |||
| 39 | /** Number of spaces to use for indention in generated code */ |
||
| 40 | private $numSpaces = 4; |
||
| 41 | |||
| 42 | /** The actual spaces to use for indention */ |
||
| 43 | private $spaces = ' '; |
||
| 44 | |||
| 45 | /** The class all generated documents should extend */ |
||
| 46 | private $classToExtend; |
||
| 47 | |||
| 48 | /** Whether or not to generate annotations */ |
||
| 49 | private $generateAnnotations = false; |
||
| 50 | |||
| 51 | /** Whether or not to generate stub methods */ |
||
| 52 | private $generateDocumentStubMethods = false; |
||
| 53 | |||
| 54 | /** Whether or not to update the document class if it exists already */ |
||
| 55 | private $updateDocumentIfExists = false; |
||
| 56 | |||
| 57 | /** Whether or not to re-generate document class if it exists already */ |
||
| 58 | private $regenerateDocumentIfExists = false; |
||
| 59 | |||
| 60 | private static $classTemplate = |
||
| 61 | '<?php |
||
| 62 | |||
| 63 | <namespace> |
||
| 64 | |||
| 65 | <imports> |
||
| 66 | |||
| 67 | <documentAnnotation> |
||
| 68 | <documentClassName> |
||
| 69 | { |
||
| 70 | <documentBody> |
||
| 71 | } |
||
| 72 | '; |
||
| 73 | |||
| 74 | private static $getMethodTemplate = |
||
| 75 | '/** |
||
| 76 | * <description> |
||
| 77 | * |
||
| 78 | * @return <variableType>$<variableName> |
||
| 79 | */ |
||
| 80 | public function <methodName>() |
||
| 81 | { |
||
| 82 | <spaces>return $this-><fieldName>; |
||
| 83 | }'; |
||
| 84 | |||
| 85 | private static $setMethodTemplate = |
||
| 86 | '/** |
||
| 87 | * <description> |
||
| 88 | * |
||
| 89 | * @param <variableType>$<variableName> |
||
| 90 | * @return $this |
||
| 91 | */ |
||
| 92 | public function <methodName>(<methodTypeHint>$<variableName><variableDefault>) |
||
| 93 | { |
||
| 94 | <spaces>$this-><fieldName> = $<variableName>; |
||
| 95 | <spaces>return $this; |
||
| 96 | }'; |
||
| 97 | |||
| 98 | private static $addMethodTemplate = |
||
| 99 | '/** |
||
| 100 | * <description> |
||
| 101 | * |
||
| 102 | * @param <variableType>$<variableName> |
||
| 103 | */ |
||
| 104 | public function <methodName>(<methodTypeHint>$<variableName>) |
||
| 105 | { |
||
| 106 | <spaces>$this-><fieldName>[] = $<variableName>; |
||
| 107 | }'; |
||
| 108 | |||
| 109 | private static $removeMethodTemplate = |
||
| 110 | '/** |
||
| 111 | * <description> |
||
| 112 | * |
||
| 113 | * @param <variableType>$<variableName> |
||
| 114 | */ |
||
| 115 | public function <methodName>(<methodTypeHint>$<variableName>) |
||
| 116 | { |
||
| 117 | <spaces>$this-><fieldName>->removeElement($<variableName>); |
||
| 118 | }'; |
||
| 119 | |||
| 120 | private static $lifecycleCallbackMethodTemplate = |
||
| 121 | '<comment> |
||
| 122 | public function <methodName>() |
||
| 123 | { |
||
| 124 | <spaces>// Add your code here |
||
| 125 | }'; |
||
| 126 | |||
| 127 | private static $constructorMethodTemplate = |
||
| 128 | 'public function __construct() |
||
| 129 | { |
||
| 130 | <collections> |
||
| 131 | } |
||
| 132 | '; |
||
| 133 | |||
| 134 | /** |
||
| 135 | * Generate and write document classes for the given array of ClassMetadataInfo instances |
||
| 136 | * |
||
| 137 | * @param array $metadatas |
||
| 138 | * @param string $outputDirectory |
||
| 139 | * @return void |
||
| 140 | */ |
||
| 141 | public function generate(array $metadatas, $outputDirectory) |
||
| 147 | |||
| 148 | /** |
||
| 149 | * Generated and write document class to disk for the given ClassMetadataInfo instance |
||
| 150 | * |
||
| 151 | * @param ClassMetadataInfo $metadata |
||
| 152 | * @param string $outputDirectory |
||
| 153 | * @throws \RuntimeException |
||
| 154 | * @return void |
||
| 155 | */ |
||
| 156 | 9 | public function writeDocumentClass(ClassMetadataInfo $metadata, $outputDirectory) |
|
| 188 | |||
| 189 | /** |
||
| 190 | * Generate a PHP5 Doctrine 2 document class from the given ClassMetadataInfo instance |
||
| 191 | * |
||
| 192 | * @param ClassMetadataInfo $metadata |
||
| 193 | * @return string $code |
||
| 194 | */ |
||
| 195 | 8 | public function generateDocumentClass(ClassMetadataInfo $metadata) |
|
| 216 | |||
| 217 | /** |
||
| 218 | * Generate the updated code for the given ClassMetadataInfo and document at path |
||
| 219 | * |
||
| 220 | * @param ClassMetadataInfo $metadata |
||
| 221 | * @param string $path |
||
| 222 | * @return string $code; |
||
| 223 | */ |
||
| 224 | 2 | public function generateUpdatedDocumentClass(ClassMetadataInfo $metadata, $path) |
|
| 234 | |||
| 235 | /** |
||
| 236 | * Set the number of spaces the exported class should have |
||
| 237 | * |
||
| 238 | * @param integer $numSpaces |
||
| 239 | * @return void |
||
| 240 | */ |
||
| 241 | public function setNumSpaces($numSpaces) |
||
| 246 | |||
| 247 | /** |
||
| 248 | * Set the extension to use when writing php files to disk |
||
| 249 | * |
||
| 250 | * @param string $extension |
||
| 251 | * @return void |
||
| 252 | */ |
||
| 253 | public function setExtension($extension) |
||
| 257 | |||
| 258 | /** |
||
| 259 | * Set the name of the class the generated classes should extend from |
||
| 260 | * |
||
| 261 | * @param string $classToExtend Class name. |
||
| 262 | * @return void |
||
| 263 | */ |
||
| 264 | 1 | public function setClassToExtend($classToExtend) |
|
| 268 | |||
| 269 | /** |
||
| 270 | * Set whether or not to generate annotations for the document |
||
| 271 | * |
||
| 272 | * @param bool $bool |
||
| 273 | * @return void |
||
| 274 | */ |
||
| 275 | 9 | public function setGenerateAnnotations($bool) |
|
| 279 | |||
| 280 | /** |
||
| 281 | * Set whether or not to try and update the document if it already exists |
||
| 282 | * |
||
| 283 | * @param bool $bool |
||
| 284 | * @return void |
||
| 285 | */ |
||
| 286 | 9 | public function setUpdateDocumentIfExists($bool) |
|
| 290 | |||
| 291 | /** |
||
| 292 | * Set whether or not to regenerate the document if it exists |
||
| 293 | * |
||
| 294 | * @param bool $bool |
||
| 295 | * @return void |
||
| 296 | */ |
||
| 297 | 9 | public function setRegenerateDocumentIfExists($bool) |
|
| 301 | |||
| 302 | /** |
||
| 303 | * Set whether or not to generate stub methods for the document |
||
| 304 | * |
||
| 305 | * @param bool $bool |
||
| 306 | * @return void |
||
| 307 | */ |
||
| 308 | 9 | public function setGenerateStubMethods($bool) |
|
| 312 | |||
| 313 | /** |
||
| 314 | * Sets a value indicating whether existing documents will be backed up. |
||
| 315 | * |
||
| 316 | * @param bool $bool True to backup existing document, false to overwrite. |
||
| 317 | */ |
||
| 318 | public function setBackupExisting($bool) |
||
| 322 | |||
| 323 | 8 | private function generateDocumentNamespace(ClassMetadataInfo $metadata) |
|
| 329 | |||
| 330 | 8 | private function generateDocumentClassName(ClassMetadataInfo $metadata) |
|
| 335 | |||
| 336 | 9 | private function generateDocumentBody(ClassMetadataInfo $metadata) |
|
| 365 | |||
| 366 | 9 | private function generateDocumentConstructor(ClassMetadataInfo $metadata) |
|
| 383 | |||
| 384 | /** |
||
| 385 | * @todo this won't work if there is a namespace in brackets and a class outside of it. |
||
| 386 | * @param string $path |
||
| 387 | */ |
||
| 388 | 2 | private function parseTokensInDocumentFile($path) |
|
| 421 | |||
| 422 | 9 | View Code Duplication | private function hasProperty($property, ClassMetadataInfo $metadata) |
| 444 | |||
| 445 | 9 | View Code Duplication | private function hasMethod($method, ClassMetadataInfo $metadata) |
| 467 | |||
| 468 | 8 | private function hasNamespace(ClassMetadataInfo $metadata) |
|
| 472 | |||
| 473 | 9 | private function extendsClass() |
|
| 477 | |||
| 478 | 2 | private function getClassToExtend() |
|
| 482 | |||
| 483 | 1 | private function getClassToExtendName() |
|
| 489 | |||
| 490 | 8 | private function getClassName(ClassMetadataInfo $metadata) |
|
| 495 | |||
| 496 | 8 | private function getNamespace(ClassMetadataInfo $metadata) |
|
| 500 | |||
| 501 | /** |
||
| 502 | * @param ClassMetadataInfo $metadata |
||
| 503 | * |
||
| 504 | * @return array |
||
| 505 | */ |
||
| 506 | 9 | protected function getTraits(ClassMetadataInfo $metadata) |
|
| 519 | |||
| 520 | 8 | private function generateDocumentImports() |
|
| 526 | |||
| 527 | 8 | private function generateDocumentDocBlock(ClassMetadataInfo $metadata) |
|
| 604 | |||
| 605 | 8 | private function generateInheritanceAnnotation(ClassMetadataInfo $metadata) |
|
| 611 | |||
| 612 | 8 | private function generateDiscriminatorFieldAnnotation(ClassMetadataInfo $metadata) |
|
| 618 | |||
| 619 | 8 | private function generateDiscriminatorMapAnnotation(ClassMetadataInfo $metadata) |
|
| 631 | |||
| 632 | 8 | private function generateDefaultDiscriminatorValueAnnotation(ClassMetadataInfo $metadata) |
|
| 638 | |||
| 639 | 8 | private function generateChangeTrackingPolicyAnnotation(ClassMetadataInfo $metadata) |
|
| 643 | |||
| 644 | 9 | private function generateDocumentStubMethods(ClassMetadataInfo $metadata) |
|
| 645 | { |
||
| 646 | 9 | $methods = array(); |
|
| 647 | |||
| 648 | 9 | foreach ($metadata->fieldMappings as $fieldMapping) { |
|
| 649 | 9 | if (isset($fieldMapping['id'])) { |
|
| 650 | 9 | View Code Duplication | if ($metadata->generatorType == ClassMetadataInfo::GENERATOR_TYPE_NONE) { |
| 651 | if ($code = $this->generateDocumentStubMethod($metadata, 'set', $fieldMapping['fieldName'], $fieldMapping['type'])) { |
||
| 652 | $methods[] = $code; |
||
| 653 | } |
||
| 654 | } |
||
| 655 | 9 | View Code Duplication | if ($code = $code = $this->generateDocumentStubMethod($metadata, 'get', $fieldMapping['fieldName'], $fieldMapping['type'])) { |
| 656 | 9 | $methods[] = $code; |
|
| 657 | } |
||
| 658 | 9 | } elseif ( ! isset($fieldMapping['association'])) { |
|
| 659 | 9 | View Code Duplication | if ($code = $code = $this->generateDocumentStubMethod($metadata, 'set', $fieldMapping['fieldName'], $fieldMapping['type'])) { |
| 660 | 9 | $methods[] = $code; |
|
| 661 | } |
||
| 662 | 9 | View Code Duplication | if ($code = $code = $this->generateDocumentStubMethod($metadata, 'get', $fieldMapping['fieldName'], $fieldMapping['type'])) { |
| 663 | 9 | $methods[] = $code; |
|
| 664 | } |
||
| 665 | 8 | } elseif ($fieldMapping['type'] === ClassMetadataInfo::ONE) { |
|
| 666 | 8 | $nullable = $this->isAssociationNullable($fieldMapping) ? 'null' : null; |
|
| 667 | 8 | View Code Duplication | if ($code = $this->generateDocumentStubMethod($metadata, 'set', $fieldMapping['fieldName'], $fieldMapping['targetDocument'] ?? null, $nullable)) { |
| 668 | 6 | $methods[] = $code; |
|
| 669 | } |
||
| 670 | 8 | View Code Duplication | if ($code = $this->generateDocumentStubMethod($metadata, 'get', $fieldMapping['fieldName'], $fieldMapping['targetDocument'] ?? null)) { |
| 671 | 8 | $methods[] = $code; |
|
| 672 | } |
||
| 673 | 6 | } elseif ($fieldMapping['type'] === ClassMetadataInfo::MANY) { |
|
| 674 | 6 | View Code Duplication | if ($code = $this->generateDocumentStubMethod($metadata, 'add', $fieldMapping['fieldName'], $fieldMapping['targetDocument'] ?? null)) { |
| 675 | 6 | $methods[] = $code; |
|
| 676 | } |
||
| 677 | 6 | View Code Duplication | if ($code = $this->generateDocumentStubMethod($metadata, 'remove', $fieldMapping['fieldName'], $fieldMapping['targetDocument'] ?? null)) { |
| 678 | 6 | $methods[] = $code; |
|
| 679 | } |
||
| 680 | 6 | if ($code = $this->generateDocumentStubMethod($metadata, 'get', $fieldMapping['fieldName'], '\Doctrine\Common\Collections\Collection')) { |
|
| 681 | 9 | $methods[] = $code; |
|
| 682 | } |
||
| 683 | } |
||
| 684 | } |
||
| 685 | |||
| 686 | 9 | return implode("\n\n", $methods); |
|
| 687 | } |
||
| 688 | |||
| 689 | /** |
||
| 690 | * @param array $fieldMapping |
||
| 691 | * |
||
| 692 | * @return bool |
||
| 693 | */ |
||
| 694 | 8 | protected function isAssociationNullable($fieldMapping) |
|
| 698 | |||
| 699 | 9 | private function generateDocumentLifecycleCallbackMethods(ClassMetadataInfo $metadata) |
|
| 717 | |||
| 718 | 9 | private function generateDocumentAssociationMappingProperties(ClassMetadataInfo $metadata) |
|
| 738 | |||
| 739 | 9 | private function generateDocumentFieldMappingProperties(ClassMetadataInfo $metadata) |
|
| 759 | |||
| 760 | 9 | private function generateDocumentStubMethod(ClassMetadataInfo $metadata, $type, $fieldName, $typeHint = null, $defaultValue = null) |
|
| 800 | |||
| 801 | 6 | private function generateLifecycleCallbackMethod($name, $methodName, ClassMetadataInfo $metadata) |
|
| 820 | |||
| 821 | 6 | private function generateAssociationMappingPropertyDocBlock(array $fieldMapping) |
|
| 822 | { |
||
| 823 | 6 | $lines = array(); |
|
| 824 | 6 | $lines[] = $this->spaces . '/**'; |
|
| 825 | 6 | $lines[] = $this->spaces . ' * @var ' . ($fieldMapping['targetDocument'] ?? 'object'); |
|
| 826 | |||
| 827 | 6 | if ($this->generateAnnotations) { |
|
| 828 | 6 | $lines[] = $this->spaces . ' *'; |
|
| 829 | |||
| 830 | 6 | $type = null; |
|
| 831 | 6 | switch ($fieldMapping['association']) { |
|
| 832 | case ClassMetadataInfo::EMBED_ONE: |
||
| 833 | $type = 'EmbedOne'; |
||
| 834 | break; |
||
| 835 | case ClassMetadataInfo::EMBED_MANY: |
||
| 836 | $type = 'EmbedMany'; |
||
| 837 | break; |
||
| 838 | case ClassMetadataInfo::REFERENCE_ONE: |
||
| 839 | 6 | $type = 'ReferenceOne'; |
|
| 840 | 6 | break; |
|
| 841 | case ClassMetadataInfo::REFERENCE_MANY: |
||
| 842 | 6 | $type = 'ReferenceMany'; |
|
| 843 | 6 | break; |
|
| 844 | } |
||
| 845 | 6 | $typeOptions = array(); |
|
| 846 | |||
| 847 | 6 | if (isset($fieldMapping['targetDocument'])) { |
|
| 848 | 6 | $typeOptions[] = 'targetDocument="' . $fieldMapping['targetDocument'] . '"'; |
|
| 849 | } |
||
| 850 | |||
| 851 | 6 | if (isset($fieldMapping['cascade']) && $fieldMapping['cascade']) { |
|
| 852 | $cascades = array(); |
||
| 853 | |||
| 854 | if ($fieldMapping['isCascadePersist']) $cascades[] = '"persist"'; |
||
| 855 | if ($fieldMapping['isCascadeRemove']) $cascades[] = '"remove"'; |
||
| 856 | if ($fieldMapping['isCascadeDetach']) $cascades[] = '"detach"'; |
||
| 857 | if ($fieldMapping['isCascadeMerge']) $cascades[] = '"merge"'; |
||
| 858 | if ($fieldMapping['isCascadeRefresh']) $cascades[] = '"refresh"'; |
||
| 859 | |||
| 860 | $typeOptions[] = 'cascade={' . implode(',', $cascades) . '}'; |
||
| 861 | } |
||
| 862 | |||
| 863 | 6 | $lines[] = $this->spaces . ' * @ODM\\' . $type . '(' . implode(', ', $typeOptions) . ')'; |
|
| 864 | } |
||
| 865 | |||
| 866 | 6 | $lines[] = $this->spaces . ' */'; |
|
| 867 | |||
| 868 | 6 | return implode("\n", $lines); |
|
| 869 | } |
||
| 870 | |||
| 871 | 7 | private function generateFieldMappingPropertyDocBlock(array $fieldMapping, ClassMetadataInfo $metadata) |
|
| 872 | { |
||
| 873 | 7 | $lines = array(); |
|
| 874 | 7 | $lines[] = $this->spaces . '/**'; |
|
| 875 | 7 | if (isset($fieldMapping['id']) && $fieldMapping['id']) { |
|
| 876 | 7 | $fieldMapping['strategy'] = $fieldMapping['strategy'] ?? ClassMetadataInfo::GENERATOR_TYPE_AUTO; |
|
| 877 | 7 | if ($fieldMapping['strategy'] === ClassMetadataInfo::GENERATOR_TYPE_AUTO) { |
|
| 878 | 6 | $lines[] = $this->spaces . ' * @var MongoDB\BSON\ObjectId $' . $fieldMapping['fieldName']; |
|
| 879 | 1 | View Code Duplication | } elseif ($fieldMapping['strategy'] === ClassMetadataInfo::GENERATOR_TYPE_INCREMENT) { |
| 880 | $lines[] = $this->spaces . ' * @var integer $' . $fieldMapping['fieldName']; |
||
| 881 | 1 | } elseif ($fieldMapping['strategy'] === ClassMetadataInfo::GENERATOR_TYPE_UUID) { |
|
| 882 | $lines[] = $this->spaces . ' * @var string $' . $fieldMapping['fieldName']; |
||
| 883 | 1 | View Code Duplication | } elseif ($fieldMapping['strategy'] === ClassMetadataInfo::GENERATOR_TYPE_NONE) { |
| 884 | $lines[] = $this->spaces . ' * @var $' . $fieldMapping['fieldName']; |
||
| 885 | } else { |
||
| 886 | 7 | $lines[] = $this->spaces . ' * @var $' . $fieldMapping['fieldName']; |
|
| 887 | } |
||
| 888 | } else { |
||
| 889 | 7 | $lines[] = $this->spaces . ' * @var ' . $fieldMapping['type'] . ' $' . $fieldMapping['fieldName']; |
|
| 890 | } |
||
| 891 | |||
| 892 | 7 | if ($this->generateAnnotations) { |
|
| 893 | 7 | $lines[] = $this->spaces . ' *'; |
|
| 894 | |||
| 895 | 7 | $field = array(); |
|
| 896 | 7 | if (isset($fieldMapping['id']) && $fieldMapping['id']) { |
|
| 897 | 7 | if (isset($fieldMapping['strategy'])) { |
|
| 898 | 7 | $field[] = 'strategy="' . $this->getIdGeneratorTypeString($metadata->generatorType) . '"'; |
|
| 899 | } |
||
| 900 | 7 | $lines[] = $this->spaces . ' * @ODM\\Id(' . implode(', ', $field) . ')'; |
|
| 901 | } else { |
||
| 902 | 7 | if (isset($fieldMapping['name'])) { |
|
| 903 | 7 | $field[] = 'name="' . $fieldMapping['name'] . '"'; |
|
| 904 | } |
||
| 905 | |||
| 906 | 7 | if (isset($fieldMapping['type'])) { |
|
| 907 | 7 | $field[] = 'type="' . $fieldMapping['type'] . '"'; |
|
| 908 | } |
||
| 909 | |||
| 910 | 7 | if (isset($fieldMapping['nullable']) && $fieldMapping['nullable'] === true) { |
|
| 911 | $field[] = 'nullable=' . var_export($fieldMapping['nullable'], true); |
||
| 912 | } |
||
| 913 | 7 | if (isset($fieldMapping['options'])) { |
|
| 914 | 1 | $options = array(); |
|
| 915 | 1 | foreach ($fieldMapping['options'] as $key => $value) { |
|
| 916 | $options[] = '"' . $key . '" = "' . $value . '"'; |
||
| 917 | } |
||
| 918 | 1 | $field[] = 'options={' . implode(', ', $options) . '}'; |
|
| 919 | } |
||
| 920 | 7 | $lines[] = $this->spaces . ' * @ODM\\Field(' . implode(', ', $field) . ')'; |
|
| 921 | } |
||
| 922 | |||
| 923 | 7 | if (isset($fieldMapping['version']) && $fieldMapping['version']) { |
|
| 924 | $lines[] = $this->spaces . ' * @ODM\\Version'; |
||
| 925 | } |
||
| 926 | } |
||
| 927 | |||
| 928 | 7 | $lines[] = $this->spaces . ' */'; |
|
| 929 | |||
| 930 | 7 | return implode("\n", $lines); |
|
| 931 | } |
||
| 932 | |||
| 933 | 9 | private function prefixCodeWithSpaces($code, $num = 1) |
|
| 943 | |||
| 944 | private function getInheritanceTypeString($type) |
||
| 945 | { |
||
| 946 | switch ($type) { |
||
| 960 | |||
| 961 | 8 | private function getChangeTrackingPolicyString($policy) |
|
| 977 | |||
| 978 | 7 | private function getIdGeneratorTypeString($type) |
|
| 1003 | } |
||
| 1004 |
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: