Completed
Push — master ( 5a1571...ae5037 )
by Rodrigo
18:56
created

WC_REST_Orders_Controller   F

Complexity

Total Complexity 60

Size/Duplication

Total Lines 272
Duplicated Lines 23.16 %

Coupling/Cohesion

Components 1
Dependencies 6

Test Coverage

Coverage 78.56%

Importance

Changes 0
Metric Value
dl 63
loc 272
ccs 88
cts 112
cp 0.7856
rs 3.6
c 0
b 0
f 0
wmc 60
lcom 1
cbo 6

4 Methods

Rating   Name   Duplication   Size   Complexity  
B calculate_coupons() 0 28 9
A purge() 0 7 3
A get_item_schema() 0 7 1
A get_collection_params() 0 16 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like WC_REST_Orders_Controller often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use WC_REST_Orders_Controller, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * REST API Orders controller
4
 *
5
 * Handles requests to the /orders endpoint.
6
 *
7
 * @package  WooCommerce/API
8
 * @since    2.6.0
9
 */
10
11
defined( 'ABSPATH' ) || exit;
12
13
/**
14
 * REST API Orders controller class.
15
 *
16
 * @package WooCommerce/API
17
 * @extends WC_REST_Orders_V2_Controller
18
 */
19
class WC_REST_Orders_Controller extends WC_REST_Orders_V2_Controller {
20
21
	/**
22
	 * Endpoint namespace.
23
	 *
24
	 * @var string
25
	 */
26
	protected $namespace = 'wc/v3';
27
28
	/**
29
	 * Calculate coupons.
30
	 *
31
	 * @throws WC_REST_Exception When fails to set any item.
32
	 * @param WP_REST_Request $request Request object.
33
	 * @param WC_Order        $order   Order data.
34
	 * @return bool
35
	 */
36 9
	protected function calculate_coupons( $request, $order ) {
37 9
		if ( ! isset( $request['coupon_lines'] ) || ! is_array( $request['coupon_lines'] ) ) {
38 6
			return false;
39
		}
40
41
		// Remove all coupons first to ensure calculation is correct.
42 3
		foreach ( $order->get_items( 'coupon' ) as $coupon ) {
43 1
			$order->remove_coupon( $coupon->get_code() );
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class WC_Order_Item as the method get_code() does only exist in the following sub-classes of WC_Order_Item: WC_Order_Item_Coupon. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
44
		}
45
46 3
		foreach ( $request['coupon_lines'] as $item ) {
47 3
			if ( is_array( $item ) ) {
48 3
				if ( empty( $item['id'] ) ) {
49 2
					if ( empty( $item['code'] ) ) {
50
						throw new WC_REST_Exception( 'woocommerce_rest_invalid_coupon', __( 'Coupon code is required.', 'woocommerce' ), 400 );
51
					}
52
53 2
					$results = $order->apply_coupon( wc_clean( $item['code'] ) );
54
55 2
					if ( is_wp_error( $results ) ) {
56 1
						throw new WC_REST_Exception( 'woocommerce_rest_' . $results->get_error_code(), $results->get_error_message(), 400 );
57
					}
58
				}
59
			}
60
		}
61
62 2
		return true;
63
	}
64
65
	/**
66
	 * Prepare a single order for create or update.
67
	 *
68
	 * @throws WC_REST_Exception When fails to set any item.
69
	 * @param  WP_REST_Request $request Request object.
70
	 * @param  bool            $creating If is creating a new object.
71
	 * @return WP_Error|WC_Data
72
	 */
73 9 View Code Duplication
	protected function prepare_object_for_database( $request, $creating = false ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
74 9
		$id        = isset( $request['id'] ) ? absint( $request['id'] ) : 0;
75 9
		$order     = new WC_Order( $id );
76 9
		$schema    = $this->get_item_schema();
77 9
		$data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) );
78
79
		// Handle all writable props.
80 9
		foreach ( $data_keys as $key ) {
81 9
			$value = $request[ $key ];
82
83 9
			if ( ! is_null( $value ) ) {
84 9
				switch ( $key ) {
85
					case 'coupon_lines':
86
					case 'status':
87
						// Change should be done later so transitions have new data.
88 6
						break;
89
					case 'billing':
90
					case 'shipping':
91 4
						$this->update_address( $order, $value, $key );
92 4
						break;
93
					case 'line_items':
94
					case 'shipping_lines':
95
					case 'fee_lines':
96 5
						if ( is_array( $value ) ) {
97 5
							foreach ( $value as $item ) {
98 5
								if ( is_array( $item ) ) {
99 5
									if ( $this->item_is_null( $item ) || ( isset( $item['quantity'] ) && 0 === $item['quantity'] ) ) {
100 1
										$order->remove_item( $item['id'] );
101
									} else {
102 4
										$this->set_item( $order, $key, $item );
103
									}
104
								}
105
							}
106
						}
107 5
						break;
108
					case 'meta_data':
109
						if ( is_array( $value ) ) {
110
							foreach ( $value as $meta ) {
111
								$order->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' );
112
							}
113
						}
114
						break;
115
					default:
116 5
						if ( is_callable( array( $order, "set_{$key}" ) ) ) {
117 5
							$order->{"set_{$key}"}( $value );
118
						}
119 5
						break;
120
				}
121
			}
122
		}
123
124
		/**
125
		 * Filters an object before it is inserted via the REST API.
126
		 *
127
		 * The dynamic portion of the hook name, `$this->post_type`,
128
		 * refers to the object type slug.
129
		 *
130
		 * @param WC_Data         $order    Object object.
131
		 * @param WP_REST_Request $request  Request object.
132
		 * @param bool            $creating If is creating a new object.
133
		 */
134 9
		return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $order, $request, $creating );
135
	}
136
137
	/**
138
	 * Save an object data.
139
	 *
140
	 * @since  3.0.0
141
	 * @throws WC_REST_Exception But all errors are validated before returning any data.
142
	 * @param  WP_REST_Request $request  Full details about the request.
143
	 * @param  bool            $creating If is creating a new object.
144
	 * @return WC_Data|WP_Error
145
	 */
146 9
	protected function save_object( $request, $creating = false ) {
147 9
		$object = null;
148
149
		try {
150 9
			$object = $this->prepare_object_for_database( $request, $creating );
151
152 9
			if ( is_wp_error( $object ) ) {
153
				return $object;
154
			}
155
156
			// Make sure gateways are loaded so hooks from gateways fire on save/create.
157 9
			WC()->payment_gateways();
158
159 9
			if ( ! is_null( $request['customer_id'] ) && 0 !== $request['customer_id'] ) {
160
				// Make sure customer exists.
161
				if ( false === get_user_by( 'id', $request['customer_id'] ) ) {
162
					throw new WC_REST_Exception( 'woocommerce_rest_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 );
163
				}
164
165
				// Make sure customer is part of blog.
166
				if ( is_multisite() && ! is_user_member_of_blog( $request['customer_id'] ) ) {
167
					add_user_to_blog( get_current_blog_id(), $request['customer_id'], 'customer' );
168
				}
169
			}
170
171 9
			if ( $creating ) {
172 3
				$object->set_created_via( 'rest-api' );
0 ignored issues
show
Bug introduced by
The method set_created_via() does not seem to exist on object<WC_Data>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
173 3
				$object->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) );
0 ignored issues
show
Bug introduced by
The method set_prices_include_tax() does not seem to exist on object<WC_Data>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
174 3
				$object->calculate_totals();
0 ignored issues
show
Bug introduced by
The method calculate_totals() does not seem to exist on object<WC_Data>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
175
			} else {
176
				// If items have changed, recalculate order totals.
177 8
				if ( isset( $request['billing'] ) || isset( $request['shipping'] ) || isset( $request['line_items'] ) || isset( $request['shipping_lines'] ) || isset( $request['fee_lines'] ) || isset( $request['coupon_lines'] ) ) {
178 5
					$object->calculate_totals( true );
0 ignored issues
show
Bug introduced by
The method calculate_totals() does not seem to exist on object<WC_Data>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
179
				}
180
			}
181
182
			// Set coupons.
183 9
			$this->calculate_coupons( $request, $object );
184
185
			// Set status.
186 8
			if ( ! empty( $request['status'] ) ) {
187 3
				$object->set_status( $request['status'] );
0 ignored issues
show
Bug introduced by
The method set_status() does not seem to exist on object<WC_Data>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
188
			}
189
190 8
			$object->save();
191
192
			// Actions for after the order is saved.
193 8
			if ( true === $request['set_paid'] ) {
194 3
				if ( $creating || $object->needs_payment() ) {
0 ignored issues
show
Bug introduced by
The method needs_payment() does not seem to exist on object<WC_Data>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
195 3
					$object->payment_complete( $request['transaction_id'] );
0 ignored issues
show
Bug introduced by
The method payment_complete() does not seem to exist on object<WC_Data>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
196
				}
197
			}
198
199 8
			return $this->get_object( $object->get_id() );
200 1
		} catch ( WC_Data_Exception $e ) {
201 1
			$this->purge( $object, $creating );
202 1
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() );
203
		} catch ( WC_REST_Exception $e ) {
204
			$this->purge( $object, $creating );
205
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
206
		}
207
	}
208
209
	/**
210
	 * Purge object when creating.
211
	 *
212
	 * @param WC_Data $object  Object data.
213
	 * @param bool    $creating If is creating a new object.
214
	 * @return bool
215
	 */
216 1
	protected function purge( $object, $creating ) {
217 1
		if ( $object instanceof WC_Data && $creating ) {
218
			return $object->delete( true );
219
		}
220
221 1
		return false;
222
	}
223
224
	/**
225
	 * Prepare objects query.
226
	 *
227
	 * @since  3.0.0
228
	 * @param  WP_REST_Request $request Full details about the request.
229
	 * @return array
230
	 */
231 2
	protected function prepare_objects_query( $request ) {
232
		// This is needed to get around an array to string notice in WC_REST_Orders_V2_Controller::prepare_objects_query.
233 2
		$statuses = $request['status'];
234 2
		unset( $request['status'] );
235 2
		$args = parent::prepare_objects_query( $request );
236
237 2
		$args['post_status'] = array();
238 2
		foreach ( $statuses as $status ) {
239 2
			if ( in_array( $status, $this->get_order_statuses(), true ) ) {
240
				$args['post_status'][] = 'wc-' . $status;
241 2
			} elseif ( 'any' === $status ) {
242
				// Set status to "any" and short-circuit out.
243 2
				$args['post_status'] = 'any';
244 2
				break;
245
			} else {
246
				$args['post_status'][] = $status;
247
			}
248
		}
249
250
		// Put the statuses back for further processing (next/prev links, etc).
251 2
		$request['status'] = $statuses;
252
253 2
		return $args;
254
	}
255
256
	/**
257
	 * Get the Order's schema, conforming to JSON Schema.
258
	 *
259
	 * @return array
260
	 */
261 426
	public function get_item_schema() {
262 426
		$schema = parent::get_item_schema();
263
264 426
		$schema['properties']['coupon_lines']['items']['properties']['discount']['readonly'] = true;
265
266 426
		return $schema;
267
	}
268
269
	/**
270
	 * Get the query params for collections.
271
	 *
272
	 * @return array
273
	 */
274 426
	public function get_collection_params() {
275 426
		$params = parent::get_collection_params();
276
277 426
		$params['status'] = array(
278 426
			'default'           => 'any',
279 426
			'description'       => __( 'Limit result set to orders which have specific statuses.', 'woocommerce' ),
280 426
			'type'              => 'array',
281
			'items'             => array(
282 426
				'type' => 'string',
283 426
				'enum' => array_merge( array( 'any', 'trash' ), $this->get_order_statuses() ),
284
			),
285 426
			'validate_callback' => 'rest_validate_request_arg',
286
		);
287
288 426
		return $params;
289
	}
290
}
291