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

WC_Structured_Data::generate_product_data()   C

Complexity

Conditions 10
Paths 74

Size

Total Lines 52
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Importance

Changes 7
Bugs 1 Features 0
Metric Value
cc 10
eloc 36
c 7
b 1
f 0
nc 74
nop 1
dl 0
loc 52
rs 6.2553

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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