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 Sensei_Question 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 Sensei_Question, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
14 | class Sensei_Question { |
||
15 | public $token; |
||
16 | public $meta_fields; |
||
17 | |||
18 | /** |
||
19 | * Constructor. |
||
20 | * @since 1.0.0 |
||
21 | */ |
||
22 | public function __construct () { |
||
23 | $this->token = 'question'; |
||
24 | $this->question_types = $this->question_types(); |
||
25 | $this->meta_fields = array( 'question_right_answer', 'question_wrong_answers' ); |
||
26 | if ( is_admin() ) { |
||
27 | // Custom Write Panel Columns |
||
28 | add_filter( 'manage_edit-question_columns', array( $this, 'add_column_headings' ), 10, 1 ); |
||
29 | add_action( 'manage_posts_custom_column', array( $this, 'add_column_data' ), 10, 2 ); |
||
30 | add_action( 'add_meta_boxes', array( $this, 'question_edit_panel_metabox' ), 10, 2 ); |
||
31 | |||
32 | // Quesitno list table filters |
||
33 | add_action( 'restrict_manage_posts', array( $this, 'filter_options' ) ); |
||
34 | add_filter( 'request', array( $this, 'filter_actions' ) ); |
||
35 | |||
36 | add_action( 'save_post', array( $this, 'save_question' ), 10, 1 ); |
||
37 | } // End If Statement |
||
38 | } // End __construct() |
||
39 | |||
40 | public function question_types() { |
||
41 | $types = array( |
||
42 | 'multiple-choice' => __( 'Multiple Choice', 'woothemes-sensei' ), |
||
43 | 'boolean' => __( 'True/False', 'woothemes-sensei' ), |
||
44 | 'gap-fill' => __( 'Gap Fill', 'woothemes-sensei' ), |
||
45 | 'single-line' => __( 'Single Line', 'woothemes-sensei' ), |
||
46 | 'multi-line' => __( 'Multi Line', 'woothemes-sensei' ), |
||
47 | 'file-upload' => __( 'File Upload', 'woothemes-sensei' ), |
||
48 | ); |
||
49 | |||
50 | return apply_filters( 'sensei_question_types', $types ); |
||
51 | } |
||
52 | |||
53 | /** |
||
54 | * Add column headings to the "lesson" post list screen. |
||
55 | * @access public |
||
56 | * @since 1.3.0 |
||
57 | * @param array $defaults |
||
58 | * @return array $new_columns |
||
59 | */ |
||
60 | View Code Duplication | public function add_column_headings ( $defaults ) { |
|
61 | $new_columns['cb'] = '<input type="checkbox" />'; |
||
62 | $new_columns['title'] = _x( 'Question', 'column name', 'woothemes-sensei' ); |
||
63 | $new_columns['question-type'] = _x( 'Type', 'column name', 'woothemes-sensei' ); |
||
64 | $new_columns['question-category'] = _x( 'Categories', 'column name', 'woothemes-sensei' ); |
||
65 | if ( isset( $defaults['date'] ) ) { |
||
66 | $new_columns['date'] = $defaults['date']; |
||
67 | } |
||
68 | |||
69 | return $new_columns; |
||
70 | } // End add_column_headings() |
||
71 | |||
72 | /** |
||
73 | * Add data for our newly-added custom columns. |
||
74 | * @access public |
||
75 | * @since 1.3.0 |
||
76 | * @param string $column_name |
||
77 | * @param int $id |
||
78 | * @return void |
||
79 | */ |
||
80 | public function add_column_data ( $column_name, $id ) { |
||
81 | global $wpdb, $post; |
||
82 | |||
83 | switch ( $column_name ) { |
||
84 | |||
85 | case 'id': |
||
86 | echo $id; |
||
87 | break; |
||
88 | |||
89 | case 'question-type': |
||
90 | $question_type = strip_tags( get_the_term_list( $id, 'question-type', '', ', ', '' ) ); |
||
91 | $output = '—'; |
||
92 | if( isset( $this->question_types[ $question_type ] ) ) { |
||
93 | $output = $this->question_types[ $question_type ]; |
||
94 | } |
||
95 | echo $output; |
||
96 | break; |
||
97 | |||
98 | case 'question-category': |
||
99 | $output = strip_tags( get_the_term_list( $id, 'question-category', '', ', ', '' ) ); |
||
100 | if( ! $output ) { |
||
101 | $output = '—'; |
||
102 | } |
||
103 | echo $output; |
||
104 | break; |
||
105 | |||
106 | default: |
||
107 | break; |
||
108 | |||
109 | } |
||
110 | |||
111 | } // End add_column_data() |
||
112 | |||
113 | public function question_edit_panel_metabox( $post_type, $post ) { |
||
114 | if( in_array( $post_type, array( 'question', 'multiple_question' ) ) ) { |
||
115 | |||
116 | $metabox_title = __( 'Question', 'woothemes-sensei' ); |
||
117 | |||
118 | if( isset( $post->ID ) ) { |
||
119 | |||
120 | $question_type = Sensei()->question->get_question_type( $post->ID ); |
||
121 | |||
122 | if( $question_type ) { |
||
123 | $type = $this->question_types[ $question_type ]; |
||
124 | if( $type ) { |
||
125 | $metabox_title = $type; |
||
126 | } |
||
127 | } |
||
128 | } |
||
129 | add_meta_box( 'question-edit-panel', $metabox_title, array( $this, 'question_edit_panel' ), 'question', 'normal', 'high' ); |
||
130 | add_meta_box( 'question-lessons-panel', __( 'Quizzes', 'woothemes-sensei' ), array( $this, 'question_lessons_panel' ), 'question', 'side', 'default' ); |
||
131 | add_meta_box( 'multiple-question-lessons-panel', __( 'Quizzes', 'woothemes-sensei' ), array( $this, 'question_lessons_panel' ), 'multiple_question', 'side', 'default' ); |
||
132 | } |
||
133 | } |
||
134 | |||
135 | public function question_edit_panel() { |
||
136 | global $post, $pagenow; |
||
137 | |||
138 | add_action( 'admin_enqueue_scripts', array( Sensei()->lesson, 'enqueue_scripts' ) ); |
||
139 | add_action( 'admin_enqueue_scripts', array( Sensei()->lesson, 'enqueue_styles' ) ); |
||
140 | |||
141 | $html = '<div id="lesson-quiz" class="single-question"><div id="add-question-main">'; |
||
142 | |||
143 | if( 'post-new.php' == $pagenow ) { |
||
144 | |||
145 | $html .= '<div id="add-question-actions">'; |
||
146 | $html .= Sensei()->lesson->quiz_panel_add( 'question' ); |
||
147 | $html .= '</div>'; |
||
148 | |||
149 | } else { |
||
150 | $question_id = $post->ID; |
||
151 | |||
152 | $question_type = Sensei()->question->get_question_type( $post->ID ); |
||
153 | |||
154 | $html .= '<div id="add-question-metadata"><table class="widefat">'; |
||
155 | $html .= Sensei()->lesson->quiz_panel_question( $question_type, 0, $question_id, 'question' ); |
||
156 | $html .= '</table></div>'; |
||
157 | } |
||
158 | |||
159 | $html .= '</div></div>'; |
||
160 | |||
161 | echo $html; |
||
162 | } |
||
163 | |||
164 | public function question_lessons_panel() { |
||
165 | global $post; |
||
166 | |||
167 | $no_lessons = sprintf( __( '%1$sThis question does not appear in any quizzes yet.%2$s', 'woothemes-sensei' ), '<em>', '</em>' ); |
||
168 | |||
169 | if( ! isset( $post->ID ) ) { |
||
170 | echo $no_lessons; |
||
171 | return; |
||
172 | } |
||
173 | |||
174 | // This retrieves those quizzes the question is directly connected to. |
||
175 | $quizzes = get_post_meta( $post->ID, '_quiz_id', false ); |
||
176 | |||
177 | // Collate all 'multiple_question' quizzes the question is part of. |
||
178 | $categories_of_question = wp_get_post_terms( $post->ID, 'question-category', array( 'fields' => 'ids' ) ); |
||
179 | if ( ! empty( $categories_of_question ) ) { |
||
180 | foreach ( $categories_of_question as $term_id ) { |
||
181 | $qargs = array( |
||
182 | 'fields' => 'ids', |
||
183 | 'post_type' => 'multiple_question', |
||
184 | 'posts_per_page' => -1, |
||
185 | 'meta_query' => array( |
||
186 | array( |
||
187 | 'key' => 'category', |
||
188 | 'value' => $term_id, |
||
189 | ), |
||
190 | ), |
||
191 | 'post_status' => 'any', |
||
192 | 'suppress_filters' => 0, |
||
193 | ); |
||
194 | $cat_question_ids = get_posts( $qargs ); |
||
195 | foreach( $cat_question_ids as $cat_question_id ) { |
||
196 | $cat_quizzes = get_post_meta( $cat_question_id, '_quiz_id', false ); |
||
197 | $quizzes = array_merge( $quizzes, $cat_quizzes ); |
||
198 | } |
||
199 | } |
||
200 | $quizzes = array_unique( array_filter( $quizzes ) ); |
||
201 | } |
||
202 | |||
203 | if( 0 == count( $quizzes ) ) { |
||
204 | echo $no_lessons; |
||
205 | return; |
||
206 | } |
||
207 | |||
208 | $lessons = false; |
||
209 | |||
210 | foreach( $quizzes as $quiz ) { |
||
211 | |||
212 | $lesson_id = get_post_meta( $quiz, '_quiz_lesson', true ); |
||
213 | |||
214 | if( ! $lesson_id ) continue; |
||
215 | |||
216 | $lessons[ $lesson_id ]['title'] = get_the_title( $lesson_id ); |
||
217 | $lessons[ $lesson_id ]['link'] = admin_url( 'post.php?post=' . $lesson_id . '&action=edit' ); |
||
218 | } |
||
219 | |||
220 | if( ! $lessons ) { |
||
221 | echo $no_lessons; |
||
222 | return; |
||
223 | } |
||
224 | |||
225 | $html = '<ul>'; |
||
226 | |||
227 | foreach( $lessons as $id => $lesson ) { |
||
228 | $html .= '<li><a href="' . esc_url( $lesson['link'] ) . '">' . esc_html( $lesson['title'] ) . '</a></li>'; |
||
229 | } |
||
230 | |||
231 | $html .= '</ul>'; |
||
232 | |||
233 | echo $html; |
||
234 | |||
235 | } |
||
236 | |||
237 | public function save_question( $post_id = 0 ) { |
||
238 | |||
239 | if( ! isset( $_POST['post_type'] |
||
240 | ) || 'question' != $_POST['post_type'] ) { |
||
241 | return; |
||
242 | } |
||
243 | |||
244 | |||
245 | |||
246 | //setup the data for saving |
||
247 | $data = $_POST ; |
||
248 | $data['quiz_id'] = 0; |
||
249 | $data['question_id'] = $post_id; |
||
250 | |||
251 | if ( ! wp_is_post_revision( $post_id ) ){ |
||
252 | |||
253 | // Unhook function to prevent infinite loops |
||
254 | remove_action( 'save_post', array( $this, 'save_question' ) ); |
||
255 | |||
256 | // Update question data |
||
257 | $question_id = Sensei()->lesson->lesson_save_question( $data, 'question' ); |
||
258 | |||
259 | // Re-hook same function |
||
260 | add_action( 'save_post', array( $this, 'save_question' ) ); |
||
261 | } |
||
262 | |||
263 | return; |
||
264 | } |
||
265 | |||
266 | /** |
||
267 | * Add options to filter the questions list table |
||
268 | * @return void |
||
269 | */ |
||
270 | public function filter_options() { |
||
271 | global $typenow; |
||
272 | |||
273 | if( is_admin() && 'question' == $typenow ) { |
||
274 | |||
275 | $output = ''; |
||
276 | |||
277 | // Question type |
||
278 | $selected = isset( $_GET['question_type'] ) ? $_GET['question_type'] : ''; |
||
279 | $type_options = '<option value="">' . __( 'All types', 'woothemes-sensei' ) . '</option>'; |
||
280 | foreach( $this->question_types as $label => $type ) { |
||
281 | $type_options .= '<option value="' . esc_attr( $label ) . '" ' . selected( $selected, $label, false ) . '>' . esc_html( $type ) . '</option>'; |
||
282 | } |
||
283 | |||
284 | $output .= '<select name="question_type" id="dropdown_question_type">'; |
||
285 | $output .= $type_options; |
||
286 | $output .= '</select>'; |
||
287 | |||
288 | // Question category |
||
289 | $cats = get_terms( 'question-category', array( 'hide_empty' => false ) ); |
||
290 | if ( ! empty( $cats ) && ! is_wp_error( $cats ) ) { |
||
291 | $selected = isset( $_GET['question_cat'] ) ? $_GET['question_cat'] : ''; |
||
292 | $cat_options = '<option value="">' . __( 'All categories', 'woothemes-sensei' ) . '</option>'; |
||
293 | foreach( $cats as $cat ) { |
||
294 | $cat_options .= '<option value="' . esc_attr( $cat->slug ) . '" ' . selected( $selected, $cat->slug, false ) . '>' . esc_html( $cat->name ) . '</option>'; |
||
295 | } |
||
296 | |||
297 | $output .= '<select name="question_cat" id="dropdown_question_cat">'; |
||
298 | $output .= $cat_options; |
||
299 | $output .= '</select>'; |
||
300 | } |
||
301 | |||
302 | echo $output; |
||
303 | } |
||
304 | } |
||
305 | |||
306 | /** |
||
307 | * Filter questions list table |
||
308 | * @param array $request Current request |
||
309 | * @return array Modified request |
||
310 | */ |
||
311 | public function filter_actions( $request ) { |
||
312 | global $typenow; |
||
313 | |||
314 | if( is_admin() && 'question' == $typenow ) { |
||
315 | |||
316 | // Question type |
||
317 | $question_type = isset( $_GET['question_type'] ) ? $_GET['question_type'] : ''; |
||
318 | View Code Duplication | if( $question_type ) { |
|
319 | $type_query = array( |
||
320 | 'taxonomy' => 'question-type', |
||
321 | 'terms' => $question_type, |
||
322 | 'field' => 'slug', |
||
323 | ); |
||
324 | $request['tax_query'][] = $type_query; |
||
325 | } |
||
326 | |||
327 | // Question category |
||
328 | $question_cat = isset( $_GET['question_cat'] ) ? $_GET['question_cat'] : ''; |
||
329 | View Code Duplication | if( $question_cat ) { |
|
330 | $cat_query = array( |
||
331 | 'taxonomy' => 'question-category', |
||
332 | 'terms' => $question_cat, |
||
333 | 'field' => 'slug', |
||
334 | ); |
||
335 | $request['tax_query'][] = $cat_query; |
||
336 | } |
||
337 | } |
||
338 | |||
339 | return $request; |
||
340 | } |
||
341 | |||
342 | /** |
||
343 | * Get the type of question by id |
||
344 | * |
||
345 | * This function uses the post terms to determine which question type |
||
346 | * the passed question id belongs to. |
||
347 | * |
||
348 | * @since 1.7.4 |
||
349 | * |
||
350 | * @param int $question_id |
||
351 | * |
||
352 | * @return string $question_type | bool |
||
353 | */ |
||
354 | public function get_question_type( $question_id ){ |
||
355 | |||
356 | View Code Duplication | if( empty( $question_id ) || ! intval( $question_id ) > 0 |
|
357 | || 'question' != get_post_type( $question_id ) ){ |
||
358 | return false; |
||
359 | } |
||
360 | |||
361 | $question_type = 'multiple-choice'; |
||
362 | $question_types = wp_get_post_terms( $question_id, 'question-type' ); |
||
363 | foreach( $question_types as $type ) { |
||
364 | $question_type = $type->slug; |
||
365 | } |
||
366 | |||
367 | return $question_type; |
||
368 | |||
369 | }// end get_question_type |
||
370 | |||
371 | /** |
||
372 | * Given a question ID, return the grade that can be achieved. |
||
373 | * |
||
374 | * @since 1.9 |
||
375 | * |
||
376 | * @param int $question_id |
||
377 | * |
||
378 | * @return int $question_grade | bool |
||
379 | */ |
||
380 | public function get_question_grade( $question_id ) { |
||
381 | |||
382 | View Code Duplication | if ( empty( $question_id ) || ! intval( $question_id ) > 0 |
|
383 | || 'question' != get_post_type( $question_id ) ) { |
||
384 | return false; |
||
385 | } |
||
386 | |||
387 | $question_grade_raw = get_post_meta( $question_id, '_question_grade', true ); |
||
388 | // If not set then default to 1... |
||
389 | if ( false === $question_grade_raw || $question_grade_raw == '' ) { |
||
390 | $question_grade = 1; |
||
391 | } |
||
392 | // ...but allow a grade of 0 for non-marked questions |
||
393 | else { |
||
394 | $question_grade = intval( $question_grade_raw ); |
||
395 | } |
||
396 | return $question_grade; |
||
397 | |||
398 | } // end get_question_grade |
||
399 | |||
400 | |||
401 | /** |
||
402 | * This function simply loads the question type template |
||
403 | * |
||
404 | * @since 1.9.0 |
||
405 | * @param $question_type |
||
406 | */ |
||
407 | public static function load_question_template( $question_type ){ |
||
411 | |||
412 | /** |
||
413 | * Echo the sensei question title. |
||
414 | * |
||
415 | * @uses WooThemes_Sensei_Question::get_the_question_title |
||
416 | * |
||
417 | * @since 1.9.0 |
||
418 | * @param $question_id |
||
419 | */ |
||
420 | public static function the_question_title( $question_id ){ |
||
421 | |||
422 | echo self::get_the_question_title( $question_id ); |
||
423 | |||
424 | }// end the_question_title |
||
425 | |||
426 | /** |
||
427 | * Generate the question title with it's grade. |
||
428 | * |
||
429 | * @since 1.9.0 |
||
430 | * |
||
431 | * @param $question_id |
||
432 | * @return string |
||
433 | */ |
||
434 | public static function get_the_question_title( $question_id ){ |
||
456 | |||
457 | /** |
||
458 | * Tech the question description |
||
459 | * |
||
460 | * @param $question_id |
||
461 | * @return string |
||
462 | */ |
||
463 | public static function get_the_question_description( $question_id ){ |
||
464 | |||
465 | $question = get_post( $question_id ); |
||
466 | |||
467 | /** |
||
468 | * Already documented within WordPress Core |
||
469 | */ |
||
473 | |||
474 | /** |
||
475 | * Output the question description |
||
476 | * |
||
477 | * @since 1.9.0 |
||
478 | * @param $question_id |
||
479 | */ |
||
480 | public static function the_question_description( $question_id ){ |
||
485 | |||
486 | /** |
||
487 | * Get the questions media markup |
||
488 | * |
||
489 | * @since 1.9.0 |
||
490 | * @param $question_id |
||
491 | * @return string |
||
492 | */ |
||
493 | public static function get_the_question_media( $question_id ){ |
||
559 | |||
560 | |||
561 | /** |
||
562 | * Output the question media |
||
563 | * |
||
564 | * @since 1.9.0 |
||
565 | * @param string $question_id |
||
566 | */ |
||
567 | public static function the_question_media( $question_id ){ |
||
572 | |||
573 | /** |
||
574 | * Output a special field for the question needed for question submission. |
||
575 | * |
||
576 | * @since 1.9.0 |
||
577 | * |
||
578 | * @param $question_id |
||
579 | */ |
||
580 | public static function the_question_hidden_fields( $question_id ){ |
||
588 | |||
589 | /** |
||
590 | * This function can only be run withing the single quiz question loop |
||
591 | * |
||
592 | * @since 1.9.0 |
||
593 | * @param $question_id |
||
594 | */ |
||
595 | public static function answer_feedback_notes( $question_id ){ |
||
640 | |||
641 | /** |
||
642 | * This function has to be run inside the quiz question loop on the single quiz page. |
||
643 | * |
||
644 | * |
||
645 | * @since 1.9.0 |
||
646 | * @param string $question_id |
||
647 | */ |
||
648 | public static function the_answer_result_indication( $question_id ){ |
||
724 | |||
725 | /** |
||
726 | * Generate the question template data and return it as an array. |
||
727 | * |
||
728 | * @since 1.9.0 |
||
729 | * |
||
730 | * @param string $question_id |
||
731 | * @param $quiz_id |
||
732 | * @return array $question_data |
||
733 | */ |
||
734 | public static function get_template_data( $question_id, $quiz_id ){ |
||
783 | |||
784 | /** |
||
785 | * Load multiple choice question data on the sensei_get_question_template_data |
||
786 | * filter. |
||
787 | * |
||
788 | * @since 1.9.0 |
||
789 | * |
||
790 | * @param $question_data |
||
791 | * @param $question_id |
||
792 | * @param $quiz_id |
||
793 | * |
||
794 | * @return array() |
||
795 | */ |
||
796 | public static function file_upload_load_question_data ( $question_data, $question_id, $quiz_id ){ |
||
853 | |||
854 | /** |
||
855 | * Load multiple choice question data on the sensei_get_question_template_data |
||
856 | * filter. |
||
857 | * |
||
858 | * @since 1.9.0 |
||
859 | * |
||
860 | * @param $question_data |
||
861 | * @param $question_id |
||
862 | * @param $quiz_id |
||
863 | * |
||
864 | * @return array() |
||
865 | */ |
||
866 | public static function multiple_choice_load_question_data( $question_data, $question_id, $quiz_id ){ |
||
1032 | |||
1033 | /** |
||
1034 | * Load the gap fill question data on the sensei_get_question_template_data |
||
1035 | * filter. |
||
1036 | * |
||
1037 | * @since 1.9.0 |
||
1038 | * |
||
1039 | * @param $question_data |
||
1040 | * @param $question_id |
||
1041 | * @param $quiz_id |
||
1042 | * |
||
1043 | * @return array() |
||
1044 | */ |
||
1045 | public static function gap_fill_load_question_data( $question_data, $question_id, $quiz_id ){ |
||
1059 | |||
1060 | |||
1061 | /** |
||
1062 | * Get the correct answer for a question |
||
1063 | * |
||
1064 | * @param $question_id |
||
1065 | * @return string $correct_answer or empty |
||
1066 | */ |
||
1067 | public static function get_correct_answer( $question_id ){ |
||
1102 | |||
1103 | } // End Class |
||
1104 | |||
1111 |
The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.
The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.
To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.