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

WC_Structured_Data::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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