Completed
Pull Request — master (#11455)
by
unknown
09:43
created

WC_Structured_Data::set_data()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 9
nc 4
nop 2
dl 0
loc 15
rs 8.8571
c 0
b 0
f 0
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    Clément Cazaud <[email protected]>
15
 */
16
class WC_Structured_Data {
17
	
18
	/**
19
	 * @var null|array $_data
20
	 */
21
	private $_data;
22
23
	/**
24
	 * Constructor.
25
	 */
26
	public function __construct() {
27
		// Generate data...
28
		add_action( 'woocommerce_before_main_content',    array( $this, 'generate_website_data' ),              30, 0 );
29
		add_action( 'woocommerce_breadcrumb',             array( $this, 'generate_breadcrumblist_data' ),       10, 1 );
30
		add_action( 'woocommerce_shop_loop',              array( $this, 'generate_product_data' ),              10, 0 );
31
		add_action( 'woocommerce_single_product_summary', array( $this, 'generate_product_data' ),              60, 0 );
32
		add_action( 'woocommerce_review_meta',            array( $this, 'generate_review_data' ),               20, 1 );
33
		add_action( 'woocommerce_email_order_details',    array( $this, 'generate_order_data' ),                20, 3 );
34
		
35
		// Output structured data...
36
		add_action( 'woocommerce_email_order_details',    array( $this, 'output_structured_data' ),             30, 0 );
37
		add_action( 'wp_footer',                          array( $this, 'output_structured_data' ),             10, 0 );
38
39
		// Filters...
40
		add_filter( 'woocommerce_structured_data_product_limit', array( $this, 'limit_product_data_in_loops' ), 10, 1 );
41
	}
42
43
	/**
44
	 * Sets `$this->_data`.
45
	 *
46
	 * @param  array $data
47
	 * @param  bool  $reset (default: false)
48
	 * @return bool
49
	 */
50
	public function set_data( $data, $reset = false ) {
51
		if ( ! isset( $data['@type'] ) ) {
52
			return false;
53
		} elseif ( ! is_string( $data['@type'] ) ) {
54
			return false;
55
		}
56
57
		if ( $reset && isset( $this->_data ) ) {
58
			unset( $this->_data );
59
		}
60
		
61
		$this->_data[] = $data;
62
63
		return true;
64
	}
65
	
66
	/**
67
	 * Gets `$this->_data`.
68
	 *
69
	 * @return array
70
	 */
71
	public function get_data() {
72
		return isset( $this->_data ) ? $this->_data : array();
73
	}
74
75
	/**
76
	 * Structures and returns data.
77
	 *
78
	 * List of types available by default for specific request:
79
	 *
80
	 * 'product',
81
	 * 'review',
82
	 * 'breadcrumblist',
83
	 * 'website',
84
	 * 'order',
85
	 *
86
	 * @param  bool|array|string $requested_types (default: false)
87
	 * @return array
88
	 */
89
	public function get_structured_data( $requested_types = false ) {
90
		$data = $this->get_data();
91
92
		if ( empty( $data ) || ( $requested_types && ! is_array( $requested_types ) && ! is_string( $requested_types ) || is_null( $requested_types ) ) ) {
93
			return array();
94
		} elseif ( $requested_types && is_string( $requested_types ) ) {
95
			$requested_types = array( $requested_types );
96
		}
97
98
		// Put together the values of same type of structured data.
99
		foreach ( $data as $value ) {
100
			$structured_data[ strtolower( $value['@type'] ) ][] = $value;
101
		}
102
103
		// Wrap the multiple values of each type inside a graph... Then add context to each type.
104
		foreach ( $structured_data as $type => $value ) {
105
			$structured_data[ $type ] = count( $value ) > 1 ? array( '@graph' => $value ) : $value[0];
106
			$structured_data[ $type ] = apply_filters( 'woocommerce_structured_data_context', array( '@context' => 'http://schema.org/' ), $structured_data, $type, $value ) + $structured_data[ $type ];
107
		}
108
109
		// If requested types, pick them up... Finally change the associative array to an indexed one.
110
		$structured_data = $requested_types ? array_values( array_intersect_key( $structured_data, array_flip( $requested_types ) ) ) : array_values( $structured_data );
111
112
		if ( ! empty( $structured_data ) ) {
113
			$structured_data = count( $structured_data ) > 1 ?  array( '@graph' => $structured_data ) : $structured_data[0];
114
		}
115
116
		return $structured_data;
117
	}
118
119
	/**
120
	 * Sanitizes, encodes and outputs structured data.
121
	 * 
122
	 * Hooked into `wp_footer` action hook.
123
	 * Hooked into `woocommerce_email_order_details` action hook.
124
	 *
125
	 * @param  bool|array|string $requested_types (default: true)
126
	 * @return bool
127
	 */
128
	public function output_structured_data( $requested_types = true ) {
129
		if ( $requested_types === true ) {
130
			$requested_types = array_filter( apply_filters( 'woocommerce_structured_data_type_for_page', array(
131
				  is_shop() || is_product_category() || is_product() ? 'product'        : null,
132
				  is_shop() && is_front_page()                       ? 'website'        : null,
133
				  is_product()                                       ? 'review'         : null,
134
				! is_shop()                                          ? 'breadcrumblist' : null,
135
				                                                       'order',
136
			) ) );
137
		}
138
139
		if ( $structured_data = $this->sanitize_data( $this->get_structured_data( $requested_types ) ) ) {
140
			echo '<script type="application/ld+json">' . wp_json_encode( $structured_data ) . '</script>';
141
			
142
			return true;
143
		} else {
144
			return false;
145
		}	
146
	}
147
148
	/**
149
	 * Sanitizes data.
150
	 *
151
	 * @param  array $data
152
	 * @return array
153
	 */
154
	public function sanitize_data( $data ) {
155
		if ( ! $data || ! is_array( $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...
156
			return array();
157
		}
158
159
		foreach ( $data as $key => $value ) {
160
			$sanitized_data[ sanitize_text_field( $key ) ] = is_array( $value ) ? $this->sanitize_data( $value ) : sanitize_text_field( $value );
161
		}
162
163
		return $sanitized_data;
164
	}
165
166
	/**
167
	 * Generates, sanitizes, encodes and outputs specific structured data type.
168
	 *
169
	 * @param  string $type
170
	 * @param  mixed  $object  (default: null)
171
	 * @param  mixed  $param_1 (default: null)
172
	 * @param  mixed  $param_2 (default: null)
173
	 * @param  mixed  $param_3 (default: null)
174
	 * @return bool
175
	 */
176
	public function generate_output_structured_data( $type, $object = null, $param_1 = null, $param_2 = null, $param_3 = null ) {
177
		if ( ! is_string( $type ) ) {
178
			return false;
179
		}
180
		
181
		$generate = 'generate_' . $type . '_data';
182
183
		if ( method_exists( $this, $generate ) && $this->$generate( $object, $param_1, $param_2, $param_3 ) ) {
184
			return $this->output_structured_data( $type );
185
		} else {
186
			return false;
187
		}
188
	}
189
190
	/**
191
	 * Limits Product structured data on taxonomies and shop page.
192
	 *
193
	 * Hooked into `woocommerce_structured_data_product_limit` filter hook.
194
	 *
195
	 * @param  bool $limit_data
196
	 * @return bool $limit_data
197
	 */
198
	public function limit_product_data_in_loops( $limit_data ) {
0 ignored issues
show
Unused Code introduced by
The parameter $limit_data is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
199
		return $limit_data = is_product_taxonomy() || is_shop() ? true : false;
0 ignored issues
show
Unused Code introduced by
$limit_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...
200
	}
201
202
	/*
203
	|--------------------------------------------------------------------------
204
	| Generators
205
	|--------------------------------------------------------------------------
206
	|
207
	| Methods for generating specific structured data types:
208
	|
209
	| - Product
210
	| - Review
211
	| - BreadcrumbList
212
	| - WebSite
213
	| - Order
214
	|
215
	| The generated data is stored into `$this->_data`.
216
	| See the methods above for handling `$this->_data`.
217
	|
218
	*/
219
220
	/**
221
	 * Generates Product structured data.
222
	 *
223
	 * Hooked into `woocommerce_single_product_summary` action hook.
224
	 * Hooked into `woocommerce_shop_loop` action hook.
225
	 *
226
	 * @param  bool|object $product    (default: false)
227
	 * @param  bool        $limit_data (default: false)
228
	 * @return bool
229
	 */
230
	public function generate_product_data( $product = false, $limit_data = false ) {
231
		if ( $product === false ) {
232
			global $product;
233
		}
234
235
		if ( ! is_object( $product ) ) {
236
			return false;
237
		}
238
239
		$limit_data = apply_filters( 'woocommerce_structured_data_product_limit', $limit_data );
240
		
241
		$markup['@type'] = 'Product';
242
		$markup['@id']   = get_permalink( $product->get_id() );
243
		$markup['url']   = $markup['@id'];
244
		$markup['name']  = $product->get_title();
245
246
		if ( $limit_data ) {
247
			return $this->set_data( apply_filters( 'woocommerce_structured_data_product_limited', $markup, $product ) );
248
		}
249
250
		if ( $is_variable = $product->is_type( 'variable' ) ) {
251
			$variations = $product->get_available_variations();
252
			
253
			foreach ( $variations as $variation ) {
254
				$product_variations[] = wc_get_product( $variation['variation_id'] );
255
			}
256
		} else {
257
			$product_variations[] = $product;
258
		}
259
260
		foreach ( $product_variations as $product_variation ) {
261
			$markup_offers[] = array(
262
				'@type'         => 'Offer',
263
				'priceCurrency' => get_woocommerce_currency(),
264
				'price'         => $product_variation->get_price(),
265
				'availability'  => 'http://schema.org/' . $stock = ( $product_variation->is_in_stock() ? 'InStock' : 'OutOfStock' ),
266
				'sku'           => $product_variation->get_sku(),
267
				'image'         => wp_get_attachment_url( $product_variation->get_image_id() ),
268
				'description'   => $is_variable ? $product_variation->get_variation_description() : '',
269
				'seller'        => array(
270
					'@type' => 'Organization',
271
					'name'  => get_bloginfo( 'name' ),
272
					'url'   => get_bloginfo( 'url' ),
273
				),
274
			);
275
		}
276
		
277
		$markup['description'] = get_the_excerpt( $product->get_id() );
278
		$markup['offers']      = $markup_offers;
279
		
280
		if ( $product->get_rating_count() ) {
281
			$markup['aggregateRating'] = array(
282
				'@type'       => 'AggregateRating',
283
				'ratingValue' => $product->get_average_rating(),
284
				'ratingCount' => $product->get_rating_count(),
285
				'reviewCount' => $product->get_review_count(),
286
			);
287
		}
288
289
		return $this->set_data( apply_filters( 'woocommerce_structured_data_product', $markup, $product ) );
290
	}
291
292
	/**
293
	 * Generates Review structured data.
294
	 *
295
	 * Hooked into `woocommerce_review_meta` action hook.
296
	 *
297
	 * @param  object $comment
298
	 * @return bool
299
	 */
300
	public function generate_review_data( $comment ) {
301
		if ( ! is_object( $comment ) ) {
302
			return false;
303
		}
304
305
		$markup['@type']         = 'Review';
306
		$markup['@id']           = get_comment_link( $comment->comment_ID );
307
		$markup['datePublished'] = get_comment_date( 'c', $comment->comment_ID );
308
		$markup['description']   = get_comment_text( $comment->comment_ID );
309
		$markup['itemReviewed']  = array(
310
			'@type' => 'Product',
311
			'name'  => get_the_title( $comment->post_ID ),
312
		);
313
		$markup['reviewRating']  = array(
314
			'@type'       => 'rating',
315
			'ratingValue' => get_comment_meta( $comment->comment_ID, 'rating', true ),
316
		);
317
		$markup['author']        = array(
318
			'@type' => 'Person',
319
			'name'  => get_comment_author( $comment->comment_ID ),
320
		);
321
		
322
		return $this->set_data( apply_filters( 'woocommerce_structured_data_review', $markup, $comment ) );
323
	}
324
325
	/**
326
	 * Generates BreadcrumbList structured data.
327
	 *
328
	 * Hooked into `woocommerce_breadcrumb` action hook.
329
	 *
330
	 * @param  object $breadcrumbs
331
	 * @return bool|void
332
	 */
333
	public function generate_breadcrumblist_data( $breadcrumbs ) {
334
		if ( ! is_object( $breadcrumbs ) ) {
335
			return false;
336
		} elseif ( ! $crumbs = $breadcrumbs->get_breadcrumb() ) {
337
			return;
338
		}
339
340
		foreach ( $crumbs as $key => $crumb ) {
341
			$markup_crumbs[ $key ] = array(
342
				'@type'    => 'ListItem',
343
				'position' => $key + 1,
344
				'item'     => array(
345
					'name' => $crumb[0],
346
				),
347
			);
348
349
			if ( ! empty( $crumb[1] ) && sizeof( $crumbs ) !== $key + 1 ) {
350
				$markup_crumbs[ $key ]['item'] += array( '@id' => $crumb[1] );
351
			}
352
		}
353
354
		$markup['@type']           = 'BreadcrumbList';
355
		$markup['itemListElement'] = $markup_crumbs;
356
357
		return $this->set_data( apply_filters( 'woocommerce_structured_data_breadcrumblist', $markup, $breadcrumbs ) );
358
	}
359
360
	/**
361
	 * Generates WebSite structured data.
362
	 *
363
	 * Hooked into `woocommerce_before_main_content` action hook.
364
	 *
365
	 * @return bool
366
	 */
367
	public function generate_website_data() {
368
		$markup['@type']           = 'WebSite';
369
		$markup['name']            = get_bloginfo( 'name' );
370
		$markup['url']             = get_bloginfo( 'url' );
371
		$markup['potentialAction'] = array(
372
			'@type'       => 'SearchAction',
373
			'target'      => get_bloginfo( 'url' ) . '/?s={search_term_string}&post_type=product',
374
			'query-input' => 'required name=search_term_string',
375
		);
376
377
		return $this->set_data( apply_filters( 'woocommerce_structured_data_website', $markup ) );
378
	}
379
	
380
	/**
381
	 * Generates Order structured data.
382
	 *
383
	 * Hooked into `woocommerce_email_order_details` action hook.
384
	 *
385
	 * @param  object    $order
386
	 * @param  bool	     $sent_to_admin (default: false)
387
	 * @param  bool	     $plain_text (default: false)
388
	 * @return bool|void
389
	 */
390
	public function generate_order_data( $order, $sent_to_admin = false, $plain_text = false ) {
391
		if ( ! is_object( $order ) ) {
392
			return false;
393
		} elseif ( $plain_text ) {
394
			return;
395
		}
396
397
		foreach ( $order->get_items() as $item ) {
398
			if ( ! apply_filters( 'woocommerce_order_item_visible', true, $item ) ) {
399
				continue;
400
			}
401
402
			$product        = apply_filters( 'woocommerce_order_item_product', $order->get_product_from_item( $item ), $item );
403
			$product_exists = is_object( $product );
404
			$is_visible     = $product_exists && $product->is_visible();
405
			$order_url      = $sent_to_admin ? admin_url( 'post.php?post=' . absint( $order->id ) . '&action=edit' ) : $order->get_view_order_url();
406
407
			$markup_offers[]  = array(
408
				'@type'              => 'Offer',
409
				'price'              => $order->get_line_subtotal( $item ),
410
				'priceCurrency'      => $order->get_currency(),
411
				'priceSpecification' => array(
412
					'price'            => $order->get_line_subtotal( $item ),
413
					'priceCurrency'    => $order->get_currency(),
414
					'eligibleQuantity' => array(
415
						'@type' => 'QuantitativeValue',
416
						'value' => apply_filters( 'woocommerce_email_order_item_quantity', $item['qty'], $item ),
417
					),
418
				),
419
				'itemOffered'        => array(
420
					'@type' => 'Product',
421
					'name'  => apply_filters( 'woocommerce_order_item_name', $item['name'], $item, $is_visible ),
422
					'sku'   => $product_exists ? $product->get_sku() : '',
423
					'image' => $product_exists ? wp_get_attachment_image_url( $product->get_image_id() ) : '',
424
					'url'   => $is_visible ? get_permalink( $product->get_id() ) : get_home_url(),
425
				),
426
				'seller'             => array(
427
					'@type' => 'Organization',
428
					'name'  => get_bloginfo( 'name' ),
429
					'url'   => get_bloginfo( 'url' ),
430
				),
431
			);
432
		}
433
434
		switch ( $order->get_status() ) {
435
			case 'pending':
436
				$order_status = 'http://schema.org/OrderPaymentDue';
437
				break;
438
			case 'processing':
439
				$order_status = 'http://schema.org/OrderProcessing';
440
				break;
441
			case 'on-hold':
442
				$order_status = 'http://schema.org/OrderProblem';
443
				break;
444
			case 'completed':
445
				$order_status = 'http://schema.org/OrderDelivered';
446
				break;
447
			case 'cancelled':
448
				$order_status = 'http://schema.org/OrderCancelled';
449
				break;
450
			case 'refunded':
451
				$order_status = 'http://schema.org/OrderReturned';
452
				break;
453
			case 'failed':
454
				$order_status = 'http://schema.org/OrderProblem';
455
				break;
456
		}
457
458
		$markup['@type']              = 'Order';
459
		$markup['url']                = $order_url;
460
		$markup['orderStatus']        = $order_status;
461
		$markup['orderNumber']        = $order->get_order_number();
462
		$markup['orderDate']          = date( 'c', $order->get_date_created() );
463
		$markup['acceptedOffer']      = $markup_offers;
464
		$markup['discount']           = $order->get_total_discount();
465
		$markup['discountCurrency']   = $order->get_currency();
466
		$markup['price']              = $order->get_total();
467
		$markup['priceCurrency']      = $order->get_currency();
468
		$markup['priceSpecification'] = array(
469
			'price'                 => $order->get_total(),
470
			'priceCurrency'         => $order->get_currency(),
471
			'valueAddedTaxIncluded' => true,
472
		);
473
		$markup['billingAddress']     = array(
474
			'@type'           => 'PostalAddress',
475
			'name'            => $order->get_formatted_billing_full_name(),
476
			'streetAddress'   => $order->get_billing_address_1(),
477
			'postalCode'      => $order->get_billing_postcode(),
478
			'addressLocality' => $order->get_billing_city(),
479
			'addressRegion'   => $order->get_billing_state(),
480
			'addressCountry'  => $order->get_billing_country(),
481
			'email'           => $order->get_billing_email(),
482
			'telephone'       => $order->get_billing_phone(),
483
		);
484
		$markup['customer']           = array(
485
			'@type' => 'Person',
486
			'name'  => $order->get_formatted_billing_full_name(),
487
		);
488
		$markup['merchant']           = array(
489
			'@type' => 'Organization',
490
			'name'  => get_bloginfo( 'name' ),
491
			'url'   => get_bloginfo( 'url' ),
492
		);
493
		$markup['potentialAction']    = array(
494
			'@type'  => 'ViewAction',
495
			'name'   => 'View Order',
496
			'url'    => $order_url,
497
			'target' => $order_url,
498
		);
499
500
		return $this->set_data( apply_filters( 'woocommerce_structured_data_order', $markup, $sent_to_admin, $order ), true );
501
	}
502
}
503