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

WC_Structured_Data::generate_product_data()   C

Complexity

Conditions 9
Paths 74

Size

Total Lines 48
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

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