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 DocumentParser 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 DocumentParser, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
27 | class DocumentParser |
||
28 | { |
||
29 | /** |
||
30 | * @const string |
||
31 | */ |
||
32 | const SUGGESTER_PROPERTY_ANNOTATION = 'ONGR\ElasticsearchBundle\Annotation\Suggester\AbstractSuggesterProperty'; |
||
33 | |||
34 | /** |
||
35 | * @const string |
||
36 | */ |
||
37 | const PROPERTY_ANNOTATION = 'ONGR\ElasticsearchBundle\Annotation\Property'; |
||
38 | |||
39 | /** |
||
40 | * @var Reader Used to read document annotations. |
||
41 | */ |
||
42 | private $reader; |
||
43 | |||
44 | /** |
||
45 | * @var DocumentFinder Used to find documents. |
||
46 | */ |
||
47 | private $finder; |
||
48 | |||
49 | /** |
||
50 | * @var array Contains gathered objects which later adds to documents. |
||
51 | */ |
||
52 | private $objects = []; |
||
53 | |||
54 | /** |
||
55 | * @var array Document properties aliases. |
||
56 | */ |
||
57 | private $aliases = []; |
||
58 | |||
59 | /** |
||
60 | * @var array Local cache for document properties. |
||
61 | */ |
||
62 | private $properties = []; |
||
63 | |||
64 | /** |
||
65 | * @param Reader $reader Used for reading annotations. |
||
66 | * @param DocumentFinder $finder Used for resolving namespaces. |
||
67 | */ |
||
68 | public function __construct(Reader $reader, DocumentFinder $finder) |
||
69 | { |
||
70 | $this->reader = $reader; |
||
71 | $this->finder = $finder; |
||
72 | $this->registerAnnotations(); |
||
73 | } |
||
74 | |||
75 | /** |
||
76 | * Parses documents by used annotations and returns mapping for elasticsearch with some extra metadata. |
||
77 | * |
||
78 | * @param \ReflectionClass $reflectionClass |
||
79 | * |
||
80 | * @return array|null |
||
81 | */ |
||
82 | public function parse(\ReflectionClass $reflectionClass) |
||
83 | { |
||
84 | /** @var Document $class */ |
||
85 | $class = $this |
||
86 | ->reader |
||
87 | ->getClassAnnotation($reflectionClass, 'ONGR\ElasticsearchBundle\Annotation\Document'); |
||
88 | |||
89 | if ($class !== null && $class->create) { |
||
90 | if ($class->parent !== null) { |
||
91 | $parent = $this->getDocumentParentType( |
||
92 | new \ReflectionClass($this->finder->getNamespace($class->parent)) |
||
93 | ); |
||
94 | } else { |
||
95 | $parent = null; |
||
96 | } |
||
97 | $type = $this->getDocumentType($reflectionClass, $class); |
||
98 | $inherit = $this->getInheritedProperties($reflectionClass); |
||
99 | |||
100 | $properties = $this->getProperties( |
||
101 | $reflectionClass, |
||
102 | array_merge($inherit, $this->getSkippedProperties($reflectionClass)) |
||
103 | ); |
||
104 | |||
105 | if (!empty($inherit)) { |
||
106 | $properties = array_merge( |
||
107 | $properties, |
||
108 | $this->getProperties($reflectionClass->getParentClass(), $inherit, true) |
||
109 | ); |
||
110 | } |
||
111 | |||
112 | return [ |
||
113 | $type => [ |
||
114 | 'properties' => $properties, |
||
115 | 'fields' => array_merge( |
||
116 | $class->dump(), |
||
117 | ['_parent' => $parent === null ? null : ['type' => $parent]] |
||
118 | ), |
||
119 | 'aliases' => $this->getAliases($reflectionClass), |
||
120 | 'objects' => $this->getObjects(), |
||
121 | 'proxyNamespace' => ProxyFactory::getProxyNamespace($reflectionClass, true), |
||
122 | 'namespace' => $reflectionClass->getName(), |
||
123 | 'class' => $reflectionClass->getShortName(), |
||
124 | ], |
||
125 | ]; |
||
126 | } |
||
127 | |||
128 | return null; |
||
129 | } |
||
130 | |||
131 | /** |
||
132 | * Returns property annotation data from reader. |
||
133 | * |
||
134 | * @param \ReflectionProperty $property |
||
135 | * |
||
136 | * @return AbstractSuggesterProperty|Property |
||
137 | */ |
||
138 | public function getPropertyAnnotationData($property) |
||
139 | { |
||
140 | $type = $this->reader->getPropertyAnnotation($property, self::PROPERTY_ANNOTATION); |
||
141 | if ($type === null) { |
||
142 | $type = $this->reader->getPropertyAnnotation($property, self::SUGGESTER_PROPERTY_ANNOTATION); |
||
143 | } |
||
144 | |||
145 | return $type; |
||
146 | } |
||
147 | |||
148 | /** |
||
149 | * Returns objects used in document. |
||
150 | * |
||
151 | * @return array |
||
152 | */ |
||
153 | private function getObjects() |
||
157 | |||
158 | /** |
||
159 | * Finds aliases for every property used in document including parent classes. |
||
160 | * |
||
161 | * @param \ReflectionClass $reflectionClass |
||
162 | * |
||
163 | * @return array |
||
164 | */ |
||
165 | private function getAliases(\ReflectionClass $reflectionClass) |
||
166 | { |
||
167 | $reflectionName = $reflectionClass->getName(); |
||
168 | if (array_key_exists($reflectionName, $this->aliases)) { |
||
169 | return $this->aliases[$reflectionName]; |
||
170 | } |
||
171 | |||
172 | $alias = []; |
||
173 | /** @var \ReflectionProperty $property */ |
||
174 | foreach ($this->getDocumentPropertiesReflection($reflectionClass) as $name => $property) { |
||
175 | $type = $this->getPropertyAnnotationData($property); |
||
176 | if ($type !== null) { |
||
177 | $alias[$type->name] = [ |
||
178 | 'propertyName' => $name, |
||
179 | 'type' => $type->type, |
||
180 | ]; |
||
181 | if ($type->objectName) { |
||
182 | $child = new \ReflectionClass($this->finder->getNamespace($type->objectName)); |
||
183 | $alias[$type->name] = array_merge( |
||
184 | $alias[$type->name], |
||
185 | [ |
||
186 | 'multiple' => $type instanceof Property ? $type->multiple : false, |
||
187 | 'aliases' => $this->getAliases($child), |
||
188 | 'proxyNamespace' => ProxyFactory::getProxyNamespace($child, true), |
||
189 | 'namespace' => $child->getName(), |
||
190 | ] |
||
191 | ); |
||
192 | } |
||
193 | } |
||
194 | } |
||
195 | |||
196 | $this->aliases[$reflectionName] = $alias; |
||
197 | |||
198 | return $this->aliases[$reflectionName]; |
||
199 | } |
||
200 | |||
201 | /** |
||
202 | * Registers annotations to registry so that it could be used by reader. |
||
203 | */ |
||
204 | private function registerAnnotations() |
||
205 | { |
||
206 | $annotations = [ |
||
207 | 'Document', |
||
208 | 'Property', |
||
209 | 'Object', |
||
210 | 'Nested', |
||
211 | 'MultiField', |
||
212 | 'Inherit', |
||
213 | 'Skip', |
||
214 | 'Suggester/CompletionSuggesterProperty', |
||
215 | 'Suggester/ContextSuggesterProperty', |
||
216 | 'Suggester/Context/CategoryContext', |
||
217 | 'Suggester/Context/GeoLocationContext', |
||
218 | ]; |
||
219 | |||
220 | foreach ($annotations as $annotation) { |
||
221 | AnnotationRegistry::registerFile(__DIR__ . "/../Annotation/{$annotation}.php"); |
||
222 | } |
||
223 | } |
||
224 | |||
225 | /** |
||
226 | * Returns document parent. |
||
227 | * |
||
228 | * @param \ReflectionClass $reflectionClass |
||
229 | * |
||
230 | * @return string|null |
||
231 | */ |
||
232 | View Code Duplication | private function getDocumentParentType(\ReflectionClass $reflectionClass) |
|
|
|||
233 | { |
||
234 | /** @var Document $class */ |
||
235 | $class = $this->reader->getClassAnnotation($reflectionClass, 'ONGR\ElasticsearchBundle\Annotation\Document'); |
||
236 | |||
237 | return $class ? $this->getDocumentType($reflectionClass, $class) : null; |
||
238 | } |
||
239 | |||
240 | /** |
||
241 | * @param \ReflectionClass $reflectionClass |
||
242 | * |
||
243 | * @return array |
||
244 | */ |
||
245 | View Code Duplication | private function getSkippedProperties(\ReflectionClass $reflectionClass) |
|
246 | { |
||
247 | /** @var Skip $class */ |
||
248 | $class = $this->reader->getClassAnnotation($reflectionClass, 'ONGR\ElasticsearchBundle\Annotation\Skip'); |
||
249 | |||
250 | return $class === null ? [] : $class->value; |
||
251 | } |
||
252 | |||
253 | /** |
||
254 | * @param \ReflectionClass $reflectionClass |
||
255 | * |
||
256 | * @return array |
||
257 | */ |
||
258 | View Code Duplication | private function getInheritedProperties(\ReflectionClass $reflectionClass) |
|
259 | { |
||
260 | /** @var Inherit $class */ |
||
261 | $class = $this->reader->getClassAnnotation($reflectionClass, 'ONGR\ElasticsearchBundle\Annotation\Inherit'); |
||
262 | |||
263 | return $class === null ? [] : $class->value; |
||
264 | } |
||
265 | |||
266 | /** |
||
267 | * Returns document type. |
||
268 | * |
||
269 | * @param \ReflectionClass $reflectionClass |
||
270 | * @param Document $document |
||
271 | * |
||
272 | * @return string |
||
273 | */ |
||
274 | private function getDocumentType(\ReflectionClass $reflectionClass, Document $document) |
||
278 | |||
279 | /** |
||
280 | * Returns all defined properties including private from parents. |
||
281 | * |
||
282 | * @param \ReflectionClass $reflectionClass |
||
283 | * |
||
284 | * @return array |
||
285 | */ |
||
286 | private function getDocumentPropertiesReflection(\ReflectionClass $reflectionClass) |
||
312 | |||
313 | /** |
||
314 | * Returns properties of reflection class. |
||
315 | * |
||
316 | * @param \ReflectionClass $reflectionClass Class to read properties from. |
||
317 | * @param array $properties Properties to skip. |
||
318 | * @param bool $flag If false exludes properties, true only includes properties. |
||
319 | * |
||
320 | * @return array |
||
321 | */ |
||
322 | private function getProperties(\ReflectionClass $reflectionClass, $properties = [], $flag = false) |
||
363 | |||
364 | /** |
||
365 | * Returns object mapping. |
||
366 | * |
||
367 | * Loads from cache if it's already loaded. |
||
368 | * |
||
369 | * @param string $objectName |
||
370 | * |
||
371 | * @return array |
||
372 | */ |
||
373 | private function getObjectMapping($objectName) |
||
385 | |||
386 | /** |
||
387 | * Returns relation mapping by its reflection. |
||
388 | * |
||
389 | * @param \ReflectionClass $reflectionClass |
||
390 | * |
||
391 | * @return array|null |
||
392 | */ |
||
393 | private function getRelationMapping(\ReflectionClass $reflectionClass) |
||
403 | } |
||
404 |
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.