GetPaid_REST_CRUD_Controller   C
last analyzed

Complexity

Total Complexity 55

Size/Duplication

Total Lines 470
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 173
c 0
b 0
f 0
dl 0
loc 470
rs 6
wmc 55

15 Methods

Rating   Name   Duplication   Size   Complexity  
A get_collection_params() 0 4 1
A filter_writable_props() 0 2 1
A create_item() 0 34 5
A update_item() 0 29 5
A get_object() 0 15 5
A save_object() 0 8 2
A get_item() 0 11 2
A prepare_links() 0 12 1
A register_namespace_routes() 0 61 1
A prepare_item_for_response() 0 21 2
C prepare_item_for_database() 0 45 12
A prepare_object_meta_data() 0 8 2
A prepare_invoice_items() 0 15 3
C prepare_object_data() 0 51 12
A object_supports_field() 0 2 1

How to fix   Complexity   

Complex Class

Complex classes like GetPaid_REST_CRUD_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.

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 GetPaid_REST_CRUD_Controller, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * GetPaid REST CRUD controller class.
4
 *
5
 * Extends the GetPaid_REST_Controller class to provide functionalities for endpoints
6
 * that use our CRUD classes
7
 *
8
 * @version 1.0.19
9
 */
10
11
defined( 'ABSPATH' ) || exit;
12
13
/**
14
 * GetPaid REST CRUD controller class.
15
 *
16
 * @package Invoicing
17
 */
18
class GetPaid_REST_CRUD_Controller extends GetPaid_REST_Controller {
19
20
	/**
21
	 * Contains this controller's class name.
22
	 *
23
	 * @var string
24
	 */
25
	public $crud_class;
26
27
	/**
28
	 * Contains the current CRUD object.
29
	 *
30
	 * @var GetPaid_Data
31
	 */
32
	protected $data_object;
33
34
	/**
35
	 * Registers the routes for the objects of the controller.
36
	 *
37
	 * @since 1.0.19
38
	 *
39
	 * @see register_rest_route()
40
	 */
41
	public function register_namespace_routes( $namespace ) {
42
43
		register_rest_route(
44
			$namespace,
45
			'/' . $this->rest_base,
46
			array(
47
				array(
48
					'methods'             => WP_REST_Server::READABLE,
49
					'callback'            => array( $this, 'get_items' ),
50
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
51
					'args'                => $this->get_collection_params(),
52
				),
53
				array(
54
					'methods'             => WP_REST_Server::CREATABLE,
55
					'callback'            => array( $this, 'create_item' ),
56
					'permission_callback' => array( $this, 'create_item_permissions_check' ),
57
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
58
				),
59
				'schema' => array( $this, 'get_public_item_schema' ),
60
			)
61
		);
62
63
		$get_item_args = array(
64
			'context' => $this->get_context_param( array( 'default' => 'view' ) ),
65
		);
66
67
		register_rest_route(
68
			$namespace,
69
			'/' . $this->rest_base . '/(?P<id>[\d]+)',
70
			array(
71
				'args'   => array(
72
					'id' => array(
73
						'description' => __( 'Unique identifier for the object.', 'invoicing' ),
74
						'type'        => 'integer',
75
					),
76
				),
77
				array(
78
					'methods'             => WP_REST_Server::READABLE,
79
					'callback'            => array( $this, 'get_item' ),
80
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
81
					'args'                => $get_item_args,
82
				),
83
				array(
84
					'methods'             => WP_REST_Server::EDITABLE,
85
					'callback'            => array( $this, 'update_item' ),
86
					'permission_callback' => array( $this, 'update_item_permissions_check' ),
87
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
88
				),
89
				array(
90
					'methods'             => WP_REST_Server::DELETABLE,
91
					'callback'            => array( $this, 'delete_item' ),
92
					'permission_callback' => array( $this, 'delete_item_permissions_check' ),
93
					'args'                => array(
94
						'force' => array(
95
							'type'        => 'boolean',
96
							'default'     => false,
97
							'description' => __( 'Whether to bypass Trash and force deletion.', 'invoicing' ),
98
						),
99
					),
100
				),
101
				'schema' => array( $this, 'get_public_item_schema' ),
102
			)
103
		);
104
105
	}
106
107
	/**
108
	 * Saves a single object.
109
	 *
110
	 * @param GetPaid_Data $object Object to save.
111
	 * @return WP_Error|GetPaid_Data
112
	 */
113
	protected function save_object( $object ) {
114
		$object->save();
115
116
		if ( ! empty( $object->last_error ) ) {
117
			return new WP_Error( 'rest_cannot_save', $object->last_error, array( 'status' => 400 ) );
118
		}
119
120
		return new $this->crud_class( $object->get_id() );
121
	}
122
123
	/**
124
	 * Retrieves a single object.
125
	 *
126
	 * @since 1.0.13
127
	 *
128
	 * @param int|WP_Post $object_id Supplied ID.
129
	 * @return GetPaid_Data|WP_Error GetPaid_Data object if ID is valid, WP_Error otherwise.
130
	 */
131
	protected function get_object( $object_id ) {
132
133
		// Do we have an object?
134
		if ( empty( $this->crud_class ) || ! class_exists( $this->crud_class ) ) {
135
			return new WP_Error( 'no_crud_class', __( 'You need to specify a CRUD class for this controller', 'invoicing' ) );
136
		}
137
138
		// Fetch the object.
139
		$object = new $this->crud_class( $object_id );
140
		if ( ! empty( $object->last_error ) ) {
141
			return new WP_Error( 'rest_object_invalid_id', $object->last_error, array( 'status' => 404 ) );
142
		}
143
144
		$this->data_object = $object;
145
		return $object->get_id() ? $object : new WP_Error( 'rest_object_invalid_id', __( 'Invalid ID.', 'invoicing' ), array( 'status' => 404 ) );
146
147
	}
148
149
	/**
150
	 * Get a single object.
151
	 *
152
	 * @param WP_REST_Request $request Full details about the request.
153
	 * @return WP_Error|WP_REST_Response
154
	 */
155
	public function get_item( $request ) {
156
157
		// Fetch the item.
158
		$object = $this->get_object( $request['id'] );
159
160
		if ( is_wp_error( $object ) ) {
161
			return $object;
162
		}
163
164
		// Generate a response.
165
		return rest_ensure_response( $this->prepare_item_for_response( $object, $request ) );
0 ignored issues
show
Bug introduced by
$object of type WP_Error is incompatible with the type GetPaid_Data expected by parameter $object of GetPaid_REST_CRUD_Contro...are_item_for_response(). ( Ignorable by Annotation )

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

165
		return rest_ensure_response( $this->prepare_item_for_response( /** @scrutinizer ignore-type */ $object, $request ) );
Loading history...
166
167
	}
168
169
	/**
170
	 * Create a single object.
171
	 *
172
	 * @param WP_REST_Request $request Full details about the request.
173
	 * @return WP_Error|WP_REST_Response
174
	 */
175
	public function create_item( $request ) {
176
177
		// Can not create an existing item.
178
		if ( ! empty( $request['id'] ) ) {
179
			/* translators: %s: post type */
180
			return new WP_Error( "getpaid_rest_{$this->rest_base}_exists", __( 'Cannot create existing resource.', 'invoicing' ), array( 'status' => 400 ) );
181
		}
182
183
		// Generate a GetPaid_Data object from the request.
184
		$object = $this->prepare_item_for_database( $request );
185
		if ( is_wp_error( $object ) ) {
186
			return $object;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $object also could return the type GetPaid_Data which is incompatible with the documented return type WP_Error|WP_REST_Response.
Loading history...
187
		}
188
189
		// Save the object.
190
		$object = $this->save_object( $object );
0 ignored issues
show
Bug introduced by
It seems like $object can also be of type WP_Error; however, parameter $object of GetPaid_REST_CRUD_Controller::save_object() does only seem to accept GetPaid_Data, 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

190
		$object = $this->save_object( /** @scrutinizer ignore-type */ $object );
Loading history...
191
		if ( is_wp_error( $object ) ) {
192
			return $object;
193
		}
194
195
		// Save special fields.
196
		$save_special = $this->update_additional_fields_for_object( $object, $request );
197
		if ( is_wp_error( $save_special ) ) {
198
			$object->delete( true );
0 ignored issues
show
Bug introduced by
The method delete() does not exist on WP_Error. ( Ignorable by Annotation )

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

198
			$object->/** @scrutinizer ignore-call */ 
199
            delete( true );

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...
199
			return $save_special;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $save_special also could return the type true which is incompatible with the documented return type WP_Error|WP_REST_Response.
Loading history...
200
		}
201
202
		$request->set_param( 'context', 'edit' );
203
		$response = $this->prepare_item_for_response( $object, $request );
0 ignored issues
show
Bug introduced by
$object of type WP_Error is incompatible with the type GetPaid_Data expected by parameter $object of GetPaid_REST_CRUD_Contro...are_item_for_response(). ( Ignorable by Annotation )

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

203
		$response = $this->prepare_item_for_response( /** @scrutinizer ignore-type */ $object, $request );
Loading history...
204
		$response = rest_ensure_response( $response );
205
		$response->set_status( 201 );
206
		$response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $object->get_id() ) ) );
0 ignored issues
show
Bug introduced by
The method get_id() does not exist on WP_Error. ( Ignorable by Annotation )

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

206
		$response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $object->/** @scrutinizer ignore-call */ get_id() ) ) );

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...
207
208
		return $response;
209
	}
210
211
	/**
212
	 * Update a single object.
213
	 *
214
	 * @param WP_REST_Request $request Full details about the request.
215
	 * @return WP_Error|WP_REST_Response
216
	 */
217
	public function update_item( $request ) {
218
219
		// Fetch the item.
220
		$object = $this->get_object( $request['id'] );
221
		if ( is_wp_error( $object ) ) {
222
			return $object;
223
		}
224
225
		// Prepare the item for saving.
226
		$object = $this->prepare_item_for_database( $request );
227
		if ( is_wp_error( $object ) ) {
228
			return $object;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $object also could return the type GetPaid_Data which is incompatible with the documented return type WP_Error|WP_REST_Response.
Loading history...
229
		}
230
231
		// Save the item.
232
		$object = $this->save_object( $object );
0 ignored issues
show
Bug introduced by
It seems like $object can also be of type WP_Error; however, parameter $object of GetPaid_REST_CRUD_Controller::save_object() does only seem to accept GetPaid_Data, 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

232
		$object = $this->save_object( /** @scrutinizer ignore-type */ $object );
Loading history...
233
		if ( is_wp_error( $object ) ) {
234
			return $object;
235
		}
236
237
		// Save special fields (those added via hooks).
238
		$save_special = $this->update_additional_fields_for_object( $object, $request );
239
		if ( is_wp_error( $save_special ) ) {
240
			return $save_special;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $save_special also could return the type true which is incompatible with the documented return type WP_Error|WP_REST_Response.
Loading history...
241
		}
242
243
		$request->set_param( 'context', 'edit' );
244
		$response = $this->prepare_item_for_response( $object, $request );
0 ignored issues
show
Bug introduced by
$object of type WP_Error is incompatible with the type GetPaid_Data expected by parameter $object of GetPaid_REST_CRUD_Contro...are_item_for_response(). ( Ignorable by Annotation )

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

244
		$response = $this->prepare_item_for_response( /** @scrutinizer ignore-type */ $object, $request );
Loading history...
245
		return rest_ensure_response( $response );
246
	}
247
248
	/**
249
	 * Prepare links for the request.
250
	 *
251
	 * @param GetPaid_Data    $object GetPaid_Data object.
252
	 * @return array Links for the given object.
253
	 */
254
	protected function prepare_links( $object ) {
255
256
		$links = array(
257
			'self'       => array(
258
				'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $object->get_id() ) ),
259
			),
260
			'collection' => array(
261
				'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ),
262
			),
263
		);
264
265
		return $links;
266
	}
267
268
	/**
269
	 * Get the query params for collections of attachments.
270
	 *
271
	 * @return array
272
	 */
273
	public function get_collection_params() {
274
		$params = parent::get_collection_params();
275
		$params['context']['default'] = 'view';
276
		return $params;
277
	}
278
279
	/**
280
	 * Only return writable props from schema.
281
	 *
282
	 * @param  array $schema Schema.
283
	 * @return bool
284
	 */
285
	public function filter_writable_props( $schema ) {
286
		return empty( $schema['readonly'] );
287
	}
288
289
	/**
290
	 * Prepare a single object for create or update.
291
	 *
292
	 * @since 1.0.19
293
	 * @param  WP_REST_Request $request Request object.
294
	 * @return GetPaid_Data|WP_Error Data object or WP_Error.
295
	 */
296
	protected function prepare_item_for_database( $request ) {
297
298
		// Do we have an object?
299
		if ( empty( $this->crud_class ) || ! class_exists( $this->crud_class ) ) {
300
			return new WP_Error( 'no_crud_class', __( 'You need to specify a CRUD class for this controller', 'invoicing' ) );
301
		}
302
303
		// Prepare the object.
304
		$id        = isset( $request['id'] ) ? absint( $request['id'] ) : 0;
305
		$object    = new $this->crud_class( $id );
306
307
		// Abort if an error exists.
308
		if ( ! empty( $object->last_error ) ) {
309
			return new WP_Error( 'invalid_item', $object->last_error );
310
		}
311
312
		$schema    = $this->get_item_schema();
313
		$data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) );
314
315
		// Handle all writable props.
316
		foreach ( $data_keys as $key ) {
317
			$value = $request[ $key ];
318
319
			if ( ! is_null( $value ) ) {
320
				switch ( $key ) {
321
322
					case 'meta_data':
323
						if ( is_array( $value ) ) {
324
							foreach ( $value as $meta ) {
325
								$object->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' );
326
							}
327
						}
328
						break;
329
330
					default:
331
						if ( is_callable( array( $object, "set_{$key}" ) ) ) {
332
							$object->{"set_{$key}"}( $value );
333
						}
334
						break;
335
				}
336
			}
337
}
338
339
		// Filters an object before it is inserted via the REST API..
340
		return apply_filters( "getpaid_rest_pre_insert_{$this->rest_base}_object", $object, $request );
341
	}
342
343
	/**
344
	 * Retrieves data from a GetPaid class.
345
	 *
346
	 * @since  1.0.19
347
	 * @param  GetPaid_Meta_Data[]    $meta_data  meta data objects.
348
	 * @return array
349
	 */
350
	protected function prepare_object_meta_data( $meta_data ) {
351
		$meta = array();
352
353
		foreach ( $meta_data as $object ) {
354
			$meta[] = $object->get_data();
355
		}
356
357
		return $meta;
358
	}
359
360
	/**
361
	 * Retrieves invoice items.
362
	 *
363
	 * @since  1.0.19
364
	 * @param  WPInv_Invoice $invoice  Invoice items.
365
	 * @param array            $fields Fields to include.
366
	 * @return array
367
	 */
368
	protected function prepare_invoice_items( $invoice ) {
369
		$items = array();
370
371
		foreach ( $invoice->get_items() as $item ) {
372
373
			$item_data = $item->prepare_data_for_saving();
374
375
			if ( 'amount' == $invoice->get_template() ) {
376
				$item_data['quantity'] = 1;
377
			}
378
379
			$items[] = $item_data;
380
		}
381
382
		return $items;
383
	}
384
385
	/**
386
	 * Retrieves data from a GetPaid class.
387
	 *
388
	 * @since  1.0.19
389
	 * @param  GetPaid_Data    $object  Data object.
390
	 * @param array            $fields Fields to include.
391
	 * @param string           $context either view or edit.
392
	 * @return array
393
	 */
394
	protected function prepare_object_data( $object, $fields, $context = 'view' ) {
395
396
		$data = array();
397
398
		// Handle all writable props.
399
		foreach ( array_keys( $this->get_schema_properties() ) as $key ) {
400
401
			// Abort if it is not included.
402
			if ( ! empty( $fields ) && ! $this->is_field_included( $key, $fields ) ) {
403
				continue;
404
			}
405
406
			// Or this current object does not support the field.
407
			if ( ! $this->object_supports_field( $object, $key ) ) {
408
				continue;
409
			}
410
411
			// Handle meta data.
412
			if ( $key == 'meta_data' ) {
413
				$data['meta_data'] = $this->prepare_object_meta_data( $object->get_meta_data() );
414
				continue;
415
			}
416
417
			// Handle items.
418
			if ( $key == 'items' && is_a( $object, 'WPInv_Invoice' ) ) {
419
				$data['items'] = $this->prepare_invoice_items( $object );
420
				continue;
421
			}
422
423
			// Booleans.
424
			if ( is_callable( array( $object, $key ) ) ) {
425
				$data[ $key ] = $object->$key( $context );
426
				continue;
427
			}
428
429
			// Get object value.
430
			if ( is_callable( array( $object, "get_{$key}" ) ) ) {
431
				$value = $object->{"get_{$key}"}( $context );
432
433
				// If the value is an instance of GetPaid_Data...
434
				if ( is_a( $value, 'GetPaid_Data' ) ) {
435
					$value = $value->get_data( $context );
0 ignored issues
show
Unused Code introduced by
The call to GetPaid_Data::get_data() has too many arguments starting with $context. ( Ignorable by Annotation )

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

435
					/** @scrutinizer ignore-call */ 
436
     $value = $value->get_data( $context );

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
436
				}
437
438
				// For objects, retrieves it's properties.
439
				$data[ $key ] = is_object( $value ) ? get_object_vars( $value ) : $value;
440
				continue;
441
			}
442
}
443
444
		return $data;
445
	}
446
447
	/**
448
	 * Checks if a key should be included in a response.
449
	 *
450
	 * @since  1.0.19
451
	 * @param  GetPaid_Data $object  Data object.
452
	 * @param  string       $field_key The key to check for.
453
	 * @return bool
454
	 */
455
	public function object_supports_field( $object, $field_key ) {
456
		return apply_filters( 'getpaid_rest_object_supports_key', true, $object, $field_key );
457
	}
458
459
	/**
460
	 * Prepare a single object output for response.
461
	 *
462
	 * @since  1.0.19
463
	 * @param  GetPaid_Data    $object  Data object.
464
	 * @param  WP_REST_Request $request Request object.
465
	 * @return WP_REST_Response
466
	 */
467
	public function prepare_item_for_response( $object, $request ) {
468
		remove_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10 );
469
470
		$this->data_object = $object;
471
472
		// Fetch the fields to include in this response.
473
		$fields = $this->get_fields_for_response( $request );
474
475
		// Prepare object data.
476
		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
477
		$data    = $this->prepare_object_data( $object, $fields, $context );
478
		$data    = $this->add_additional_fields_to_object( $data, $request );
479
		$data    = $this->limit_object_to_requested_fields( $data, $fields );
480
		$data    = $this->filter_response_by_context( $data, $context );
481
482
		// Prepare the response.
483
		$response = rest_ensure_response( $data );
484
		$response->add_links( $this->prepare_links( $object, $request ) );
0 ignored issues
show
Unused Code introduced by
The call to GetPaid_REST_CRUD_Controller::prepare_links() has too many arguments starting with $request. ( Ignorable by Annotation )

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

484
		$response->add_links( $this->/** @scrutinizer ignore-call */ prepare_links( $object, $request ) );

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
485
486
		// Filter item response.
487
		return apply_filters( "getpaid_rest_prepare_{$this->rest_base}_object", $response, $object, $request );
488
	}
489
490
}
491