Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
94.64% |
53 / 56 |
|
77.78% |
7 / 9 |
CRAP | |
0.00% |
0 / 1 |
| Sync_Configuration | |
96.36% |
53 / 55 |
|
77.78% |
7 / 9 |
20 | |
0.00% |
0 / 1 |
| register | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
3.03 | |||
| is_woocommerce_active | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
| configure_sync | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
2 | |||
| register_analytics_sync_module | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| get_jetpack_sync_config | |
100.00% |
20 / 20 |
|
100.00% |
1 / 1 |
1 | |||
| remove_duplicate_woocommerce_analytics_module | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
| expand_full_sync_config | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
| skip_reports_data_before_analytics_full_sync | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
4.13 | |||
| add_meta_to_sync_post_meta_whitelist | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * Opt-in to the shared WooCommerce Analytics sync module. |
| 4 | * |
| 5 | * @package automattic/jetpack-woocommerce-stats |
| 6 | */ |
| 7 | |
| 8 | namespace Automattic\Jetpack\WooCommerceStats; |
| 9 | |
| 10 | use Automattic\Jetpack\Config; |
| 11 | use Automattic\Jetpack\PremiumAnalytics\Sync\Sync_Status_Tracker; |
| 12 | use Automattic\Jetpack\Sync\Data_Settings; |
| 13 | use Automattic\Jetpack\Sync\Modules\Meta as Meta_Module; |
| 14 | use Automattic\Jetpack\Sync\Modules\Posts as Posts_Module; |
| 15 | use Automattic\Jetpack\Sync\Modules\Term_Relationships as Term_Relationships_Module; |
| 16 | use Automattic\Jetpack\Sync\Modules\Terms as Terms_Module; |
| 17 | use Automattic\Jetpack\Sync\Modules\WooCommerce_Analytics as WooCommerce_Analytics_Module; |
| 18 | |
| 19 | defined( 'ABSPATH' ) || exit; |
| 20 | |
| 21 | /** |
| 22 | * Opts in to the shared WooCommerce Analytics sync module and registers the sync configuration |
| 23 | * the WooCommerce section reads. |
| 24 | */ |
| 25 | class Sync_Configuration { |
| 26 | |
| 27 | /** |
| 28 | * FQCN of the Analytics module shipped by the standalone WooCommerce Analytics plugin. |
| 29 | * |
| 30 | * Must track that plugin's class: if it drifts, both modules load under the same |
| 31 | * name and every analytics event syncs twice. Moot once that plugin consumes the |
| 32 | * shared module, since the class strings then match. |
| 33 | * |
| 34 | * @since 0.3.0 |
| 35 | * @var string |
| 36 | */ |
| 37 | const ANALYTICS_PLUGIN_MODULE_FQCN = 'Automattic\\WooCommerce\\Analytics\\Internal\\Jetpack\\Sync\\Modules\\Analytics'; |
| 38 | |
| 39 | /** |
| 40 | * Bookings post meta to add to Sync's post meta whitelist. Bookings are synced |
| 41 | * via the Posts + Meta modules; there is no dedicated bookings sync module. |
| 42 | * |
| 43 | * Product meta needed by analytics reports is whitelisted by the shared module. |
| 44 | * |
| 45 | * @static |
| 46 | * @var array |
| 47 | */ |
| 48 | private static $postmeta_to_sync = array( |
| 49 | '_booking_parent_id', |
| 50 | '_booking_duplicate_of', |
| 51 | '_booking_product_id', |
| 52 | '_booking_resource_id', |
| 53 | '_booking_order_id', |
| 54 | '_booking_order_item_id', |
| 55 | '_booking_customer_id', |
| 56 | '_booking_start', |
| 57 | '_booking_end', |
| 58 | '_booking_all_day', |
| 59 | '_booking_persons', |
| 60 | '_booking_cost', |
| 61 | '_booking_date_cancelled', |
| 62 | '_booking_attendance_status', |
| 63 | ); |
| 64 | |
| 65 | /** |
| 66 | * Whether register() already ran in this request. |
| 67 | * |
| 68 | * @var bool |
| 69 | */ |
| 70 | private static $registered = false; |
| 71 | |
| 72 | /** |
| 73 | * Entry point called from Analytics_Dashboard::init(), once per request however many plugins bundle the package. Schedules the Sync hookups on |
| 74 | * plugins_loaded; the actual registration is a no-op unless WooCommerce is active |
| 75 | * (see {@see configure_sync()}). |
| 76 | * |
| 77 | * Call it before plugins_loaded completes: the Config built in configure_sync() wires |
| 78 | * Sync\Main::configure() from a plugins_loaded priority 2 handler that never fires later. |
| 79 | * |
| 80 | * @return void |
| 81 | */ |
| 82 | public static function register(): void { |
| 83 | if ( self::$registered ) { |
| 84 | return; |
| 85 | } |
| 86 | self::$registered = true; |
| 87 | $instance = new self(); |
| 88 | |
| 89 | // plugins_loaded priority 1: every plugin has loaded for the WooCommerce guard, and the |
| 90 | // Config constructed in configure_sync() still gets its priority 2 handler in this cycle. |
| 91 | if ( did_action( 'plugins_loaded' ) ) { |
| 92 | $instance->configure_sync(); |
| 93 | } else { |
| 94 | add_action( 'plugins_loaded', array( $instance, 'configure_sync' ), 1 ); |
| 95 | } |
| 96 | } |
| 97 | |
| 98 | /** |
| 99 | * Whether WooCommerce is active in the current request. |
| 100 | * |
| 101 | * @return bool |
| 102 | */ |
| 103 | private static function is_woocommerce_active(): bool { |
| 104 | return class_exists( 'WooCommerce' ) || function_exists( 'WC' ); |
| 105 | } |
| 106 | |
| 107 | /** |
| 108 | * Register the Jetpack Sync filters and ensure the Sync feature when WooCommerce |
| 109 | * is active. |
| 110 | * |
| 111 | * @return void |
| 112 | */ |
| 113 | public function configure_sync(): void { |
| 114 | if ( ! self::is_woocommerce_active() ) { |
| 115 | return; |
| 116 | } |
| 117 | |
| 118 | // The shared module is registered through the Sync config below; this only drops a duplicate. |
| 119 | add_filter( 'jetpack_sync_modules', array( $this, 'remove_duplicate_woocommerce_analytics_module' ), PHP_INT_MAX ); |
| 120 | add_filter( 'jetpack_full_sync_config', array( $this, 'expand_full_sync_config' ) ); |
| 121 | add_filter( 'jetpack_sync_post_meta_whitelist', array( $this, 'add_meta_to_sync_post_meta_whitelist' ) ); |
| 122 | add_filter( 'jetpack_sync_before_enqueue_woocommerce_analytics_sync_reports_data', array( $this, 'skip_reports_data_before_analytics_full_sync' ) ); |
| 123 | add_filter( 'jetpack_sync_before_enqueue_woocommerce_analytics_delete_reports_data', array( $this, 'skip_reports_data_before_analytics_full_sync' ) ); |
| 124 | add_filter( 'jetpack_premium_analytics_sync_modules', array( $this, 'register_analytics_sync_module' ) ); |
| 125 | |
| 126 | ( new Config() )->ensure( 'sync', $this->get_jetpack_sync_config() ); |
| 127 | |
| 128 | ( new Lookup_Data_Backfill() )->register(); |
| 129 | } |
| 130 | |
| 131 | /** |
| 132 | * Register Woo's module with the analytics sync tracker. |
| 133 | * |
| 134 | * @param string[] $modules Registered analytics module names. |
| 135 | * @return string[] |
| 136 | */ |
| 137 | public function register_analytics_sync_module( array $modules ): array { |
| 138 | $modules[] = 'woocommerce_analytics'; |
| 139 | return $modules; |
| 140 | } |
| 141 | |
| 142 | /** |
| 143 | * Jetpack Sync module configuration. |
| 144 | * |
| 145 | * MUST_SYNC_DATA_SETTINGS is merged in because Data_Settings falls back to the full default |
| 146 | * whitelist for any filter a consumer leaves out, which would widen standalone sites. |
| 147 | * |
| 148 | * @return array Jetpack Sync config array. |
| 149 | */ |
| 150 | private function get_jetpack_sync_config(): array { |
| 151 | return array_merge_recursive( |
| 152 | Data_Settings::MUST_SYNC_DATA_SETTINGS, |
| 153 | array( |
| 154 | 'jetpack_sync_modules' => array( |
| 155 | WooCommerce_Analytics_Module::class, |
| 156 | Meta_Module::class, |
| 157 | Posts_Module::class, |
| 158 | Terms_Module::class, |
| 159 | Term_Relationships_Module::class, |
| 160 | ), |
| 161 | // Listed explicitly so the contract does not depend on which other Sync modules load. |
| 162 | 'jetpack_sync_options_whitelist' => array( |
| 163 | 'woocommerce_custom_orders_table_enabled', // Required for HPOS checksums. |
| 164 | 'woocommerce_excluded_report_order_statuses', // Required for generating analytics reports. |
| 165 | 'woocommerce_date_type', // Date used to determine the date range for analytics reports. |
| 166 | ), |
| 167 | 'jetpack_sync_constants_whitelist' => array( |
| 168 | // Syncing it makes WPCOM provision the WC Analytics tables (WOOA7S-1643). WC_ANALYTICS_VERSION |
| 169 | // belongs to the standalone plugin and would only sync null on a PA-only store. |
| 170 | 'JETPACK_PREMIUM_ANALYTICS__VERSION', |
| 171 | ), |
| 172 | ) |
| 173 | ); |
| 174 | } |
| 175 | |
| 176 | /** |
| 177 | * Drop the standalone plugin's Analytics module in favor of the shared one. |
| 178 | * |
| 179 | * Sync dedups by class name only, so both would load and sync every event twice. The shared |
| 180 | * module wins because the released standalone one syncs no lookup data, while the sync package |
| 181 | * advertises the lookup checksum tables for any module of this name. |
| 182 | * |
| 183 | * Runs at PHP_INT_MAX because Data_Settings re-asserts its whole module list at priority 10. |
| 184 | * |
| 185 | * @param array|mixed $modules Current Sync module class names. |
| 186 | * @return array|mixed Updated Sync module class names. |
| 187 | */ |
| 188 | public function remove_duplicate_woocommerce_analytics_module( $modules ) { |
| 189 | // An emptied list is a kill switch (Jetpack's uninstaller uses one); leave it alone. |
| 190 | if ( ! is_array( $modules ) || empty( $modules ) ) { |
| 191 | return $modules; |
| 192 | } |
| 193 | |
| 194 | return array_values( array_diff( $modules, array( self::ANALYTICS_PLUGIN_MODULE_FQCN ) ) ); |
| 195 | } |
| 196 | |
| 197 | /** |
| 198 | * Add the Analytics module to full sync, first in line. |
| 199 | * |
| 200 | * @param array $config Current full-sync configuration. |
| 201 | * @return array Updated full-sync configuration. |
| 202 | */ |
| 203 | public function expand_full_sync_config( array $config ): array { |
| 204 | // Terms and term relationships must be synced before posts. |
| 205 | if ( isset( $config['posts'] ) ) { |
| 206 | unset( $config['posts'] ); |
| 207 | $config += array( 'posts' => 1 ); |
| 208 | } |
| 209 | |
| 210 | if ( ! isset( $config['woocommerce_analytics'] ) ) { |
| 211 | $config = array( 'woocommerce_analytics' => 1 ) + $config; |
| 212 | } |
| 213 | |
| 214 | return $config; |
| 215 | } |
| 216 | |
| 217 | /** |
| 218 | * Drop per-order analytics reports data until an analytics full sync has started. |
| 219 | * |
| 220 | * That full sync sends the reports data of every order and overwrites whatever was synced before |
| 221 | * it. Gated on the start, not the end, so orders that change while it runs still sync. |
| 222 | * |
| 223 | * @param array|mixed $args Sync action arguments. |
| 224 | * @return array|mixed|false The arguments, or false to drop the action. |
| 225 | */ |
| 226 | public function skip_reports_data_before_analytics_full_sync( $args ) { |
| 227 | // An older copy of the dashboard package cannot tell, so order changes sync as they did before the gate. |
| 228 | if ( ! method_exists( Sync_Status_Tracker::class, 'has_module_full_sync_started' ) ) { |
| 229 | return $args; |
| 230 | } |
| 231 | |
| 232 | // A store the standalone plugin fully synced holds its orders on WordPress.com already. |
| 233 | if ( (int) get_option( Lookup_Data_Backfill::PLUGIN_FULL_SYNC_OPTION, 0 ) > 0 ) { |
| 234 | return $args; |
| 235 | } |
| 236 | |
| 237 | return Sync_Status_Tracker::has_module_full_sync_started( 'woocommerce_analytics' ) ? $args : false; |
| 238 | } |
| 239 | |
| 240 | /** |
| 241 | * Add Bookings post meta to Sync's post meta whitelist. |
| 242 | * Any changes to these meta will be synced to WordPress.com. |
| 243 | * |
| 244 | * @param array $whitelist Existing post meta whitelist. |
| 245 | * @return array Updated post meta whitelist. |
| 246 | */ |
| 247 | public function add_meta_to_sync_post_meta_whitelist( array $whitelist ): array { |
| 248 | return array_merge( self::$postmeta_to_sync, $whitelist ); |
| 249 | } |
| 250 | } |