Completed
Push — add/woo-analytics-support-bund... ( d6d3ac )
by
unknown
11:03
created

capture_cart_event()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 5
nc 4
nop 2
dl 0
loc 11
rs 9.2
c 0
b 0
f 0
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
		add_action( 'woocommerce_bundled_item_before_add_to_cart', array( $this, 'capture_bundle_add_to_cart' ), 10, 6 );
37
38
		// single product page view
39
		add_action( 'woocommerce_after_single_product', array( $this, 'capture_product_view' ) );
40
41
		add_action( 'woocommerce_after_cart', array( $this, 'remove_from_cart' ) );
42
		add_action( 'woocommerce_after_mini_cart', array( $this, 'remove_from_cart' ) );
43
		add_action( 'wcct_before_cart_widget', array( $this, 'remove_from_cart' ) );
44
		add_filter( 'woocommerce_cart_item_remove', array( $this, 'remove_from_cart_attributes' ), 10, 2 );
45
46
		// cart checkout
47
		add_action( 'woocommerce_after_checkout_form', array( $this, 'checkout_process' ) );
48
49
		// order confirmed
50
		add_action( 'woocommerce_thankyou', array( $this, 'order_process' ), 10, 1 );
51
		add_action( 'woocommerce_after_cart', array( $this, 'remove_from_cart_via_quantity' ), 10, 1 );
52
	}
53
54
	/**
55
	 * Make _wca available to queue events
56
	 */
57
	public function wp_head_top() {
58
		if ( is_cart() || is_checkout() || is_checkout_pay_page() || is_order_received_page() || is_add_payment_method_page() ) {
59
			$prevent_referrer_code = "<script>window._wca_prevent_referrer = true;</script>";
60
			echo "$prevent_referrer_code\r\n";
61
		}
62
		$wca_code = "<script>window._wca = window._wca || [];</script>";
63
		echo "$wca_code\r\n";
64
	}
65
66
67
	/**
68
	 * Place script to call s.js, Store Analytics
69
	 */
70
	public function wp_head_bottom() {
71
		$filename = 's-' . gmdate( 'YW' ) . '.js';
72
		$async_code = "<script async src='https://stats.wp.com/" . $filename . "'></script>";
73
		echo "$async_code\r\n";
74
	}
75
76
	/**
77
	 * On product lists or other non-product pages, add an event listener to "Add to Cart" button click
78
	 */
79
	public function loop_session_events() {
80
		$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...
81
82
		// check for previous add-to-cart cart events
83
		$data = WC()->session->get( 'wca_session_data' );
84
		if ( ! empty( $data ) ) {
85
			foreach ( $data as $data_instance ) {
86
				$product = wc_get_product( $data_instance['product_id'] );
87
				if ( ! $product ) {
88
					continue;
89
				}
90
				$product_details = $this->get_product_details( $product );
91
				wc_enqueue_js(
92
					"_wca.push( {
93
							'_en': '" . esc_js( $data_instance['event'] ) . "',
94
							'blog_id': '" . esc_js( $blogid ) . "',
95
							'pi': '" . esc_js( $data_instance['product_id'] ) . "',
96
							'pn': '" . esc_js( $product_details['name'] ) . "',
97
							'pc': '" . esc_js( $product_details['category'] ) . "',
98
							'pp': '" . esc_js( $product_details['price'] ) . "',
99
							'pq': '" . esc_js( $data_instance['quantity'] ) . "',
100
							'ui': '" . esc_js( $this->get_user_id() ) . "',
101
						} );"
102
				);
103
			}
104
			// clear data
105
			WC()->session->set( 'wca_session_data', '' );
106
		}
107
	}
108
109
	/**
110
	 * On the cart page, add an event listener for removal of product click
111
	 */
112 View Code Duplication
	public function remove_from_cart() {
113
114
		// We listen at div.woocommerce because the cart 'form' contents get forcibly
115
		// updated and subsequent removals from cart would then not have this click
116
		// handler attached.
117
		$blogid = Jetpack::get_option( 'id' );
118
		wc_enqueue_js(
119
			"jQuery( 'div.woocommerce' ).on( 'click', 'a.remove', function() {
120
				var productID = jQuery( this ).data( 'product_id' );
121
				var quantity = jQuery( this ).parent().parent().find( '.qty' ).val()
122
				var productDetails = {
123
					'id': productID,
124
					'quantity': quantity ? quantity : '1',
125
				};
126
				_wca.push( {
127
					'_en': 'woocommerceanalytics_remove_from_cart',
128
					'blog_id': '" . esc_js( $blogid ) . "',
129
					'pi': productDetails.id,
130
					'pq': productDetails.quantity,
131
					'ui': '" . esc_js( $this->get_user_id() ) . "',
132
				} );
133
			} );"
134
		);
135
	}
136
137
	/**
138
	 * Adds the product ID to the remove product link (for use by remove_from_cart above) if not present
139
	 *
140
	 * @param string $url url.
141
	 * @param string $key key.
142
	 * @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...
143
	 */
144 View Code Duplication
	public function remove_from_cart_attributes( $url, $key ) {
145
		if ( false !== strpos( $url, 'data-product_id' ) ) {
146
			return $url;
147
		}
148
149
		$item    = WC()->cart->get_cart_item( $key );
150
		$product = $item['data'];
151
152
		$new_attributes = sprintf(
153
			'href="%s" data-product_id="%s" data-product_sku="%s"',
154
			esc_attr( $url ),
155
			esc_attr( $product->get_id() ),
156
			esc_attr( $product->get_sku() )
157
		);
158
		$url = str_replace( 'href=', $new_attributes, $url );
159
		return $url;
160
	}
161
162
	/**
163
	 * Gather relevant product information
164
	 *
165
	 * @param array $product product
166
	 * @return array
167
	 */
168
	public function get_product_details( $product ) {
169
		return array(
170
			'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...
171
			'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...
172
			'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...
173
			'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...
174
		);
175
	}
176
177
	/**
178
	 * Track a product page view
179
	 */
180
	public function capture_product_view() {
181
182
		global $product;
183
		$blogid = Jetpack::get_option( 'id' );
184
		$product_details = $this->get_product_details( $product );
185
186
		wc_enqueue_js(
187
			"_wca.push( {
188
				'_en': 'woocommerceanalytics_product_view',
189
				'blog_id': '" . esc_js( $blogid ) . "',
190
				'pi': '" . esc_js( $product_details['id'] ) . "',
191
				'pn': '" . esc_js( $product_details['name'] ) . "',
192
				'pc': '" . esc_js( $product_details['category'] ) . "',
193
				'pp': '" . esc_js( $product_details['price'] ) . "',
194
				'ui': '" . esc_js( $this->get_user_id() ) . "',
195
			} );"
196
		);
197
	}
198
199
	/**
200
	 * On the Checkout page, trigger an event for each product in the cart
201
	 */
202
	public function checkout_process() {
203
204
		$universal_commands = array();
205
		$cart               = WC()->cart->get_cart();
206
		$blogid             = Jetpack::get_option( 'id' );
207
208
		foreach ( $cart as $cart_item_key => $cart_item ) {
209
			/**
210
			* This filter is already documented in woocommerce/templates/cart/cart.php
211
			*/
212
			$product = apply_filters( 'woocommerce_cart_item_product', $cart_item['data'], $cart_item, $cart_item_key );
213
214
			if ( ! $product ) {
215
				continue;
216
			}
217
218
			$product_details = $this->get_product_details( $product );
219
220
			$universal_commands[] = "_wca.push( {
221
				'_en': 'woocommerceanalytics_product_checkout',
222
				'blog_id': '" . esc_js( $blogid ) . "',
223
				'pi': '" . esc_js( $product_details['id'] ) . "',
224
				'pn': '" . esc_js( $product_details['name'] ) . "',
225
				'pc': '" . esc_js( $product_details['category'] ) . "',
226
				'pp': '" . esc_js( $product_details['price'] ) . "',
227
				'pq': '" . esc_js( $cart_item['quantity'] ) . "',
228
				'ui': '" . esc_js( $this->get_user_id() ) . "',
229
			} );";
230
		}
231
232
		wc_enqueue_js( implode( "\r\n", $universal_commands ) );
233
	}
234
235
	/**
236
	 * After the checkout process, fire an event for each item in the order
237
	 *
238
	 * @param string $order_id Order Id.
239
	 */
240
	public function order_process( $order_id ) {
241
		$order              = wc_get_order( $order_id );
242
		$universal_commands = array();
243
		$blogid             = Jetpack::get_option( 'id' );
244
245
		// loop through products in the order and queue a purchase event.
246
		foreach ( $order->get_items() as $order_item_id => $order_item ) {
247
			$product = $order->get_product_from_item( $order_item );
248
249
			$product_details = $this->get_product_details( $product );
250
251
			$universal_commands[] = "_wca.push( {
252
				'_en': 'woocommerceanalytics_product_purchase',
253
				'blog_id': '" . esc_js( $blogid ) . "',
254
				'pi': '" . esc_js( $product_details['id'] ) . "',
255
				'pn': '" . esc_js( $product_details['name'] ) . "',
256
				'pc': '" . esc_js( $product_details['category'] ) . "',
257
				'pp': '" . esc_js( $product_details['price'] ) . "',
258
				'pq': '" . esc_js( $order_item->get_quantity() ) . "',
259
				'oi': '" . esc_js( $order->get_order_number() ) . "',
260
				'ui': '" . esc_js( $this->get_user_id() ) . "',
261
			} );";
262
		}
263
264
		wc_enqueue_js( implode( "\r\n", $universal_commands ) );
265
	}
266
267
	/**
268
	 * Listen for clicks on the "Update Cart" button to know if an item has been removed by
269
	 * updating its quantity to zero
270
	 */
271 View Code Duplication
	public function remove_from_cart_via_quantity() {
272
		$blogid = Jetpack::get_option( 'id' );
273
274
		wc_enqueue_js( "
275
			jQuery( 'button[name=update_cart]' ).on( 'click', function() {
276
				var cartItems = jQuery( '.cart_item' );
277
				cartItems.each( function( item ) {
278
					var qty = jQuery( this ).find( 'input.qty' );
279
					if ( qty && qty.val() === '0' ) {
280
						var productID = jQuery( this ).find( '.product-remove a' ).data( 'product_id' );
281
						_wca.push( {
282
							'_en': 'woocommerceanalytics_remove_from_cart',
283
							'blog_id': '" . esc_js( $blogid ) . "',
284
							'pi': productID,
285
							'ui': '" . esc_js( $this->get_user_id() ) . "',
286
						} );
287
					}
288
				} );
289
			} );
290
		" );
291
	}
292
293
	/**
294
	 * Get the current user id
295
	 *
296
	 * @return int
297
	 */
298
	public function get_user_id() {
299
		if ( is_user_logged_in() ) {
300
			$blogid = Jetpack::get_option( 'id' );
301
			$userid = get_current_user_id();
302
			return $blogid . ":" . $userid;
303
		}
304
		return 'null';
305
	}
306
307
	/**
308
		* @param $cart_item_key
309
		* @param $product_id
310
		* @param $quantity
311
		* @param $variation_id
312
		* @param $variation
313
		* @param $cart_item_data
314
		*/
315
	public function capture_add_to_cart( $cart_item_key, $product_id, $quantity, $variation_id, $variation, $cart_item_data ) {
316
		$this->capture_cart_event( $product_id, $quantity );
317
	}
318
319
	/**
320
		* @param $product_id
321
		* @param $quantity
322
		* @param $variation_id
323
		* @param $variations
324
		* @param $bundled_item_cart_data
325
		*/
326
	public function capture_bundle_add_to_cart( $product_id, $quantity, $variation_id, $variations, $bundled_item_cart_data ) {
327
		$this->capture_cart_event( $product_id, $quantity );
328
	}
329
330
	/**
331
	 * @param $product_id
332
	 * @param $quantity
333
	 * @param $event
334
	 */
335
	public function capture_cart_event ( $product_id, $quantity ) {
336
		$referer_postid = isset( $_SERVER['HTTP_REFERER'] ) ? url_to_postid( $_SERVER['HTTP_REFERER'] ) : 0;
337
338
		// if the referring post is not a product OR the product being added is not the same as post
339
		// (eg. related product list on single product page) then include a product view event
340
		if ( ! wc_get_product( $referer_postid ) || $product_id != $referer_postid ) {
341
			$this->capture_event_in_session_data( $product_id, $quantity, 'woocommerceanalytics_product_view' );
342
		}
343
		// add cart event to the session data
344
		$this->capture_event_in_session_data( $product_id, $quantity, 'woocommerceanalytics_add_to_cart' );
345
	}
346
347
	/**
348
	 * @param $product_id
349
	 * @param $quantity
350
	 * @param $event
351
	 */
352
	public function capture_event_in_session_data( $product_id, $quantity, $event ) {
353
354
		$product = wc_get_product( $product_id );
355
		if ( ! $product ) {
356
			return;
357
		}
358
359
		$quantity = ( $quantity == 0 ) ? 1 : $quantity;
360
361
		// check for existing data
362
		$data = WC()->session->get( 'wca_session_data' );
363
		if ( empty( $data ) || ! is_array( $data ) ) {
364
			$data = array();
365
		}
366
367
		// extract new event data
368
		$new_data = array(
369
			'event' => $event,
370
			'product_id' => (string) $product_id,
371
			'quantity' => (string) $quantity,
372
		);
373
374
		// append new data
375
		$data[] = $new_data;
376
377
		WC()->session->set( 'wca_session_data', $data );
378
	}
379
380
	/**
381
	 * Gets product categories or varation attributes as a formatted concatenated string
382
	 *
383
	 * @param object $product WC_Product.
384
	 * @return string
385
	 */
386 View Code Duplication
	public function get_product_categories_concatenated( $product ) {
387
388
		if ( ! $product ) {
389
			return '';
390
		}
391
392
		$variation_data = $product->is_type( 'variation' ) ? wc_get_product_variation_attributes( $product->get_id() ) : '';
393
		if ( is_array( $variation_data ) && ! empty( $variation_data ) ) {
394
			$line = wc_get_formatted_variation( $variation_data, true );
395
		} else {
396
			$out = array();
397
			$categories = get_the_terms( $product->get_id(), 'product_cat' );
398
			if ( $categories ) {
399
				foreach ( $categories as $category ) {
400
					$out[] = $category->name;
401
				}
402
			}
403
			$line = join( '/', $out );
404
		}
405
		return $line;
406
	}
407
408
}
409