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 Element 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 Element, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
8 | class Element extends AbstractToken implements Cleanable, ContainsChildren, Removable |
||
9 | { |
||
10 | const ATTR_BOOL = 'ci_boo'; // boolean |
||
11 | const ATTR_CI_ENUM = 'ci_enu'; // case-insensitive enumeration |
||
12 | const ATTR_CI_SSENUM = 'ci_sse'; // case-insensitive space-separated enumeration |
||
13 | const ATTR_INT = 'ci_int'; // integer |
||
14 | const ATTR_JS = 'cs_jsc'; // javascript |
||
15 | const ATTR_CI_STRING = 'ci_str'; // case-insensitive string |
||
16 | const ATTR_CS_STRING = 'cs_str'; // case-sensitive string |
||
17 | const ATTR_URI = 'cs_uri'; // uri |
||
18 | |||
19 | /** @var array */ |
||
20 | protected $attributes; |
||
21 | |||
22 | /** @var array[Token] */ |
||
23 | protected $children; |
||
24 | |||
25 | /** @var string */ |
||
26 | private $name; |
||
27 | |||
28 | /** |
||
29 | * Constructor |
||
30 | */ |
||
31 | 91 | public function __construct(Configuration $configuration, $name, array $attributes = array()) |
|
32 | { |
||
33 | 91 | parent::__construct(Token::ELEMENT, $configuration); |
|
34 | |||
35 | 91 | $this->attributes = array(); |
|
36 | 91 | foreach ($attributes as $key => $value) { |
|
37 | 49 | $this->addAttribute($key, $value); |
|
38 | 91 | } |
|
39 | |||
40 | 91 | $this->children = array(); |
|
41 | 91 | $this->setName($name); |
|
42 | 91 | } |
|
43 | |||
44 | /** |
||
45 | * Getter for 'attributes'. |
||
46 | */ |
||
47 | 3 | public function getAttributes() |
|
48 | { |
||
49 | 3 | return $this->attributes; |
|
50 | 2 | } |
|
51 | |||
52 | 2 | public function getAttribute($key) |
|
53 | { |
||
54 | 2 | if (!$this->hasAttribute($key)) { |
|
55 | throw new \InvalidArgumentException('Invalid attribute key: ' . $key); |
||
56 | } |
||
57 | |||
58 | 1 | return $this->attributes[$key]; |
|
59 | } |
||
60 | |||
61 | /** |
||
62 | * Hasser for 'attributes'. |
||
63 | * |
||
64 | * @param string $key |
||
65 | * |
||
66 | * @return bool True if the attribute is present. |
||
67 | */ |
||
68 | 22 | public function hasAttribute($key) |
|
69 | { |
||
70 | 22 | return array_key_exists($key, $this->attributes); |
|
71 | } |
||
72 | |||
73 | 51 | public function addAttribute($key, $value) |
|
74 | { |
||
75 | 51 | $key = trim(strtolower($key)); |
|
76 | 51 | if ($key == '') { |
|
77 | 1 | throw new \InvalidArgumentException('Invalid empty attribute key.'); |
|
78 | 1 | } |
|
79 | |||
80 | 50 | $this->attributes[$key] = $value; |
|
81 | |||
82 | 50 | return $this; |
|
83 | } |
||
84 | |||
85 | 2 | public function removeAttribute($key) |
|
86 | { |
||
87 | 2 | $key = trim(strtolower($key)); |
|
88 | 2 | if (isset($this->attributes[$key])) { |
|
89 | 2 | unset($this->attributes[$key]); |
|
90 | |||
91 | 2 | return true; |
|
92 | } |
||
93 | |||
94 | 2 | return false; |
|
95 | } |
||
96 | |||
97 | /** |
||
98 | * Required by ContainsChildren interface. |
||
99 | */ |
||
100 | 2 | public function getChildren() |
|
101 | { |
||
102 | 2 | return $this->children; |
|
103 | } |
||
104 | |||
105 | /** |
||
106 | * Required by ContainsChildren interface. |
||
107 | */ |
||
108 | 1 | public function hasChild(Token $token) |
|
109 | { |
||
110 | 1 | return array_search($token, $this->children, true) !== false; |
|
111 | } |
||
112 | |||
113 | /** |
||
114 | * Required by ContainsChildren interface. |
||
115 | */ |
||
116 | 82 | public function appendChild(Token $token) |
|
117 | { |
||
118 | 82 | $token->setParent($this); |
|
119 | 82 | $this->children[] = $token; |
|
120 | |||
121 | 82 | return $this; |
|
122 | } |
||
123 | |||
124 | /** |
||
125 | * Required by ContainsChildren interface. |
||
126 | */ |
||
127 | 4 | public function prependChild(Token $token) |
|
128 | { |
||
129 | 4 | $token->setParent($this); |
|
130 | 4 | array_unshift($this->children, $token); |
|
131 | |||
132 | 4 | return $this; |
|
133 | } |
||
134 | |||
135 | /** |
||
136 | * Required by the ContainsChildren interface. |
||
137 | */ |
||
138 | 27 | View Code Duplication | public function removeChild(Token $token) |
|
|||
139 | { |
||
140 | 27 | $key = array_search($token, $this->children, true); |
|
141 | 27 | if ($key !== false) { |
|
142 | 27 | unset($this->children[$key]); |
|
143 | |||
144 | 27 | return true; |
|
145 | } |
||
146 | |||
147 | 1 | return false; |
|
148 | } |
||
149 | |||
150 | /** |
||
151 | * Getter for 'name'. |
||
152 | */ |
||
153 | 77 | public function getName() |
|
154 | { |
||
155 | 77 | return $this->name; |
|
156 | } |
||
157 | |||
158 | /** |
||
159 | * Chainable setter for 'name'. |
||
160 | */ |
||
161 | 91 | public function setName($name) |
|
162 | { |
||
163 | 91 | if (!is_string($name)) { |
|
164 | 1 | throw new \InvalidArgumentException('Element name must be string type.'); |
|
165 | } |
||
166 | |||
167 | 91 | $this->name = trim(strtolower($name)); |
|
168 | |||
169 | 91 | return $this; |
|
170 | } |
||
171 | |||
172 | /** |
||
173 | * Required by the Cleanable interface. |
||
174 | */ |
||
175 | 78 | public function clean(LoggerInterface $logger) |
|
176 | { |
||
177 | 78 | if ($this->configuration->get('clean-strategy') == Configuration::CLEAN_STRATEGY_NONE) { |
|
178 | 1 | return true; |
|
179 | } |
||
180 | |||
181 | // Remove non-standard attributes. |
||
182 | 77 | if ($this->configuration->get('clean-strategy') != Configuration::CLEAN_STRATEGY_LENIENT) { |
|
183 | 77 | foreach ($this->attributes as $name => $value) { |
|
184 | 41 | $attributeParameters = $this->getAttributeParameters($name); |
|
185 | 41 | if (empty($attributeParameters)) { |
|
186 | 7 | $logger->debug('Removing non-standard attribute "' . $name . '" from ' . $this); |
|
187 | 7 | unset($this->attributes[$name]); |
|
188 | 7 | } |
|
189 | 77 | } |
|
190 | 77 | } |
|
191 | |||
192 | 77 | foreach ($this->attributes as $name => $value) { |
|
193 | // Validate attribute value. |
||
194 | 42 | $attributeParameters = $this->getAttributeParameters($name); |
|
195 | 42 | if (empty($attributeParameters)) { |
|
196 | $attributeParameters = array( |
||
197 | 5 | 'name' => $name, |
|
198 | 5 | 'regex' => '/\S*/i', |
|
199 | 'valueType' => self::ATTR_CS_STRING |
||
200 | 5 | ); |
|
201 | 5 | } |
|
202 | |||
203 | list($caseSensitivity, $attributeType) = |
||
204 | 42 | explode('_', $attributeParameters['valueType']); |
|
205 | |||
206 | // Handle case-insensitivity. |
||
207 | // Standard is case-insensitive attribute values should be lower case. |
||
208 | 42 | if ($caseSensitivity == 'ci') { |
|
209 | 10 | $newValue = strtolower($value); |
|
210 | 10 | if ($newValue !== $value) { |
|
211 | 2 | $logger->debug('The value for the attribute "' . $name . '" is case-insensitive. The value has been converted to lower case. Element: ' . $this); |
|
212 | 2 | $this->attributes[$name] = $newValue; |
|
213 | 2 | } |
|
214 | 10 | } |
|
215 | |||
216 | 42 | switch (substr($attributeType, 0, 3)) { |
|
217 | 42 | case 'enu': // enumeration |
|
218 | /// @todo |
||
219 | 1 | break; |
|
220 | |||
221 | 41 | case 'uri': // URI |
|
222 | /// @todo |
||
223 | 13 | break; |
|
224 | 42 | } |
|
225 | 77 | } |
|
226 | |||
227 | 77 | $doCleanResult = $this->doClean($logger); |
|
228 | 77 | if (!$doCleanResult && $this->configuration->get('clean-strategy') !== Configuration::CLEAN_STRATEGY_LENIENT) { |
|
229 | 16 | return false; |
|
230 | } |
||
231 | |||
232 | // Clean children. |
||
233 | 77 | return AbstractToken::cleanChildTokens( |
|
234 | 77 | $this->configuration, |
|
235 | 77 | $this->children, |
|
236 | $logger |
||
237 | 77 | ); |
|
238 | } |
||
239 | |||
240 | 43 | protected function doClean(LoggerInterface $logger) |
|
241 | { |
||
242 | 43 | return true; |
|
243 | } |
||
244 | |||
245 | 42 | protected function getAllowedAttributes() |
|
246 | { |
||
247 | return array( |
||
248 | // Global Attributes |
||
249 | 42 | '/^accesskey$/i' => self::ATTR_CS_STRING, |
|
250 | 42 | '/^class$/i' => self::ATTR_CS_STRING, |
|
251 | 42 | '/^contenteditable$/i' => self::ATTR_CS_STRING, |
|
252 | 42 | '/^contextmenu$/i' => self::ATTR_CS_STRING, |
|
253 | 42 | '/^data-\S/i' => self::ATTR_CS_STRING, |
|
254 | 42 | '/^dir$/i' => self::ATTR_CI_ENUM . '("ltr","rtl"|"ltr")', |
|
255 | 42 | '/^draggable$/i' => self::ATTR_CS_STRING, |
|
256 | 42 | '/^dropzone$/i' => self::ATTR_CS_STRING, |
|
257 | 42 | '/^hidden$/i' => self::ATTR_CS_STRING, |
|
258 | 42 | '/^id$/i' => self::ATTR_CS_STRING, |
|
259 | 42 | '/^is$/i' => self::ATTR_CS_STRING, |
|
260 | 42 | '/^itemid$/i' => self::ATTR_CS_STRING, |
|
261 | 42 | '/^itemprop$/i' => self::ATTR_CS_STRING, |
|
262 | 42 | '/^itemref$/i' => self::ATTR_CS_STRING, |
|
263 | 42 | '/^itemscope$/i' => self::ATTR_CS_STRING, |
|
264 | 42 | '/^itemtype$/i' => self::ATTR_CS_STRING, |
|
265 | 42 | '/^lang$/i' => self::ATTR_CI_STRING, |
|
266 | 42 | '/^slot$/i' => self::ATTR_CS_STRING, |
|
267 | 42 | '/^spellcheck$/i' => self::ATTR_CS_STRING, |
|
268 | 42 | '/^style$/i' => self::ATTR_CS_STRING, |
|
269 | 42 | '/^tabindex$/i' => self::ATTR_CS_STRING, |
|
270 | 42 | '/^title$/i' => self::ATTR_CS_STRING, |
|
271 | 42 | '/^translate$/i' => self::ATTR_CI_ENUM . '("yes","no",""|"yes")', |
|
272 | |||
273 | // Event Handler Content Attributes |
||
274 | // https://html.spec.whatwg.org/multipage/webappapis.html#event-handler-content-attributes |
||
275 | 42 | '/^onabort$/i' => self::ATTR_JS, |
|
276 | 42 | '/^onautocomplete$/i' => self::ATTR_JS, |
|
277 | 42 | '/^onautocompleteerror$/i' => self::ATTR_JS, |
|
278 | 42 | '/^onblur$/i' => self::ATTR_JS, |
|
279 | 42 | '/^oncancel$/i' => self::ATTR_JS, |
|
280 | 42 | '/^oncanplay$/i' => self::ATTR_JS, |
|
281 | 42 | '/^oncanplaythrough$/i' => self::ATTR_JS, |
|
282 | 42 | '/^onchange$/i' => self::ATTR_JS, |
|
283 | 42 | '/^onclick$/i' => self::ATTR_JS, |
|
284 | 42 | '/^onclose$/i' => self::ATTR_JS, |
|
285 | 42 | '/^oncontextmenu$/i' => self::ATTR_JS, |
|
286 | 42 | '/^oncuechange$/i' => self::ATTR_JS, |
|
287 | 42 | '/^ondblclick$/i' => self::ATTR_JS, |
|
288 | 42 | '/^ondrag$/i' => self::ATTR_JS, |
|
289 | 42 | '/^ondragend$/i' => self::ATTR_JS, |
|
290 | 42 | '/^ondragenter$/i' => self::ATTR_JS, |
|
291 | 42 | '/^ondragexit$/i' => self::ATTR_JS, |
|
292 | 42 | '/^ondragleave$/i' => self::ATTR_JS, |
|
293 | 42 | '/^ondragover$/i' => self::ATTR_JS, |
|
294 | 42 | '/^ondragstart$/i' => self::ATTR_JS, |
|
295 | 42 | '/^ondrop$/i' => self::ATTR_JS, |
|
296 | 42 | '/^ondurationchange$/i' => self::ATTR_JS, |
|
297 | 42 | '/^onemptied$/i' => self::ATTR_JS, |
|
298 | 42 | '/^onended$/i' => self::ATTR_JS, |
|
299 | 42 | '/^onerror$/i' => self::ATTR_JS, |
|
300 | 42 | '/^onfocus$/i' => self::ATTR_JS, |
|
301 | 42 | '/^oninput$/i' => self::ATTR_JS, |
|
302 | 42 | '/^oninvalid$/i' => self::ATTR_JS, |
|
303 | 42 | '/^onkeydown$/i' => self::ATTR_JS, |
|
304 | 42 | '/^onkeypress$/i' => self::ATTR_JS, |
|
305 | 42 | '/^onkeyup$/i' => self::ATTR_JS, |
|
306 | 42 | '/^onload$/i' => self::ATTR_JS, |
|
307 | 42 | '/^onloadeddata$/i' => self::ATTR_JS, |
|
308 | 42 | '/^onloadedmetadata$/i' => self::ATTR_JS, |
|
309 | 42 | '/^onloadstart$/i' => self::ATTR_JS, |
|
310 | 42 | '/^onmousedown$/i' => self::ATTR_JS, |
|
311 | 42 | '/^onmouseenter$/i' => self::ATTR_JS, |
|
312 | 42 | '/^onmouseleave$/i' => self::ATTR_JS, |
|
313 | 42 | '/^onmousemove$/i' => self::ATTR_JS, |
|
314 | 42 | '/^onmouseout$/i' => self::ATTR_JS, |
|
315 | 42 | '/^onmouseover$/i' => self::ATTR_JS, |
|
316 | 42 | '/^onmouseup$/i' => self::ATTR_JS, |
|
317 | 42 | '/^onwheel$/i' => self::ATTR_JS, |
|
318 | 42 | '/^onpause$/i' => self::ATTR_JS, |
|
319 | 42 | '/^onplay$/i' => self::ATTR_JS, |
|
320 | 42 | '/^onplaying$/i' => self::ATTR_JS, |
|
321 | 42 | '/^onprogress$/i' => self::ATTR_JS, |
|
322 | 42 | '/^onratechange$/i' => self::ATTR_JS, |
|
323 | 42 | '/^onreset$/i' => self::ATTR_JS, |
|
324 | 42 | '/^onresize$/i' => self::ATTR_JS, |
|
325 | 42 | '/^onscroll$/i' => self::ATTR_JS, |
|
326 | 42 | '/^onseeked$/i' => self::ATTR_JS, |
|
327 | 42 | '/^onseeking$/i' => self::ATTR_JS, |
|
328 | 42 | '/^onselect$/i' => self::ATTR_JS, |
|
329 | 42 | '/^onshow$/i' => self::ATTR_JS, |
|
330 | 42 | '/^onstalled$/i' => self::ATTR_JS, |
|
331 | 42 | '/^onsubmit$/i' => self::ATTR_JS, |
|
332 | 42 | '/^onsuspend$/i' => self::ATTR_JS, |
|
333 | 42 | '/^ontimeupdate$/i' => self::ATTR_JS, |
|
334 | 42 | '/^ontoggle$/i' => self::ATTR_JS, |
|
335 | 42 | '/^onvolumechange$/i' => self::ATTR_JS, |
|
336 | 42 | '/^onwaiting$/i' => self::ATTR_JS, |
|
337 | |||
338 | // WAI-ARIA |
||
339 | // https://w3c.github.io/aria/aria/aria.html |
||
340 | 42 | '/^role$/i' => self::ATTR_CI_STRING, |
|
341 | |||
342 | // ARIA global states and properties |
||
343 | 42 | '/^aria-atomic$/i' => self::ATTR_CS_STRING, |
|
344 | 42 | '/^aria-busy$/i' => self::ATTR_CS_STRING, |
|
345 | 42 | '/^aria-controls$/i' => self::ATTR_CS_STRING, |
|
346 | 42 | '/^aria-current$/i' => self::ATTR_CS_STRING, |
|
347 | 42 | '/^aria-describedby$/i' => self::ATTR_CS_STRING, |
|
348 | 42 | '/^aria-details$/i' => self::ATTR_CS_STRING, |
|
349 | 42 | '/^aria-disabled$/i' => self::ATTR_CS_STRING, |
|
350 | 42 | '/^aria-dropeffect$/i' => self::ATTR_CS_STRING, |
|
351 | 42 | '/^aria-errormessage$/i' => self::ATTR_CS_STRING, |
|
352 | 42 | '/^aria-flowto$/i' => self::ATTR_CS_STRING, |
|
353 | 42 | '/^aria-grabbed$/i' => self::ATTR_CS_STRING, |
|
354 | 42 | '/^aria-haspopup$/i' => self::ATTR_CS_STRING, |
|
355 | 42 | '/^aria-hidden$/i' => self::ATTR_CS_STRING, |
|
356 | 42 | '/^aria-invalid$/i' => self::ATTR_CS_STRING, |
|
357 | 42 | '/^aria-label$/i' => self::ATTR_CS_STRING, |
|
358 | 42 | '/^aria-labelledby$/i' => self::ATTR_CS_STRING, |
|
359 | 42 | '/^aria-live$/i' => self::ATTR_CS_STRING, |
|
360 | 42 | '/^aria-owns$/i' => self::ATTR_CS_STRING, |
|
361 | 42 | '/^aria-relevant$/i' => self::ATTR_CS_STRING, |
|
362 | 42 | '/^aria-roledescription$/i' => self::ATTR_CS_STRING, |
|
363 | |||
364 | // ARIA widget attributes |
||
365 | 42 | '/^aria-autocomplete$/i' => self::ATTR_CS_STRING, |
|
366 | 42 | '/^aria-checked$/i' => self::ATTR_CS_STRING, |
|
367 | 42 | '/^aria-expanded$/i' => self::ATTR_CS_STRING, |
|
368 | 42 | '/^aria-level$/i' => self::ATTR_CS_STRING, |
|
369 | 42 | '/^aria-modal$/i' => self::ATTR_CS_STRING, |
|
370 | 42 | '/^aria-multiline$/i' => self::ATTR_CS_STRING, |
|
371 | 42 | '/^aria-multiselectable$/i' => self::ATTR_CS_STRING, |
|
372 | 42 | '/^aria-orientation$/i' => self::ATTR_CS_STRING, |
|
373 | 42 | '/^aria-placeholder$/i' => self::ATTR_CS_STRING, |
|
374 | 42 | '/^aria-pressed$/i' => self::ATTR_CS_STRING, |
|
375 | 42 | '/^aria-readonly$/i' => self::ATTR_CS_STRING, |
|
376 | 42 | '/^aria-required$/i' => self::ATTR_CS_STRING, |
|
377 | 42 | '/^aria-selected$/i' => self::ATTR_CS_STRING, |
|
378 | 42 | '/^aria-sort$/i' => self::ATTR_CS_STRING, |
|
379 | 42 | '/^aria-valuemax$/i' => self::ATTR_CS_STRING, |
|
380 | 42 | '/^aria-valuemin$/i' => self::ATTR_CS_STRING, |
|
381 | 42 | '/^aria-valuenow$/i' => self::ATTR_CS_STRING, |
|
382 | 42 | '/^aria-valuetext$/i' => self::ATTR_CS_STRING, |
|
383 | |||
384 | // ARIA relationship attributes |
||
385 | 42 | '/^aria-activedescendant$/i' => self::ATTR_CS_STRING, |
|
386 | 42 | '/^aria-colcount$/i' => self::ATTR_CS_STRING, |
|
387 | 42 | '/^aria-colindex$/i' => self::ATTR_CS_STRING, |
|
388 | 42 | '/^aria-colspan$/i' => self::ATTR_CS_STRING, |
|
389 | 42 | '/^aria-posinset$/i' => self::ATTR_CS_STRING, |
|
390 | 42 | '/^aria-rowcount$/i' => self::ATTR_CS_STRING, |
|
391 | 42 | '/^aria-rowindex$/i' => self::ATTR_CS_STRING, |
|
392 | 42 | '/^aria-rowspan$/i' => self::ATTR_CS_STRING, |
|
393 | '/^aria-setsize$/i' => self::ATTR_CS_STRING |
||
394 | 42 | ); |
|
395 | } |
||
396 | |||
397 | 42 | protected function getAttributeParameters($name) |
|
398 | { |
||
399 | 42 | $allowedAttributes = $this->getAllowedAttributes(); |
|
400 | 42 | foreach ($allowedAttributes as $attrRegex => $valueType) { |
|
401 | 42 | if (preg_match($attrRegex, $name) === 1) { |
|
402 | return array( |
||
403 | 42 | 'name' => $name, |
|
404 | 42 | 'regex' => $attrRegex, |
|
405 | 'valueType' => $valueType |
||
406 | 42 | ); |
|
407 | } |
||
408 | 33 | } |
|
409 | |||
410 | 7 | return array(); |
|
411 | } |
||
412 | |||
413 | /** |
||
414 | * Required by the Removable interface. |
||
415 | */ |
||
416 | 75 | View Code Duplication | public function remove(LoggerInterface $logger) |
417 | { |
||
418 | 75 | $hasRemovableElements = $this->configuration->get('element-blacklist') != ''; |
|
419 | 75 | $hasRemovableTypes = $this->configuration->get('type-blacklist') != ''; |
|
420 | 75 | foreach ($this->children as $child) { |
|
421 | // Check types. |
||
422 | 73 | if ($hasRemovableTypes && |
|
423 | 73 | !$this->configuration->isAllowedType($child->getType())) { |
|
424 | 2 | $logger->debug('Removing ' . $child); |
|
425 | 2 | $this->removeChild($child); |
|
426 | |||
427 | 2 | continue; |
|
428 | } |
||
429 | |||
430 | // Check elements. |
||
431 | 71 | if ($hasRemovableElements && |
|
432 | 71 | $child->getType() == Token::ELEMENT && |
|
433 | 71 | !$this->configuration->isAllowedElement($child->getName())) { |
|
434 | 3 | $logger->debug('Removing ' . $child); |
|
435 | 3 | $this->removeChild($child); |
|
436 | |||
437 | 3 | continue; |
|
438 | } |
||
439 | |||
440 | // Check children. |
||
441 | 71 | if ($child instanceof Removable) { |
|
442 | 62 | $child->remove($logger); |
|
443 | 62 | } |
|
444 | 75 | } |
|
445 | 75 | } |
|
446 | |||
447 | /** |
||
448 | * Required by the Token interface. |
||
449 | */ |
||
450 | 7 | public function toHtml($prefix, $suffix) |
|
461 | |||
462 | 81 | protected function buildStartTag($prefix, $suffix, $forceOpen = false) |
|
479 | |||
480 | 79 | protected function buildChildrenHtml($prefix, $suffix) |
|
494 | |||
495 | 43 | public function __toString() |
|
499 | } |
||
500 |
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.