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 Field 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 Field, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
13 | class Field { |
||
14 | /** |
||
15 | * Stores all the field Backbone templates |
||
16 | * |
||
17 | * @see factory() |
||
18 | * @see add_template() |
||
19 | * @var array |
||
20 | */ |
||
21 | protected $templates = array(); |
||
22 | |||
23 | /** |
||
24 | * Globally unique field identificator. Generated randomly |
||
25 | * |
||
26 | * @var string |
||
27 | */ |
||
28 | protected $id; |
||
29 | |||
30 | /** |
||
31 | * Stores the initial <kbd>$type</kbd> variable passed to the <code>factory()</code> method |
||
32 | * |
||
33 | * @see factory |
||
34 | * @var string |
||
35 | */ |
||
36 | public $type; |
||
37 | |||
38 | /** |
||
39 | * Field value |
||
40 | * |
||
41 | * @var mixed |
||
42 | */ |
||
43 | protected $value; |
||
44 | |||
45 | /** |
||
46 | * Default field value |
||
47 | * |
||
48 | * @var mixed |
||
49 | */ |
||
50 | protected $default_value; |
||
51 | |||
52 | /** |
||
53 | * Sanitized field name used as input name attribute during field render |
||
54 | * |
||
55 | * @see factory() |
||
56 | * @see set_name() |
||
57 | * @var string |
||
58 | */ |
||
59 | protected $name; |
||
60 | |||
61 | /** |
||
62 | * The base field name which is used in the container. |
||
63 | * |
||
64 | * @see set_base_name() |
||
65 | * @var string |
||
66 | */ |
||
67 | protected $base_name; |
||
68 | |||
69 | /** |
||
70 | * Field name used as label during field render |
||
71 | * |
||
72 | * @see factory() |
||
73 | * @see set_label() |
||
74 | * @var string |
||
75 | */ |
||
76 | protected $label; |
||
77 | |||
78 | /** |
||
79 | * Additional text containing information and guidance for the user |
||
80 | * |
||
81 | * @see help_text() |
||
82 | * @var string |
||
83 | */ |
||
84 | protected $help_text; |
||
85 | |||
86 | /** |
||
87 | * Field DataStore instance to which save, load and delete calls are delegated |
||
88 | * |
||
89 | * @see set_datastore() |
||
90 | * @see get_datastore() |
||
91 | * @var Datastore_Interface |
||
92 | */ |
||
93 | protected $store; |
||
94 | |||
95 | /** |
||
96 | * The type of the container this field is in |
||
97 | * |
||
98 | * @see get_context() |
||
99 | * @var string |
||
100 | */ |
||
101 | protected $context; |
||
102 | |||
103 | /** |
||
104 | * Whether or not this value should be auto loaded. Applicable to theme options only. |
||
105 | * |
||
106 | * @see set_autoload() |
||
107 | * @var bool |
||
108 | **/ |
||
109 | protected $autoload = false; |
||
110 | |||
111 | /** |
||
112 | * Whether or not this field will be initialized when the field is in the viewport (visible). |
||
113 | * |
||
114 | * @see set_lazyload() |
||
115 | * @var bool |
||
116 | **/ |
||
117 | protected $lazyload = false; |
||
118 | |||
119 | /** |
||
120 | * The width of the field. |
||
121 | * |
||
122 | * @see set_width() |
||
123 | * @var int |
||
124 | **/ |
||
125 | protected $width = 0; |
||
126 | |||
127 | /** |
||
128 | * Custom CSS classes. |
||
129 | * |
||
130 | * @see add_class() |
||
131 | * @var array |
||
132 | **/ |
||
133 | protected $classes = array(); |
||
134 | |||
135 | /** |
||
136 | * Whether or not this field is required. |
||
137 | * |
||
138 | * @see set_required() |
||
139 | * @var bool |
||
140 | **/ |
||
141 | protected $required = false; |
||
142 | |||
143 | /** |
||
144 | * Prefix to be prepended to the field name during load, save, delete and <strong>render</strong> |
||
145 | * |
||
146 | * @var string |
||
147 | **/ |
||
148 | protected $name_prefix = '_'; |
||
149 | |||
150 | /** |
||
151 | * Stores the field conditional logic rules. |
||
152 | * |
||
153 | * @var array |
||
154 | **/ |
||
155 | protected $conditional_logic = array(); |
||
156 | |||
157 | /** |
||
158 | * Create a new field of type $type and name $name and label $label. |
||
159 | * |
||
160 | * @param string $type |
||
161 | * @param string $name lower case and underscore-delimited |
||
162 | * @param string $label (optional) Automatically generated from $name if not present |
||
163 | * @return object $field |
||
164 | **/ |
||
165 | 15 | public static function factory( $type, $name, $label = null ) { |
|
166 | // backward compatibility: `file` type used to be called `attachment` |
||
167 | 15 | if ( $type === 'attachment' ) { |
|
168 | $type = 'file'; |
||
169 | } |
||
170 | |||
171 | 15 | $type = str_replace( ' ', '_', ucwords( str_replace( '_', ' ', $type ) ) ); |
|
172 | |||
173 | 15 | $class = __NAMESPACE__ . '\\' . $type . '_Field'; |
|
174 | |||
175 | 15 | View Code Duplication | if ( ! class_exists( $class ) ) { |
1 ignored issue
–
show
|
|||
176 | 4 | Incorrect_Syntax_Exception::raise( 'Unknown field "' . $type . '".' ); |
|
177 | 1 | $class = __NAMESPACE__ . '\\Broken_Field'; |
|
178 | 1 | } |
|
179 | |||
180 | 12 | if ( strpos( $name, '-' ) !== false ) { |
|
181 | 1 | Incorrect_Syntax_Exception::raise( 'Forbidden character "-" in name "' . $name . '".' ); |
|
182 | $class = __NAMESPACE__ . '\\Broken_Field'; |
||
183 | } |
||
184 | |||
185 | 11 | $field = new $class( $name, $label ); |
|
186 | 10 | $field->type = $type; |
|
187 | 10 | $field->add_template( $field->get_type(), array( $field, 'template' ) ); |
|
188 | |||
189 | 10 | return $field; |
|
190 | } |
||
191 | |||
192 | /** |
||
193 | * An alias of factory(). |
||
194 | * |
||
195 | * @see Field::factory() |
||
196 | **/ |
||
197 | 15 | public static function make( $type, $name, $label = null ) { |
|
198 | 15 | return self::factory( $type, $name, $label ); |
|
199 | } |
||
200 | |||
201 | /** |
||
202 | * Create a field from a certain type with the specified label. |
||
203 | * @param string $name Field name |
||
204 | * @param string $label Field label |
||
205 | */ |
||
206 | 11 | protected function __construct( $name, $label ) { |
|
207 | 11 | $this->set_name( $name ); |
|
208 | 10 | $this->set_label( $label ); |
|
209 | 10 | $this->set_base_name( $name ); |
|
210 | |||
211 | // Pick random ID |
||
212 | 10 | $random_string = md5( mt_rand() . $this->get_name() . $this->get_label() ); |
|
213 | 10 | $random_string = substr( $random_string, 0, 5 ); // 5 chars should be enough |
|
214 | 10 | $this->id = 'carbon-' . $random_string; |
|
215 | |||
216 | 10 | $this->init(); |
|
217 | 10 | if ( is_admin() ) { |
|
218 | $this->admin_init(); |
||
219 | } |
||
220 | |||
221 | 10 | add_action( 'admin_print_scripts', array( $this, 'admin_hook_scripts' ) ); |
|
222 | 10 | add_action( 'admin_print_styles', array( $this, 'admin_hook_styles' ) ); |
|
223 | 10 | } |
|
224 | |||
225 | /** |
||
226 | * Perform instance initialization after calling setup() |
||
227 | **/ |
||
228 | public function init() {} |
||
229 | |||
230 | /** |
||
231 | * Instance initialization when in the admin area. |
||
232 | * Called during object construction. |
||
233 | **/ |
||
234 | public function admin_init() {} |
||
235 | |||
236 | /** |
||
237 | * Enqueue admin scripts. |
||
238 | * Called once per field type. |
||
239 | **/ |
||
240 | public function admin_enqueue_scripts() {} |
||
241 | |||
242 | /** |
||
243 | * Prints the main Underscore template |
||
244 | **/ |
||
245 | public function template() { } |
||
246 | |||
247 | /** |
||
248 | * Returns all the Backbone templates |
||
249 | * |
||
250 | * @return array |
||
251 | **/ |
||
252 | public function get_templates() { |
||
255 | |||
256 | /** |
||
257 | * Adds a new Backbone template |
||
258 | **/ |
||
259 | public function add_template( $name, $callback ) { |
||
262 | |||
263 | /** |
||
264 | * Delegate load to the field DataStore instance |
||
265 | **/ |
||
266 | public function load() { |
||
273 | |||
274 | /** |
||
275 | * Delegate save to the field DataStore instance |
||
276 | **/ |
||
277 | public function save() { |
||
280 | |||
281 | /** |
||
282 | * Delegate delete to the field DataStore instance |
||
283 | **/ |
||
284 | public function delete() { |
||
287 | |||
288 | /** |
||
289 | * Load the field value from an input array based on it's name |
||
290 | * |
||
291 | * @param array $input (optional) Array of field names and values. Defaults to $_POST |
||
292 | **/ |
||
293 | public function set_value_from_input( $input = null ) { |
||
304 | |||
305 | /** |
||
306 | * Assign DataStore instance for use during load, save and delete |
||
307 | * |
||
308 | * @param object $store |
||
309 | * @return object $this |
||
310 | **/ |
||
311 | public function set_datastore( Datastore_Interface $store ) { |
||
315 | |||
316 | /** |
||
317 | * Return the DataStore instance used by the field |
||
318 | * |
||
319 | * @return object $store |
||
320 | **/ |
||
321 | public function get_datastore() { |
||
324 | |||
325 | /** |
||
326 | * Assign the type of the container this field is in |
||
327 | * |
||
328 | * @param string |
||
329 | * @return object $this |
||
330 | **/ |
||
331 | public function set_context( $context ) { |
||
335 | |||
336 | /** |
||
337 | * Return the type of the container this field is in |
||
338 | * |
||
339 | * @return string |
||
340 | **/ |
||
341 | public function get_context() { |
||
344 | |||
345 | /** |
||
346 | * Directly modify the field value |
||
347 | * |
||
348 | * @param mixed $value |
||
349 | **/ |
||
350 | public function set_value( $value ) { |
||
353 | |||
354 | /** |
||
355 | * Set default field value |
||
356 | * |
||
357 | * @param mixed $default_value |
||
358 | **/ |
||
359 | public function set_default_value( $default_value ) { |
||
363 | |||
364 | /** |
||
365 | * Get default field value |
||
366 | * |
||
367 | * @return mixed |
||
368 | **/ |
||
369 | public function get_default_value() { |
||
372 | |||
373 | /** |
||
374 | * Return the field value |
||
375 | * |
||
376 | * @return mixed |
||
377 | **/ |
||
378 | public function get_value() { |
||
381 | |||
382 | /** |
||
383 | * Set field name. |
||
384 | * Use only if you are completely aware of what you are doing. |
||
385 | * |
||
386 | * @param string $name Field name, either sanitized or not |
||
387 | **/ |
||
388 | public function set_name( $name ) { |
||
401 | |||
402 | /** |
||
403 | * Return the field name |
||
404 | * |
||
405 | * @return string |
||
406 | **/ |
||
407 | public function get_name() { |
||
410 | |||
411 | /** |
||
412 | * Set field base name as defined in the container. |
||
413 | **/ |
||
414 | public function set_base_name( $name ) { |
||
417 | |||
418 | /** |
||
419 | * Return the field base name. |
||
420 | * |
||
421 | * @return string |
||
422 | **/ |
||
423 | public function get_base_name() { |
||
426 | |||
427 | /** |
||
428 | * Set field name prefix. Calling this method will update the current field |
||
429 | * name and the conditional logic fields. |
||
430 | * |
||
431 | * @param string $prefix |
||
432 | * @return object $this |
||
433 | **/ |
||
434 | public function set_prefix( $prefix ) { |
||
442 | |||
443 | /** |
||
444 | * Set field label. |
||
445 | * |
||
446 | * @param string $label If null, the label will be generated from the field name |
||
447 | **/ |
||
448 | public function set_label( $label ) { |
||
449 | // Try to guess field label from it's name |
||
450 | View Code Duplication | if ( is_null( $label ) ) { |
|
451 | // remove the leading underscore(if it's there) |
||
452 | $label = preg_replace( '~^_~', '', $this->name ); |
||
453 | |||
454 | // remove the leading "crb_"(if it's there) |
||
455 | $label = preg_replace( '~^crb_~', '', $label ); |
||
456 | |||
457 | // split the name into words and make them capitalized |
||
458 | $label = mb_convert_case( str_replace( '_', ' ', $label ), MB_CASE_TITLE ); |
||
459 | } |
||
460 | |||
461 | $this->label = $label; |
||
462 | } |
||
463 | |||
464 | /** |
||
465 | * Return field label. |
||
466 | * |
||
467 | * @return string |
||
468 | **/ |
||
469 | public function get_label() { |
||
472 | |||
473 | /** |
||
474 | * Set additional text to be displayed during field render, |
||
475 | * containing information and guidance for the user |
||
476 | * |
||
477 | * @return object $this |
||
478 | **/ |
||
479 | public function set_help_text( $help_text ) { |
||
483 | |||
484 | /** |
||
485 | * Alias for set_help_text() |
||
486 | * |
||
487 | * @see set_help_text() |
||
488 | * @return object $this |
||
489 | **/ |
||
490 | public function help_text( $help_text ) { |
||
493 | |||
494 | /** |
||
495 | * Return the field help text |
||
496 | * |
||
497 | * @return object $this |
||
498 | **/ |
||
499 | public function get_help_text() { |
||
502 | |||
503 | /** |
||
504 | * Whether or not this value should be auto loaded. Applicable to theme options only. |
||
505 | * |
||
506 | * @param bool $autoload |
||
507 | * @return object $this |
||
508 | **/ |
||
509 | public function set_autoload( $autoload ) { |
||
513 | |||
514 | /** |
||
515 | * Return whether or not this value should be auto loaded. |
||
516 | * |
||
517 | * @return bool |
||
518 | **/ |
||
519 | public function get_autoload() { |
||
522 | |||
523 | /** |
||
524 | * Whether or not this field will be initialized when the field is in the viewport (visible). |
||
525 | * |
||
526 | * @param bool $lazyload |
||
527 | * @return object $this |
||
528 | **/ |
||
529 | public function set_lazyload( $lazyload ) { |
||
533 | |||
534 | /** |
||
535 | * Return whether or not this field should be lazyloaded. |
||
536 | * |
||
537 | * @return bool |
||
538 | **/ |
||
539 | public function get_lazyload() { |
||
542 | |||
543 | /** |
||
544 | * Set the field width. |
||
545 | * |
||
546 | * @param int $width |
||
547 | * @return object $this |
||
548 | **/ |
||
549 | public function set_width( $width ) { |
||
553 | |||
554 | /** |
||
555 | * Get the field width. |
||
556 | * |
||
557 | * @return int $width |
||
558 | **/ |
||
559 | public function get_width() { |
||
562 | |||
563 | /** |
||
564 | * Add custom CSS class to the field html container. |
||
565 | * |
||
566 | * @param string|array $classes |
||
567 | * @return object $this |
||
568 | **/ |
||
569 | public function add_class( $classes ) { |
||
577 | |||
578 | /** |
||
579 | * Get the field custom CSS classes. |
||
580 | * |
||
581 | * @return array |
||
582 | **/ |
||
583 | public function get_classes() { |
||
586 | |||
587 | /** |
||
588 | * Whether this field is mandatory for the user |
||
589 | * |
||
590 | * @param bool $required |
||
591 | * @return object $this |
||
592 | **/ |
||
593 | public function set_required( $required ) { |
||
597 | |||
598 | /** |
||
599 | * HTML id attribute getter. |
||
600 | * @return string |
||
601 | */ |
||
602 | 1 | public function get_id() { |
|
605 | |||
606 | /** |
||
607 | * HTML id attribute setter |
||
608 | * @param string $id |
||
609 | */ |
||
610 | 1 | public function set_id( $id ) { |
|
613 | |||
614 | /** |
||
615 | * Return whether this field is mandatory for the user |
||
616 | * |
||
617 | * @return bool |
||
618 | **/ |
||
619 | public function is_required() { |
||
622 | |||
623 | /** |
||
624 | * Returns the type of the field based on the class. |
||
625 | * The class is stripped by the "CarbonFields" prefix. |
||
626 | * Also the "Field" suffix is removed. |
||
627 | * Then underscores and backslashes are removed. |
||
628 | * |
||
629 | * @return string |
||
630 | */ |
||
631 | public function get_type() { |
||
636 | |||
637 | /** |
||
638 | * Cleans up an object class for usage as HTML class |
||
639 | * |
||
640 | * @return string |
||
641 | */ |
||
642 | protected function clean_type( $type ) { |
||
653 | |||
654 | /** |
||
655 | * Return an array of html classes to be used for the field container |
||
656 | * |
||
657 | * @return array |
||
658 | */ |
||
659 | public function get_html_class() { |
||
676 | |||
677 | /** |
||
678 | * Allows the value of a field to be processed after loading. |
||
679 | * Can be implemented by the extending class if necessary. |
||
680 | * |
||
681 | * @return array |
||
682 | */ |
||
683 | public function process_value() { |
||
686 | |||
687 | /** |
||
688 | * Returns an array that holds the field data, suitable for JSON representation. |
||
689 | * This data will be available in the Underscore template and the Backbone Model. |
||
690 | * |
||
691 | * @param bool $load Should the value be loaded from the database or use the value from the current instance. |
||
692 | * @return array |
||
693 | */ |
||
694 | public function to_json( $load ) { |
||
720 | |||
721 | /** |
||
722 | * Set the field visibility conditional logic. |
||
723 | * |
||
724 | * @param array |
||
725 | */ |
||
726 | 8 | public function set_conditional_logic( $rules ) { |
|
731 | |||
732 | /** |
||
733 | * Get the conditional logic rules |
||
734 | * |
||
735 | * @return array |
||
736 | */ |
||
737 | 3 | public function get_conditional_logic() { |
|
740 | |||
741 | /** |
||
742 | * Validate and parse the conditional logic rules. |
||
743 | * |
||
744 | * @param array $rules |
||
745 | * @return array |
||
746 | */ |
||
747 | protected function parse_conditional_rules( $rules ) { |
||
748 | if ( ! is_array( $rules ) ) { |
||
749 | Incorrect_Syntax_Exception::raise( 'Conditional logic rules argument should be an array.' ); |
||
750 | } |
||
751 | |||
752 | $allowed_operators = array( '=', '!=', '>', '>=', '<', '<=', 'IN', 'NOT IN' ); |
||
753 | $allowed_relations = array( 'AND', 'OR' ); |
||
754 | |||
755 | $parsed_rules = array( |
||
756 | 'relation' => 'AND', |
||
757 | 'rules' => array(), |
||
758 | ); |
||
759 | |||
760 | foreach ( $rules as $key => $rule ) { |
||
761 | // Check if we have a relation key |
||
762 | if ( $key === 'relation' ) { |
||
763 | $relation = strtoupper( $rule ); |
||
764 | |||
765 | View Code Duplication | if ( ! in_array( $relation, $allowed_relations ) ) { |
|
1 ignored issue
–
show
|
|||
766 | Incorrect_Syntax_Exception::raise( 'Invalid relation type ' . $rule . '. ' . |
||
767 | 'The rule should be one of the following: "' . implode( '", "', $allowed_relations ) . '"' ); |
||
768 | } |
||
769 | |||
770 | $parsed_rules['relation'] = $relation; |
||
771 | continue; |
||
772 | } |
||
773 | |||
774 | // Check if the rule is valid |
||
775 | if ( ! is_array( $rule ) || empty( $rule['field'] ) ) { |
||
776 | Incorrect_Syntax_Exception::raise( 'Invalid conditional logic rule format. ' . |
||
777 | 'The rule should be an array with the "field" key set.' ); |
||
778 | } |
||
779 | |||
780 | // Check the compare operator |
||
781 | if ( empty( $rule['compare'] ) ) { |
||
782 | $rule['compare'] = '='; |
||
783 | } |
||
784 | View Code Duplication | if ( ! in_array( $rule['compare'], $allowed_operators ) ) { |
|
1 ignored issue
–
show
|
|||
785 | Incorrect_Syntax_Exception::raise( 'Invalid conditional logic compare operator: <code>' . |
||
786 | $rule['compare'] . '</code><br>Allowed operators are: <code>' . |
||
787 | implode( ', ', $allowed_operators ) . '</code>' ); |
||
788 | } |
||
789 | if ( $rule['compare'] === 'IN' || $rule['compare'] === 'NOT IN' ) { |
||
790 | if ( ! is_array( $rule['value'] ) ) { |
||
791 | Incorrect_Syntax_Exception::raise( 'Invalid conditional logic value format. ' . |
||
792 | 'An array is expected, when using the "' . $rule['compare'] . '" operator.' ); |
||
793 | } |
||
794 | } |
||
795 | |||
796 | // Check the value |
||
797 | if ( ! isset( $rule['value'] ) ) { |
||
798 | $rule['value'] = ''; |
||
799 | } |
||
800 | |||
801 | $parsed_rules['rules'][] = $rule; |
||
802 | } |
||
803 | |||
804 | return $parsed_rules; |
||
805 | } |
||
806 | |||
807 | |||
808 | /** |
||
809 | * Hook administration scripts. |
||
810 | */ |
||
811 | public function admin_hook_scripts() { |
||
834 | |||
835 | /** |
||
836 | * Hook administration styles. |
||
837 | */ |
||
838 | public function admin_hook_styles() { |
||
841 | } // END Field |
||
842 |
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.