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
|
|
|
* @version 2.7.0 |
12
|
|
|
* @package WooCommerce/Classes |
13
|
|
|
* @category Class |
14
|
|
|
* @author Clement Cazaud |
15
|
|
|
*/ |
16
|
|
|
class WC_Structured_Data { |
17
|
|
|
|
18
|
|
|
/** |
19
|
|
|
* @var array Partially structured data |
20
|
|
|
*/ |
21
|
|
|
private $data; |
22
|
|
|
|
23
|
|
|
/** |
24
|
|
|
* Checks if the passed $json variable is an array and stores it into $this->data... |
25
|
|
|
* |
26
|
|
|
* @param array $json Partially structured data |
27
|
|
|
* @return bool Returns false If the param $json is not an array |
28
|
|
|
*/ |
29
|
|
|
private function set_data( $json ) { |
30
|
|
|
if ( ! is_array( $json ) ) { |
31
|
|
|
return false; |
32
|
|
|
} |
33
|
|
|
|
34
|
|
|
$this->data[] = $json; |
35
|
|
|
} |
36
|
|
|
|
37
|
|
|
/** |
38
|
|
|
* Structures and returns the data... |
39
|
|
|
* |
40
|
|
|
* @return array If data is set, returns the structured data, otherwise returns empty array |
41
|
|
|
*/ |
42
|
|
|
private function get_data() { |
43
|
|
|
if ( ! $this->data ) { |
|
|
|
|
44
|
|
|
return array(); |
45
|
|
|
} |
46
|
|
|
|
47
|
|
|
foreach ( $this->data as $value ) { |
48
|
|
|
$type = isset( $value['@type'] ) ? $value['@type'] : false; |
49
|
|
|
|
50
|
|
|
if ( 'Product' === $type || 'SoftwareApplication' === $type || 'MusicAlbum' === $type ) { |
51
|
|
|
$products[] = $value; |
52
|
|
|
} elseif ( 'Review' === $type ) { |
53
|
|
|
$reviews[] = $value; |
54
|
|
|
} elseif ( 'WebSite' === $type || 'BreadcrumbList' === $type ) { |
55
|
|
|
$data[] = $value; |
56
|
|
|
} |
57
|
|
|
} |
58
|
|
|
|
59
|
|
|
if ( isset( $products ) ) { |
60
|
|
|
if ( count( $products ) > 1 ) { |
61
|
|
|
$data[] = array( '@graph' => $products ); |
62
|
|
|
} else { |
63
|
|
|
$data[] = isset( $reviews ) ? $products[0] + array( 'review' => $reviews ) : $products[0]; |
64
|
|
|
} |
65
|
|
|
} |
66
|
|
|
|
67
|
|
|
if ( ! isset( $data ) ) { |
68
|
|
|
return array(); |
69
|
|
|
} |
70
|
|
|
|
71
|
|
|
$context['@context'] = apply_filters( 'woocommerce_structured_data_context', 'http://schema.org/' ); |
72
|
|
|
|
73
|
|
|
foreach( $data as $key => $value ) { |
74
|
|
|
$data[ $key ] = $context + $value; |
75
|
|
|
} |
76
|
|
|
|
77
|
|
|
if ( count( $data ) > 1 ) { |
78
|
|
|
return $data = array( '@graph' => $data ); |
|
|
|
|
79
|
|
|
} else { |
80
|
|
|
return $data[0]; |
81
|
|
|
} |
82
|
|
|
} |
83
|
|
|
|
84
|
|
|
/** |
85
|
|
|
* Contructor |
86
|
|
|
*/ |
87
|
|
|
public function __construct() { |
88
|
|
|
add_action( 'woocommerce_before_main_content', array( $this, 'generate_shop_data' ) ); |
89
|
|
|
add_action( 'woocommerce_breadcrumb', array( $this, 'generate_breadcrumb_data' ), 10, 1 ); |
90
|
|
|
add_action( 'woocommerce_before_shop_loop_item', array( $this, 'generate_product_category_data' ) ); |
91
|
|
|
add_action( 'woocommerce_single_product_summary', array( $this, 'generate_product_data' ) ); |
92
|
|
|
add_action( 'woocommerce_review_meta', array( $this, 'generate_product_review_data' ), 10, 1 ); |
93
|
|
|
add_action( 'wp_footer', array( $this, 'enqueue_data' ) ); |
94
|
|
|
} |
95
|
|
|
|
96
|
|
|
/** |
97
|
|
|
* Sanitizes, encodes and echoes the structured data into `wp_footer` action hook. |
98
|
|
|
*/ |
99
|
|
|
public function enqueue_data() { |
100
|
|
|
if ( $data = $this->get_data() ) { |
101
|
|
|
$data = $this->sanitize_data( $data ); |
102
|
|
|
|
103
|
|
|
echo '<script type="application/ld+json">' . wp_json_encode( $data ) . '</script>'; |
104
|
|
|
} |
105
|
|
|
} |
106
|
|
|
|
107
|
|
|
/** |
108
|
|
|
* Sanitizes the structured data. |
109
|
|
|
* |
110
|
|
|
* @param array $data |
111
|
|
|
* @return array $sanitized_data |
112
|
|
|
*/ |
113
|
|
|
private function sanitize_data( $data ) { |
114
|
|
|
foreach ( $data as $key => $value ) { |
115
|
|
|
$sanitized_data[ sanitize_text_field( $key ) ] = is_array( $value ) ? $this->sanitize_data( $value ) : sanitize_text_field( $value ); |
116
|
|
|
} |
117
|
|
|
|
118
|
|
|
return $sanitized_data; |
119
|
|
|
} |
120
|
|
|
|
121
|
|
|
/** |
122
|
|
|
* Generates the product category structured data... |
123
|
|
|
* Hooked into the `woocommerce_before_shop_loop_item` action hook... |
124
|
|
|
*/ |
125
|
|
|
public function generate_product_category_data() { |
126
|
|
|
if ( ! is_product_category() && ! is_shop() ) { |
127
|
|
|
return; |
128
|
|
|
} |
129
|
|
|
|
130
|
|
|
$this->generate_product_data(); |
131
|
|
|
} |
132
|
|
|
|
133
|
|
|
/** |
134
|
|
|
* Generates the product structured data... |
135
|
|
|
* Hooked into the `woocommerce_single_product_summary` action hook... |
136
|
|
|
* Applies the `woocommerce_structured_data_product` filter hook for clean structured data customization... |
137
|
|
|
*/ |
138
|
|
|
public function generate_product_data() { |
139
|
|
|
global $product; |
140
|
|
|
|
141
|
|
|
if ( $product->is_downloadable() ) { |
142
|
|
|
switch ( $product->download_type ) { |
143
|
|
|
case 'application' : |
144
|
|
|
$type = "SoftwareApplication"; |
145
|
|
|
break; |
146
|
|
|
case 'music' : |
147
|
|
|
$type = "MusicAlbum"; |
148
|
|
|
break; |
149
|
|
|
default : |
150
|
|
|
$type = "Product"; |
151
|
|
|
break; |
152
|
|
|
} |
153
|
|
|
} else { |
154
|
|
|
$type = "Product"; |
155
|
|
|
} |
156
|
|
|
|
157
|
|
|
$json['@type'] = $type; |
158
|
|
|
$json['@id'] = get_the_permalink(); |
159
|
|
|
$json['name'] = get_the_title(); |
160
|
|
|
$json['description'] = get_the_excerpt(); |
161
|
|
|
$json['url'] = get_the_permalink(); |
162
|
|
|
|
163
|
|
|
if ( $product->get_rating_count() ) { |
164
|
|
|
$json['aggregateRating'] = array( |
165
|
|
|
'@type' => 'AggregateRating', |
166
|
|
|
'ratingValue' => $product->get_average_rating(), |
167
|
|
|
'ratingCount' => $product->get_rating_count(), |
168
|
|
|
'reviewCount' => $product->get_review_count() |
169
|
|
|
); |
170
|
|
|
} |
171
|
|
|
|
172
|
|
|
if ( $is_multi_variation = count( $product->get_children() ) > 1 ? true : false ) { |
173
|
|
|
$variations = $product->get_available_variations(); |
174
|
|
|
} else { |
175
|
|
|
$variations = array( null ); |
176
|
|
|
} |
177
|
|
|
|
178
|
|
|
foreach ( $variations as $variation ) { |
179
|
|
|
$product_variation = $is_multi_variation ? wc_get_product( $variation['variation_id'] ) : $product; |
180
|
|
|
|
181
|
|
|
$json_offers[] = array( |
182
|
|
|
'@type' => 'Offer', |
183
|
|
|
'priceCurrency' => get_woocommerce_currency(), |
184
|
|
|
'price' => $product_variation->get_price(), |
185
|
|
|
'availability' => 'http://schema.org/' . $stock = ( $product_variation->is_in_stock() ? 'InStock' : 'OutOfStock' ), |
186
|
|
|
'sku' => $product_variation->get_sku(), |
187
|
|
|
'image' => wp_get_attachment_url( $product_variation->get_image_id() ), |
188
|
|
|
'description' => $is_multi_variation ? $product_variation->get_variation_description() : '' |
189
|
|
|
); |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
$json['offers'] = $json_offers; |
193
|
|
|
|
194
|
|
|
$this->set_data( apply_filters( 'woocommerce_structured_data_product', $json, $product ) ); |
195
|
|
|
} |
196
|
|
|
|
197
|
|
|
/** |
198
|
|
|
* Generates the product review structured data... |
199
|
|
|
* Hooked into the `woocommerce_review_meta` action hook... |
200
|
|
|
* Applies the `woocommerce_structured_data_product_review` filter hook for clean structured data customization... |
201
|
|
|
* |
202
|
|
|
* @param object $comment From `woocommerce_review_meta` action hook |
203
|
|
|
*/ |
204
|
|
|
public function generate_product_review_data( $comment ) { |
205
|
|
|
|
206
|
|
|
$json['@type'] = 'Review'; |
207
|
|
|
$json['@id'] = get_the_permalink() . '#li-comment-' . get_comment_ID(); |
208
|
|
|
$json['datePublished'] = get_comment_date( 'c' ); |
209
|
|
|
$json['description'] = get_comment_text(); |
210
|
|
|
$json['reviewRating'] = array( |
211
|
|
|
'@type' => 'rating', |
212
|
|
|
'ratingValue' => intval( get_comment_meta( $comment->comment_ID, 'rating', true ) ) |
213
|
|
|
); |
214
|
|
|
$json['author'] = array( |
215
|
|
|
'@type' => 'Person', |
216
|
|
|
'name' => get_comment_author() |
217
|
|
|
); |
218
|
|
|
|
219
|
|
|
$this->set_data( apply_filters( 'woocommerce_structured_data_product_review', $json, $comment ) ); |
220
|
|
|
} |
221
|
|
|
|
222
|
|
|
/** |
223
|
|
|
* Generates the breadcrumbs structured data... |
224
|
|
|
* Hooked into the `woocommerce_breadcrumb` action hook... |
225
|
|
|
* Applies the `woocommerce_structured_data_breadcrumb` filter hook for clean structured data customization... |
226
|
|
|
* |
227
|
|
|
* @param array $args From `woocommerce_breadcrumb` action hook |
228
|
|
|
*/ |
229
|
|
|
public function generate_breadcrumb_data( $args ) { |
230
|
|
|
if ( empty( $args['breadcrumb'] ) ) { |
231
|
|
|
return; |
232
|
|
|
} |
233
|
|
|
|
234
|
|
|
$breadcrumb = $args['breadcrumb']; |
235
|
|
|
$position = 1; |
236
|
|
|
|
237
|
|
|
foreach ( $breadcrumb as $key => $value ) { |
238
|
|
|
if ( ! empty( $value[1] ) && sizeof( $breadcrumb ) !== $key + 1 ) { |
239
|
|
|
$json_crumbs_item = array( |
240
|
|
|
'@id' => $value[1], |
241
|
|
|
'name' => $value[0] |
242
|
|
|
); |
243
|
|
|
} else { |
244
|
|
|
$json_crumbs_item = array( |
245
|
|
|
'name' => $value[0] |
246
|
|
|
); |
247
|
|
|
} |
248
|
|
|
|
249
|
|
|
$json_crumbs[] = array( |
250
|
|
|
'@type' => 'ListItem', |
251
|
|
|
'position' => $position ++, |
252
|
|
|
'item' => $json_crumbs_item |
253
|
|
|
); |
254
|
|
|
} |
255
|
|
|
|
256
|
|
|
$json['@type'] = 'BreadcrumbList'; |
257
|
|
|
$json['itemListElement'] = $json_crumbs; |
258
|
|
|
|
259
|
|
|
$this->set_data( apply_filters( 'woocommerce_structured_data_breadcrumb', $json, $breadcrumb ) ); |
260
|
|
|
} |
261
|
|
|
|
262
|
|
|
/** |
263
|
|
|
* Generates the shop related structured data... |
264
|
|
|
* Hooked into the `woocommerce_before_main_content` action hook... |
265
|
|
|
* Applies the `woocommerce_structured_data_shop` filter hook for clean structured data customization... |
266
|
|
|
*/ |
267
|
|
|
public function generate_shop_data() { |
268
|
|
|
if ( ! is_shop() || ! is_front_page() ) { |
269
|
|
|
return; |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
$json['@type'] = 'WebSite'; |
273
|
|
|
$json['name'] = get_bloginfo( 'name' ); |
274
|
|
|
$json['url'] = get_bloginfo( 'url' ); |
275
|
|
|
$json['potentialAction'] = array( |
276
|
|
|
'@type' => 'SearchAction', |
277
|
|
|
'target' => get_bloginfo( 'url' ) . '/?s={search_term_string}&post_type=product', |
278
|
|
|
'query-input' => 'required name=search_term_string' |
279
|
|
|
); |
280
|
|
|
|
281
|
|
|
$this->set_data( apply_filters( 'woocommerce_structured_data_shop', $json ) ); |
282
|
|
|
} |
283
|
|
|
} |
284
|
|
|
|
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.