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 WC_WCCOM_Site_Installer 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 WC_WCCOM_Site_Installer, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
16 | class WC_WCCOM_Site_Installer { |
||
17 | |||
18 | /** |
||
19 | * Default state. |
||
20 | * |
||
21 | * @var array |
||
22 | */ |
||
23 | private static $default_state = array( |
||
24 | 'status' => 'idle', |
||
25 | 'steps' => array(), |
||
26 | 'current_step' => null, |
||
27 | ); |
||
28 | |||
29 | /** |
||
30 | * Represents product step state. |
||
31 | * |
||
32 | * @var array |
||
33 | */ |
||
34 | private static $default_step_state = array( |
||
35 | 'download_url' => '', |
||
36 | 'product_type' => '', |
||
37 | 'last_step' => '', |
||
38 | 'last_error' => '', |
||
39 | 'download_path' => '', |
||
40 | 'unpacked_path' => '', |
||
41 | 'installed_path' => '', |
||
42 | 'activate' => false, |
||
43 | ); |
||
44 | |||
45 | /** |
||
46 | * Product install steps. Each step is a method name in this class that |
||
47 | * will be passed with product ID arg \WP_Upgrader instance. |
||
48 | * |
||
49 | * @var array |
||
50 | */ |
||
51 | private static $install_steps = array( |
||
52 | 'get_product_info', |
||
53 | 'download_product', |
||
54 | 'unpack_product', |
||
55 | 'move_product', |
||
56 | 'activate_product', |
||
57 | ); |
||
58 | |||
59 | /** |
||
60 | * Get the product install state. |
||
61 | * |
||
62 | * @since 3.7.0 |
||
63 | * @param string $key Key in state data. If empty key is passed array of |
||
64 | * state will be returned. |
||
65 | * @return array Product install state. |
||
66 | */ |
||
67 | public static function get_state( $key = '' ) { |
||
68 | $state = WC_Helper_Options::get( 'product_install', self::$default_state ); |
||
69 | if ( ! empty( $key ) ) { |
||
70 | return isset( $state[ $key ] ) ? $state[ $key ] : null; |
||
71 | } |
||
72 | |||
73 | return $state; |
||
74 | } |
||
75 | |||
76 | /** |
||
77 | * Update the product install state. |
||
78 | * |
||
79 | * @since 3.7.0 |
||
80 | * @param string $key Key in state data. |
||
81 | * @param mixed $value Value. |
||
82 | */ |
||
83 | public static function update_state( $key, $value ) { |
||
89 | |||
90 | /** |
||
91 | * Reset product install state. |
||
92 | * |
||
93 | * @since 3.7.0 |
||
94 | * @param array $products List of product IDs. |
||
95 | */ |
||
96 | public static function reset_state( $products = array() ) { |
||
|
|||
97 | WC()->queue()->cancel_all( 'woocommerce_wccom_install_products' ); |
||
98 | WC_Helper_Options::update( 'product_install', self::$default_state ); |
||
99 | } |
||
100 | |||
101 | /** |
||
102 | * Schedule installing given list of products. |
||
103 | * |
||
104 | * @since 3.7.0 |
||
105 | * @param array $products Array of products where key is product ID and |
||
106 | * element is install args. |
||
107 | * @return array State. |
||
108 | */ |
||
109 | public static function schedule_install( $products ) { |
||
110 | $state = self::get_state(); |
||
111 | $status = ! empty( $state['status'] ) ? $state['status'] : ''; |
||
112 | if ( 'in-progress' === $status ) { |
||
113 | return $state; |
||
114 | } |
||
115 | self::update_state( 'status', 'in-progress' ); |
||
116 | |||
117 | $steps = array_fill_keys( array_keys( $products ), self::$default_step_state ); |
||
118 | self::update_state( 'steps', $steps ); |
||
119 | |||
120 | self::update_state( 'current_step', null ); |
||
121 | |||
122 | $args = array( |
||
123 | 'products' => $products, |
||
124 | ); |
||
125 | |||
126 | WC()->queue()->cancel_all( 'woocommerce_wccom_install_products', $args ); |
||
127 | WC()->queue()->add( 'woocommerce_wccom_install_products', $args ); |
||
128 | |||
129 | return self::get_state(); |
||
130 | } |
||
131 | |||
132 | /** |
||
133 | * Install a given product IDs. |
||
134 | * |
||
135 | * Run via `woocommerce_wccom_install_products` hook. |
||
136 | * |
||
137 | * @since 3.7.0 |
||
138 | * @param array $products Array of products where key is product ID and |
||
139 | * element is install args. |
||
140 | */ |
||
141 | public static function install( $products ) { |
||
158 | |||
159 | /** |
||
160 | * Finish installation by updating the state. |
||
161 | * |
||
162 | * @since 3.7.0 |
||
163 | */ |
||
164 | private static function finish_installation() { |
||
165 | $state = self::get_state(); |
||
166 | if ( empty( $state['steps'] ) ) { |
||
167 | return; |
||
168 | } |
||
169 | |||
170 | foreach ( $state['steps'] as $step ) { |
||
171 | if ( ! empty( $step['last_error'] ) ) { |
||
172 | $state['status'] = 'has_error'; |
||
173 | break; |
||
174 | } |
||
175 | } |
||
176 | |||
177 | if ( 'has_error' !== $state['status'] ) { |
||
178 | $state['status'] = 'finished'; |
||
179 | } |
||
180 | |||
181 | WC_Helper_Options::update( 'product_install', $state ); |
||
182 | } |
||
183 | |||
184 | /** |
||
185 | * Install a single product given its ID. |
||
186 | * |
||
187 | * @since 3.7.0 |
||
188 | * @param int $product_id Product ID. |
||
189 | * @param array $install_args Install args. |
||
190 | * @param \WP_Upgrader $upgrader Core class to handle installation. |
||
191 | */ |
||
192 | private static function install_product( $product_id, $install_args, $upgrader ) { |
||
193 | foreach ( self::$install_steps as $step ) { |
||
194 | self::do_install_step( $product_id, $install_args, $step, $upgrader ); |
||
195 | } |
||
196 | } |
||
197 | |||
198 | /** |
||
199 | * Perform product installation step. |
||
200 | * |
||
201 | * @since 3.7.0 |
||
202 | * @param int $product_id Product ID. |
||
203 | * @param array $install_args Install args. |
||
204 | * @param string $step Installation step. |
||
205 | * @param \WP_Upgrader $upgrader Core class to handle installation. |
||
206 | */ |
||
207 | private static function do_install_step( $product_id, $install_args, $step, $upgrader ) { |
||
208 | $state_steps = self::get_state( 'steps' ); |
||
209 | if ( empty( $state_steps[ $product_id ] ) ) { |
||
210 | $state_steps[ $product_id ] = self::$default_step_state; |
||
211 | } |
||
212 | |||
213 | if ( ! empty( $state_steps[ $product_id ]['last_error'] ) ) { |
||
214 | return; |
||
215 | } |
||
216 | |||
217 | $state_steps[ $product_id ]['last_step'] = $step; |
||
218 | |||
219 | if ( ! empty( $install_args['activate'] ) ) { |
||
220 | $state_steps[ $product_id ]['activate'] = true; |
||
221 | } |
||
222 | |||
223 | self::update_state( |
||
224 | 'current_step', |
||
225 | array( |
||
226 | 'product_id' => $product_id, |
||
227 | 'step' => $step, |
||
228 | ) |
||
229 | ); |
||
230 | |||
231 | $result = call_user_func( array( __CLASS__, $step ), $product_id, $upgrader ); |
||
232 | if ( is_wp_error( $result ) ) { |
||
233 | $state_steps[ $product_id ]['last_error'] = $result->get_error_message(); |
||
234 | } else { |
||
235 | switch ( $step ) { |
||
236 | case 'get_product_info': |
||
237 | $state_steps[ $product_id ]['download_url'] = $result['download_url']; |
||
238 | $state_steps[ $product_id ]['product_type'] = $result['product_type']; |
||
239 | break; |
||
240 | case 'download_product': |
||
241 | $state_steps[ $product_id ]['download_path'] = $result; |
||
242 | break; |
||
243 | case 'unpack_product': |
||
244 | $state_steps[ $product_id ]['unpacked_path'] = $result; |
||
245 | break; |
||
246 | case 'move_product': |
||
247 | $state_steps[ $product_id ]['installed_path'] = $result['destination']; |
||
248 | break; |
||
249 | } |
||
250 | } |
||
251 | |||
252 | self::update_state( 'steps', $state_steps ); |
||
253 | } |
||
254 | |||
255 | /** |
||
256 | * Get product info from its ID. |
||
257 | * |
||
258 | * @since 3.7.0 |
||
259 | * @param int $product_id Product ID. |
||
260 | * @return bool|\WP_Error |
||
261 | */ |
||
262 | private static function get_product_info( $product_id ) { |
||
263 | $product_info = array( |
||
264 | 'download_url' => '', |
||
265 | 'product_type' => '', |
||
266 | ); |
||
267 | |||
268 | // Get product info from woocommerce.com. |
||
269 | $request = WC_Helper_API::get( |
||
270 | add_query_arg( |
||
271 | array( 'product_id' => absint( $product_id ) ), |
||
272 | 'info' |
||
273 | ), |
||
274 | array( |
||
275 | 'authenticated' => true, |
||
276 | ) |
||
277 | ); |
||
278 | |||
279 | if ( 200 !== wp_remote_retrieve_response_code( $request ) ) { |
||
280 | return new WP_Error( 'product_info_failed', __( 'Failed to retrieve product info from woocommerce.com', 'woocommerce' ) ); |
||
281 | } |
||
282 | |||
283 | $result = json_decode( wp_remote_retrieve_body( $request ), true ); |
||
284 | |||
285 | $product_info['product_type'] = $result['_product_type']; |
||
286 | |||
287 | if ( ! empty( $result['_wporg_product'] ) && ! empty( $result['download_link'] ) ) { |
||
288 | // For wporg product, download is set already from info response. |
||
289 | $product_info['download_url'] = $result['download_link']; |
||
290 | } elseif ( ! WC_Helper::has_product_subscription( $product_id ) ) { |
||
291 | // Non-wporg product needs subscription. |
||
292 | return new WP_Error( 'missing_subscription', __( 'Missing product subscription', 'woocommerce' ) ); |
||
293 | } else { |
||
294 | // Retrieve download URL for non-wporg product. |
||
295 | WC_Helper_Updater::flush_updates_cache(); |
||
296 | $updates = WC_Helper_Updater::get_update_data(); |
||
297 | if ( empty( $updates[ $product_id ]['package'] ) ) { |
||
298 | return new WP_Error( 'missing_product_package', __( 'Could not find product package.', 'woocommerce' ) ); |
||
299 | } |
||
300 | |||
301 | $product_info['download_url'] = $updates[ $product_id ]['package']; |
||
302 | } |
||
303 | |||
304 | return $product_info; |
||
305 | } |
||
306 | |||
307 | /** |
||
308 | * Download product by its ID and returns the path of the zip package. |
||
309 | * |
||
310 | * @since 3.7.0 |
||
311 | * @param int $product_id Product ID. |
||
312 | * @param \WP_Upgrader $upgrader Core class to handle installation. |
||
313 | * @return \WP_Error|string |
||
314 | */ |
||
315 | View Code Duplication | private static function download_product( $product_id, $upgrader ) { |
|
316 | $steps = self::get_state( 'steps' ); |
||
317 | if ( empty( $steps[ $product_id ]['download_url'] ) ) { |
||
318 | return new WP_Error( 'missing_download_url', __( 'Could not find download url for the product.', 'woocommerce' ) ); |
||
319 | } |
||
320 | return $upgrader->download_package( $steps[ $product_id ]['download_url'] ); |
||
321 | } |
||
322 | |||
323 | /** |
||
324 | * Unpack downloaded product. |
||
325 | * |
||
326 | * @since 3.7.0 |
||
327 | * @param int $product_id Product ID. |
||
328 | * @param \WP_Upgrader $upgrader Core class to handle installation. |
||
329 | * @return \WP_Error|string |
||
330 | */ |
||
331 | View Code Duplication | private static function unpack_product( $product_id, $upgrader ) { |
|
332 | $steps = self::get_state( 'steps' ); |
||
333 | if ( empty( $steps[ $product_id ]['download_path'] ) ) { |
||
334 | return new WP_Error( 'missing_download_path', __( 'Could not find download path.', 'woocommerce' ) ); |
||
335 | } |
||
336 | |||
337 | return $upgrader->unpack_package( $steps[ $product_id ]['download_path'], true ); |
||
338 | } |
||
339 | |||
340 | /** |
||
341 | * Move product to plugins directory. |
||
342 | * |
||
343 | * @since 3.7.0 |
||
344 | * @param int $product_id Product ID. |
||
345 | * @param \WP_Upgrader $upgrader Core class to handle installation. |
||
346 | * @return array|\WP_Error |
||
347 | */ |
||
348 | private static function move_product( $product_id, $upgrader ) { |
||
349 | $steps = self::get_state( 'steps' ); |
||
350 | if ( empty( $steps[ $product_id ]['unpacked_path'] ) ) { |
||
351 | return new WP_Error( 'missing_unpacked_path', __( 'Could not find unpacked path.', 'woocommerce' ) ); |
||
352 | } |
||
353 | |||
354 | $destination = 'plugin' === $steps[ $product_id ]['product_type'] |
||
355 | ? WP_PLUGIN_DIR |
||
356 | : get_theme_root(); |
||
357 | |||
358 | $package = array( |
||
359 | 'source' => $steps[ $product_id ]['unpacked_path'], |
||
360 | 'destination' => $destination, |
||
361 | 'clear_working' => true, |
||
362 | 'hook_extra' => array( |
||
363 | 'type' => $steps[ $product_id ]['product_type'], |
||
364 | 'action' => 'install', |
||
365 | ), |
||
366 | ); |
||
367 | |||
368 | return $upgrader->install_package( $package ); |
||
369 | } |
||
370 | |||
371 | /** |
||
372 | * Activate product given its product ID. |
||
373 | * |
||
374 | * @since 3.7.0 |
||
375 | * @param int $product_id Product ID. |
||
376 | * @return \WP_Error|null |
||
377 | */ |
||
378 | private static function activate_product( $product_id ) { |
||
379 | $steps = self::get_state( 'steps' ); |
||
380 | if ( ! $steps[ $product_id ]['activate'] ) { |
||
381 | return null; |
||
382 | } |
||
383 | |||
384 | if ( 'plugin' === $steps[ $product_id ]['product_type'] ) { |
||
385 | return self::activate_plugin( $product_id ); |
||
386 | } |
||
387 | return self::activate_theme( $product_id ); |
||
388 | } |
||
389 | |||
390 | /** |
||
391 | * Activate plugin given its product ID. |
||
392 | * |
||
393 | * @since 3.7.0 |
||
394 | * @param int $product_id Product ID. |
||
395 | * @return \WP_Error|null |
||
396 | */ |
||
397 | View Code Duplication | private static function activate_plugin( $product_id ) { |
|
398 | // Clear plugins cache used in `WC_Helper::get_local_woo_plugins`. |
||
399 | wp_clean_plugins_cache(); |
||
400 | $filename = false; |
||
401 | |||
402 | // If product is WP.org one, find out its filename. |
||
403 | $dir_name = self::get_wporg_product_dir_name( $product_id ); |
||
404 | if ( false !== $dir_name ) { |
||
405 | $filename = self::get_wporg_plugin_main_file( $dir_name ); |
||
406 | } |
||
407 | |||
408 | if ( false === $filename ) { |
||
409 | $plugins = wp_list_filter( |
||
410 | WC_Helper::get_local_woo_plugins(), |
||
411 | array( |
||
412 | '_product_id' => $product_id, |
||
413 | ) |
||
414 | ); |
||
415 | |||
416 | $filename = is_array( $plugins ) && ! empty( $plugins ) ? key( $plugins ) : ''; |
||
417 | } |
||
418 | |||
419 | if ( empty( $filename ) ) { |
||
420 | return new WP_Error( 'unknown_filename', __( 'Unknown product filename.', 'woocommerce' ) ); |
||
421 | } |
||
422 | |||
423 | return activate_plugin( $filename ); |
||
424 | } |
||
425 | |||
426 | /** |
||
427 | * Activate theme given its product ID. |
||
428 | * |
||
429 | * @since 3.7.0 |
||
430 | * @param int $product_id Product ID. |
||
431 | * @return \WP_Error|null |
||
432 | */ |
||
433 | View Code Duplication | private static function activate_theme( $product_id ) { |
|
434 | // Clear plugins cache used in `WC_Helper::get_local_woo_themes`. |
||
435 | wp_clean_themes_cache(); |
||
436 | $theme_slug = false; |
||
437 | |||
438 | // If product is WP.org theme, find out its slug. |
||
439 | $dir_name = self::get_wporg_product_dir_name( $product_id ); |
||
440 | if ( false !== $dir_name ) { |
||
441 | $theme_slug = basename( $dir_name ); |
||
442 | } |
||
443 | |||
444 | if ( false === $theme_slug ) { |
||
445 | $themes = wp_list_filter( |
||
446 | WC_Helper::get_local_woo_themes(), |
||
447 | array( |
||
448 | '_product_id' => $product_id, |
||
449 | ) |
||
450 | ); |
||
451 | |||
452 | $theme_slug = is_array( $themes ) && ! empty( $themes ) ? dirname( key( $themes ) ) : ''; |
||
453 | } |
||
454 | |||
455 | if ( empty( $theme_slug ) ) { |
||
456 | return new WP_Error( 'unknown_filename', __( 'Unknown product filename.', 'woocommerce' ) ); |
||
457 | } |
||
458 | |||
459 | return switch_theme( $theme_slug ); |
||
460 | } |
||
461 | |||
462 | /** |
||
463 | * Get installed directory of WP.org product. |
||
464 | * |
||
465 | * @since 3.7.0 |
||
466 | * @param int $product_id Product ID. |
||
467 | * @return bool|string |
||
468 | */ |
||
469 | private static function get_wporg_product_dir_name( $product_id ) { |
||
470 | $steps = self::get_state( 'steps' ); |
||
471 | $product = $steps[ $product_id ]; |
||
472 | |||
473 | if ( empty( $product['download_url'] ) || empty( $product['installed_path'] ) ) { |
||
474 | return false; |
||
475 | } |
||
476 | |||
477 | // Check whether product was downloaded from WordPress.org. |
||
478 | $parsed_url = wp_parse_url( $product['download_url'] ); |
||
479 | if ( ! empty( $parsed_url['host'] ) && 'downloads.wordpress.org' !== $parsed_url['host'] ) { |
||
480 | return false; |
||
481 | } |
||
482 | |||
483 | return basename( $product['installed_path'] ); |
||
484 | } |
||
485 | |||
486 | /** |
||
487 | * Get WP.org plugin's main file. |
||
488 | * |
||
489 | * @since 3.7.0 |
||
490 | * @param string $dir Directory name of the plugin. |
||
491 | * @return bool|string |
||
492 | */ |
||
493 | private static function get_wporg_plugin_main_file( $dir ) { |
||
510 | } |
||
511 |
This check looks from parameters that have been defined for a function or method, but which are not used in the method body.