Completed
Push — update/security-widget-languag... ( 75ac2c...0fa1b8 )
by
unknown
126:04 queued 116:11
created

Jetpack_WooCommerce_Analytics_Universal   B

Complexity

Total Complexity 41

Size/Duplication

Total Lines 367
Duplicated Lines 22.62 %

Coupling/Cohesion

Components 1
Dependencies 1

Importance

Changes 0
Metric Value
wmc 41
lcom 1
cbo 1
dl 83
loc 367
rs 8.2769
c 0
b 0
f 0

15 Methods

Rating   Name   Duplication   Size   Complexity  
A wp_head_bottom() 0 5 1
B __construct() 0 28 1
B wp_head_top() 0 8 6
B loop_session_events() 0 29 4
B remove_from_cart() 24 24 1
A get_product_details() 0 8 1
A capture_product_view() 0 18 1
B checkout_process() 0 32 3
B order_process() 0 26 2
A remove_from_cart_via_quantity() 21 21 1
A get_user_id() 0 8 2
A capture_add_to_cart() 0 10 4
B capture_event_in_session_data() 0 27 5
B get_product_categories_concatenated() 21 21 7
A remove_from_cart_attributes() 17 17 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

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 Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Jetpack_WooCommerce_Analytics_Universal 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 Jetpack_WooCommerce_Analytics_Universal, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Jetpack_WooCommerce_Analytics_Universal
4
 *
5
 * @package Jetpack
6
 * @author Automattic
7
 */
8
9
/**
10
 * Bail if accessed directly
11
 */
12
if ( ! defined( 'ABSPATH' ) ) {
13
	exit;
14
}
15
16
/**
17
 * Class Jetpack_WooCommerce_Analytics_Universal
18
 * Filters and Actions added to Store pages to perform analytics
19
 */
20
class Jetpack_WooCommerce_Analytics_Universal {
21
	/**
22
	 * Jetpack_WooCommerce_Analytics_Universal constructor.
23
	 */
24
	public function __construct() {
25
		// loading _wca
26
		add_action( 'wp_head', array( $this, 'wp_head_top' ), 1 );
27
28
		// add to carts from non-product pages or lists (search, store etc.)
29
		add_action( 'wp_head', array( $this, 'loop_session_events' ), 2 );
30
31
		// loading s.js
32
		add_action( 'wp_head', array( $this, 'wp_head_bottom' ), 999999 );
33
34
		// Capture cart events
35
		add_action( 'woocommerce_add_to_cart', array( $this, 'capture_add_to_cart' ), 10, 6 );
36
37
		// single product page view
38
		add_action( 'woocommerce_after_single_product', array( $this, 'capture_product_view' ) );
39
40
		add_action( 'woocommerce_after_cart', array( $this, 'remove_from_cart' ) );
41
		add_action( 'woocommerce_after_mini_cart', array( $this, 'remove_from_cart' ) );
42
		add_action( 'wcct_before_cart_widget', array( $this, 'remove_from_cart' ) );
43
		add_filter( 'woocommerce_cart_item_remove_link', array( $this, 'remove_from_cart_attributes' ), 10, 2 );
44
45
		// cart checkout
46
		add_action( 'woocommerce_after_checkout_form', array( $this, 'checkout_process' ) );
47
48
		// order confirmed
49
		add_action( 'woocommerce_thankyou', array( $this, 'order_process' ), 10, 1 );
50
		add_action( 'woocommerce_after_cart', array( $this, 'remove_from_cart_via_quantity' ), 10, 1 );
51
	}
52
53
	/**
54
	 * Make _wca available to queue events
55
	 */
56
	public function wp_head_top() {
57
		if ( is_cart() || is_checkout() || is_checkout_pay_page() || is_order_received_page() || is_add_payment_method_page() ) {
58
			$prevent_referrer_code = "<script>window._wca_prevent_referrer = true;</script>";
59
			echo "$prevent_referrer_code\r\n";
60
		}
61
		$wca_code = "<script>window._wca = window._wca || [];</script>";
62
		echo "$wca_code\r\n";
63
	}
64
65
66
	/**
67
	 * Place script to call s.js, Store Analytics
68
	 */
69
	public function wp_head_bottom() {
70
		$filename = 's-' . gmdate( 'YW' ) . '.js';
71
		$async_code = "<script async src='https://stats.wp.com/" . $filename . "'></script>";
72
		echo "$async_code\r\n";
73
	}
74
75
	/**
76
	 * On product lists or other non-product pages, add an event listener to "Add to Cart" button click
77
	 */
78
	public function loop_session_events() {
79
		$blogid   = Jetpack::get_option( 'id' );
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned correctly; expected 1 space but found 3 spaces

This check looks for improperly formatted assignments.

Every assignment must have exactly one space before and one space after the equals operator.

To illustrate:

$a = "a";
$ab = "ab";
$abc = "abc";

will have no issues, while

$a   = "a";
$ab  = "ab";
$abc = "abc";

will report issues in lines 1 and 2.

Loading history...
80
81
		// check for previous add-to-cart cart events
82
		$data = WC()->session->get( 'wca_session_data' );
83
		if ( ! empty( $data ) ) {
84
			foreach ( $data as $data_instance ) {
85
				$product = wc_get_product( $data_instance['product_id'] );
86
				if ( ! $product ) {
87
					continue;
88
				}
89
				$product_details = $this->get_product_details( $product );
90
				wc_enqueue_js(
91
					"_wca.push( {
92
							'_en': '" . esc_js( $data_instance['event'] ) . "',
93
							'blog_id': '" . esc_js( $blogid ) . "',
94
							'pi': '" . esc_js( $data_instance['product_id'] ) . "',
95
							'pn': '" . esc_js( $product_details['name'] ) . "',
96
							'pc': '" . esc_js( $product_details['category'] ) . "',
97
							'pp': '" . esc_js( $product_details['price'] ) . "',
98
							'pq': '" . esc_js( $data_instance['quantity'] ) . "',
99
							'ui': '" . esc_js( $this->get_user_id() ) . "',
100
						} );"
101
				);
102
			}
103
			// clear data
104
			WC()->session->set( 'wca_session_data', '' );
105
		}
106
	}
107
108
	/**
109
	 * On the cart page, add an event listener for removal of product click
110
	 */
111 View Code Duplication
	public function remove_from_cart() {
112
113
		// We listen at div.woocommerce because the cart 'form' contents get forcibly
114
		// updated and subsequent removals from cart would then not have this click
115
		// handler attached.
116
		$blogid = Jetpack::get_option( 'id' );
117
		wc_enqueue_js(
118
			"jQuery( 'div.woocommerce' ).on( 'click', 'a.remove', function() {
119
				var productID = jQuery( this ).data( 'product_id' );
120
				var quantity = jQuery( this ).parent().parent().find( '.qty' ).val()
121
				var productDetails = {
122
					'id': productID,
123
					'quantity': quantity ? quantity : '1',
124
				};
125
				_wca.push( {
126
					'_en': 'woocommerceanalytics_remove_from_cart',
127
					'blog_id': '" . esc_js( $blogid ) . "',
128
					'pi': productDetails.id,
129
					'pq': productDetails.quantity,
130
					'ui': '" . esc_js( $this->get_user_id() ) . "',
131
				} );
132
			} );"
133
		);
134
	}
135
136
	/**
137
	 * Adds the product ID to the remove product link (for use by remove_from_cart above) if not present
138
	 *
139
	 * @param string $url url.
140
	 * @param string $key key.
141
	 * @return mixed.
0 ignored issues
show
Documentation introduced by
The doc-type mixed. could not be parsed: Unknown type name "mixed." at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
142
	 */
143 View Code Duplication
	public function remove_from_cart_attributes( $url, $key ) {
144
		if ( false !== strpos( $url, 'data-product_id' ) ) {
145
			return $url;
146
		}
147
148
		$item    = WC()->cart->get_cart_item( $key );
149
		$product = $item['data'];
150
151
		$new_attributes = sprintf(
152
			'href="%s" data-product_id="%s" data-product_sku="%s"',
153
			esc_attr( $url ),
154
			esc_attr( $product->get_id() ),
155
			esc_attr( $product->get_sku() )
156
		);
157
		$url = str_replace( 'href=', $new_attributes, $url );
158
		return $url;
159
	}
160
161
	/**
162
	 * Gather relevant product information
163
	 *
164
	 * @param array $product product
165
	 * @return array
166
	 */
167
	public function get_product_details( $product ) {
168
		return array(
169
			'id'       => $product->get_id(),
0 ignored issues
show
Bug introduced by
The method get_id cannot be called on $product (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
170
			'name'     => $product->get_title(),
0 ignored issues
show
Bug introduced by
The method get_title cannot be called on $product (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
171
			'category' => $this->get_product_categories_concatenated( $product ),
0 ignored issues
show
Documentation introduced by
$product is of type array, but the function expects a object.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
172
			'price'    => $product->get_price(),
0 ignored issues
show
Bug introduced by
The method get_price cannot be called on $product (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
173
		);
174
	}
175
176
	/**
177
	 * Track a product page view
178
	 */
179
	public function capture_product_view() {
180
181
		global $product;
182
		$blogid = Jetpack::get_option( 'id' );
183
		$product_details = $this->get_product_details( $product );
184
185
		wc_enqueue_js(
186
			"_wca.push( {
187
				'_en': 'woocommerceanalytics_product_view',
188
				'blog_id': '" . esc_js( $blogid ) . "',
189
				'pi': '" . esc_js( $product_details['id'] ) . "',
190
				'pn': '" . esc_js( $product_details['name'] ) . "',
191
				'pc': '" . esc_js( $product_details['category'] ) . "',
192
				'pp': '" . esc_js( $product_details['price'] ) . "',
193
				'ui': '" . esc_js( $this->get_user_id() ) . "',
194
			} );"
195
		);
196
	}
197
198
	/**
199
	 * On the Checkout page, trigger an event for each product in the cart
200
	 */
201
	public function checkout_process() {
202
203
		$universal_commands = array();
204
		$cart               = WC()->cart->get_cart();
205
		$blogid             = Jetpack::get_option( 'id' );
206
207
		foreach ( $cart as $cart_item_key => $cart_item ) {
208
			/**
209
			* This filter is already documented in woocommerce/templates/cart/cart.php
210
			*/
211
			$product = apply_filters( 'woocommerce_cart_item_product', $cart_item['data'], $cart_item, $cart_item_key );
212
213
			if ( ! $product ) {
214
				continue;
215
			}
216
217
			$product_details = $this->get_product_details( $product );
218
219
			$universal_commands[] = "_wca.push( {
220
				'_en': 'woocommerceanalytics_product_checkout',
221
				'blog_id': '" . esc_js( $blogid ) . "',
222
				'pi': '" . esc_js( $product_details['id'] ) . "',
223
				'pn': '" . esc_js( $product_details['name'] ) . "',
224
				'pc': '" . esc_js( $product_details['category'] ) . "',
225
				'pp': '" . esc_js( $product_details['price'] ) . "',
226
				'pq': '" . esc_js( $cart_item['quantity'] ) . "',
227
				'ui': '" . esc_js( $this->get_user_id() ) . "',
228
			} );";
229
		}
230
231
		wc_enqueue_js( implode( "\r\n", $universal_commands ) );
232
	}
233
234
	/**
235
	 * After the checkout process, fire an event for each item in the order
236
	 *
237
	 * @param string $order_id Order Id.
238
	 */
239
	public function order_process( $order_id ) {
240
		$order              = wc_get_order( $order_id );
241
		$universal_commands = array();
242
		$blogid             = Jetpack::get_option( 'id' );
243
244
		// loop through products in the order and queue a purchase event.
245
		foreach ( $order->get_items() as $order_item_id => $order_item ) {
246
			$product = $order->get_product_from_item( $order_item );
247
248
			$product_details = $this->get_product_details( $product );
249
250
			$universal_commands[] = "_wca.push( {
251
				'_en': 'woocommerceanalytics_product_purchase',
252
				'blog_id': '" . esc_js( $blogid ) . "',
253
				'pi': '" . esc_js( $product_details['id'] ) . "',
254
				'pn': '" . esc_js( $product_details['name'] ) . "',
255
				'pc': '" . esc_js( $product_details['category'] ) . "',
256
				'pp': '" . esc_js( $product_details['price'] ) . "',
257
				'pq': '" . esc_js( $order_item->get_quantity() ) . "',
258
				'oi': '" . esc_js( $order->get_order_number() ) . "',
259
				'ui': '" . esc_js( $this->get_user_id() ) . "',
260
			} );";
261
		}
262
263
		wc_enqueue_js( implode( "\r\n", $universal_commands ) );
264
	}
265
266
	/**
267
	 * Listen for clicks on the "Update Cart" button to know if an item has been removed by
268
	 * updating its quantity to zero
269
	 */
270 View Code Duplication
	public function remove_from_cart_via_quantity() {
271
		$blogid = Jetpack::get_option( 'id' );
272
273
		wc_enqueue_js( "
274
			jQuery( 'button[name=update_cart]' ).on( 'click', function() {
275
				var cartItems = jQuery( '.cart_item' );
276
				cartItems.each( function( item ) {
277
					var qty = jQuery( this ).find( 'input.qty' );
278
					if ( qty && qty.val() === '0' ) {
279
						var productID = jQuery( this ).find( '.product-remove a' ).data( 'product_id' );
280
						_wca.push( {
281
							'_en': 'woocommerceanalytics_remove_from_cart',
282
							'blog_id': '" . esc_js( $blogid ) . "',
283
							'pi': productID,
284
							'ui': '" . esc_js( $this->get_user_id() ) . "',
285
						} );
286
					}
287
				} );
288
			} );
289
		" );
290
	}
291
292
	/**
293
	 * Get the current user id
294
	 *
295
	 * @return int
296
	 */
297
	public function get_user_id() {
298
		if ( is_user_logged_in() ) {
299
			$blogid = Jetpack::get_option( 'id' );
300
			$userid = get_current_user_id();
301
			return $blogid . ":" . $userid;
302
		}
303
		return 'null';
304
	}
305
306
	/**
307
		* @param $cart_item_key
308
		* @param $product_id
309
		* @param $quantity
310
		* @param $variation_id
311
		* @param $variation
312
		* @param $cart_item_data
313
		*/
314
	public function capture_add_to_cart( $cart_item_key, $product_id, $quantity, $variation_id, $variation, $cart_item_data ) {
315
		$referer_postid = isset( $_SERVER['HTTP_REFERER'] ) ? url_to_postid( $_SERVER['HTTP_REFERER'] ) : 0;
316
		// if the referring post is not a product OR the product being added is not the same as post
317
		// (eg. related product list on single product page) then include a product view event
318
		if ( ! wc_get_product( $referer_postid ) || $product_id != $referer_postid ) {
319
			$this->capture_event_in_session_data( $product_id, $quantity, 'woocommerceanalytics_product_view' );
320
		}
321
		// add cart event to the session data
322
		$this->capture_event_in_session_data( $product_id, $quantity, 'woocommerceanalytics_add_to_cart' );
323
	}
324
325
	/**
326
	 * @param $product_id
327
	 * @param $quantity
328
	 * @param $event
329
	 */
330
	public function capture_event_in_session_data( $product_id, $quantity, $event ) {
331
332
		$product = wc_get_product( $product_id );
333
		if ( ! $product ) {
334
			return;
335
		}
336
337
		$quantity = ( $quantity == 0 ) ? 1 : $quantity;
338
339
		// check for existing data
340
		$data = WC()->session->get( 'wca_session_data' );
341
		if ( empty( $data ) || ! is_array( $data ) ) {
342
			$data = array();
343
		}
344
345
		// extract new event data
346
		$new_data = array(
347
			'event' => $event,
348
			'product_id' => (string) $product_id,
349
			'quantity' => (string) $quantity,
350
		);
351
352
		// append new data
353
		$data[] = $new_data;
354
355
		WC()->session->set( 'wca_session_data', $data );
356
	}
357
358
	/**
359
	 * Gets product categories or varation attributes as a formatted concatenated string
360
	 *
361
	 * @param object $product WC_Product.
362
	 * @return string
363
	 */
364 View Code Duplication
	public function get_product_categories_concatenated( $product ) {
365
366
		if ( ! $product ) {
367
			return '';
368
		}
369
370
		$variation_data = $product->is_type( 'variation' ) ? wc_get_product_variation_attributes( $product->get_id() ) : '';
371
		if ( is_array( $variation_data ) && ! empty( $variation_data ) ) {
372
			$line = wc_get_formatted_variation( $variation_data, true );
373
		} else {
374
			$out = array();
375
			$categories = get_the_terms( $product->get_id(), 'product_cat' );
376
			if ( $categories ) {
377
				foreach ( $categories as $category ) {
378
					$out[] = $category->name;
379
				}
380
			}
381
			$line = join( '/', $out );
382
		}
383
		return $line;
384
	}
385
386
}
387