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 SMlite 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 SMlite, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
85 | class SMlite extends AbstractModel |
||
86 | { |
||
87 | /** |
||
88 | * This array contains list of all tags found inside template. |
||
89 | */ |
||
90 | public $tags = array(); |
||
91 | |||
92 | /** |
||
93 | * When cloning region inside a template, it's tag becomes a top_tag of a new |
||
94 | * template. Since SMlite 1.1 it's present in new template and can be used. |
||
95 | */ |
||
96 | public $top_tag = null; |
||
97 | |||
98 | /** |
||
99 | * This is a parsed contents of the template. |
||
100 | */ |
||
101 | public $template = array(); // private |
||
102 | |||
103 | public $settings = array(); |
||
104 | |||
105 | /** |
||
106 | * Type of resource to look for pathFinder |
||
107 | */ |
||
108 | public $template_type = 'template'; |
||
109 | |||
110 | /** |
||
111 | * list of updated tags with values. |
||
112 | */ |
||
113 | public $updated_tag_list = array(); |
||
114 | |||
115 | private $cache; |
||
116 | |||
117 | /** |
||
118 | * Which file template is coming from. |
||
119 | */ |
||
120 | public $origin_filename = null; |
||
121 | |||
122 | protected $tmp_template; |
||
123 | |||
124 | public function getTagVal($tag) |
||
128 | |||
129 | /** |
||
130 | * This function specifies default settings for SMlite. Use |
||
131 | * 2nd argument for constructor to redefine those settings. |
||
132 | * |
||
133 | * A small note why I decided on .html extension. I want to |
||
134 | * point out that template files are and should be valid HTML |
||
135 | * documents. With .html extension those files will be properly |
||
136 | * rendered inside web browser, properly understood inside text |
||
137 | * editor or will be properly treated with wysiwyg html editors. |
||
138 | * |
||
139 | * @return array |
||
140 | */ |
||
141 | public function getDefaultSettings() |
||
150 | |||
151 | // Template creation, interface functions |
||
152 | public function init() |
||
153 | { |
||
154 | parent::init(); |
||
155 | $this->cache = &$this->app->smlite_cache; |
||
|
|||
156 | |||
157 | $this->settings = $this->getDefaultSettings(); |
||
158 | $this->settings['extension'] = $this->app->getConfig('smlite/extension', '.html'); |
||
159 | } |
||
160 | |||
161 | public function __clone() |
||
174 | |||
175 | public function exception($message = 'Undefined Exception', $type = null, $code = null) |
||
181 | |||
182 | public function cloneRegion($tag) |
||
183 | { |
||
184 | /* |
||
185 | * Sometimes you will want to put branch into different class. This function will create |
||
186 | * new class for you. |
||
187 | */ |
||
188 | if ($this->isTopTag($tag)) { |
||
189 | /** @type self $new */ |
||
190 | $new = $this->newInstance(); |
||
191 | $new->template = unserialize(serialize($this->template)); |
||
192 | $new->top_tag = $tag; |
||
193 | $new->settings = $this->settings; |
||
194 | $new->origin_filename = $this->origin_filename; |
||
195 | $new->rebuildTags(); |
||
196 | |||
197 | return $new; |
||
198 | } |
||
199 | |||
200 | if (!$this->hasTag($tag)) { |
||
201 | $o = $this->owner ? ' for '.$this->owner->__toString() : ''; |
||
202 | throw new BaseException("No such tag ($tag) in template$o. Tags are: ". |
||
203 | implode(', ', array_keys($this->tags))); |
||
204 | } |
||
205 | $class_name = get_class($this); |
||
206 | /** @type self $new */ |
||
207 | $new = $this->add($class_name); |
||
208 | try { |
||
209 | $new->template = unserialize(serialize($this->tags[$tag][0])); |
||
210 | if (is_string($new->template)) { |
||
211 | $new->template = array($new->template); |
||
212 | } |
||
213 | } catch (PDOException $e) { |
||
214 | throw $this->exception('PDO got stuck in template') |
||
215 | ->addMoreInfo('tag', $tag) |
||
216 | ->addMoreInfo('tags', var_export($this->tags, true)); |
||
217 | } |
||
218 | $new->top_tag = $tag; |
||
219 | $new->settings = $this->settings; |
||
220 | |||
221 | return $new->rebuildTags(); |
||
222 | } |
||
223 | |||
224 | // Misc functions |
||
225 | public function dumpTags() |
||
233 | |||
234 | // Operation with regions inside template |
||
235 | /** |
||
236 | * Finds tag and returns contents. |
||
237 | * |
||
238 | * THIS FUNTION IS DANGEROUS! |
||
239 | * - if you want a rendered region, use renderRegion() |
||
240 | * - if you want a sub-template use cloneRegion() |
||
241 | * |
||
242 | * - if you want to copy part of template to other SMlite object, |
||
243 | * do not forget to call rebuildTags() if you plan to refer them. |
||
244 | * Not calling rebuildTags() will render template properly anyway. |
||
245 | * |
||
246 | * If tag is defined multiple times, first region is returned. |
||
247 | */ |
||
248 | public function get($tag) |
||
249 | { |
||
250 | if ($this->isTopTag($tag)) { |
||
251 | return $this->template; |
||
252 | } |
||
253 | $v = $this->tags[$tag][0]; |
||
254 | if (is_array($v) && count($v) == 1) { |
||
255 | $v = array_shift($v); |
||
256 | } |
||
257 | |||
258 | return $v; |
||
259 | } |
||
260 | public function appendHTML($tag, $value) |
||
264 | |||
265 | /** |
||
266 | * This appends static content to region refered by a tag. This function |
||
267 | * is useful when you are adding more rows to a list or table. |
||
268 | * |
||
269 | * If tag is used for several regions inside template, they all will be |
||
270 | * appended with new data. |
||
271 | */ |
||
272 | public function append($tag, $value, $encode = true) |
||
273 | { |
||
274 | if ($value instanceof URL) { |
||
275 | $value = $value->__toString(); |
||
276 | } |
||
277 | // Temporary here until we finish testing |
||
278 | View Code Duplication | if ($encode |
|
279 | && $value != $this->app->encodeHtmlChars($value, ENT_NOQUOTES) |
||
280 | && $this->app->getConfig('html_injection_debug', false) |
||
281 | ) { |
||
282 | throw $this->exception('Attempted to supply html string through append()') |
||
283 | ->addMoreInfo('val', var_export($value, true)) |
||
284 | ->addMoreInfo('enc', var_export($this->app->encodeHtmlChars($value, ENT_NOQUOTES), true)) |
||
285 | //->addAction('ignore','Ignore tag'.$tag) |
||
286 | ; |
||
287 | } |
||
288 | if ($encode) { |
||
289 | $value = $this->app->encodeHtmlChars($value, ENT_NOQUOTES); |
||
290 | } |
||
291 | if ($this->isTopTag($tag)) { |
||
292 | $this->template[] = $value; |
||
293 | |||
294 | return $this; |
||
295 | } |
||
296 | if (!isset($this->tags[$tag]) || !is_array($this->tags[$tag])) { |
||
297 | throw $this->exception("Cannot append to tag $tag") |
||
298 | ->addMoreInfo('by', $this->owner); |
||
299 | } |
||
300 | foreach ($this->tags[$tag] as $key => $_) { |
||
301 | if (!is_array($this->tags[$tag][$key])) { |
||
302 | //throw new BaseException("Problem appending '". |
||
303 | // $this->app->encodeHtmlChars($value)."' to '$tag': key=$key"); |
||
304 | $this->tags[$tag][$key] = array($this->tags[$tag][$key]); |
||
305 | } |
||
306 | $this->tags[$tag][$key][] = $value; |
||
307 | } |
||
308 | |||
309 | return $this; |
||
310 | } |
||
311 | public function setHTML($tag, $value = null) |
||
315 | /** |
||
316 | * Provided that the HTML tag contains ICU-compatible message format |
||
317 | * string, it will be localized then integrated with passed arguments. |
||
318 | */ |
||
319 | public function setMessage($tag, $args = array()) |
||
342 | public function set($tag, $value = null, $encode = true) |
||
343 | { |
||
344 | /* |
||
345 | * This function will replace region refered by $tag to a new content. |
||
346 | * |
||
347 | * If tag is used several times, all regions are replaced. |
||
348 | * |
||
349 | * ALTERNATIVE USE(2) of this function is to pass associative array as |
||
350 | * a single argument. This will assign multiple tags with one call. |
||
351 | * Sample use is: |
||
352 | * |
||
353 | * set($_GET); |
||
354 | * |
||
355 | * would read and set multiple region values from $_GET array. |
||
356 | * |
||
357 | * ALTERNATIVE USE(3) of this function is to pass 2 arrays. First array |
||
358 | * will contain tag names and 2nd array will contain their values. |
||
359 | */ |
||
360 | if (is_object($tag)) { |
||
361 | $tag = $tag->get(); |
||
362 | } |
||
363 | if (is_array($tag)) { |
||
364 | if (is_null($value)) { |
||
365 | // USE(2) |
||
366 | foreach ($tag as $s => $v) { |
||
367 | $this->trySet($s, $v, $encode); |
||
368 | } |
||
369 | |||
370 | return $this; |
||
371 | } |
||
372 | if (is_array($value)) { |
||
373 | // USE(2) |
||
374 | reset($tag); |
||
375 | reset($value); |
||
376 | while (list(, $s) = each($tag)) { |
||
377 | list(, $v) = each($value); |
||
378 | $this->set($s, $v, $encode); |
||
379 | } |
||
380 | |||
381 | return $this; |
||
382 | } |
||
383 | $this->fatal('Incorrect argument types when calling SMlite::set(). Check documentation.'); |
||
384 | } |
||
385 | if ($value instanceof URL) { |
||
386 | $value = $value->__toString(); |
||
387 | } |
||
388 | if (is_array($value)) { |
||
389 | return $this; |
||
390 | } |
||
391 | |||
392 | View Code Duplication | if ($encode |
|
393 | && $value != $this->app->encodeHtmlChars($value, ENT_NOQUOTES) |
||
394 | && $this->app->getConfig('html_injection_debug', false) |
||
395 | ) { |
||
396 | throw $this->exception('Attempted to supply html string through set()') |
||
397 | ->addMoreInfo('val', var_export($value, true)) |
||
398 | ->addMoreInfo('enc', var_export($this->app->encodeHtmlChars($value, ENT_NOQUOTES), true)) |
||
399 | //->addAction('ignore','Ignore tag'.$tag) |
||
400 | ; |
||
401 | } |
||
402 | if ($encode) { |
||
403 | $value = $this->app->encodeHtmlChars($value, ENT_NOQUOTES); |
||
404 | } |
||
405 | if ($this->isTopTag($tag)) { |
||
406 | $this->template = $value; |
||
407 | |||
408 | return $this; |
||
409 | } |
||
410 | if (!isset($this->tags[$tag]) || !is_array($this->tags[$tag])) { |
||
411 | $o = $this->owner ? $this->owner->__toString() : 'none'; |
||
412 | throw $this->exception('No such tag in template') |
||
413 | ->addMoreInfo('tag', $tag) |
||
414 | ->addMoreInfo('owner', $o) |
||
415 | ->addMoreInfo('tags', implode(', ', array_keys($this->tags))); |
||
416 | } |
||
417 | foreach ($this->tags[$tag] as $key => $_) { |
||
418 | $this->tags[$tag][$key] = $value; |
||
419 | } |
||
420 | $this->updated_tag_list[$tag] = $value; |
||
421 | |||
422 | return $this; |
||
423 | } |
||
424 | /** Check if tag is present inside template */ |
||
425 | public function hasTag($tag) |
||
526 | |||
527 | // template loading and parsing |
||
528 | public function findTemplate($template_name) |
||
541 | /** |
||
542 | * @param string $template_string |
||
543 | * @return $this |
||
544 | */ |
||
545 | public function loadTemplateFromString($template_string) |
||
556 | /** |
||
557 | * @param string $template_name |
||
558 | * @param string $ext |
||
559 | * @return $this |
||
560 | */ |
||
561 | public function loadTemplate($template_name, $ext = null) |
||
562 | { |
||
563 | /* |
||
564 | * Load template from file |
||
565 | */ |
||
566 | if (!$this->app) { |
||
567 | throw new Exception('Broken Link'); |
||
568 | } |
||
569 | if ($this->cache[$template_name.$ext]) { |
||
570 | $this->template = unserialize($this->cache[$template_name.$ext]); |
||
571 | $this->rebuildTags(); |
||
572 | |||
573 | return $this; |
||
574 | } |
||
575 | |||
576 | $tempext = null; |
||
577 | if ($ext !== null) { |
||
578 | $tempext = $this->settings['extension']; |
||
579 | $this->settings['extension'] = $ext; |
||
580 | } |
||
581 | $this->tmp_template = $this->findTemplate($template_name); |
||
582 | |||
583 | $this->template = array(); |
||
584 | $this->tags = array(); |
||
585 | $this->updated_tag_list = array(); |
||
586 | |||
587 | $this->parseTemplate($this->template); |
||
588 | if ($ext !== null) { |
||
589 | $this->settings['extension'] = $tempext; |
||
590 | } |
||
591 | |||
592 | $this->cache[$template_name.$ext] = serialize($this->template); |
||
593 | |||
594 | return $this; |
||
595 | } |
||
596 | /** |
||
597 | * @param array &$template |
||
598 | * @param int $level |
||
599 | * @param int $pc |
||
600 | * @return string |
||
601 | */ |
||
602 | public function parseTemplate(&$template, $level = 0, $pc = 0) |
||
603 | { |
||
604 | /* |
||
605 | * private function |
||
606 | * |
||
607 | * This is a main function, which actually parses template. It's recursive and it |
||
608 | * calls itself. Empty array should be passed |
||
609 | */ |
||
610 | // TODO when we go into sublevel, we should set the number of |
||
611 | // the tag so that there is NO double numbers in template COMPLETELY |
||
612 | // May be this way is dirty, need to look for better solution... |
||
613 | $c = pow(100, $level) + $pc; |
||
614 | while (strlen($this->tmp_template)) { |
||
615 | $text = $this->myStrTok($this->tmp_template, $this->settings['ldelim']); |
||
616 | if ($text !== '') { |
||
617 | $template[] = $text; |
||
618 | } |
||
619 | $tag = trim($this->myStrTok($this->tmp_template, $this->settings['rdelim'])); |
||
620 | if (isset($tag) && $tag) { |
||
621 | if ($tag[0] == '$') { |
||
622 | $tag = substr($tag, 1); |
||
623 | $template[$tag.'#'.$c] = array(); |
||
624 | $this->registerTag($tag, $c, $template[$tag.'#'.$c]); |
||
625 | } elseif ($tag[0] == '/') { |
||
626 | $tag = substr($tag, 1); |
||
627 | |||
628 | return $tag; |
||
629 | } elseif (substr($tag, -1) == '/') { |
||
630 | $tag = substr($tag, 0, -1); |
||
631 | $template[$tag.'#'.$c] = array(); |
||
632 | $this->registerTag($tag, $c, $template[$tag.'#'.$c]); |
||
633 | } else { |
||
634 | $template[$tag.'#'.$c] = array(); |
||
635 | $this->registerTag($tag, $c, $template[$tag.'#'.$c]); |
||
636 | $xtag = $this->parseTemplate($template[$tag.'#'.$c], $level + 1, $c); |
||
637 | if ($xtag && $tag != $xtag) { |
||
638 | throw $this->exception('Closing tag missmatch.') |
||
639 | ->addMoreInfo('opening', $tag) |
||
640 | ->addMoreInfo('closing', $xtag); |
||
641 | } |
||
642 | } |
||
643 | } |
||
644 | ++$c; |
||
645 | } |
||
646 | |||
647 | return 'end_of_file'; |
||
648 | } |
||
649 | public function registerTag($key, $npk, &$ref) |
||
650 | { |
||
651 | if (!$key) { |
||
652 | return; |
||
653 | } |
||
654 | if (isset($npk)) { |
||
655 | $this->tags[$key.'#'.$npk][] = &$ref; |
||
656 | } |
||
657 | $this->tags[$key][] = &$ref; |
||
658 | } |
||
659 | /** |
||
660 | * @return boolean |
||
661 | */ |
||
662 | public function isTopTag($tag) |
||
668 | |||
669 | /** |
||
670 | * Rebuild tags of existing array structure |
||
671 | * |
||
672 | * This function walks through template and rebuilds list of tags. You need it in case you |
||
673 | * changed already parsed template. |
||
674 | * |
||
675 | * @return $this |
||
676 | */ |
||
677 | public function rebuildTags() |
||
678 | { |
||
679 | $this->tags = array(); |
||
680 | $this->updated_tag_list = array(); |
||
681 | $this->rebuildTagsRegion($this->template); |
||
682 | |||
683 | return $this; |
||
684 | } |
||
685 | |||
686 | /** |
||
687 | * @param array &$branch |
||
688 | */ |
||
689 | public function rebuildTagsRegion(&$branch) |
||
709 | |||
710 | /** |
||
711 | * Template rendering (array -> string) |
||
712 | * |
||
713 | * @return string|array |
||
714 | */ |
||
715 | public function render() |
||
722 | /** |
||
723 | * @param string|array &$chunk |
||
724 | * @return string|array |
||
725 | */ |
||
726 | View Code Duplication | public function renderRegion(&$chunk) |
|
747 | |||
748 | // For debuging of template. Only allow to debug initial template. |
||
749 | // In future should be extended somehow with recursiveRender to also allow |
||
750 | // debugging of templates of entire object tree. |
||
751 | /** |
||
752 | * @return string |
||
753 | */ |
||
754 | public function debugRender() |
||
755 | { |
||
756 | return $this->debugRenderRegion($this->template); |
||
757 | } |
||
758 | /** |
||
759 | * @param string|array &$chunk |
||
760 | * @return string |
||
761 | */ |
||
762 | public function debugRenderRegion(&$chunk) |
||
763 | { |
||
764 | // output templates |
||
765 | $t = array( |
||
766 | 'tag-html' => '<span class="tag-html" style="color:black;">%s</span>', |
||
767 | 'tag-open' => '<span class="tag-container" '. |
||
768 | 'onmouseover="$(this).css(\'background\',\'lightgray\');" '. |
||
769 | 'onmouseout="$(this).css(\'background\',\'transparent\');">'. |
||
770 | '<span style="color:blue;cursor:pointer;" title="Start tag" '. |
||
771 | 'onclick="$(this).next().toggle();">[%s]</span>'. |
||
772 | '<span>', |
||
773 | 'tag-close' => '</span>'. |
||
774 | '<span style="color:blue;cursor:pointer;" title="End tag" '. |
||
775 | 'onclick="$(this).prev().toggle();">[/%s]</span>'. |
||
776 | '</span>', |
||
777 | ); |
||
778 | $result = ''; |
||
779 | |||
780 | // simple HTML |
||
781 | if (!is_array($chunk)) { |
||
782 | $s = preg_replace('/[\n|\r]{1,}/', '<br>', htmlentities($chunk)); |
||
783 | |||
784 | return sprintf($t['tag-html'], $s); |
||
785 | } |
||
786 | // recursion |
||
787 | foreach ($chunk as $key => $_chunk) { |
||
788 | $tag = substr($key, 0, strpos($key, '#')); |
||
789 | if (!is_numeric($key)) { |
||
790 | $result .= sprintf($t['tag-open'], $tag); |
||
791 | } |
||
792 | $result .= $this->debugRenderRegion($_chunk); |
||
793 | if (!is_numeric($key)) { |
||
794 | $result .= sprintf($t['tag-close'], $tag); |
||
795 | } |
||
796 | } |
||
797 | |||
798 | return $result; |
||
799 | } |
||
800 | |||
801 | // {{{ Misc functions |
||
802 | |||
803 | /** |
||
804 | * @param string &$string |
||
805 | * @param string $tok |
||
806 | * @return string |
||
807 | */ |
||
808 | public function myStrTok(&$string, $tok) |
||
825 | |||
826 | // }}} |
||
827 | } |
||
828 |
An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.
If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.