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 Complex_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 Complex_Field, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
15 | class Complex_Field extends Field { |
||
16 | const LAYOUT_GRID = 'grid'; // default |
||
17 | const LAYOUT_LIST = 'list'; // deprecated |
||
18 | const LAYOUT_TABBED = 'tabbed'; // deprecated |
||
19 | const LAYOUT_TABBED_HORIZONTAL = 'tabbed-horizontal'; |
||
20 | const LAYOUT_TABBED_VERTICAL = 'tabbed-vertical'; |
||
21 | |||
22 | protected $fields = array(); |
||
23 | protected $values = array(); |
||
24 | protected $groups = array(); |
||
25 | |||
26 | protected $layout = self::LAYOUT_GRID; |
||
27 | protected $values_min = -1; |
||
28 | protected $values_max = -1; |
||
29 | protected $collapsed = false; |
||
30 | |||
31 | public $labels = array( |
||
32 | 'singular_name' => 'Entry', |
||
33 | 'plural_name' => 'Entries', |
||
34 | ); |
||
35 | |||
36 | /** |
||
37 | * Initialization tasks. |
||
38 | */ |
||
39 | public function init() { |
||
40 | $this->labels = array( |
||
41 | 'singular_name' => __( 'Entry', 'carbon-fields' ), |
||
42 | 'plural_name' => __( 'Entries', 'carbon-fields' ), |
||
43 | ); |
||
44 | |||
45 | // Include the complex group Underscore templates |
||
46 | $this->add_template( 'Complex-Group', array( $this, 'template_group' ) ); |
||
47 | $this->add_template( 'Complex-Group-Tab-Item', array( $this, 'template_group_tab_item' ) ); |
||
48 | |||
49 | parent::init(); |
||
50 | } |
||
51 | |||
52 | /** |
||
53 | * Add a set/group of fields. |
||
54 | * |
||
55 | * @return $this |
||
56 | */ |
||
57 | public function add_fields() { |
||
58 | $argv = func_get_args(); |
||
59 | $argc = count( $argv ); |
||
60 | |||
61 | if ( $argc == 1 ) { |
||
62 | $fields = $argv[0]; |
||
63 | $name = ''; |
||
64 | $label = null; |
||
65 | } else if ( $argc == 2 ) { |
||
66 | View Code Duplication | if ( is_array( $argv[0] ) ) { |
|
1 ignored issue
–
show
|
|||
67 | list( $fields, $name ) = $argv; |
||
68 | } else { |
||
69 | list( $name, $fields ) = $argv; |
||
70 | } |
||
71 | $label = null; |
||
72 | } else if ( $argc == 3 ) { |
||
73 | View Code Duplication | if ( is_array( $argv[0] ) ) { |
|
1 ignored issue
–
show
|
|||
74 | list( $fields, $name, $label ) = $argv; |
||
75 | } else { |
||
76 | list( $name, $label, $fields ) = $argv; |
||
77 | } |
||
78 | } |
||
79 | |||
80 | if ( array_key_exists( '_' . $name, $this->groups ) ) { |
||
81 | Incorrect_Syntax_Exception::raise( 'Group with name "' . $name . '" in Complex Field "' . $this->get_label() . '" already exists.' ); |
||
82 | } |
||
83 | |||
84 | $group = new Group_Field( $name, $label, $fields ); |
||
85 | |||
86 | $this->groups[ $group->get_name() ] = $group; |
||
87 | |||
88 | return $this; |
||
89 | } |
||
90 | |||
91 | /** |
||
92 | * Set the group label Underscore template. |
||
93 | * |
||
94 | * @param string|callable $template |
||
95 | * @return $this |
||
96 | */ |
||
97 | public function set_header_template( $template ) { |
||
98 | if ( count( $this->groups ) === 0 ) { |
||
99 | Incorrect_Syntax_Exception::raise( "Can't set group label template. There are no present groups for Complex Field " . $this->get_label() . '.' ); |
||
100 | } |
||
101 | |||
102 | $template = is_callable( $template ) ? call_user_func( $template ) : $template; |
||
103 | |||
104 | // Assign the template to the group that was added last |
||
105 | $values = array_values( $this->groups ); |
||
106 | $group = end( $values ); |
||
107 | $group->set_label_template( $template ); |
||
108 | |||
109 | // Include the group label Underscore template |
||
110 | $this->add_template( $group->get_group_id(), array( $group, 'template_label' ) ); |
||
111 | |||
112 | $this->groups[ $group->get_name() ] = $group; |
||
113 | |||
114 | return $this; |
||
115 | } |
||
116 | |||
117 | /** |
||
118 | * Retrieve all groups of fields. |
||
119 | * |
||
120 | * @return array $fields |
||
121 | */ |
||
122 | public function get_fields() { |
||
123 | $fields = array(); |
||
124 | |||
125 | foreach ( $this->groups as $group ) { |
||
126 | $group_fields = $group->get_fields(); |
||
127 | |||
128 | $fields = array_merge( $fields, $group_fields ); |
||
129 | } |
||
130 | |||
131 | return $fields; |
||
132 | } |
||
133 | |||
134 | /** |
||
135 | * Set the field labels. |
||
136 | * Currently supported values: |
||
137 | * - singular_name - the singular entry label |
||
138 | * - plural_name - the plural entries label |
||
139 | * |
||
140 | * @param array $labels Labels |
||
141 | */ |
||
142 | public function setup_labels( $labels ) { |
||
143 | $this->labels = array_merge( $this->labels, $labels ); |
||
144 | return $this; |
||
145 | } |
||
146 | |||
147 | /** |
||
148 | * Set the datastore of this field. |
||
149 | * |
||
150 | * @param Datastore_Interface $store |
||
151 | */ |
||
152 | public function set_datastore( Datastore_Interface $store ) { |
||
153 | $this->store = $store; |
||
154 | |||
155 | foreach ( $this->groups as $group ) { |
||
156 | $group->set_datastore( $this->store ); |
||
157 | } |
||
158 | } |
||
159 | |||
160 | /** |
||
161 | * Load the field value from an input array based on it's name. |
||
162 | * |
||
163 | * @param array $input (optional) Array of field names and values. Defaults to $_POST |
||
164 | **/ |
||
165 | public function set_value_from_input( $input = null ) { |
||
166 | $this->values = array(); |
||
167 | |||
168 | if ( is_null( $input ) ) { |
||
169 | $input = $_POST; |
||
170 | } |
||
171 | |||
172 | if ( ! isset( $input[ $this->get_name() ] ) ) { |
||
173 | return; |
||
174 | } |
||
175 | |||
176 | $input_groups = $input[ $this->get_name() ]; |
||
177 | $index = 0; |
||
178 | |||
179 | foreach ( $input_groups as $values ) { |
||
180 | $value_group = array(); |
||
181 | if ( ! isset( $values['group'] ) || ! isset( $this->groups[ $values['group'] ] ) ) { |
||
182 | continue; |
||
183 | } |
||
184 | |||
185 | $group = $this->groups[ $values['group'] ]; |
||
186 | unset( $values['group'] ); |
||
187 | |||
188 | $group_fields = $group->get_fields(); |
||
189 | |||
190 | // trim input values to those used by the field |
||
191 | $group_field_names = array_flip( $group->get_field_names() ); |
||
192 | $values = array_intersect_key( $values, $group_field_names ); |
||
193 | |||
194 | foreach ( $group_fields as $field ) { |
||
195 | // set value from the group |
||
196 | $tmp_field = clone $field; |
||
197 | if ( is_a( $tmp_field, __NAMESPACE__ . '\\Complex_Field' ) ) { |
||
198 | if ( ! isset( $values[ $tmp_field->get_name() ] ) ) { |
||
199 | continue; // bail if the complex field is empty |
||
200 | } |
||
201 | |||
202 | $new_name = $this->get_name() . $group->get_name() . '-' . $field->get_name() . '_' . $index; |
||
203 | $new_values = array( $new_name => $values[ $tmp_field->get_name() ] ); |
||
204 | |||
205 | $tmp_field->set_name( $new_name ); |
||
206 | $tmp_field->set_value_from_input( $new_values ); |
||
207 | } else { |
||
208 | $tmp_field->set_value_from_input( $values ); |
||
209 | } |
||
210 | |||
211 | // update name to group name |
||
212 | $tmp_field->set_name( $this->get_name() . $group->get_name() . '-' . $field->get_name() . '_' . $index ); |
||
213 | $value_group[] = $tmp_field; |
||
214 | } |
||
215 | |||
216 | $this->values[] = $value_group; |
||
217 | $index++; |
||
218 | } |
||
219 | } |
||
220 | |||
221 | /** |
||
222 | * Load all groups of fields and their data. |
||
223 | */ |
||
224 | public function load() { |
||
228 | |||
229 | /** |
||
230 | * Save all contained groups of fields. |
||
231 | */ |
||
232 | public function save() { |
||
241 | |||
242 | /** |
||
243 | * Delete the values of all contained fields. |
||
244 | */ |
||
245 | public function delete() { |
||
248 | |||
249 | /** |
||
250 | * Load and parse the field data. |
||
251 | */ |
||
252 | public function load_values() { |
||
255 | |||
256 | /** |
||
257 | * Load and parse the field data from the database. |
||
258 | */ |
||
259 | public function load_values_from_db() { |
||
266 | |||
267 | /** |
||
268 | * Load and parse a raw set of field data. |
||
269 | * |
||
270 | * @param array $values Raw data entries |
||
271 | * @return array Processed data entries |
||
272 | */ |
||
273 | public function load_values_from_array( $values ) { |
||
293 | |||
294 | /** |
||
295 | * Parse groups of raw field data into the actual field hierarchy. |
||
296 | * |
||
297 | * @param array $group_rows Group rows |
||
298 | */ |
||
299 | public function process_loaded_values( $group_rows ) { |
||
358 | |||
359 | /** |
||
360 | * Retrieve the field values. |
||
361 | * @return array |
||
362 | */ |
||
363 | public function get_values() { |
||
366 | |||
367 | /** |
||
368 | * Generate and set the field prefix. |
||
369 | * @param string $prefix |
||
370 | */ |
||
371 | public function set_prefix( $prefix ) { |
||
372 | parent::set_prefix( $prefix ); |
||
373 | |||
374 | foreach ( $this->groups as $group ) { |
||
375 | $group->set_prefix( $prefix ); |
||
376 | } |
||
377 | } |
||
378 | |||
379 | /** |
||
380 | * Returns an array that holds the field data, suitable for JSON representation. |
||
381 | * This data will be available in the Underscore template and the Backbone Model. |
||
382 | * |
||
383 | * @param bool $load Should the value be loaded from the database or use the value from the current instance. |
||
384 | * @return array |
||
385 | */ |
||
386 | public function to_json( $load ) { |
||
387 | $complex_data = parent::to_json( $load ); |
||
388 | |||
389 | $groups_data = array(); |
||
390 | $values_data = array(); |
||
391 | |||
392 | foreach ( $this->groups as $group ) { |
||
393 | $groups_data[] = $group->to_json( false ); |
||
394 | } |
||
395 | |||
396 | foreach ( $this->values as $fields ) { |
||
397 | $group = $this->get_group_by_name( $fields['type'] ); |
||
398 | unset( $fields['type'] ); |
||
399 | |||
400 | $data = array( |
||
401 | 'name' => $group->get_name(), |
||
402 | 'label' => $group->get_label(), |
||
403 | 'group_id' => $group->get_group_id(), |
||
404 | 'fields' => array(), |
||
405 | ); |
||
406 | |||
407 | foreach ( $fields as $index => $field ) { |
||
408 | $data['fields'][] = $field->to_json( false ); |
||
409 | } |
||
410 | |||
411 | $values_data[] = $data; |
||
412 | } |
||
413 | |||
414 | $complex_data = array_merge( $complex_data, array( |
||
415 | 'layout' => $this->layout, |
||
416 | 'labels' => $this->labels, |
||
417 | 'min' => $this->get_min(), |
||
418 | 'max' => $this->get_max(), |
||
419 | 'multiple_groups' => count( $groups_data ) > 1, |
||
420 | 'groups' => $groups_data, |
||
421 | 'value' => $values_data, |
||
422 | 'collapsed' => $this->collapsed, |
||
423 | ) ); |
||
424 | |||
425 | return $complex_data; |
||
426 | } |
||
427 | |||
428 | /** |
||
429 | * The main Underscore template. |
||
430 | */ |
||
431 | public function template() { |
||
432 | ?> |
||
433 | <div class="carbon-subcontainer carbon-grid {{ multiple_groups ? 'multiple-groups' : '' }}"> |
||
434 | <div class="carbon-empty-row carbon-empty-row-visible"> |
||
435 | {{{ crbl10n.complex_no_rows.replace('%s', labels.plural_name) }}} |
||
436 | </div> |
||
437 | |||
438 | <div class="groups-wrapper layout-{{ layout }}"> |
||
439 | <# if (layout === '<?php echo self::LAYOUT_TABBED_HORIZONTAL ?>' || layout === '<?php echo self::LAYOUT_TABBED_VERTICAL ?>' ) { #> |
||
440 | <div class="group-tabs-nav-holder"> |
||
441 | <ul class="group-tabs-nav"></ul> |
||
442 | |||
443 | <div class="carbon-actions"> |
||
444 | <div class="carbon-button"> |
||
445 | <a href="#" class="button" data-group="{{{ multiple_groups ? '' : groups[0].name }}}"> |
||
446 | + |
||
447 | </a> |
||
448 | |||
449 | <# if (multiple_groups) { #> |
||
450 | <ul> |
||
451 | <# _.each(groups, function(group) { #> |
||
452 | <li><a href="#" data-group="{{{ group.name }}}">{{{ group.label }}}</a></li> |
||
453 | <# }); #> |
||
454 | </ul> |
||
455 | <# } #> |
||
456 | </div> |
||
457 | </div> |
||
458 | </div><!-- /.group-tabs-nav-holder --> |
||
459 | <# } #> |
||
460 | |||
461 | <div class="carbon-groups-holder"></div> |
||
462 | <div class="clear"></div> |
||
463 | </div> |
||
464 | |||
465 | <div class="carbon-actions"> |
||
466 | <div class="carbon-button"> |
||
467 | <a href="#" class="button" data-group="{{{ multiple_groups ? '' : groups[0].name }}}"> |
||
468 | {{{ crbl10n.complex_add_button.replace('%s', labels.singular_name) }}} |
||
469 | </a> |
||
470 | |||
471 | <# if (multiple_groups) { #> |
||
472 | <ul> |
||
473 | <# _.each(groups, function(group) { #> |
||
474 | <li><a href="#" data-group="{{{ group.name }}}">{{{ group.label }}}</a></li> |
||
475 | <# }); #> |
||
476 | </ul> |
||
477 | <# } #> |
||
478 | </div> |
||
479 | </div> |
||
480 | </div> |
||
481 | <?php |
||
482 | } |
||
483 | |||
484 | /** |
||
485 | * The Underscore template for the complex field group. |
||
486 | */ |
||
487 | public function template_group() { |
||
488 | ?> |
||
489 | <div id="carbon-{{{ complex_name }}}-complex-container" class="carbon-row carbon-group-row" data-group-id="{{ id }}"> |
||
490 | <input type="hidden" name="{{{ complex_name + '[' + index + ']' }}}[group]" value="{{ name }}" /> |
||
491 | |||
492 | <div class="carbon-drag-handle"> |
||
493 | <span class="group-number">{{{ order + 1 }}}</span><span class="group-name">{{{ label_template || label }}}</span> |
||
494 | </div> |
||
495 | |||
496 | <div class="carbon-group-actions carbon-group-actions-{{ layout }}"> |
||
497 | <a class="carbon-btn-duplicate dashicons-before dashicons-admin-page" href="#" title="<?php esc_attr_e( 'Clone', 'carbon_fields' ); ?>"> |
||
498 | <?php _e( 'Clone', 'carbon_fields' ); ?> |
||
499 | </a> |
||
500 | |||
501 | <a class="carbon-btn-remove dashicons-before dashicons-trash" href="#" title="<?php esc_attr_e( 'Remove', 'carbon_fields' ); ?>"> |
||
502 | <?php _e( 'Remove', 'carbon_fields' ); ?> |
||
503 | </a> |
||
504 | |||
505 | <a class="carbon-btn-collapse dashicons-before dashicons-arrow-up" href="#" title="<?php esc_attr_e( 'Collapse/Expand', 'carbon_fields' ); ?>"> |
||
506 | <?php _e( 'Collapse/Expand', 'carbon_fields' ); ?> |
||
507 | </a> |
||
508 | </div> |
||
509 | |||
510 | <div class="fields-container"> |
||
511 | <# _.each(fields, function(field) { #> |
||
512 | <div class="carbon-row carbon-subrow subrow-{{{ field.type }}} {{{ field.classes.join(' ') }}}"> |
||
513 | <label for="{{{ complex_id + '-' + field.id + '-' + index }}}"> |
||
514 | {{ field.label }} |
||
515 | |||
516 | <# if (field.required) { #> |
||
517 | <span class="carbon-required">*</span> |
||
518 | <# } #> |
||
519 | </label> |
||
520 | |||
521 | <div class="field-holder {{{ complex_id + '-' + field.id + '-' + index }}}"></div> |
||
522 | |||
523 | <# if (field.help_text) { #> |
||
524 | <em class="help-text"> |
||
525 | {{{ field.help_text }}} |
||
526 | </em> |
||
527 | <# } #> |
||
528 | |||
529 | <em class="carbon-error"></em> |
||
530 | </div> |
||
531 | <# }) #> |
||
532 | </div> |
||
533 | </div> |
||
534 | <?php |
||
535 | } |
||
536 | |||
537 | /** |
||
538 | * The Underscore template for the group item tab. |
||
539 | */ |
||
540 | public function template_group_tab_item() { |
||
541 | ?> |
||
542 | <li class="group-tab-item" data-group-id="{{ id }}"> |
||
543 | <a href="#"> |
||
544 | <span class="group-handle"></span> |
||
545 | |||
546 | <# if (label_template || label) { #> |
||
547 | <span class="group-name">{{{ label_template || label }}}</span> |
||
548 | <# } #> |
||
549 | <span class="group-number">{{{ order + 1 }}}</span> |
||
550 | <span class="dashicons dashicons-warning carbon-complex-group-error-badge" ></span> |
||
551 | </a> |
||
552 | </li> |
||
553 | <?php |
||
554 | } |
||
555 | |||
556 | /** |
||
557 | * Modify the layout of this field. |
||
558 | * |
||
559 | * @param string $layout |
||
560 | */ |
||
561 | public function set_layout( $layout ) { |
||
592 | |||
593 | /** |
||
594 | * Set the minimum number of entries. |
||
595 | * |
||
596 | * @param int $min |
||
597 | */ |
||
598 | public function set_min( $min ) { |
||
602 | |||
603 | /** |
||
604 | * Get the minimum number of entries. |
||
605 | * |
||
606 | * @return int $min |
||
607 | */ |
||
608 | public function get_min() { |
||
611 | |||
612 | /** |
||
613 | * Set the maximum number of entries. |
||
614 | * |
||
615 | * @param int $max |
||
616 | */ |
||
617 | public function set_max( $max ) { |
||
621 | |||
622 | /** |
||
623 | * Get the maximum number of entries. |
||
624 | * |
||
625 | * @return int $max |
||
626 | */ |
||
627 | public function get_max() { |
||
630 | |||
631 | /** |
||
632 | * Change the groups initial collapse state. |
||
633 | * This state relates to the state of which the groups are rendered. |
||
634 | * |
||
635 | * @param bool $collapsed |
||
636 | */ |
||
637 | public function set_collapsed( $collapsed = true ) { |
||
638 | $this->collapsed = $collapsed; |
||
639 | |||
640 | return $this; |
||
641 | } |
||
642 | |||
643 | /** |
||
644 | * Retrieve the groups of this field. |
||
645 | * |
||
646 | * @return array |
||
647 | */ |
||
648 | public function get_group_names() { |
||
651 | |||
652 | /** |
||
653 | * Retrieve a group by its name. |
||
654 | * |
||
655 | * @param string $group_name Group name |
||
656 | * @return Group_Field $group_object Group object |
||
657 | */ |
||
658 | public function get_group_by_name( $group_name ) { |
||
669 | } |
||
670 |
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.