Completed
Pull Request — master (#11455)
by
unknown
10:06
created

WC_Structured_Data   C

Complexity

Total Complexity 71

Size/Duplication

Total Lines 454
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 0

Importance

Changes 5
Bugs 1 Features 0
Metric Value
c 5
b 1
f 0
dl 0
loc 454
rs 5.5904
wmc 71
lcom 1
cbo 0

13 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 12 1
B set_data() 0 13 5
A get_data() 0 5 2
C set_structured_data() 0 32 7
C get_structured_data() 0 31 11
A enqueue_data() 0 8 2
A sanitize_data() 0 11 4
A generate_product_category_data() 0 7 3
C generate_product_data() 0 62 11
A generate_product_review_data() 0 21 1
B generate_breadcrumb_data() 0 32 5
A generate_shop_data() 0 16 3
F generate_email_order_data() 0 110 16

How to fix   Complexity   

Complex Class

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

1
<?php
2
3
if ( ! defined( 'ABSPATH' ) ) {
4
	exit; // Exit if accessed directly
5
}
6
7
/**
8
 * Structured data's handler and generator using JSON-LD format.
9
 *
10
 * @class     WC_Structured_Data
11
 * @version   2.7.0
12
 * @package   WooCommerce/Classes
13
 * @category  Class
14
 * @author    Clement Cazaud
15
 */
16
class WC_Structured_Data {
17
	
18
	/**
19
	 * @var array Partially structured data from `generate_*` methods
20
	 */
21
	private $_data;
22
23
	/**
24
	 * @var array Structured data
25
	 */
26
	private $_structured_data;
27
28
	/**
29
	 * Constructor.
30
	 */
31
	public function __construct() {
32
		// Generate data...
33
		add_action( 'woocommerce_before_main_content', array( $this, 'generate_shop_data' ), 30 );
34
		add_action( 'woocommerce_breadcrumb', array( $this, 'generate_breadcrumb_data' ), 10, 1 );
35
		add_action( 'woocommerce_before_shop_loop_item', array( $this, 'generate_product_category_data' ), 20 );
36
		add_action( 'woocommerce_single_product_summary', array( $this, 'generate_product_data' ), 60 );
37
		add_action( 'woocommerce_review_meta', array( $this, 'generate_product_review_data' ), 20, 1 );
38
		add_action( 'woocommerce_email_order_details', array( $this, 'generate_email_order_data' ), 20, 4 );
39
		// Enqueue structured data...
40
		add_action( 'woocommerce_email_order_details', array( $this, 'enqueue_data' ), 30 );
41
		add_action( 'wp_footer', array( $this, 'enqueue_data' ) );
42
	}
43
44
	/**
45
	 * Sets `$this->_data` if `$json` is valid.
46
	 *
47
	 * @param  array $json Partially structured data from `generate_*` methods
48
	 * @param  bool  $overwrite (default: false)
49
	 * @return bool `false` if invalid `$json`, otherwise `true`
50
	 */
51
	public function set_data( $json, $overwrite = false ) {
52
		if ( ! is_array( $json ) || ! array_key_exists( '@type', $json ) ) {
53
			return false;
54
		}
55
56
		if ( $overwrite && isset( $this->_data ) ) {
57
			unset( $this->_data );
58
		}
59
		
60
		$this->_data[] = $json;
61
62
		return true;
63
	}
64
	
65
	/**
66
	 * Gets `$this->_data`.
67
	 *
68
	 * @return array $data Or empty array if `$this->_data` is not set
69
	 */
70
	public function get_data() {
71
		$data = isset( $this->_data ) ? $this->_data : array();
72
			
73
		return $data;
74
	}
75
76
	/**
77
	 * Sets `$this->_structured_data`.
78
	 *
79
	 * @return bool `false` if there is no `$this->_data` to structure, otherwise `true`
80
	 */
81
	public function set_structured_data() {
82
		if ( ! isset( $this->_data ) ) {
83
			return false;
84
		}
85
86
		foreach ( $this->get_data() as $value ) {
87
			$type = $value['@type'];
88
89
			switch ( $type ) {
90
				case 'MusicAlbum':
91
				case 'SoftwareApplication':
92
					$type = 'Product';
93
					break;
94
			}
95
96
			$data[ $type ][] = $value;
97
		}
98
99
		foreach ( $data as $type => $value ) {
100
			if ( count( $value ) > 1 ) {
101
				$data[ $type ] = array( '@graph' => $value );
102
			} else {
103
				$data[ $type ] = $value[0];
104
			}
105
106
			$data[ $type ] = apply_filters( 'woocommerce_structured_data_context', array( '@context' => 'http://schema.org/' ), $data, $type, $value ) + $data[ $type ];
107
		}
108
109
		$this->_structured_data = $data;
110
111
		return true;
112
	}
113
114
	/**
115
	 * Gets `$this->_structured_data` if `$requested_types` is valid.
116
	 *
117
	 * @param  mixed $requested_types bool|array (default: false) Array of requested types
118
	 * @return array $structured_data Or empty array if there is no structured data or if `$requested_types` is not valid
119
	 */
120
	public function get_structured_data( $requested_types = false ) {
121
		if ( ! $this->set_structured_data() ) {
122
			return array();
123
		} elseif ( $requested_types && ! is_array( $requested_types ) ) {
124
			return array();
125
		}
126
127
		if ( $requested_types ) {
128
			foreach ( $this->_structured_data as $type => $value ) {
129
				foreach ( $requested_types as $requested_type ) {
130
					if ( $requested_type === $type ) {
131
						$structured_data[] = $value;
132
					}
133
				}
134
			}
135
136
			if ( ! isset( $structured_data ) ) {
137
				return array();
138
			}
139
		} else {
140
			foreach ( $this->_structured_data as $value ) {
141
				$structured_data[] = $value;
142
			}
143
		}
144
145
		if ( count( $structured_data ) > 1 ) {
146
			return $structured_data = array( '@graph' => $structured_data );
0 ignored issues
show
Unused Code introduced by
$structured_data is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
147
		} else {
148
			return $structured_data[0];
149
		}
150
	}
151
152
	/**
153
	 * Sanitizes, encodes and echoes structured data.
154
	 * Hooked into the `wp_footer` action hook.
155
	 * Hooked into the `woocommerce_email_order_details` action hook.
156
	 */
157
	public function enqueue_data( $requested_types = false ) {
158
		if ( $structured_data = $this->sanitize_data( $this->get_structured_data( $requested_types ) ) ) {
159
			// Testing/Debugging
160
			//echo json_encode( $structured_data, JSON_UNESCAPED_SLASHES );
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
161
			
162
			echo '<script type="application/ld+json">' . wp_json_encode( $structured_data ) . '</script>';
163
		}
164
	}
165
166
	/**
167
	 * Sanitizes structured data.
168
	 *
169
	 * @param  array $data
170
	 * @return array $sanitized_data Or empty array if there is no data to sanitize
171
	 */
172
	public function sanitize_data( $data ) {
173
		if ( ! $data ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $data of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
174
			return array();
175
		}
176
177
		foreach ( $data as $key => $value ) {
178
			$sanitized_data[ sanitize_text_field( $key ) ] = is_array( $value ) ? $this->sanitize_data( $value ) : sanitize_text_field( $value );
179
		}
180
181
		return $sanitized_data;
182
	}
183
184
	/**
185
	 * Generates structured data for product categories.
186
	 * Hooked into the `woocommerce_before_shop_loop_item` action hook.
187
	 */
188
	public function generate_product_category_data() {
189
		if ( ! is_product_category() && ! is_shop() ) {
190
			return;
191
		}
192
		
193
		$this->generate_product_data();
194
	}
195
	
196
	/**
197
	 * Generates structured data for single products.
198
	 * Hooked into the `woocommerce_single_product_summary` action hook.
199
	 */
200
	public function generate_product_data() {
201
		global $product;
202
203
		if ( $is_multi_variation = count( $product->get_children() ) > 1 ? true : false ) {
204
			$variations = $product->get_available_variations();
205
		} else {
206
			$variations = array( null );
207
		}
208
209
		foreach ( $variations as $variation ) {
210
			$product_variation = $is_multi_variation ? wc_get_product( $variation['variation_id'] ) : $product;
211
			
212
			$json_offers[] = array(
213
				'@type'         => 'Offer',
214
				'priceCurrency' => get_woocommerce_currency(),
215
				'price'         => $product_variation->get_price(),
216
				'availability'  => 'http://schema.org/' . $stock = ( $product_variation->is_in_stock() ? 'InStock' : 'OutOfStock' ),
217
				'sku'           => $product_variation->get_sku(),
218
				'image'         => wp_get_attachment_url( $product_variation->get_image_id() ),
219
				'description'   => $is_multi_variation ? $product_variation->get_variation_description() : '',
220
				'seller'        => array(
221
					'@type' => 'Organization',
222
					'name'  => get_bloginfo( 'name' ),
223
					'url'   => get_bloginfo( 'url' ),
224
				),
225
			);
226
		}
227
		
228
		if ( $product->is_downloadable() ) {
229
			switch ( $product->download_type ) {
230
				case 'application' :
231
					$type = "SoftwareApplication";
232
					break;
233
				case 'music' :
234
					$type = "MusicAlbum";
235
					break;
236
				default :
237
					$type = "Product";
238
					break;
239
			}
240
		} else {
241
			$type = "Product";
242
		}
243
244
		$json['@type']       = $type;
245
		$json['@id']         = get_the_permalink();
246
		$json['name']        = get_the_title();
247
		$json['description'] = get_the_excerpt();
248
		$json['url']         = get_the_permalink();
249
		$json['offers']      = $json_offers;
250
		
251
		if ( $product->get_rating_count() ) {
252
			$json['aggregateRating'] = array(
253
				'@type'       => 'AggregateRating',
254
				'ratingValue' => $product->get_average_rating(),
255
				'ratingCount' => $product->get_rating_count(),
256
				'reviewCount' => $product->get_review_count(),
257
			);
258
		}
259
260
		$this->set_data( apply_filters( 'woocommerce_structured_data_product', $json, $product ) );
261
	}
262
263
	/**
264
	 * Generates structured data for product reviews.
265
	 * Hooked into the `woocommerce_review_meta` action hook.
266
	 *
267
	 * @param object $comment
268
	 */
269
	public function generate_product_review_data( $comment ) {
270
271
		$json['@type']         = 'Review';
272
		$json['@id']           = get_the_permalink() . '#li-comment-' . get_comment_ID();
273
		$json['datePublished'] = get_comment_date( 'c' );
274
		$json['description']   = get_comment_text();
275
		$json['itemReviewed']  = array(
276
			'@type' => 'Product',
277
			'name'  => get_the_title(),
278
		);
279
		$json['reviewRating']  = array(
280
			'@type'       => 'rating',
281
			'ratingValue' => intval( get_comment_meta( $comment->comment_ID, 'rating', true ) ),
282
		);
283
		$json['author']        = array(
284
			'@type' => 'Person',
285
			'name'  => get_comment_author(),
286
		);
287
		
288
		$this->set_data( apply_filters( 'woocommerce_structured_data_product_review', $json, $comment ) );
289
	}
290
291
	/**
292
	 * Generates structured data for the breadcrumb.
293
	 * Hooked into the `woocommerce_breadcrumb` action hook.
294
	 *
295
	 * @param array $args
296
	 */
297
	public function generate_breadcrumb_data( $args ) {
298
		if ( empty( $args['breadcrumb'] ) ) {
299
			return;
300
		}
301
302
		$breadcrumb = $args['breadcrumb'];
303
		$position   = 1;
304
305
		foreach ( $breadcrumb as $key => $value ) {
306
			if ( ! empty( $value[1] ) && sizeof( $breadcrumb ) !== $key + 1 ) {
307
				$json_crumbs_item = array(
308
					'@id'  => $value[1],
309
					'name' => $value[0],
310
				);
311
			} else {
312
				$json_crumbs_item = array(
313
					'name' => $value[0]
314
				);
315
			}
316
317
			$json_crumbs[] = array(
318
				'@type'    => 'ListItem',
319
				'position' => $position ++,
320
				'item'     => $json_crumbs_item,
321
			);
322
		}
323
324
		$json['@type']           = 'BreadcrumbList';
325
		$json['itemListElement'] = $json_crumbs;
326
327
		$this->set_data( apply_filters( 'woocommerce_structured_data_breadcrumb', $json, $breadcrumb ) );
328
	}
329
330
	/**
331
	 * Generates structured data related to the shop.
332
	 * Hooked into the `woocommerce_before_main_content` action hook.
333
	 */
334
	public function generate_shop_data() {
335
		if ( ! is_shop() || ! is_front_page() ) {
336
			return;
337
		}
338
339
		$json['@type']           = 'WebSite';
340
		$json['name']            = get_bloginfo( 'name' );
341
		$json['url']             = get_bloginfo( 'url' );
342
		$json['potentialAction'] = array(
343
			'@type'       => 'SearchAction',
344
			'target'      => get_bloginfo( 'url' ) . '/?s={search_term_string}&post_type=product',
345
			'query-input' => 'required name=search_term_string',
346
		);
347
348
		$this->set_data( apply_filters( 'woocommerce_structured_data_shop', $json ) );
349
	}
350
	
351
	/**
352
	 * Generates structured data for the email order.
353
	 * Hooked into the `woocommerce_email_order_details` action hook.
354
	 * 
355
	 * @param mixed $order
356
	 * @param bool	$sent_to_admin (default: false)
357
	 * @param bool	$plain_text (default: false)
358
	 */
359
	public function generate_email_order_data( $order, $sent_to_admin = false, $plain_text = false ) {
360
		if ( $plain_text ) {
361
			return;
362
		}
363
364
		foreach ( $order->get_items() as $item ) {
365
			if ( ! apply_filters( 'woocommerce_order_item_visible', true, $item ) ) {
366
				continue;
367
			}
368
369
			$product        = apply_filters( 'woocommerce_order_item_product', $order->get_product_from_item( $item ), $item );
370
			$product_exists = is_object( $product );
371
			$is_visible     = $product_exists && $product->is_visible();
372
			$order_url      = $sent_to_admin ? admin_url( 'post.php?post=' . absint( $order->id ) . '&action=edit' ) : $order->get_view_order_url();
373
374
			$json_offers[]  = array(
375
				'@type'              => 'Offer',
376
				'price'              => $order->get_line_subtotal( $item ),
377
				'priceCurrency'      => $order->get_currency(),
378
				'priceSpecification' => array(
379
					'price'            => $order->get_line_subtotal( $item ),
380
					'priceCurrency'    => $order->get_currency(),
381
					'eligibleQuantity' => array(
382
						'@type' => 'QuantitativeValue',
383
						'value' => apply_filters( 'woocommerce_email_order_item_quantity', $item['qty'], $item ),
384
					),
385
				),
386
				'itemOffered'        => array(
387
					'@type' => 'Product',
388
					'name'  => apply_filters( 'woocommerce_order_item_name', $item['name'], $item, $is_visible ),
389
					'sku'   => $product_exists ? $product->get_sku() : '',
390
					'image' => $product_exists ? wp_get_attachment_image_url( $product->get_image_id() ) : '',
391
					'url'   => $is_visible ? get_permalink( $product->get_id() ) : get_home_url(),
392
				),
393
				'seller'             => array(
394
					'@type' => 'Organization',
395
					'name'  => get_bloginfo( 'name' ),
396
					'url'   => get_bloginfo( 'url' ),
397
				),
398
			);
399
		}
400
401
		switch ( $order->get_status() ) {
402
			case 'pending':
403
				$order_status = 'http://schema.org/OrderPaymentDue';
404
				break;
405
			case 'processing':
406
				$order_status = 'http://schema.org/OrderProcessing';
407
				break;
408
			case 'on-hold':
409
				$order_status = 'http://schema.org/OrderProblem';
410
				break;
411
			case 'completed':
412
				$order_status = 'http://schema.org/OrderDelivered';
413
				break;
414
			case 'cancelled':
415
				$order_status = 'http://schema.org/OrderCancelled';
416
				break;
417
			case 'refunded':
418
				$order_status = 'http://schema.org/OrderReturned';
419
				break;
420
			case 'failed':
421
				$order_status = 'http://schema.org/OrderProblem';
422
				break;
423
		}
424
425
		$json['@type']              = 'Order';
426
		$json['orderStatus']        = $order_status;
427
		$json['orderNumber']        = $order->get_order_number();
428
		$json['orderDate']          = date( 'c', $order->get_date_created() );
429
		$json['url']                = $order_url;
430
		$json['acceptedOffer']      = $json_offers;
431
		$json['discount']           = $order->get_total_discount();
432
		$json['discountCurrency']   = $order->get_currency();
433
		$json['price']              = $order->get_total();
434
		$json['priceCurrency']      = $order->get_currency();
435
		$json['priceSpecification'] = array(
436
			'price'                 => $order->get_total(),
437
			'priceCurrency'         => $order->get_currency(),
438
			'valueAddedTaxIncluded' => true,
439
		);
440
		$json['billingAddress']     = array(
441
			'@type'           => 'PostalAddress',
442
			'name'            => $order->get_formatted_billing_full_name(),
443
			'streetAddress'   => $order->get_billing_address_1(),
444
			'postalCode'      => $order->get_billing_postcode(),
445
			'addressLocality' => $order->get_billing_city(),
446
			'addressRegion'   => $order->get_billing_state(),
447
			'addressCountry'  => $order->get_billing_country(),
448
			'email'           => $order->get_billing_email(),
449
			'telephone'       => $order->get_billing_phone(),
450
		);
451
		$json['customer']           = array(
452
			'@type' => 'Person',
453
			'name'  => $order->get_formatted_billing_full_name(),
454
		);
455
		$json['merchant']           = array(
456
			'@type' => 'Organization',
457
			'name'  => get_bloginfo( 'name' ),
458
			'url'   => get_bloginfo( 'url' ),
459
		);
460
		$json['potentialAction']    = array(
461
			'@type'  => 'ViewAction',
462
			'name'   => 'View Order',
463
			'url'    => $order_url,
464
			'target' => $order_url,
465
		);
466
467
		$this->set_data( apply_filters( 'woocommerce_structured_data_email_order', $json, $sent_to_admin, $order ), true );
468
	}
469
}
470