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!=
, orswitch
conditions), values of different types might be equal.For
string
values, the empty string''
is a special case, in particular the following results might be unexpected: