Completed
Pull Request — master (#11455)
by
unknown
08:14
created

WC_Structured_Data::generate_breadcrumb_data()   B

Complexity

Conditions 6
Paths 7

Size

Total Lines 27
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 1 Features 0
Metric Value
cc 6
eloc 16
c 5
b 1
f 0
nc 7
nop 1
dl 0
loc 27
rs 8.439
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
 * @since     2.7.0
12
 * @version   2.7.0
13
 * @package   WooCommerce/Classes
14
 * @author    Clement Cazaud <[email protected]>
15
 */
16
class WC_Structured_Data {
17
	
18
	/**
19
	 * @var null|array $_data Partially structured data from `generate_*` methods
20
	 */
21
	private $_data;
22
23
	/**
24
	 * @var null|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_website_data' ),        30, 0 );
34
		add_action( 'woocommerce_breadcrumb',             array( $this, 'generate_breadcrumb_data' ),     10, 1 );
35
		add_action( 'woocommerce_shop_loop',              array( $this, 'generate_product_data' ),        10, 0 );
36
		add_action( 'woocommerce_single_product_summary', array( $this, 'generate_product_data' ),        60, 0 );
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, 3 );
39
		// Enqueue structured data...
40
		add_action( 'woocommerce_email_order_details',    array( $this, 'enqueue_data' ),                 30, 0 );
41
		add_action( 'wp_footer',                          array( $this, 'enqueue_data_type_for_page' ),   10, 0 );
42
	}
43
44
	/**
45
	 * Sets `$this->_data`.
46
	 *
47
	 * @param  array $data
48
	 * @param  bool $reset (default: false)
49
	 * @return bool
50
	 */
51
	public function set_data( $data, $reset = false ) {
52
		if ( ! is_array( $data ) || ! array_key_exists( '@type', $data ) ) {
53
			return false;
54
		}
55
56
		if ( $reset && isset( $this->_data ) ) {
57
			unset( $this->_data );
58
		}
59
		
60
		$this->_data[] = $data;
61
62
		return true;
63
	}
64
	
65
	/**
66
	 * Gets `$this->_data`.
67
	 *
68
	 * @return array $data
69
	 */
70
	public function get_data() {
71
		return $data = isset( $this->_data ) ? $this->_data : array();
0 ignored issues
show
Unused Code introduced by
$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...
72
	}
73
74
	/**
75
	 * Sets `$this->_structured_data`.
76
	 *
77
	 * @return bool
78
	 */
79
	public function set_structured_data() {
80
		if ( empty( $this->get_data() ) ) {
81
			return false;
82
		}
83
84
		foreach ( $this->get_data() as $value ) {
85
			$structured_data[ $value['@type'] ][] = $value;
86
		}
87
88
		foreach ( $structured_data as $type => $value ) {
89
			if ( count( $value ) > 1 ) {
90
				$structured_data[ $type ] = array( '@graph' => $value );
91
			} else {
92
				$structured_data[ $type ] = $value[0];
93
			}
94
95
			$structured_data[ $type ] = apply_filters( 'woocommerce_structured_data_context', array( '@context' => 'http://schema.org/' ), $structured_data, $type, $value ) + $structured_data[ $type ];
96
		}
97
98
		$this->_structured_data = $structured_data;
99
100
		return true;
101
	}
102
103
	/**
104
	 * Gets `$this->_structured_data`.
105
	 * 
106
	 * @return array $structured_data
107
	 */
108
	public function get_structured_data() {
109
		$this->set_structured_data();
110
111
		return $structured_data = isset( $this->_structured_data ) ? $this->_structured_data : array();
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...
112
	}
113
114
	/**
115
	 * Sanitizes, encodes and echoes structured data.
116
	 * 
117
	 * List of the types available by default for specific request:
118
	 * 'Product',
119
	 * 'Review',
120
	 * 'BreadcrumbList',
121
	 * 'WebSite',
122
	 * 'Order',
123
	 *
124
	 * @uses   `woocommerce_email_order_details` action hook
125
	 * @param  bool|array $requested_types (default: false)
126
	 * @return bool
127
	 */
128
	public function enqueue_data( $requested_types = false ) {
129
		if ( ! $structured_data = $this->get_structured_data() ) {
130
			return false;
131
		}
132
133
		if ( $requested_types ) {
134
			if ( ! is_array( $requested_types ) ) {
135
				return false;
136
			}
137
138
			foreach ( $structured_data as $type => $value ) {
139
				foreach ( $requested_types as $requested_type ) {
140
					if ( $requested_type === $type ) {
141
						$json[] = $value;
142
					}
143
				}
144
			}
145
146
			if ( ! isset( $json ) ) {
147
				return false;
148
			}
149
		} else {
150
			foreach ( $structured_data as $value ) {
151
				$json[] = $value;
152
			}
153
		}
154
155
		if ( count( $json ) > 1 ) {
156
			$json = array( '@graph' => $json );
157
		} else {
158
			$json = $json[0];
159
		}
160
161
		if ( $json = $this->sanitize_data( $json ) ) {
162
			// Testing/Debugging
163
			//echo json_encode( $json, 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...
164
			echo '<script type="application/ld+json">' . wp_json_encode( $json ) . '</script>';
165
166
			return true;
167
		}
168
		
169
		return false;
170
	}
171
172
	/**
173
	 * Sanitizes, encodes and echoes specific structured data type on scpecific page.
174
	 * 
175
	 * @uses   `wp_footer` action hook
176
	 * @return bool
177
	 */
178
	public function enqueue_data_type_for_page() {
179
		$requested_types = apply_filters( 'woocommerce_structured_data_type_for_page', array(
180
			is_shop() && is_front_page() ? 'WebSite' : null,
181
			                               'BreadcrumbList',
182
			                               'Product',
183
			                               'Review',
184
		) );
185
186
		return $this->enqueue_data( $requested_types );
187
	}
188
189
	/**
190
	 * Sanitizes data.
191
	 *
192
	 * @param  array $data
193
	 * @return array
194
	 */
195
	public function sanitize_data( $data ) {
196
		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...
197
			return array();
198
		}
199
200
		foreach ( $data as $key => $value ) {
201
			$sanitized_data[ sanitize_text_field( $key ) ] = is_array( $value ) ? $this->sanitize_data( $value ) : sanitize_text_field( $value );
202
		}
203
204
		return $sanitized_data;
205
	}
206
207
	/**
208
	 * Generates Product structured data.
209
	 *
210
	 * @uses   `woocommerce_single_product_summary` action hook
211
	 * @uses   `woocommerce_shop_loop` action hook
212
	 * @param  bool|object $product (default: false)
213
	 * @return bool
214
	 */
215
	public function generate_product_data( $product = false ) {
216
		if ( ! $product ) {
217
			global $product;
218
		}
219
220
		if ( ! is_object( $product ) ) {
221
			return false;
222
		}
223
224
		if ( $is_multi_variation = count( $product->get_children() ) > 1 ? true : false ) {
225
			$variations = $product->get_available_variations();
226
		} else {
227
			$variations = array( null );
228
		}
229
230
		foreach ( $variations as $variation ) {
231
			$product_variation = $is_multi_variation ? wc_get_product( $variation['variation_id'] ) : $product;
232
			
233
			$markup_offers[] = array(
234
				'@type'         => 'Offer',
235
				'priceCurrency' => get_woocommerce_currency(),
236
				'price'         => $product_variation->get_price(),
237
				'availability'  => 'http://schema.org/' . $stock = ( $product_variation->is_in_stock() ? 'InStock' : 'OutOfStock' ),
238
				'sku'           => $product_variation->get_sku(),
239
				'image'         => wp_get_attachment_url( $product_variation->get_image_id() ),
240
				'description'   => $is_multi_variation ? $product_variation->get_variation_description() : '',
241
				'seller'        => array(
242
					'@type' => 'Organization',
243
					'name'  => get_bloginfo( 'name' ),
244
					'url'   => get_bloginfo( 'url' ),
245
				),
246
			);
247
		}
248
		
249
		$markup['@type']       = 'Product';
250
		$markup['@id']         = get_the_permalink();
251
		$markup['name']        = get_the_title();
252
		$markup['description'] = get_the_excerpt();
253
		$markup['url']         = get_the_permalink();
254
		$markup['offers']      = $markup_offers;
255
		
256
		if ( $product->get_rating_count() ) {
257
			$markup['aggregateRating'] = array(
258
				'@type'       => 'AggregateRating',
259
				'ratingValue' => $product->get_average_rating(),
260
				'ratingCount' => $product->get_rating_count(),
261
				'reviewCount' => $product->get_review_count(),
262
			);
263
		}
264
265
		return $this->set_data( apply_filters( 'woocommerce_structured_data_product', $markup, $product ) );
266
	}
267
268
	/**
269
	 * Generates Product Review structured data.
270
	 *
271
	 * @uses   `woocommerce_review_meta` action hook
272
	 * @param  object $comment
273
	 * @return bool
274
	 */
275
	public function generate_product_review_data( $comment ) {
276
		if ( ! is_object( $comment ) ) {
277
			return false;
278
		}
279
280
		$markup['@type']         = 'Review';
281
		$markup['@id']           = get_the_permalink() . '#li-comment-' . get_comment_ID();
282
		$markup['datePublished'] = get_comment_date( 'c' );
283
		$markup['description']   = get_comment_text();
284
		$markup['itemReviewed']  = array(
285
			'@type' => 'Product',
286
			'name'  => get_the_title(),
287
		);
288
		$markup['reviewRating']  = array(
289
			'@type'       => 'rating',
290
			'ratingValue' => intval( get_comment_meta( $comment->comment_ID, 'rating', true ) ),
291
		);
292
		$markup['author']        = array(
293
			'@type' => 'Person',
294
			'name'  => get_comment_author(),
295
		);
296
		
297
		return $this->set_data( apply_filters( 'woocommerce_structured_data_product_review', $markup, $comment ) );
298
	}
299
300
	/**
301
	 * Generates BreadcrumbList structured data.
302
	 *
303
	 * @uses   `woocommerce_breadcrumb` action hook
304
	 * @param  array $breadcrumb
305
	 * @return bool|void
306
	 */
307
	public function generate_breadcrumb_data( $breadcrumb ) {
308
		if ( ! is_array( $breadcrumb ) ) {
309
			return false;
310
		}
311
		
312
		if ( empty( $breadcrumb = $breadcrumb['breadcrumb'] ) ) {
313
			return;
314
		}
315
316
		$position = 1;
317
318
		foreach ( $breadcrumb as $key => $value ) {
319
			$markup_crumbs[] = array(
320
				'@type'    => 'ListItem',
321
				'position' => $position ++,
322
				'item'     => array(
323
					'@id'  => ! empty( $value[1] ) && sizeof( $breadcrumb ) !== $key + 1 ? $value[1] : '#',
324
					'name' => $value[0],
325
				),
326
			);
327
		}
328
329
		$markup['@type']           = 'BreadcrumbList';
330
		$markup['itemListElement'] = $markup_crumbs;
331
332
		return $this->set_data( apply_filters( 'woocommerce_structured_data_breadcrumb', $markup, $breadcrumb ) );
333
	}
334
335
	/**
336
	 * Generates WebSite structured data.
337
	 *
338
	 * @uses  `woocommerce_before_main_content` action hook
339
	 * @return bool
340
	 */
341
	public function generate_website_data() {
342
		$markup['@type']           = 'WebSite';
343
		$markup['name']            = get_bloginfo( 'name' );
344
		$markup['url']             = get_bloginfo( 'url' );
345
		$markup['potentialAction'] = array(
346
			'@type'       => 'SearchAction',
347
			'target'      => get_bloginfo( 'url' ) . '/?s={search_term_string}&post_type=product',
348
			'query-input' => 'required name=search_term_string',
349
		);
350
351
		return $this->set_data( apply_filters( 'woocommerce_structured_data_website', $markup ) );
352
	}
353
	
354
	/**
355
	 * Generates Email Order structured data.
356
	 *
357
	 * @uses   `woocommerce_email_order_details` action hook
358
	 * @param  object $order
359
	 * @param  bool	$sent_to_admin (default: false)
360
	 * @param  bool	$plain_text (default: false)
361
	 * @return bool|void
362
	 */
363
	public function generate_email_order_data( $order, $sent_to_admin = false, $plain_text = false ) {
364
		if ( ! is_object( $order ) ) {
365
			return false;
366
		}
367
		
368
		if ( $plain_text ) {
369
			return;
370
		}
371
372
		foreach ( $order->get_items() as $item ) {
373
			if ( ! apply_filters( 'woocommerce_order_item_visible', true, $item ) ) {
374
				continue;
375
			}
376
377
			$product        = apply_filters( 'woocommerce_order_item_product', $order->get_product_from_item( $item ), $item );
378
			$product_exists = is_object( $product );
379
			$is_visible     = $product_exists && $product->is_visible();
380
			$order_url      = $sent_to_admin ? admin_url( 'post.php?post=' . absint( $order->id ) . '&action=edit' ) : $order->get_view_order_url();
381
382
			$markup_offers[]  = array(
383
				'@type'              => 'Offer',
384
				'price'              => $order->get_line_subtotal( $item ),
385
				'priceCurrency'      => $order->get_currency(),
386
				'priceSpecification' => array(
387
					'price'            => $order->get_line_subtotal( $item ),
388
					'priceCurrency'    => $order->get_currency(),
389
					'eligibleQuantity' => array(
390
						'@type' => 'QuantitativeValue',
391
						'value' => apply_filters( 'woocommerce_email_order_item_quantity', $item['qty'], $item ),
392
					),
393
				),
394
				'itemOffered'        => array(
395
					'@type' => 'Product',
396
					'name'  => apply_filters( 'woocommerce_order_item_name', $item['name'], $item, $is_visible ),
397
					'sku'   => $product_exists ? $product->get_sku() : '',
398
					'image' => $product_exists ? wp_get_attachment_image_url( $product->get_image_id() ) : '',
399
					'url'   => $is_visible ? get_permalink( $product->get_id() ) : get_home_url(),
400
				),
401
				'seller'             => array(
402
					'@type' => 'Organization',
403
					'name'  => get_bloginfo( 'name' ),
404
					'url'   => get_bloginfo( 'url' ),
405
				),
406
			);
407
		}
408
409
		switch ( $order->get_status() ) {
410
			case 'pending':
411
				$order_status = 'http://schema.org/OrderPaymentDue';
412
				break;
413
			case 'processing':
414
				$order_status = 'http://schema.org/OrderProcessing';
415
				break;
416
			case 'on-hold':
417
				$order_status = 'http://schema.org/OrderProblem';
418
				break;
419
			case 'completed':
420
				$order_status = 'http://schema.org/OrderDelivered';
421
				break;
422
			case 'cancelled':
423
				$order_status = 'http://schema.org/OrderCancelled';
424
				break;
425
			case 'refunded':
426
				$order_status = 'http://schema.org/OrderReturned';
427
				break;
428
			case 'failed':
429
				$order_status = 'http://schema.org/OrderProblem';
430
				break;
431
		}
432
433
		$markup['@type']              = 'Order';
434
		$markup['orderStatus']        = $order_status;
435
		$markup['orderNumber']        = $order->get_order_number();
436
		$markup['orderDate']          = date( 'c', $order->get_date_created() );
437
		$markup['url']                = $order_url;
438
		$markup['acceptedOffer']      = $markup_offers;
439
		$markup['discount']           = $order->get_total_discount();
440
		$markup['discountCurrency']   = $order->get_currency();
441
		$markup['price']              = $order->get_total();
442
		$markup['priceCurrency']      = $order->get_currency();
443
		$markup['priceSpecification'] = array(
444
			'price'                 => $order->get_total(),
445
			'priceCurrency'         => $order->get_currency(),
446
			'valueAddedTaxIncluded' => true,
447
		);
448
		$markup['billingAddress']     = array(
449
			'@type'           => 'PostalAddress',
450
			'name'            => $order->get_formatted_billing_full_name(),
451
			'streetAddress'   => $order->get_billing_address_1(),
452
			'postalCode'      => $order->get_billing_postcode(),
453
			'addressLocality' => $order->get_billing_city(),
454
			'addressRegion'   => $order->get_billing_state(),
455
			'addressCountry'  => $order->get_billing_country(),
456
			'email'           => $order->get_billing_email(),
457
			'telephone'       => $order->get_billing_phone(),
458
		);
459
		$markup['customer']           = array(
460
			'@type' => 'Person',
461
			'name'  => $order->get_formatted_billing_full_name(),
462
		);
463
		$markup['merchant']           = array(
464
			'@type' => 'Organization',
465
			'name'  => get_bloginfo( 'name' ),
466
			'url'   => get_bloginfo( 'url' ),
467
		);
468
		$markup['potentialAction']    = array(
469
			'@type'  => 'ViewAction',
470
			'name'   => 'View Order',
471
			'url'    => $order_url,
472
			'target' => $order_url,
473
		);
474
475
		return $this->set_data( apply_filters( 'woocommerce_structured_data_email_order', $markup, $sent_to_admin, $order ), true );
476
	}
477
}
478