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 Full_Sync 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 Full_Sync, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
23 | const STATUS_OPTION_PREFIX = 'jetpack_sync_full_'; |
||
24 | const FULL_SYNC_TIMEOUT = 3600; |
||
25 | |||
26 | public function name() { |
||
27 | return 'full-sync'; |
||
28 | } |
||
29 | |||
30 | function init_full_sync_listeners( $callable ) { |
||
31 | // synthetic actions for full sync |
||
32 | add_action( 'jetpack_full_sync_start', $callable, 10, 3 ); |
||
33 | add_action( 'jetpack_full_sync_end', $callable, 10, 2 ); |
||
34 | add_action( 'jetpack_full_sync_cancelled', $callable ); |
||
35 | } |
||
36 | |||
37 | function init_before_send() { |
||
38 | // this is triggered after actions have been processed on the server |
||
39 | add_action( 'jetpack_sync_processed_actions', array( $this, 'update_sent_progress_action' ) ); |
||
40 | } |
||
41 | |||
42 | function start( $module_configs = null ) { |
||
43 | $was_already_running = $this->is_started() && ! $this->is_finished(); |
||
44 | |||
45 | // remove all evidence of previous full sync items and status |
||
46 | $this->reset_data(); |
||
47 | |||
48 | if ( $was_already_running ) { |
||
49 | /** |
||
50 | * Fires when a full sync is cancelled. |
||
51 | * |
||
52 | * @since 4.2.0 |
||
53 | */ |
||
54 | do_action( 'jetpack_full_sync_cancelled' ); |
||
55 | } |
||
56 | |||
57 | $this->update_status_option( 'started', time() ); |
||
58 | $this->update_status_option( 'params', $module_configs ); |
||
59 | |||
60 | $enqueue_status = array(); |
||
61 | $full_sync_config = array(); |
||
62 | $include_empty = false; |
||
63 | $empty = array(); |
||
64 | // default value is full sync |
||
65 | if ( ! is_array( $module_configs ) ) { |
||
66 | $module_configs = array(); |
||
67 | $include_empty = true; |
||
68 | foreach ( Modules::get_modules() as $module ) { |
||
69 | $module_configs[ $module->name() ] = true; |
||
70 | } |
||
71 | } |
||
72 | |||
73 | // set default configuration, calculate totals, and save configuration if totals > 0 |
||
74 | foreach ( Modules::get_modules() as $module ) { |
||
75 | $module_name = $module->name(); |
||
76 | $module_config = isset( $module_configs[ $module_name ] ) ? $module_configs[ $module_name ] : false; |
||
77 | |||
78 | if ( ! $module_config ) { |
||
79 | continue; |
||
80 | } |
||
81 | |||
82 | if ( 'users' === $module_name && 'initial' === $module_config ) { |
||
83 | $module_config = $module->get_initial_sync_user_config(); |
||
84 | } |
||
85 | |||
86 | $enqueue_status[ $module_name ] = false; |
||
87 | |||
88 | $total_items = $module->estimate_full_sync_actions( $module_config ); |
||
89 | |||
90 | // if there's information to process, configure this module |
||
91 | if ( ! is_null( $total_items ) && $total_items > 0 ) { |
||
92 | $full_sync_config[ $module_name ] = $module_config; |
||
93 | $enqueue_status[ $module_name ] = array( |
||
94 | $total_items, // total |
||
95 | 0, // queued |
||
96 | false, // current state |
||
97 | ); |
||
98 | } elseif ( $include_empty && $total_items === 0 ) { |
||
99 | $empty[ $module_name ] = true; |
||
100 | } |
||
101 | } |
||
102 | |||
103 | $this->set_config( $full_sync_config ); |
||
104 | $this->set_enqueue_status( $enqueue_status ); |
||
105 | |||
106 | $range = $this->get_content_range( $full_sync_config ); |
||
107 | /** |
||
108 | * Fires when a full sync begins. This action is serialized |
||
109 | * and sent to the server so that it knows a full sync is coming. |
||
110 | * |
||
111 | * @since 4.2.0 |
||
112 | * @since 7.3.0 Added $range arg. |
||
113 | * @since 7.4.0 Added $empty arg. |
||
114 | * |
||
115 | * @param array $full_sync_config Sync configuration for all sync modules. |
||
116 | * @param array $range Range of the sync items, containing min and max IDs for some item types. |
||
117 | * @param array $empty The modules with no items to sync during a full sync. |
||
118 | */ |
||
119 | do_action( 'jetpack_full_sync_start', $full_sync_config, $range, $empty ); |
||
120 | |||
121 | $this->continue_enqueuing( $full_sync_config, $enqueue_status ); |
||
122 | |||
123 | return true; |
||
124 | } |
||
125 | |||
126 | function continue_enqueuing( $configs = null, $enqueue_status = null ) { |
||
127 | if ( ! $this->is_started() || $this->get_status_option( 'queue_finished' ) ) { |
||
128 | return; |
||
129 | } |
||
130 | |||
131 | // if full sync queue is full, don't enqueue more items |
||
132 | $max_queue_size_full_sync = Settings::get_setting( 'max_queue_size_full_sync' ); |
||
133 | $full_sync_queue = new Queue( 'full_sync' ); |
||
134 | |||
135 | $available_queue_slots = $max_queue_size_full_sync - $full_sync_queue->size(); |
||
136 | |||
137 | if ( $available_queue_slots <= 0 ) { |
||
138 | return; |
||
139 | } else { |
||
140 | $remaining_items_to_enqueue = min( Settings::get_setting( 'max_enqueue_full_sync' ), $available_queue_slots ); |
||
141 | } |
||
142 | |||
143 | if ( ! $configs ) { |
||
144 | $configs = $this->get_config(); |
||
145 | } |
||
146 | |||
147 | if ( ! $enqueue_status ) { |
||
148 | $enqueue_status = $this->get_enqueue_status(); |
||
149 | } |
||
150 | |||
151 | foreach ( Modules::get_modules() as $module ) { |
||
152 | $module_name = $module->name(); |
||
153 | |||
154 | // skip module if not configured for this sync or module is done |
||
155 | if ( ! isset( $configs[ $module_name ] ) |
||
156 | || // no module config |
||
157 | ! $configs[ $module_name ] |
||
158 | || // no enqueue status |
||
159 | ! $enqueue_status[ $module_name ] |
||
160 | || // finished enqueuing this module |
||
161 | true === $enqueue_status[ $module_name ][2] ) { |
||
162 | continue; |
||
163 | } |
||
164 | |||
165 | list( $items_enqueued, $next_enqueue_state ) = $module->enqueue_full_sync_actions( $configs[ $module_name ], $remaining_items_to_enqueue, $enqueue_status[ $module_name ][2] ); |
||
166 | |||
167 | $enqueue_status[ $module_name ][2] = $next_enqueue_state; |
||
168 | |||
169 | // if items were processed, subtract them from the limit |
||
170 | if ( ! is_null( $items_enqueued ) && $items_enqueued > 0 ) { |
||
171 | $enqueue_status[ $module_name ][1] += $items_enqueued; |
||
172 | $remaining_items_to_enqueue -= $items_enqueued; |
||
173 | } |
||
174 | |||
175 | // stop processing if we've reached our limit of items to enqueue |
||
176 | if ( 0 >= $remaining_items_to_enqueue ) { |
||
177 | $this->set_enqueue_status( $enqueue_status ); |
||
178 | return; |
||
179 | } |
||
180 | } |
||
181 | |||
182 | $this->set_enqueue_status( $enqueue_status ); |
||
183 | |||
184 | // setting autoload to true means that it's faster to check whether we should continue enqueuing |
||
185 | $this->update_status_option( 'queue_finished', time(), true ); |
||
186 | |||
187 | $range = $this->get_content_range( $configs ); |
||
188 | |||
189 | /** |
||
190 | * Fires when a full sync ends. This action is serialized |
||
191 | * and sent to the server. |
||
192 | * |
||
193 | * @since 4.2.0 |
||
194 | * @since 7.3.0 Added $range arg. |
||
195 | * |
||
196 | * @param string $checksum Deprecated since 7.3.0 - @see https://github.com/Automattic/jetpack/pull/11945/ |
||
197 | * @param array $range Range of the sync items, containing min and max IDs for some item types. |
||
198 | */ |
||
199 | do_action( 'jetpack_full_sync_end', '', $range ); |
||
200 | } |
||
201 | |||
202 | function get_range( $type ) { |
||
203 | global $wpdb; |
||
204 | if ( ! in_array( $type, array( 'comments', 'posts' ) ) ) { |
||
205 | return array(); |
||
206 | } |
||
207 | |||
208 | switch ( $type ) { |
||
209 | case 'posts': |
||
210 | $table = $wpdb->posts; |
||
211 | $id = 'ID'; |
||
212 | $where_sql = Settings::get_blacklisted_post_types_sql(); |
||
213 | |||
214 | break; |
||
215 | case 'comments': |
||
216 | $table = $wpdb->comments; |
||
217 | $id = 'comment_ID'; |
||
218 | $where_sql = Settings::get_comments_filter_sql(); |
||
219 | break; |
||
220 | } |
||
221 | $results = $wpdb->get_results( "SELECT MAX({$id}) as max, MIN({$id}) as min, COUNT({$id}) as count FROM {$table} WHERE {$where_sql}" ); |
||
222 | if ( isset( $results[0] ) ) { |
||
223 | return $results[0]; |
||
224 | } |
||
225 | |||
226 | return array(); |
||
227 | } |
||
228 | |||
229 | private function get_content_range( $config ) { |
||
230 | $range = array(); |
||
231 | // Only when we are sending the whole range do we want to send also the range |
||
232 | if ( isset( $config['posts'] ) && $config['posts'] === true ) { |
||
233 | $range['posts'] = $this->get_range( 'posts' ); |
||
234 | } |
||
235 | |||
236 | if ( isset( $config['comments'] ) && $config['comments'] === true ) { |
||
237 | $range['comments'] = $this->get_range( 'comments' ); |
||
238 | } |
||
239 | return $range; |
||
240 | } |
||
241 | |||
242 | function update_sent_progress_action( $actions ) { |
||
243 | // quick way to map to first items with an array of arrays |
||
244 | $actions_with_counts = array_count_values( array_filter( array_map( array( $this, 'get_action_name' ), $actions ) ) ); |
||
245 | |||
246 | // Total item counts for each action. |
||
247 | $actions_with_total_counts = $this->get_actions_totals( $actions ); |
||
248 | |||
249 | if ( ! $this->is_started() || $this->is_finished() ) { |
||
250 | return; |
||
251 | } |
||
252 | |||
253 | if ( isset( $actions_with_counts['jetpack_full_sync_start'] ) ) { |
||
254 | $this->update_status_option( 'send_started', time() ); |
||
255 | } |
||
256 | |||
257 | foreach ( Modules::get_modules() as $module ) { |
||
258 | $module_actions = $module->get_full_sync_actions(); |
||
259 | $status_option_name = "{$module->name()}_sent"; |
||
260 | $total_option_name = "{$status_option_name}_total"; |
||
261 | $items_sent = $this->get_status_option( $status_option_name, 0 ); |
||
262 | $items_sent_total = $this->get_status_option( $total_option_name, 0 ); |
||
263 | |||
264 | foreach ( $module_actions as $module_action ) { |
||
265 | if ( isset( $actions_with_counts[ $module_action ] ) ) { |
||
266 | $items_sent += $actions_with_counts[ $module_action ]; |
||
267 | } |
||
268 | |||
269 | if ( ! empty( $actions_with_total_counts[ $module_action ] ) ) { |
||
270 | $items_sent_total += $actions_with_total_counts[ $module_action ]; |
||
271 | } |
||
272 | } |
||
273 | |||
274 | if ( $items_sent > 0 ) { |
||
275 | $this->update_status_option( $status_option_name, $items_sent ); |
||
276 | } |
||
277 | |||
278 | if ( 0 !== $items_sent_total ) { |
||
279 | $this->update_status_option( $total_option_name, $items_sent_total ); |
||
280 | } |
||
281 | } |
||
282 | |||
283 | if ( isset( $actions_with_counts['jetpack_full_sync_end'] ) ) { |
||
284 | $this->update_status_option( 'finished', time() ); |
||
285 | } |
||
286 | } |
||
287 | |||
288 | public function get_action_name( $queue_item ) { |
||
289 | if ( is_array( $queue_item ) && isset( $queue_item[0] ) ) { |
||
290 | return $queue_item[0]; |
||
291 | } |
||
292 | return false; |
||
293 | } |
||
294 | |||
295 | /** |
||
296 | * Retrieve the total number of items we're syncing in a particular queue item (action). |
||
297 | * `$queue_item[1]` is expected to contain chunks of items, and `$queue_item[1][0]` |
||
298 | * represents the first (and only) chunk of items to sync in that action. |
||
299 | * |
||
300 | * @param array $queue_item Item of the sync queue that corresponds to a particular action. |
||
301 | * @return int Total number of items in the action. |
||
302 | */ |
||
303 | public function get_action_totals( $queue_item ) { |
||
304 | if ( is_array( $queue_item ) && isset( $queue_item[1][0] ) ) { |
||
305 | if ( is_array( $queue_item[1][0] ) ) { |
||
306 | // Let's count the items we sync in this action. |
||
307 | return count( $queue_item[1][0] ); |
||
308 | } |
||
309 | // -1 indicates that this action syncs all items by design. |
||
310 | return -1; |
||
311 | } |
||
312 | return 0; |
||
313 | } |
||
314 | |||
315 | /** |
||
316 | * Retrieve the total number of items for a set of actions, grouped by action name. |
||
317 | * |
||
318 | * @param array $actions An array of actions. |
||
319 | * @return array An array, representing the total number of items, grouped per action. |
||
320 | */ |
||
321 | public function get_actions_totals( $actions ) { |
||
322 | $totals = array(); |
||
323 | |||
324 | foreach ( $actions as $action ) { |
||
325 | $name = $this->get_action_name( $action ); |
||
326 | $action_totals = $this->get_action_totals( $action ); |
||
327 | if ( ! isset( $totals[ $name ] ) ) { |
||
328 | $totals[ $name ] = 0; |
||
329 | } |
||
330 | $totals[ $name ] += $action_totals; |
||
331 | } |
||
332 | |||
333 | return $totals; |
||
334 | } |
||
335 | |||
336 | public function is_started() { |
||
337 | return ! ! $this->get_status_option( 'started' ); |
||
338 | } |
||
339 | |||
340 | public function is_finished() { |
||
341 | return ! ! $this->get_status_option( 'finished' ); |
||
342 | } |
||
343 | |||
344 | public function get_status() { |
||
345 | $status = array( |
||
346 | 'started' => $this->get_status_option( 'started' ), |
||
347 | 'queue_finished' => $this->get_status_option( 'queue_finished' ), |
||
348 | 'send_started' => $this->get_status_option( 'send_started' ), |
||
349 | 'finished' => $this->get_status_option( 'finished' ), |
||
350 | 'sent' => array(), |
||
351 | 'sent_total' => array(), |
||
352 | 'queue' => array(), |
||
353 | 'config' => $this->get_status_option( 'params' ), |
||
354 | 'total' => array(), |
||
355 | ); |
||
356 | |||
357 | $enqueue_status = $this->get_enqueue_status(); |
||
358 | |||
359 | foreach ( Modules::get_modules() as $module ) { |
||
360 | $name = $module->name(); |
||
361 | |||
362 | if ( ! isset( $enqueue_status[ $name ] ) ) { |
||
363 | continue; |
||
364 | } |
||
365 | |||
366 | list( $total, $queued, $state ) = $enqueue_status[ $name ]; |
||
367 | |||
368 | if ( $total ) { |
||
369 | $status['total'][ $name ] = $total; |
||
370 | } |
||
371 | |||
372 | if ( $queued ) { |
||
373 | $status['queue'][ $name ] = $queued; |
||
374 | } |
||
375 | |||
376 | if ( $sent = $this->get_status_option( "{$name}_sent" ) ) { |
||
377 | $status['sent'][ $name ] = $sent; |
||
378 | } |
||
379 | |||
380 | $sent_total = $this->get_status_option( "{$name}_sent_total" ); |
||
381 | if ( $sent_total ) { |
||
382 | $status['sent_total'][ $name ] = $sent_total; |
||
383 | } |
||
384 | } |
||
385 | |||
386 | return $status; |
||
387 | } |
||
388 | |||
389 | public function clear_status() { |
||
390 | $prefix = self::STATUS_OPTION_PREFIX; |
||
391 | \Jetpack_Options::delete_raw_option( "{$prefix}_started" ); |
||
392 | \Jetpack_Options::delete_raw_option( "{$prefix}_params" ); |
||
393 | \Jetpack_Options::delete_raw_option( "{$prefix}_queue_finished" ); |
||
394 | \Jetpack_Options::delete_raw_option( "{$prefix}_send_started" ); |
||
395 | \Jetpack_Options::delete_raw_option( "{$prefix}_finished" ); |
||
396 | |||
397 | $this->delete_enqueue_status(); |
||
398 | |||
399 | foreach ( Modules::get_modules() as $module ) { |
||
400 | \Jetpack_Options::delete_raw_option( "{$prefix}_{$module->name()}_sent" ); |
||
401 | \Jetpack_Options::delete_raw_option( "{$prefix}_{$module->name()}_sent_total" ); |
||
402 | } |
||
403 | } |
||
404 | |||
405 | public function reset_data() { |
||
406 | $this->clear_status(); |
||
407 | $this->delete_config(); |
||
408 | |||
409 | $listener = Listener::get_instance(); |
||
410 | $listener->get_full_sync_queue()->reset(); |
||
411 | } |
||
412 | |||
413 | private function get_status_option( $name, $default = null ) { |
||
414 | $value = \Jetpack_Options::get_raw_option( self::STATUS_OPTION_PREFIX . "_$name", $default ); |
||
415 | |||
416 | return is_numeric( $value ) ? intval( $value ) : $value; |
||
417 | } |
||
418 | |||
419 | private function update_status_option( $name, $value, $autoload = false ) { |
||
420 | \Jetpack_Options::update_raw_option( self::STATUS_OPTION_PREFIX . "_$name", $value, $autoload ); |
||
421 | } |
||
422 | |||
423 | private function set_enqueue_status( $new_status ) { |
||
424 | \Jetpack_Options::update_raw_option( 'jetpack_sync_full_enqueue_status', $new_status ); |
||
425 | } |
||
426 | |||
427 | private function delete_enqueue_status() { |
||
428 | return \Jetpack_Options::delete_raw_option( 'jetpack_sync_full_enqueue_status' ); |
||
429 | } |
||
430 | |||
431 | private function get_enqueue_status() { |
||
432 | return \Jetpack_Options::get_raw_option( 'jetpack_sync_full_enqueue_status' ); |
||
433 | } |
||
434 | |||
435 | private function set_config( $config ) { |
||
436 | \Jetpack_Options::update_raw_option( 'jetpack_sync_full_config', $config ); |
||
437 | } |
||
438 | |||
439 | private function delete_config() { |
||
440 | return \Jetpack_Options::delete_raw_option( 'jetpack_sync_full_config' ); |
||
441 | } |
||
442 | |||
443 | private function get_config() { |
||
444 | return \Jetpack_Options::get_raw_option( 'jetpack_sync_full_config' ); |
||
445 | } |
||
446 | |||
447 | private function write_option( $name, $value ) { |
||
448 | // we write our own option updating code to bypass filters/caching/etc on set_option/get_option |
||
449 | global $wpdb; |
||
450 | $serialized_value = maybe_serialize( $value ); |
||
451 | // try updating, if no update then insert |
||
452 | // TODO: try to deal with the fact that unchanged values can return updated_num = 0 |
||
453 | // below we used "insert ignore" to at least suppress the resulting error |
||
454 | $updated_num = $wpdb->query( |
||
455 | $wpdb->prepare( |
||
456 | "UPDATE $wpdb->options SET option_value = %s WHERE option_name = %s", |
||
457 | $serialized_value, |
||
458 | $name |
||
459 | ) |
||
460 | ); |
||
461 | |||
462 | if ( ! $updated_num ) { |
||
463 | $updated_num = $wpdb->query( |
||
464 | $wpdb->prepare( |
||
465 | "INSERT IGNORE INTO $wpdb->options ( option_name, option_value, autoload ) VALUES ( %s, %s, 'no' )", |
||
466 | $name, |
||
467 | $serialized_value |
||
468 | ) |
||
469 | ); |
||
470 | } |
||
471 | return $updated_num; |
||
472 | } |
||
473 | |||
474 | private function read_option( $name, $default = null ) { |
||
475 | global $wpdb; |
||
476 | $value = $wpdb->get_var( |
||
477 | $wpdb->prepare( |
||
478 | "SELECT option_value FROM $wpdb->options WHERE option_name = %s LIMIT 1", |
||
479 | $name |
||
480 | ) |
||
481 | ); |
||
482 | $value = maybe_unserialize( $value ); |
||
483 | |||
484 | if ( $value === null && $default !== null ) { |
||
485 | return $default; |
||
486 | } |
||
487 | |||
488 | return $value; |
||
489 | } |
||
490 | } |
||
491 |