Completed
Pull Request — master (#11455)
by
unknown
07:51
created

WC_Structured_Data::generate_review_data()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 24
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 2
eloc 17
nc 2
nop 1
dl 0
loc 24
rs 8.9713
c 1
b 0
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
	 * 'product',
77
	 * 'review',
78
	 * 'breadcrumblist',
79
	 * 'website',
80
	 * 'order',
81
	 *
82
	 * @param  bool|array|string $requested_types (default: false)
83
	 * @return array
84
	 */
85
	public function get_structured_data( $requested_types = false ) {
86
		if ( empty( $this->get_data() ) || ( $requested_types && ! is_array( $requested_types ) && ! is_string( $requested_types ) || is_null( $requested_types ) ) ) {
87
			return array();
88
		}
89
90
		foreach ( $this->get_data() as $value ) {
91
			$type = strtolower( $value['@type'] );
92
			$data[ $type ][] = $value;
93
		}
94
95
		foreach ( $data as $type => $value ) {
96
			$data[ $type ] = count( $value ) > 1 ? array( '@graph' => $value ) : $value[0];
97
			$data[ $type ] = apply_filters( 'woocommerce_structured_data_context', array( '@context' => 'http://schema.org/' ), $data, $type, $value ) + $data[ $type ];
98
		}
99
100
		if ( $requested_types ) {
101
			if ( is_string( $requested_types ) ) {
102
				$requested_types = array( $requested_types );
103
			}
104
105
			foreach ( $data as $type => $value ) {
106
				foreach ( $requested_types as $requested_type ) {
0 ignored issues
show
Bug introduced by
The expression $requested_types of type boolean|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

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