Completed
Push — master ( 836ece...f8a28a )
by Mike
03:59
created

ProductRequest::get_product_object()   B

Complexity

Conditions 7
Paths 12

Size

Total Lines 27
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 18
nc 12
nop 0
dl 0
loc 27
rs 8.8333
c 0
b 0
f 0
1
<?php
2
/**
3
 * Convert data in the product schema format to a product object.
4
 *
5
 * @package WooCommerce/RestApi
6
 */
7
8
namespace WooCommerce\RestApi\Controllers\Version4\Schema;
9
10
defined( 'ABSPATH' ) || exit;
11
12
/**
13
 * ProductRequest class.
14
 */
15
class ProductRequest extends AbstractRequest {
16
17
	/**
18
	 * Convert request to object.
19
	 *
20
	 * @return \WC_Product_Simple|\WC_Product_Grouped|\WC_Product_Variable|\WC_Product_External
21
	 */
22
	public function prepare_object() {
23
		$object = $this->get_product_object();
24
25
		$this->set_common_props( $object );
26
		$this->set_meta_data( $object );
27
28
		switch ( $object->get_type() ) {
29
			case 'grouped':
30
				$this->set_grouped_props( $object );
31
				break;
32
			case 'variable':
33
				$this->set_variable_props( $object );
34
				break;
35
			case 'external':
36
				$this->set_external_props( $object );
37
				break;
38
		}
39
40
		if ( $object->get_downloadable() ) {
41
			$this->set_downloadable_props( $object );
42
		}
43
44
		return $object;
45
	}
46
47
	/**
48
	 * Get product object from request args.
49
	 *
50
	 * @throws \WC_REST_Exception Will throw an exception if the resulting product object is invalid.
51
	 * @return \WC_Product_Simple|\WC_Product_Grouped|\WC_Product_Variable|\WC_Product_External
52
	 */
53
	protected function get_product_object() {
54
		$id   = (int) $this->get_param( 'id', 0 );
55
		$type = $this->get_param( 'type', '' );
56
57
		if ( $type ) {
58
			$classname = \WC_Product_Factory::get_classname_from_product_type( $type );
59
			if ( $classname && class_exists( '\\' . $classname ) ) {
60
				$classname = '\\' . $classname;
61
			} else {
62
				$classname = '\WC_Product_Simple';
63
			}
64
			$object = new $classname( $id );
65
		} elseif ( $id ) {
66
			$object = wc_get_product( $id );
67
		} else {
68
			$object = new \WC_Product_Simple();
69
		}
70
71
		if ( ! $object ) {
72
			throw new \WC_REST_Exception( 'woocommerce_rest_invalid_product_id', __( 'Invalid product.', 'woocommerce' ), 404 );
73
		}
74
75
		if ( $object->is_type( 'variation' ) ) {
76
			throw new \WC_REST_Exception( 'woocommerce_rest_invalid_product_id', __( 'To manipulate product variations you should use the /products/&lt;product_id&gt;/variations/&lt;id&gt; endpoint.', 'woocommerce' ), 404 );
77
		}
78
79
		return $object;
80
	}
81
82
	/**
83
	 * Set common product props.
84
	 *
85
	 * @param \WC_Product_Simple|\WC_Product_Grouped|\WC_Product_Variable|\WC_Product_External $object Product object reference.
86
	 */
87
	protected function set_common_props( &$object ) {
88
		$props = [
89
			'name',
90
			'sku',
91
			'description',
92
			'short_description',
93
			'slug',
94
			'menu_order',
95
			'reviews_allowed',
96
			'virtual',
97
			'tax_status',
98
			'tax_class',
99
			'catalog_visibility',
100
			'purchase_note',
101
			'status',
102
			'featured',
103
			'regular_price',
104
			'sale_price',
105
			'date_on_sale_from',
106
			'date_on_sale_from_gmt',
107
			'date_on_sale_to',
108
			'date_on_sale_to_gmt',
109
			'parent_id',
110
			'sold_individually',
111
			'manage_stock',
112
			'backorders',
113
			'stock_status',
114
			'stock_quantity',
115
			'downloadable',
116
			'date_created',
117
			'date_created_gmt',
118
			'upsell_ids',
119
			'cross_sell_ids',
120
			'images',
121
			'categories',
122
			'tags',
123
			'attributes',
124
			'weight',
125
			'dimensions',
126
			'shipping_class',
127
		];
128
129
		$request_props = array_intersect_key( $this->request, array_flip( $props ) );
130
		$prop_values   = [];
131
132
		foreach ( $request_props as $prop => $value ) {
133
			switch ( $prop ) {
134
				case 'date_created':
135
				case 'date_created_gmt':
136
					$prop_values[ $prop ] = rest_parse_date( $value );
137
					break;
138
				case 'upsell_ids':
139
				case 'cross_sell_ids':
140
					$prop_values[ $prop ] = wp_parse_id_list( $value );
141
					break;
142
				case 'images':
143
					$images      = $this->parse_images_field( $value, $object );
144
					$prop_values = array_merge( $prop_values, $images );
145
					break;
146
				case 'categories':
147
					$prop_values['category_ids'] = $this->parse_terms_field( $value );
148
					break;
149
				case 'tags':
150
					$prop_values['tag_ids'] = $this->parse_terms_field( $value );
151
					break;
152
				case 'attributes':
153
					$prop_values['attributes'] = $this->parse_attributes_field( $value );
154
					break;
155
				case 'dimensions':
156
					$dimensions  = $this->parse_dimensions_fields( $value );
157
					$prop_values = array_merge( $prop_values, $dimensions );
158
					break;
159
				case 'shipping_class':
160
					$prop_values['shipping_class_id'] = $this->parse_shipping_class( $value, $object );
161
					break;
162
				default:
163
					$prop_values[ $prop ] = $value;
164
			}
165
		}
166
167
		foreach ( $prop_values as $prop => $value ) {
168
			$object->{"set_$prop"}( $value );
169
		}
170
	}
171
172
	/**
173
	 * Set grouped product props.
174
	 *
175
	 * @param \WC_Product_Grouped $object Product object reference.
176
	 */
177
	protected function set_grouped_props( &$object ) {
178
		$children = $this->get_param( 'grouped_products', null );
179
180
		if ( ! is_null( $children ) ) {
181
			$object->set_children( $children );
182
		}
183
	}
184
185
	/**
186
	 * Set variable product props.
187
	 *
188
	 * @param \WC_Product_Variable $object Product object reference.
189
	 */
190
	protected function set_variable_props( &$object ) {
191
		$default_attributes = $this->get_param( 'default_attributes', null );
192
193
		if ( ! is_null( $default_attributes ) ) {
194
			$object->set_default_attributes( $this->parse_default_attributes( $default_attributes, $object ) );
195
		}
196
	}
197
198
	/**
199
	 * Set external product props.
200
	 *
201
	 * @param \WC_Product_External $object Product object reference.
202
	 */
203
	protected function set_external_props( &$object ) {
204
		$button_text  = $this->get_param( 'button_text', null );
205
		$external_url = $this->get_param( 'external_url', null );
206
207
		if ( ! is_null( $button_text ) ) {
208
			$object->set_button_text( $button_text );
209
		}
210
211
		if ( ! is_null( $external_url ) ) {
212
			$object->set_product_url( $external_url );
213
		}
214
	}
215
216
	/**
217
	 * Set downloadable product props.
218
	 *
219
	 * @param \WC_Product_Simple|\WC_Product_Grouped|\WC_Product_Variable|\WC_Product_External $object Product object reference.
220
	 */
221
	protected function set_downloadable_props( &$object ) {
222
		$download_limit  = $this->get_param( 'download_limit', null );
223
		$download_expiry = $this->get_param( 'download_expiry', null );
224
		$downloads       = $this->get_param( 'downloads', null );
225
226
		if ( ! is_null( $download_limit ) ) {
227
			$object->set_download_limit( $download_limit );
228
		}
229
230
		if ( ! is_null( $download_expiry ) ) {
231
			$object->set_download_expiry( $download_expiry );
232
		}
233
234
		if ( ! is_null( $downloads ) ) {
235
			$object->set_downloads( $this->parse_downloads_field( $downloads ) );
236
		}
237
	}
238
239
	/**
240
	 * Set product object's attributes.
241
	 *
242
	 * @param array $raw_attributes Attribute data from request.
243
	 * @return array
244
	 */
245
	protected function parse_attributes_field( $raw_attributes ) {
246
		$attributes = array();
247
248
		foreach ( $raw_attributes as $attribute ) {
249
			// Check ID for global attributes or name for product attributes.
250
			if ( ! empty( $attribute['id'] ) ) {
251
				$attribute_id   = absint( $attribute['id'] );
252
				$attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id );
253
			} elseif ( ! empty( $attribute['name'] ) ) {
254
				$attribute_id   = 0;
255
				$attribute_name = wc_clean( $attribute['name'] );
256
			}
257
258
			if ( ! $attribute_name || ! isset( $attribute['options'] ) ) {
259
				continue;
260
			}
261
262
			if ( ! is_array( $attribute['options'] ) ) {
263
				// Text based attributes - Posted values are term names.
264
				$attribute['options'] = explode( \WC_DELIMITER, $attribute['options'] );
0 ignored issues
show
Bug introduced by
The constant WC_DELIMITER was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
265
			}
266
267
			if ( $attribute_id ) {
268
				$attribute['options'] = array_filter( array_map( 'wc_sanitize_term_text_based', $attribute['options'] ), 'strlen' );
269
			}
270
271
			$attribute_object = new \WC_Product_Attribute();
272
			$attribute_object->set_id( $attribute_id );
273
			$attribute_object->set_name( $attribute_name );
274
			$attribute_object->set_options( $attribute['options'] );
275
			$attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' );
0 ignored issues
show
Bug introduced by
IssetNode ? (string)absi...bute['position']) : '0' of type string is incompatible with the type integer expected by parameter $value of WC_Product_Attribute::set_position(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

275
			$attribute_object->set_position( /** @scrutinizer ignore-type */ isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' );
Loading history...
276
			$attribute_object->set_visible( ! empty( $attribute['visible'] ) ? 1 : 0 );
0 ignored issues
show
Bug introduced by
! empty($attribute['visible']) ? 1 : 0 of type integer is incompatible with the type boolean expected by parameter $value of WC_Product_Attribute::set_visible(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

276
			$attribute_object->set_visible( /** @scrutinizer ignore-type */ ! empty( $attribute['visible'] ) ? 1 : 0 );
Loading history...
277
			$attribute_object->set_variation( ! empty( $attribute['variation'] ) ? 1 : 0 );
0 ignored issues
show
Bug introduced by
! empty($attribute['variation']) ? 1 : 0 of type integer is incompatible with the type boolean expected by parameter $value of WC_Product_Attribute::set_variation(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

277
			$attribute_object->set_variation( /** @scrutinizer ignore-type */ ! empty( $attribute['variation'] ) ? 1 : 0 );
Loading history...
278
			$attributes[] = $attribute_object;
279
		}
280
		return $attributes;
281
	}
282
283
	/**
284
	 * Set product images.
285
	 *
286
	 * @throws \WC_REST_Exception REST API exceptions.
287
	 * @param array                                                                            $images  Images data.
288
	 * @param \WC_Product_Simple|\WC_Product_Grouped|\WC_Product_Variable|\WC_Product_External $object Product object.
289
	 * @return array
290
	 */
291
	protected function parse_images_field( $images, $object ) {
292
		$response = [
293
			'image_id'          => '',
294
			'gallery_image_ids' => [],
295
		];
296
297
		$images = is_array( $images ) ? array_filter( $images ) : [];
0 ignored issues
show
introduced by
The condition is_array($images) is always true.
Loading history...
298
299
		if ( empty( $images ) ) {
300
			return $response;
301
		}
302
303
		foreach ( $images as $index => $image ) {
304
			$attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0;
305
306
			if ( 0 === $attachment_id && isset( $image['src'] ) ) {
307
				$upload = wc_rest_upload_image_from_url( esc_url_raw( $image['src'] ) );
308
309
				if ( is_wp_error( $upload ) ) {
310
					if ( ! apply_filters( 'woocommerce_rest_suppress_image_upload_error', false, $upload, $object->get_id(), $images ) ) {
311
						throw new \WC_REST_Exception( 'woocommerce_product_image_upload_error', $upload->get_error_message(), 400 );
312
					} else {
313
						continue;
314
					}
315
				}
316
317
				$attachment_id = wc_rest_set_uploaded_image_as_attachment( $upload, $object->get_id() );
0 ignored issues
show
Bug introduced by
It seems like $upload can also be of type WP_Error; however, parameter $upload of wc_rest_set_uploaded_image_as_attachment() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

317
				$attachment_id = wc_rest_set_uploaded_image_as_attachment( /** @scrutinizer ignore-type */ $upload, $object->get_id() );
Loading history...
318
			}
319
320
			if ( ! wp_attachment_is_image( $attachment_id ) ) {
321
				/* translators: %s: image ID */
322
				throw new \WC_REST_Exception( 'woocommerce_product_invalid_image_id', sprintf( __( '#%s is an invalid image ID.', 'woocommerce' ), $attachment_id ), 400 );
323
			}
324
325
			if ( 0 === $index ) {
326
				$response['image_id'] = $attachment_id;
327
			} else {
328
				$response['gallery_image_ids'][] = $attachment_id;
329
			}
330
331
			// Set the image alt if present.
332
			if ( ! empty( $image['alt'] ) ) {
333
				update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) );
334
			}
335
336
			// Set the image name if present.
337
			if ( ! empty( $image['name'] ) ) {
338
				wp_update_post(
339
					array(
340
						'ID'         => $attachment_id,
341
						'post_title' => $image['name'],
342
					)
343
				);
344
			}
345
		}
346
347
		return $response;
348
	}
349
350
	/**
351
	 * Parse dimensions.
352
	 *
353
	 * @param array $dimensions Product dimensions.
354
	 * @return array
355
	 */
356
	protected function parse_dimensions_fields( $dimensions ) {
357
		$response = [];
358
359
		if ( isset( $dimensions['length'] ) ) {
360
			$response['length'] = $dimensions['length'];
361
		}
362
363
		if ( isset( $dimensions['width'] ) ) {
364
			$response['width'] = $dimensions['width'];
365
		}
366
367
		if ( isset( $dimensions['height'] ) ) {
368
			$response['height'] = $dimensions['height'];
369
		}
370
371
		return $response;
372
	}
373
374
	/**
375
	 * Parse shipping class.
376
	 *
377
	 * @param string                                                                           $shipping_class Shipping class slug.
378
	 * @param \WC_Product_Simple|\WC_Product_Grouped|\WC_Product_Variable|\WC_Product_External $object Product object.
379
	 * @return int
380
	 */
381
	protected function parse_shipping_class( $shipping_class, $object ) {
382
		$data_store = $object->get_data_store();
383
		return $data_store->get_shipping_class_id_by_slug( wc_clean( $shipping_class ) );
384
	}
385
386
	/**
387
	 * Parse downloadable files.
388
	 *
389
	 * @param array $downloads  Downloads data.
390
	 * @return array
391
	 */
392
	protected function parse_downloads_field( $downloads ) {
393
		$files = array();
394
		foreach ( $downloads as $key => $file ) {
395
			if ( empty( $file['file'] ) ) {
396
				continue;
397
			}
398
399
			$download = new \WC_Product_Download();
400
			$download->set_id( ! empty( $file['id'] ) ? $file['id'] : wp_generate_uuid4() );
401
			$download->set_name( $file['name'] ? $file['name'] : wc_get_filename_from_url( $file['file'] ) );
402
			$download->set_file( $file['file'] );
403
			$files[] = $download;
404
		}
405
		return $files;
406
	}
407
408
	/**
409
	 * Save taxonomy terms.
410
	 *
411
	 * @param array $terms Terms data.
412
	 * @return array
413
	 */
414
	protected function parse_terms_field( $terms ) {
415
		return wp_list_pluck( $terms, 'id' );
416
	}
417
418
	/**
419
	 * Save default attributes.
420
	 *
421
	 * @param array                $raw_default_attributes Default attributes.
422
	 * @param \WC_Product_Variable $object Product object reference.
423
	 * @return array
424
	 */
425
	protected function parse_default_attributes( $raw_default_attributes, $object ) {
426
		$attributes         = $object->get_attributes();
427
		$default_attributes = array();
428
429
		foreach ( $raw_default_attributes as $attribute ) {
430
			$attribute_id   = 0;
431
			$attribute_name = '';
432
433
			// Check ID for global attributes or name for product attributes.
434
			if ( ! empty( $attribute['id'] ) ) {
435
				$attribute_id   = absint( $attribute['id'] );
436
				$attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id );
437
			} elseif ( ! empty( $attribute['name'] ) ) {
438
				$attribute_name = sanitize_title( $attribute['name'] );
439
			}
440
441
			if ( ! $attribute_id && ! $attribute_name ) {
442
				continue;
443
			}
444
445
			if ( isset( $attributes[ $attribute_name ] ) ) {
446
				$_attribute = $attributes[ $attribute_name ];
447
448
				if ( $_attribute['is_variation'] ) {
449
					$value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : '';
450
451
					if ( ! empty( $_attribute['is_taxonomy'] ) ) {
452
						// If dealing with a taxonomy, we need to get the slug from the name posted to the API.
453
						$term = get_term_by( 'name', $value, $attribute_name );
454
455
						if ( $term && ! is_wp_error( $term ) ) {
456
							$value = $term->slug;
457
						} else {
458
							$value = sanitize_title( $value );
459
						}
460
					}
461
462
					if ( $value ) {
463
						$default_attributes[ $attribute_name ] = $value;
464
					}
465
				}
466
			}
467
		}
468
469
		return $default_attributes;
470
	}
471
}
472