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!=
, 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: