Passed
Push — master ( aa41b1...2b3c87 )
by Mike
04:52
created

ProductRequest::parse_default_attributes()   C

Complexity

Conditions 13
Paths 46

Size

Total Lines 45
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 13
eloc 25
nc 46
nop 2
dl 0
loc 45
rs 6.6166
c 0
b 0
f 0

How to fix   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
 * 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 );
0 ignored issues
show
Bug introduced by
Are you sure WC_Product_Factory::get_...rom_product_type($type) of type false|string can be used in concatenation? ( Ignorable by Annotation )

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

58
			$classname = '\\' . /** @scrutinizer ignore-type */ \WC_Product_Factory::get_classname_from_product_type( $type );
Loading history...
59
			$object    = class_exists( $classname ) ? new $classname( $id ) : new \WC_Product_Simple( $id );
60
		} elseif ( $id ) {
61
			$object = wc_get_product( $id );
62
		} else {
63
			$object = new \WC_Product_Simple();
64
		}
65
66
		if ( ! $object ) {
67
			throw new \WC_REST_Exception( 'woocommerce_rest_invalid_product_id', __( 'Invalid product.', 'woocommerce' ), 404 );
68
		}
69
70
		if ( $object->is_type( 'variation' ) ) {
71
			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 );
72
		}
73
74
		return $object;
75
	}
76
77
	/**
78
	 * Set common product props.
79
	 *
80
	 * @param \WC_Product_Simple|\WC_Product_Grouped|\WC_Product_Variable|\WC_Product_External $object Product object reference.
81
	 */
82
	protected function set_common_props( &$object ) {
83
		$props = [
84
			'name',
85
			'sku',
86
			'description',
87
			'short_description',
88
			'slug',
89
			'menu_order',
90
			'reviews_allowed',
91
			'virtual',
92
			'tax_status',
93
			'tax_class',
94
			'catalog_visibility',
95
			'purchase_note',
96
			'status',
97
			'featured',
98
			'regular_price',
99
			'sale_price',
100
			'date_on_sale_from',
101
			'date_on_sale_from_gmt',
102
			'date_on_sale_to',
103
			'date_on_sale_to_gmt',
104
			'parent_id',
105
			'sold_individually',
106
			'manage_stock',
107
			'backorders',
108
			'stock_status',
109
			'stock_quantity',
110
			'downloadable',
111
			'date_created',
112
			'date_created_gmt',
113
			'upsell_ids',
114
			'cross_sell_ids',
115
			'images',
116
			'categories',
117
			'tags',
118
			'attributes',
119
			'weight',
120
			'dimensions',
121
			'shipping_class',
122
		];
123
124
		$request_props = array_intersect_key( $this->request, array_flip( $props ) );
125
		$prop_values   = [];
126
127
		foreach ( $request_props as $prop => $value ) {
128
			switch ( $prop ) {
129
				case 'date_created':
130
				case 'date_created_gmt':
131
					$prop_values[ $prop ] = rest_parse_date( $value );
132
					break;
133
				case 'upsell_ids':
134
				case 'cross_sell_ids':
135
					$prop_values[ $prop ] = wp_parse_id_list( $value );
136
					break;
137
				case 'images':
138
					$images      = $this->parse_images_field( $value, $object );
139
					$prop_values = array_merge( $prop_values, $images );
140
					break;
141
				case 'categories':
142
					$prop_values['category_ids'] = $this->parse_terms_field( $value );
143
					break;
144
				case 'tags':
145
					$prop_values['tag_ids'] = $this->parse_terms_field( $value );
146
					break;
147
				case 'attributes':
148
					$prop_values['attributes'] = $this->parse_attributes_field( $value );
149
					break;
150
				case 'dimensions':
151
					$dimensions  = $this->parse_dimensions_fields( $value );
152
					$prop_values = array_merge( $prop_values, $dimensions );
153
					break;
154
				case 'shipping_class':
155
					$prop_values['shipping_class_id'] = $this->parse_shipping_class( $value, $object );
156
					break;
157
				default:
158
					$prop_values[ $prop ] = $value;
159
			}
160
		}
161
162
		foreach ( $prop_values as $prop => $value ) {
163
			$object->{"set_$prop"}( $value );
164
		}
165
	}
166
167
	/**
168
	 * Set grouped product props.
169
	 *
170
	 * @param \WC_Product_Grouped $object Product object reference.
171
	 */
172
	protected function set_grouped_props( &$object ) {
173
		$children = $this->get_param( 'grouped_products', null );
174
175
		if ( ! is_null( $children ) ) {
176
			$object->set_children( $children );
177
		}
178
	}
179
180
	/**
181
	 * Set variable product props.
182
	 *
183
	 * @param \WC_Product_Variable $object Product object reference.
184
	 */
185
	protected function set_variable_props( &$object ) {
186
		$default_attributes = $this->get_param( 'default_attributes', null );
187
188
		if ( ! is_null( $default_attributes ) ) {
189
			$object->set_default_attributes( $this->parse_default_attributes( $default_attributes, $object ) );
190
		}
191
	}
192
193
	/**
194
	 * Set external product props.
195
	 *
196
	 * @param \WC_Product_External $object Product object reference.
197
	 */
198
	protected function set_external_props( &$object ) {
199
		$button_text  = $this->get_param( 'button_text', null );
200
		$external_url = $this->get_param( 'external_url', null );
201
202
		if ( ! is_null( $button_text ) ) {
203
			$object->set_button_text( $button_text );
204
		}
205
206
		if ( ! is_null( $external_url ) ) {
207
			$object->set_product_url( $external_url );
208
		}
209
	}
210
211
	/**
212
	 * Set downloadable product props.
213
	 *
214
	 * @param \WC_Product_Simple|\WC_Product_Grouped|\WC_Product_Variable|\WC_Product_External $object Product object reference.
215
	 */
216
	protected function set_downloadable_props( &$object ) {
217
		$download_limit  = $this->get_param( 'download_limit', null );
218
		$download_expiry = $this->get_param( 'download_expiry', null );
219
		$downloads       = $this->get_param( 'downloads', null );
220
221
		if ( ! is_null( $download_limit ) ) {
222
			$object->set_download_limit( $download_limit );
223
		}
224
225
		if ( ! is_null( $download_expiry ) ) {
226
			$object->set_download_expiry( $download_expiry );
227
		}
228
229
		if ( ! is_null( $downloads ) ) {
230
			$object->set_downloads( $this->parse_downloads_field( $downloads ) );
231
		}
232
	}
233
234
	/**
235
	 * Set meta data.
236
	 *
237
	 * @param \WC_Product_Simple|\WC_Product_Grouped|\WC_Product_Variable|\WC_Product_External $object Product object reference.
238
	 */
239
	protected function set_meta_data( &$object ) {
240
		$meta_data = $this->get_param( 'meta_data', null );
241
242
		if ( ! is_null( $meta_data ) ) {
243
			foreach ( $meta_data as $meta ) {
244
				$object->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' );
0 ignored issues
show
Bug introduced by
It seems like IssetNode ? $meta['id'] : '' can also be of type string; however, parameter $meta_id of WC_Data::update_meta_data() does only seem to accept integer, 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

244
				$object->update_meta_data( $meta['key'], $meta['value'], /** @scrutinizer ignore-type */ isset( $meta['id'] ) ? $meta['id'] : '' );
Loading history...
245
			}
246
		}
247
	}
248
249
	/**
250
	 * Set product object's attributes.
251
	 *
252
	 * @param array $raw_attributes Attribute data from request.
253
	 * @return array
254
	 */
255
	protected function parse_attributes_field( $raw_attributes ) {
256
		$attributes = array();
257
258
		foreach ( $raw_attributes as $attribute ) {
259
			$attribute_id   = 0;
260
			$attribute_name = '';
261
262
			// Check ID for global attributes or name for product attributes.
263
			if ( ! empty( $attribute['id'] ) ) {
264
				$attribute_id   = absint( $attribute['id'] );
265
				$attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id );
266
			} elseif ( ! empty( $attribute['name'] ) ) {
267
				$attribute_name = wc_clean( $attribute['name'] );
268
			}
269
270
			if ( ! $attribute_id && ! $attribute_name ) {
271
				continue;
272
			}
273
274
			if ( $attribute_id ) {
275
276
				if ( isset( $attribute['options'] ) ) {
277
					$options = $attribute['options'];
278
279
					if ( ! is_array( $attribute['options'] ) ) {
280
						// Text based attributes - Posted values are term names.
281
						$options = explode( WC_DELIMITER, $options );
0 ignored issues
show
Bug introduced by
The constant WooCommerce\RestApi\Cont...on4\Schema\WC_DELIMITER was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
282
					}
283
284
					$values = array_map( 'wc_sanitize_term_text_based', $options );
285
					$values = array_filter( $values, 'strlen' );
286
				} else {
287
					$values = array();
288
				}
289
290
				if ( ! empty( $values ) ) {
291
					// Add attribute to array, but don't set values.
292
					$attribute_object = new \WC_Product_Attribute();
293
					$attribute_object->set_id( $attribute_id );
294
					$attribute_object->set_name( $attribute_name );
0 ignored issues
show
Bug introduced by
$attribute_name of type array|string is incompatible with the type integer expected by parameter $value of WC_Product_Attribute::set_name(). ( Ignorable by Annotation )

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

294
					$attribute_object->set_name( /** @scrutinizer ignore-type */ $attribute_name );
Loading history...
295
					$attribute_object->set_options( $values );
296
					$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

296
					$attribute_object->set_position( /** @scrutinizer ignore-type */ isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' );
Loading history...
297
					$attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 );
0 ignored issues
show
Bug introduced by
IssetNode && $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

297
					$attribute_object->set_visible( /** @scrutinizer ignore-type */ ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 );
Loading history...
298
					$attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 );
0 ignored issues
show
Bug introduced by
IssetNode && $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

298
					$attribute_object->set_variation( /** @scrutinizer ignore-type */ ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 );
Loading history...
299
					$attributes[] = $attribute_object;
300
				}
301
			} elseif ( isset( $attribute['options'] ) ) {
302
				// Custom attribute - Add attribute to array and set the values.
303
				if ( is_array( $attribute['options'] ) ) {
304
					$values = $attribute['options'];
305
				} else {
306
					$values = explode( WC_DELIMITER, $attribute['options'] );
307
				}
308
				$attribute_object = new \WC_Product_Attribute();
309
				$attribute_object->set_name( $attribute_name );
310
				$attribute_object->set_options( $values );
311
				$attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' );
312
				$attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 );
313
				$attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 );
314
				$attributes[] = $attribute_object;
315
			}
316
		}
317
		return $attributes;
318
	}
319
320
	/**
321
	 * Set product images.
322
	 *
323
	 * @throws \WC_REST_Exception REST API exceptions.
324
	 * @param array                                                                            $images  Images data.
325
	 * @param \WC_Product_Simple|\WC_Product_Grouped|\WC_Product_Variable|\WC_Product_External $object Product object.
326
	 * @return array
327
	 */
328
	protected function parse_images_field( $images, $object ) {
329
		$response = [
330
			'image_id'          => '',
331
			'gallery_image_ids' => [],
332
		];
333
334
		$images = is_array( $images ) ? array_filter( $images ) : [];
0 ignored issues
show
introduced by
The condition is_array($images) is always true.
Loading history...
335
336
		if ( empty( $images ) ) {
337
			return $response;
338
		}
339
340
		foreach ( $images as $index => $image ) {
341
			$attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0;
342
343
			if ( 0 === $attachment_id && isset( $image['src'] ) ) {
344
				$upload = wc_rest_upload_image_from_url( esc_url_raw( $image['src'] ) );
345
346
				if ( is_wp_error( $upload ) ) {
347
					if ( ! apply_filters( 'woocommerce_rest_suppress_image_upload_error', false, $upload, $object->get_id(), $images ) ) {
348
						throw new \WC_REST_Exception( 'woocommerce_product_image_upload_error', $upload->get_error_message(), 400 );
349
					} else {
350
						continue;
351
					}
352
				}
353
354
				$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

354
				$attachment_id = wc_rest_set_uploaded_image_as_attachment( /** @scrutinizer ignore-type */ $upload, $object->get_id() );
Loading history...
355
			}
356
357
			if ( ! wp_attachment_is_image( $attachment_id ) ) {
358
				/* translators: %s: image ID */
359
				throw new \WC_REST_Exception( 'woocommerce_product_invalid_image_id', sprintf( __( '#%s is an invalid image ID.', 'woocommerce' ), $attachment_id ), 400 );
360
			}
361
362
			if ( 0 === $index ) {
363
				$response['image_id'] = $attachment_id;
364
			} else {
365
				$response['gallery_image_ids'][] = $attachment_id;
366
			}
367
368
			// Set the image alt if present.
369
			if ( ! empty( $image['alt'] ) ) {
370
				update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) );
371
			}
372
373
			// Set the image name if present.
374
			if ( ! empty( $image['name'] ) ) {
375
				wp_update_post(
376
					array(
377
						'ID'         => $attachment_id,
378
						'post_title' => $image['name'],
379
					)
380
				);
381
			}
382
		}
383
384
		return $response;
385
	}
386
387
	/**
388
	 * Parse dimensions.
389
	 *
390
	 * @param array $dimensions Product dimensions.
391
	 * @return array
392
	 */
393
	protected function parse_dimensions_fields( $dimensions ) {
394
		$response = [];
395
396
		if ( isset( $dimensions['length'] ) ) {
397
			$response['length'] = $dimensions['length'];
398
		}
399
400
		if ( isset( $dimensions['width'] ) ) {
401
			$response['width'] = $dimensions['width'];
402
		}
403
404
		if ( isset( $dimensions['height'] ) ) {
405
			$response['height'] = $dimensions['height'];
406
		}
407
408
		return $response;
409
	}
410
411
	/**
412
	 * Parse shipping class.
413
	 *
414
	 * @param string                                                                           $shipping_class Shipping class slug.
415
	 * @param \WC_Product_Simple|\WC_Product_Grouped|\WC_Product_Variable|\WC_Product_External $object Product object.
416
	 * @return int
417
	 */
418
	protected function parse_shipping_class( $shipping_class, $object ) {
419
		$data_store = $object->get_data_store();
420
		return $data_store->get_shipping_class_id_by_slug( wc_clean( $shipping_class ) );
421
	}
422
423
	/**
424
	 * Parse downloadable files.
425
	 *
426
	 * @param array $downloads  Downloads data.
427
	 * @return array
428
	 */
429
	protected function parse_downloads_field( $downloads ) {
430
		$files = array();
431
		foreach ( $downloads as $key => $file ) {
432
			if ( empty( $file['file'] ) ) {
433
				continue;
434
			}
435
436
			$download = new \WC_Product_Download();
437
			$download->set_id( ! empty( $file['id'] ) ? $file['id'] : wp_generate_uuid4() );
438
			$download->set_name( $file['name'] ? $file['name'] : wc_get_filename_from_url( $file['file'] ) );
439
			$download->set_file( $file['file'] );
440
			$files[] = $download;
441
		}
442
		return $files;
443
	}
444
445
	/**
446
	 * Save taxonomy terms.
447
	 *
448
	 * @param array $terms Terms data.
449
	 * @return array
450
	 */
451
	protected function parse_terms_field( $terms ) {
452
		return wp_list_pluck( $terms, 'id' );
453
	}
454
455
	/**
456
	 * Save default attributes.
457
	 *
458
	 * @param array                $raw_default_attributes Default attributes.
459
	 * @param \WC_Product_Variable $object Product object reference.
460
	 * @return array
461
	 */
462
	protected function parse_default_attributes( $raw_default_attributes, $object ) {
463
		$attributes         = $object->get_attributes();
464
		$default_attributes = array();
465
466
		foreach ( $raw_default_attributes as $attribute ) {
467
			$attribute_id   = 0;
468
			$attribute_name = '';
469
470
			// Check ID for global attributes or name for product attributes.
471
			if ( ! empty( $attribute['id'] ) ) {
472
				$attribute_id   = absint( $attribute['id'] );
473
				$attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id );
474
			} elseif ( ! empty( $attribute['name'] ) ) {
475
				$attribute_name = sanitize_title( $attribute['name'] );
476
			}
477
478
			if ( ! $attribute_id && ! $attribute_name ) {
479
				continue;
480
			}
481
482
			if ( isset( $attributes[ $attribute_name ] ) ) {
483
				$_attribute = $attributes[ $attribute_name ];
484
485
				if ( $_attribute['is_variation'] ) {
486
					$value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : '';
487
488
					if ( ! empty( $_attribute['is_taxonomy'] ) ) {
489
						// If dealing with a taxonomy, we need to get the slug from the name posted to the API.
490
						$term = get_term_by( 'name', $value, $attribute_name );
491
492
						if ( $term && ! is_wp_error( $term ) ) {
493
							$value = $term->slug;
494
						} else {
495
							$value = sanitize_title( $value );
496
						}
497
					}
498
499
					if ( $value ) {
500
						$default_attributes[ $attribute_name ] = $value;
501
					}
502
				}
503
			}
504
		}
505
506
		return $default_attributes;
507
	}
508
}
509