Completed
Push — master ( 3be021...a518b9 )
by Mike
04:08
created

ProductAttributes   A

Complexity

Total Complexity 34

Size/Duplication

Total Lines 472
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 209
dl 0
loc 472
rs 9.68
c 0
b 0
f 0
wmc 34

14 Methods

Rating   Name   Duplication   Size   Complexity  
A get_attribute() 0 19 3
A flush_rewrite_rules() 0 2 1
A get_collection_params() 0 5 1
A get_items() 0 10 2
A validate_attribute_slug() 0 13 5
A get_item() 0 10 2
A get_item_schema() 0 52 1
A get_taxonomy() 0 12 3
A register_routes() 0 73 1
A create_item() 0 42 5
A update_item() 0 41 3
A delete_item() 0 40 5
A prepare_links() 0 12 1
A get_data_for_response() 0 8 1
1
<?php
2
/**
3
 * REST API Product Attributes controller
4
 *
5
 * Handles requests to the products/attributes endpoint.
6
 *
7
 * @package Automattic/WooCommerce/RestApi
8
 */
9
10
namespace Automattic\WooCommerce\RestApi\Controllers\Version4;
11
12
defined( 'ABSPATH' ) || exit;
13
14
/**
15
 * REST API Product Attributes controller class.
16
 */
17
class ProductAttributes extends AbstractController {
18
19
	/**
20
	 * Route base.
21
	 *
22
	 * @var string
23
	 */
24
	protected $rest_base = 'products/attributes';
25
26
	/**
27
	 * Permission to check.
28
	 *
29
	 * @var string
30
	 */
31
	protected $resource_type = 'attributes';
32
33
	/**
34
	 * Attribute name.
35
	 *
36
	 * @var string
37
	 */
38
	protected $attribute = '';
39
40
	/**
41
	 * Register the routes for product attributes.
42
	 */
43
	public function register_routes() {
44
		register_rest_route(
45
			$this->namespace,
46
			'/' . $this->rest_base,
47
			array(
48
				array(
49
					'methods'             => \WP_REST_Server::READABLE,
50
					'callback'            => array( $this, 'get_items' ),
51
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
52
					'args'                => $this->get_collection_params(),
53
				),
54
				array(
55
					'methods'             => \WP_REST_Server::CREATABLE,
56
					'callback'            => array( $this, 'create_item' ),
57
					'permission_callback' => array( $this, 'create_item_permissions_check' ),
58
					'args'                => array_merge(
59
						$this->get_endpoint_args_for_item_schema( \WP_REST_Server::CREATABLE ),
60
						array(
61
							'name' => array(
62
								'description' => __( 'Name for the resource.', 'woocommerce' ),
63
								'type'        => 'string',
64
								'required'    => true,
65
							),
66
						)
67
					),
68
				),
69
				'schema' => array( $this, 'get_public_item_schema' ),
70
			),
71
			true
72
		);
73
74
		register_rest_route(
75
			$this->namespace,
76
			'/' . $this->rest_base . '/(?P<id>[\d]+)',
77
			array(
78
				'args' => array(
79
					'id' => array(
80
						'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
81
						'type'        => 'integer',
82
					),
83
				),
84
				array(
85
					'methods'             => \WP_REST_Server::READABLE,
86
					'callback'            => array( $this, 'get_item' ),
87
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
88
					'args'                => array(
89
						'context'         => $this->get_context_param( array( 'default' => 'view' ) ),
90
					),
91
				),
92
				array(
93
					'methods'             => \WP_REST_Server::EDITABLE,
94
					'callback'            => array( $this, 'update_item' ),
95
					'permission_callback' => array( $this, 'update_item_permissions_check' ),
96
					'args'                => $this->get_endpoint_args_for_item_schema( \WP_REST_Server::EDITABLE ),
97
				),
98
				array(
99
					'methods'             => \WP_REST_Server::DELETABLE,
100
					'callback'            => array( $this, 'delete_item' ),
101
					'permission_callback' => array( $this, 'delete_item_permissions_check' ),
102
					'args'                => array(
103
						'force' => array(
104
							'default'     => true,
105
							'type'        => 'boolean',
106
							'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ),
107
						),
108
					),
109
				),
110
				'schema' => array( $this, 'get_public_item_schema' ),
111
			),
112
			true
113
		);
114
115
		$this->register_batch_route();
116
	}
117
118
	/**
119
	 * Get all attributes.
120
	 *
121
	 * @param \WP_REST_Request $request Request params.
122
	 * @return array
123
	 */
124
	public function get_items( $request ) {
125
		$attributes = wc_get_attribute_taxonomies();
126
		$data       = array();
127
		foreach ( $attributes as $attribute_obj ) {
128
			$attribute = $this->prepare_item_for_response( $attribute_obj, $request );
129
			$attribute = $this->prepare_response_for_collection( $attribute );
130
			$data[]    = $attribute;
131
		}
132
133
		return rest_ensure_response( $data );
134
	}
135
136
	/**
137
	 * Create a single attribute.
138
	 *
139
	 * @param \WP_REST_Request $request Full details about the request.
140
	 * @return \WP_REST_Request|\WP_Error
141
	 */
142
	public function create_item( $request ) {
143
		global $wpdb;
144
145
		$id = wc_create_attribute(
146
			array(
147
				'name'         => $request['name'],
148
				'slug'         => wc_sanitize_taxonomy_name( stripslashes( $request['slug'] ) ),
149
				'type'         => ! empty( $request['type'] ) ? $request['type'] : 'select',
150
				'order_by'     => ! empty( $request['order_by'] ) ? $request['order_by'] : 'menu_order',
151
				'has_archives' => true === $request['has_archives'],
152
			)
153
		);
154
155
		// Checks for errors.
156
		if ( is_wp_error( $id ) ) {
157
			return new \WP_Error( 'woocommerce_rest_cannot_create', $id->get_error_message(), array( 'status' => 400 ) );
158
		}
159
160
		$attribute = $this->get_attribute( $id );
0 ignored issues
show
Bug introduced by
It seems like $id can also be of type WP_Error; however, parameter $id of Automattic\WooCommerce\R...ibutes::get_attribute() 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

160
		$attribute = $this->get_attribute( /** @scrutinizer ignore-type */ $id );
Loading history...
161
162
		if ( is_wp_error( $attribute ) ) {
163
			return $attribute;
164
		}
165
166
		$this->update_additional_fields_for_object( $attribute, $request );
0 ignored issues
show
Bug introduced by
$attribute of type Automattic\WooCommerce\R...sion4\stdClass|WP_Error is incompatible with the type array expected by parameter $object of WP_REST_Controller::upda...nal_fields_for_object(). ( Ignorable by Annotation )

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

166
		$this->update_additional_fields_for_object( /** @scrutinizer ignore-type */ $attribute, $request );
Loading history...
167
168
		/**
169
		 * Fires after a single product attribute is created or updated via the REST API.
170
		 *
171
		 * @param stdObject       $attribute Inserted attribute object.
172
		 * @param \WP_REST_Request $request   Request object.
173
		 * @param boolean         $creating  True when creating attribute, false when updating.
174
		 */
175
		do_action( 'woocommerce_rest_insert_product_attribute', $attribute, $request, true );
176
177
		$request->set_param( 'context', 'edit' );
178
		$response = $this->prepare_item_for_response( $attribute, $request );
179
		$response = rest_ensure_response( $response );
180
		$response->set_status( 201 );
181
		$response->header( 'Location', rest_url( '/' . $this->namespace . '/' . $this->rest_base . '/' . $attribute->attribute_id ) );
0 ignored issues
show
Bug introduced by
The property attribute_id does not seem to exist on WP_Error.
Loading history...
182
183
		return $response;
184
	}
185
186
	/**
187
	 * Get a single attribute.
188
	 *
189
	 * @param \WP_REST_Request $request Full details about the request.
190
	 * @return \WP_REST_Request|\WP_Error
191
	 */
192
	public function get_item( $request ) {
193
		$attribute = $this->get_attribute( (int) $request['id'] );
194
195
		if ( is_wp_error( $attribute ) ) {
196
			return $attribute;
197
		}
198
199
		$response = $this->prepare_item_for_response( $attribute, $request );
200
201
		return rest_ensure_response( $response );
202
	}
203
204
	/**
205
	 * Update a single term from a taxonomy.
206
	 *
207
	 * @param \WP_REST_Request $request Full details about the request.
208
	 * @return \WP_REST_Request|\WP_Error
209
	 */
210
	public function update_item( $request ) {
211
		global $wpdb;
212
213
		$id     = (int) $request['id'];
214
		$edited = wc_update_attribute(
215
			$id,
216
			array(
217
				'name'         => $request['name'],
218
				'slug'         => wc_sanitize_taxonomy_name( stripslashes( $request['slug'] ) ),
219
				'type'         => $request['type'],
220
				'order_by'     => $request['order_by'],
221
				'has_archives' => $request['has_archives'],
222
			)
223
		);
224
225
		// Checks for errors.
226
		if ( is_wp_error( $edited ) ) {
227
			return new \WP_Error( 'woocommerce_rest_cannot_edit', $edited->get_error_message(), array( 'status' => 400 ) );
228
		}
229
230
		$attribute = $this->get_attribute( $id );
231
232
		if ( is_wp_error( $attribute ) ) {
233
			return $attribute;
234
		}
235
236
		$this->update_additional_fields_for_object( $attribute, $request );
0 ignored issues
show
Bug introduced by
$attribute of type Automattic\WooCommerce\R...sion4\stdClass|WP_Error is incompatible with the type array expected by parameter $object of WP_REST_Controller::upda...nal_fields_for_object(). ( Ignorable by Annotation )

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

236
		$this->update_additional_fields_for_object( /** @scrutinizer ignore-type */ $attribute, $request );
Loading history...
237
238
		/**
239
		 * Fires after a single product attribute is created or updated via the REST API.
240
		 *
241
		 * @param stdObject       $attribute Inserted attribute object.
242
		 * @param \WP_REST_Request $request   Request object.
243
		 * @param boolean         $creating  True when creating attribute, false when updating.
244
		 */
245
		do_action( 'woocommerce_rest_insert_product_attribute', $attribute, $request, false );
246
247
		$request->set_param( 'context', 'edit' );
248
		$response = $this->prepare_item_for_response( $attribute, $request );
249
250
		return rest_ensure_response( $response );
251
	}
252
253
	/**
254
	 * Delete a single attribute.
255
	 *
256
	 * @param \WP_REST_Request $request Full details about the request.
257
	 * @return \WP_REST_Response|\WP_Error
258
	 */
259
	public function delete_item( $request ) {
260
		$force = isset( $request['force'] ) ? (bool) $request['force'] : false;
261
262
		// We don't support trashing for this type, error out.
263
		if ( ! $force ) {
264
			return new \WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Resource does not support trashing.', 'woocommerce' ), array( 'status' => 501 ) );
265
		}
266
267
		$attribute = $this->get_attribute( (int) $request['id'] );
268
269
		if ( is_wp_error( $attribute ) ) {
270
			return $attribute;
271
		}
272
273
		$request->set_param( 'context', 'edit' );
274
		$previous = $this->prepare_item_for_response( $attribute, $request );
275
		$deleted  = wc_delete_attribute( $attribute->attribute_id );
0 ignored issues
show
Bug introduced by
The property attribute_id does not seem to exist on WP_Error.
Loading history...
276
277
		if ( false === $deleted ) {
278
			return new \WP_Error( 'woocommerce_rest_cannot_delete', __( 'The resource cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) );
279
		}
280
281
		$response = new \WP_REST_Response();
282
		$response->set_data(
283
			array(
284
				'deleted'  => true,
285
				'previous' => $previous->get_data(),
286
			)
287
		);
288
289
		/**
290
		 * Fires after a single attribute is deleted via the REST API.
291
		 *
292
		 * @param stdObject        $attribute     The deleted attribute.
293
		 * @param \WP_REST_Response $response The response data.
294
		 * @param \WP_REST_Request  $request  The request sent to the API.
295
		 */
296
		do_action( 'woocommerce_rest_delete_product_attribute', $attribute, $response, $request );
297
298
		return $response;
299
	}
300
301
	/**
302
	 * Get data for this object in the format of this endpoint's schema.
303
	 *
304
	 * @param object           $object Object to prepare.
305
	 * @param \WP_REST_Request $request Request object.
306
	 * @return array Array of data in the correct format.
307
	 */
308
	protected function get_data_for_response( $object, $request ) {
309
		return array(
310
			'id'           => (int) $object->attribute_id,
311
			'name'         => $object->attribute_label,
312
			'slug'         => wc_attribute_taxonomy_name( $object->attribute_name ),
313
			'type'         => $object->attribute_type,
314
			'order_by'     => $object->attribute_orderby,
315
			'has_archives' => (bool) $object->attribute_public,
316
		);
317
	}
318
319
	/**
320
	 * Prepare links for the request.
321
	 *
322
	 * @param mixed            $item Object to prepare.
323
	 * @param \WP_REST_Request $request Request object.
324
	 * @return array
325
	 */
326
	protected function prepare_links( $item, $request ) {
327
		$base  = '/' . $this->namespace . '/' . $this->rest_base;
328
		$links = array(
329
			'self' => array(
330
				'href' => rest_url( trailingslashit( $base ) . $item->attribute_id ),
331
			),
332
			'collection' => array(
333
				'href' => rest_url( $base ),
334
			),
335
		);
336
337
		return $links;
338
	}
339
340
	/**
341
	 * Get the Attribute's schema, conforming to JSON Schema.
342
	 *
343
	 * @return array
344
	 */
345
	public function get_item_schema() {
346
		$schema = array(
347
			'$schema'              => 'http://json-schema.org/draft-04/schema#',
348
			'title'                => 'product_attribute',
349
			'type'                 => 'object',
350
			'properties'           => array(
351
				'id' => array(
352
					'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
353
					'type'        => 'integer',
354
					'context'     => array( 'view', 'edit' ),
355
					'readonly'    => true,
356
				),
357
				'name' => array(
358
					'description' => __( 'Attribute name.', 'woocommerce' ),
359
					'type'        => 'string',
360
					'context'     => array( 'view', 'edit' ),
361
					'arg_options' => array(
362
						'sanitize_callback' => 'sanitize_text_field',
363
					),
364
				),
365
				'slug' => array(
366
					'description' => __( 'An alphanumeric identifier for the resource unique to its type.', 'woocommerce' ),
367
					'type'        => 'string',
368
					'context'     => array( 'view', 'edit' ),
369
					'arg_options' => array(
370
						'sanitize_callback' => 'sanitize_title',
371
					),
372
				),
373
				'type' => array(
374
					'description' => __( 'Type of attribute.', 'woocommerce' ),
375
					'type'        => 'string',
376
					'default'     => 'select',
377
					'enum'        => array_keys( wc_get_attribute_types() ),
378
					'context'     => array( 'view', 'edit' ),
379
				),
380
				'order_by' => array(
381
					'description' => __( 'Default sort order.', 'woocommerce' ),
382
					'type'        => 'string',
383
					'default'     => 'menu_order',
384
					'enum'        => array( 'menu_order', 'name', 'name_num', 'id' ),
385
					'context'     => array( 'view', 'edit' ),
386
				),
387
				'has_archives' => array(
388
					'description' => __( 'Enable/Disable attribute archives.', 'woocommerce' ),
389
					'type'        => 'boolean',
390
					'default'     => false,
391
					'context'     => array( 'view', 'edit' ),
392
				),
393
			),
394
		);
395
396
		return $this->add_additional_fields_schema( $schema );
397
	}
398
399
	/**
400
	 * Get the query params for collections
401
	 *
402
	 * @return array
403
	 */
404
	public function get_collection_params() {
405
		$params = array();
406
		$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
407
408
		return $params;
409
	}
410
411
	/**
412
	 * Get attribute name.
413
	 *
414
	 * @param \WP_REST_Request $request Full details about the request.
415
	 * @return string
416
	 */
417
	protected function get_taxonomy( $request ) {
418
		if ( '' !== $this->attribute ) {
419
			return $this->attribute;
420
		}
421
422
		if ( $request['id'] ) {
423
			$name = wc_attribute_taxonomy_name_by_id( (int) $request['id'] );
424
425
			$this->attribute = $name;
426
		}
427
428
		return $this->attribute;
429
	}
430
431
	/**
432
	 * Get attribute data.
433
	 *
434
	 * @param int $id Attribute ID.
435
	 * @return stdClass|\WP_Error
0 ignored issues
show
Bug introduced by
The type Automattic\WooCommerce\R...llers\Version4\stdClass was not found. Did you mean stdClass? If so, make sure to prefix the type with \.
Loading history...
436
	 */
437
	protected function get_attribute( $id ) {
438
		global $wpdb;
439
440
		$attribute = $wpdb->get_row(
441
			$wpdb->prepare(
442
				"
443
				SELECT *
444
				FROM {$wpdb->prefix}woocommerce_attribute_taxonomies
445
				WHERE attribute_id = %d
446
				",
447
				$id
448
			)
449
		);
450
451
		if ( is_wp_error( $attribute ) || is_null( $attribute ) ) {
452
			return new \WP_Error( 'woocommerce_rest_attribute_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) );
453
		}
454
455
		return $attribute;
456
	}
457
458
	/**
459
	 * Validate attribute slug.
460
	 *
461
	 * @deprecated 3.2.0
462
	 * @param string $slug Slug being set.
463
	 * @param bool   $new_data Is the data new or old.
464
	 * @return bool|\WP_Error
465
	 */
466
	protected function validate_attribute_slug( $slug, $new_data = true ) {
467
		if ( strlen( $slug ) >= 28 ) {
468
			/* Translators: %s slug. */
469
			return new \WP_Error( 'woocommerce_rest_invalid_product_attribute_slug_too_long', sprintf( __( 'Slug "%s" is too long (28 characters max). Shorten it, please.', 'woocommerce' ), $slug ), array( 'status' => 400 ) );
470
		} elseif ( wc_check_if_attribute_name_is_reserved( $slug ) ) {
471
			/* Translators: %s slug. */
472
			return new \WP_Error( 'woocommerce_rest_invalid_product_attribute_slug_reserved_name', sprintf( __( 'Slug "%s" is not allowed because it is a reserved term. Change it, please.', 'woocommerce' ), $slug ), array( 'status' => 400 ) );
473
		} elseif ( $new_data && taxonomy_exists( wc_attribute_taxonomy_name( $slug ) ) ) {
474
			/* Translators: %s slug. */
475
			return new \WP_Error( 'woocommerce_rest_invalid_product_attribute_slug_already_exists', sprintf( __( 'Slug "%s" is already in use. Change it, please.', 'woocommerce' ), $slug ), array( 'status' => 400 ) );
476
		}
477
478
		return true;
479
	}
480
481
	/**
482
	 * Schedule to flush rewrite rules.
483
	 *
484
	 * @deprecated 3.2.0
485
	 * @since 3.0.0
486
	 */
487
	protected function flush_rewrite_rules() {
488
		wp_schedule_single_event( time(), 'woocommerce_flush_rewrite_rules' );
489
	}
490
}
491