WP_REST_Controller   C
last analyzed

Complexity

Total Complexity 71

Size/Duplication

Total Lines 537
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 0

Importance

Changes 0
Metric Value
dl 0
loc 537
rs 5.5904
c 0
b 0
f 0
wmc 71
lcom 1
cbo 0

26 Methods

Rating   Name   Duplication   Size   Complexity  
A register_routes() 0 3 1
A get_items_permissions_check() 0 5 1
A get_items() 0 5 1
A get_item_permissions_check() 0 5 1
A get_item() 0 5 1
A create_item_permissions_check() 0 5 1
A create_item() 0 5 1
A update_item_permissions_check() 0 5 1
A update_item() 0 5 1
A delete_item_permissions_check() 0 5 1
A delete_item() 0 5 1
A prepare_item_for_database() 0 5 1
A prepare_item_for_response() 0 5 1
A prepare_response_for_collection() 0 20 4
C filter_response_by_context() 0 29 11
A get_item_schema() 0 3 1
A get_public_item_schema() 0 12 3
B get_collection_params() 0 28 1
B get_context_param() 0 23 5
A add_additional_fields_to_object() 0 15 3
A update_additional_fields_for_object() 0 18 4
B add_additional_fields_schema() 0 22 4
B get_additional_fields() 0 18 5
A get_object_type() 0 9 3
C get_endpoint_args_for_item_schema() 0 53 13
A get_post() 0 15 1

How to fix   Complexity   

Complex Class

Complex classes like WP_REST_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 WP_REST_Controller, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
4
abstract class WP_REST_Controller {
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
5
6
	/**
7
	 * The namespace of this controller's route.
8
	 *
9
	 * @var string
10
	 */
11
	protected $namespace;
12
13
	/**
14
	 * The base of this controller's route.
15
	 *
16
	 * @var string
17
	 */
18
	protected $rest_base;
19
20
	/**
21
	 * Register the routes for the objects of the controller.
22
	 */
23
	public function register_routes() {
24
		_doing_it_wrong( 'WP_REST_Controller::register_routes', __( 'The register_routes() method must be overriden' ), 'WPAPI-2.0' );
25
	}
26
27
	/**
28
	 * Check if a given request has access to get items.
29
	 *
30
	 * @param WP_REST_Request $request Full data about the request.
31
	 * @return WP_Error|boolean
32
	 */
33
	public function get_items_permissions_check( $request ) {
34
		return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be over-ridden in subclass." ), __METHOD__ ), array(
35
			'status' => 405,
36
		) );
37
	}
38
39
	/**
40
	 * Get a collection of items.
41
	 *
42
	 * @param WP_REST_Request $request Full data about the request.
43
	 * @return WP_Error|WP_REST_Response
44
	 */
45
	public function get_items( $request ) {
46
		return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be over-ridden in subclass." ), __METHOD__ ), array(
47
			'status' => 405,
48
		) );
49
	}
50
51
	/**
52
	 * Check if a given request has access to get a specific item.
53
	 *
54
	 * @param WP_REST_Request $request Full data about the request.
55
	 * @return WP_Error|boolean
56
	 */
57
	public function get_item_permissions_check( $request ) {
58
		return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be over-ridden in subclass." ), __METHOD__ ), array(
59
			'status' => 405,
60
		) );
61
	}
62
63
	/**
64
	 * Get one item from the collection.
65
	 *
66
	 * @param WP_REST_Request $request Full data about the request.
67
	 * @return WP_Error|WP_REST_Response
68
	 */
69
	public function get_item( $request ) {
70
		return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be over-ridden in subclass." ), __METHOD__ ), array(
71
			'status' => 405,
72
		) );
73
	}
74
75
	/**
76
	 * Check if a given request has access to create items.
77
	 *
78
	 * @param WP_REST_Request $request Full data about the request.
79
	 * @return WP_Error|boolean
80
	 */
81
	public function create_item_permissions_check( $request ) {
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
82
		return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be over-ridden in subclass." ), __METHOD__ ), array(
83
			'status' => 405,
84
		) );
85
	}
86
87
	/**
88
	 * Create one item from the collection.
89
	 *
90
	 * @param WP_REST_Request $request Full data about the request.
91
	 * @return WP_Error|WP_REST_Response
92
	 */
93
	public function create_item( $request ) {
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
94
		return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be over-ridden in subclass." ), __METHOD__ ), array(
95
			'status' => 405,
96
		) );
97
	}
98
99
	/**
100
	 * Check if a given request has access to update a specific item.
101
	 *
102
	 * @param WP_REST_Request $request Full data about the request.
103
	 * @return WP_Error|boolean
104
	 */
105
	public function update_item_permissions_check( $request ) {
106
		return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be over-ridden in subclass." ), __METHOD__ ), array(
107
			'status' => 405,
108
		) );
109
	}
110
111
	/**
112
	 * Update one item from the collection.
113
	 *
114
	 * @param WP_REST_Request $request Full data about the request.
115
	 * @return WP_Error|WP_REST_Response
116
	 */
117
	public function update_item( $request ) {
118
		return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be over-ridden in subclass." ), __METHOD__ ), array(
119
			'status' => 405,
120
		) );
121
	}
122
123
	/**
124
	 * Check if a given request has access to delete a specific item.
125
	 *
126
	 * @param WP_REST_Request $request Full data about the request.
127
	 * @return WP_Error|boolean
128
	 */
129
	public function delete_item_permissions_check( $request ) {
130
		return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be over-ridden in subclass." ), __METHOD__ ), array(
131
			'status' => 405,
132
		) );
133
	}
134
135
	/**
136
	 * Delete one item from the collection.
137
	 *
138
	 * @param WP_REST_Request $request Full data about the request.
139
	 * @return WP_Error|WP_REST_Response
140
	 */
141
	public function delete_item( $request ) {
142
		return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be over-ridden in subclass." ), __METHOD__ ), array(
143
			'status' => 405,
144
		) );
145
	}
146
147
	/**
148
	 * Prepare the item for create or update operation.
149
	 *
150
	 * @param WP_REST_Request $request Request object.
151
	 * @return WP_Error|object $prepared_item
152
	 */
153
	protected function prepare_item_for_database( $request ) {
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
154
		return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be over-ridden in subclass." ), __METHOD__ ), array(
155
			'status' => 405,
156
		) );
157
	}
158
159
	/**
160
	 * Prepare the item for the REST response.
161
	 *
162
	 * @param mixed           $item WordPress representation of the item.
163
	 * @param WP_REST_Request $request Request object.
164
	 * @return WP_REST_Response $response
165
	 */
166
	public function prepare_item_for_response( $item, $request ) {
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
167
		return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be over-ridden in subclass." ), __METHOD__ ), array(
168
			'status' => 405,
169
		) );
170
	}
171
172
	/**
173
	 * Prepare a response for inserting into a collection.
174
	 *
175
	 * @param WP_REST_Response $response Response object.
176
	 * @return array Response data, ready for insertion into collection data.
177
	 */
178
	public function prepare_response_for_collection( $response ) {
179
		if ( ! ( $response instanceof WP_REST_Response ) ) {
0 ignored issues
show
Bug introduced by
The class WP_REST_Response does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
180
			return $response;
181
		}
182
183
		$data = (array) $response->get_data();
184
		$server = rest_get_server();
185
186
		if ( method_exists( $server, 'get_compact_response_links' ) ) {
187
			$links = call_user_func( array( $server, 'get_compact_response_links' ), $response );
188
		} else {
189
			$links = call_user_func( array( $server, 'get_response_links' ), $response );
190
		}
191
192
		if ( ! empty( $links ) ) {
193
			$data['_links'] = $links;
194
		}
195
196
		return $data;
197
	}
198
199
	/**
200
	 * Filter a response based on the context defined in the schema.
201
	 *
202
	 * @param array  $data
203
	 * @param string $context
204
	 * @return array
205
	 */
206
	public function filter_response_by_context( $data, $context ) {
207
208
		$schema = $this->get_item_schema();
209
		foreach ( $data as $key => $value ) {
210
			if ( empty( $schema['properties'][ $key ] ) || empty( $schema['properties'][ $key ]['context'] ) ) {
211
				continue;
212
			}
213
214
			if ( ! in_array( $context, $schema['properties'][ $key ]['context'] ) ) {
215
				unset( $data[ $key ] );
216
				continue;
217
			}
218
219
			if ( 'object' === $schema['properties'][ $key ]['type'] && ! empty( $schema['properties'][ $key ]['properties'] ) ) {
220
				foreach ( $schema['properties'][ $key ]['properties'] as $attribute => $details ) {
221
					if ( empty( $details['context'] ) ) {
222
						continue;
223
					}
224
					if ( ! in_array( $context, $details['context'] ) ) {
225
						if ( isset( $data[ $key ][ $attribute ] ) ) {
226
							unset( $data[ $key ][ $attribute ] );
227
						}
228
					}
229
				}
230
			}
231
		}
232
233
		return $data;
234
	}
235
236
	/**
237
	 * Get the item's schema, conforming to JSON Schema.
238
	 *
239
	 * @return array
240
	 */
241
	public function get_item_schema() {
242
		return $this->add_additional_fields_schema( array() );
243
	}
244
245
	/**
246
	 * Get the item's schema for display / public consumption purposes.
247
	 *
248
	 * @return array
249
	 */
250
	public function get_public_item_schema() {
251
252
		$schema = $this->get_item_schema();
253
254
		foreach ( $schema['properties'] as &$property ) {
255
			if ( isset( $property['arg_options'] ) ) {
256
				unset( $property['arg_options'] );
257
			}
258
		}
259
260
		return $schema;
261
	}
262
263
	/**
264
	 * Get the query params for collections.
265
	 *
266
	 * @return array
267
	 */
268
	public function get_collection_params() {
269
		return array(
270
			'context'                => $this->get_context_param(),
271
			'page'                   => array(
272
				'description'        => __( 'Current page of the collection.' ),
273
				'type'               => 'integer',
274
				'default'            => 1,
275
				'sanitize_callback'  => 'absint',
276
				'validate_callback'  => 'rest_validate_request_arg',
277
				'minimum'            => 1,
278
			),
279
			'per_page'               => array(
280
				'description'        => __( 'Maximum number of items to be returned in result set.' ),
281
				'type'               => 'integer',
282
				'default'            => 10,
283
				'minimum'            => 1,
284
				'maximum'            => 100,
285
				'sanitize_callback'  => 'absint',
286
				'validate_callback'  => 'rest_validate_request_arg',
287
			),
288
			'search'                 => array(
289
				'description'        => __( 'Limit results to those matching a string.' ),
290
				'type'               => 'string',
291
				'sanitize_callback'  => 'sanitize_text_field',
292
				'validate_callback'  => 'rest_validate_request_arg',
293
			),
294
		);
295
	}
296
297
	/**
298
	 * Get the magical context param.
299
	 *
300
	 * Ensures consistent description between endpoints, and populates enum from schema.
301
	 *
302
	 * @param array $args
303
	 * @return array
304
	 */
305
	public function get_context_param( $args = array() ) {
306
		$param_details = array(
307
			'description'        => __( 'Scope under which the request is made; determines fields present in response.' ),
308
			'type'               => 'string',
309
			'sanitize_callback'  => 'sanitize_key',
310
			'validate_callback'  => 'rest_validate_request_arg',
311
		);
312
		$schema = $this->get_item_schema();
313
		if ( empty( $schema['properties'] ) ) {
314
			return array_merge( $param_details, $args );
315
		}
316
		$contexts = array();
317
		foreach ( $schema['properties'] as $attributes ) {
318
			if ( ! empty( $attributes['context'] ) ) {
319
				$contexts = array_merge( $contexts, $attributes['context'] );
320
			}
321
		}
322
		if ( ! empty( $contexts ) ) {
323
			$param_details['enum'] = array_unique( $contexts );
324
			rsort( $param_details['enum'] );
325
		}
326
		return array_merge( $param_details, $args );
327
	}
328
329
	/**
330
	 * Add the values from additional fields to a data object.
331
	 *
332
	 * @param array           $object
333
	 * @param WP_REST_Request $request
334
	 * @return array modified object with additional fields.
335
	 */
336
	protected function add_additional_fields_to_object( $object, $request ) {
337
338
		$additional_fields = $this->get_additional_fields();
339
340
		foreach ( $additional_fields as $field_name => $field_options ) {
341
342
			if ( ! $field_options['get_callback'] ) {
343
				continue;
344
			}
345
346
			$object[ $field_name ] = call_user_func( $field_options['get_callback'], $object, $field_name, $request, $this->get_object_type() );
347
		}
348
349
		return $object;
350
	}
351
352
	/**
353
	 * Update the values of additional fields added to a data object.
354
	 *
355
	 * @param array           $object
356
	 * @param WP_REST_Request $request
357
	 */
358
	protected function update_additional_fields_for_object( $object, $request ) {
359
360
		$additional_fields = $this->get_additional_fields();
361
362
		foreach ( $additional_fields as $field_name => $field_options ) {
363
364
			if ( ! $field_options['update_callback'] ) {
365
				continue;
366
			}
367
368
			// Don't run the update callbacks if the data wasn't passed in the request.
369
			if ( ! isset( $request[ $field_name ] ) ) {
370
				continue;
371
			}
372
373
			call_user_func( $field_options['update_callback'], $request[ $field_name ], $object, $field_name, $request, $this->get_object_type() );
374
		}
375
	}
376
377
	/**
378
	 * Add the schema from additional fields to an schema array.
379
	 *
380
	 * The type of object is inferred from the passed schema.
381
	 *
382
	 * @param array $schema Schema array.
383
	 */
384
	protected function add_additional_fields_schema( $schema ) {
385
		if ( empty( $schema['title'] ) ) {
386
			return $schema;
387
		}
388
389
		/**
390
		 * Can't use $this->get_object_type otherwise we cause an inf loop.
391
		 */
392
		$object_type = $schema['title'];
393
394
		$additional_fields = $this->get_additional_fields( $object_type );
395
396
		foreach ( $additional_fields as $field_name => $field_options ) {
397
			if ( ! $field_options['schema'] ) {
398
				continue;
399
			}
400
401
			$schema['properties'][ $field_name ] = $field_options['schema'];
402
		}
403
404
		return $schema;
405
	}
406
407
	/**
408
	 * Get all the registered additional fields for a given object-type.
409
	 *
410
	 * @param  string $object_type
411
	 * @return array
412
	 */
413
	protected function get_additional_fields( $object_type = null ) {
414
415
		if ( ! $object_type ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $object_type of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
416
			$object_type = $this->get_object_type();
417
		}
418
419
		if ( ! $object_type ) {
420
			return array();
421
		}
422
423
		global $wp_rest_additional_fields;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
424
425
		if ( ! $wp_rest_additional_fields || ! isset( $wp_rest_additional_fields[ $object_type ] ) ) {
426
			return array();
427
		}
428
429
		return $wp_rest_additional_fields[ $object_type ];
430
	}
431
432
	/**
433
	 * Get the object type this controller is responsible for managing.
434
	 *
435
	 * @return string
436
	 */
437
	protected function get_object_type() {
438
		$schema = $this->get_item_schema();
439
440
		if ( ! $schema || ! isset( $schema['title'] ) ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $schema of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
441
			return null;
442
		}
443
444
		return $schema['title'];
445
	}
446
447
	/**
448
	 * Get an array of endpoint arguments from the item schema for the controller.
449
	 *
450
	 * @param string $method HTTP method of the request. The arguments
451
	 *                       for `CREATABLE` requests are checked for required
452
	 *                       values and may fall-back to a given default, this
453
	 *                       is not done on `EDITABLE` requests. Default is
454
	 *                       WP_REST_Server::CREATABLE.
455
	 * @return array $endpoint_args
456
	 */
457
	public function get_endpoint_args_for_item_schema( $method = WP_REST_Server::CREATABLE ) {
458
459
		$schema                = $this->get_item_schema();
460
		$schema_properties     = ! empty( $schema['properties'] ) ? $schema['properties'] : array();
461
		$endpoint_args = array();
462
463
		foreach ( $schema_properties as $field_id => $params ) {
464
465
			// Arguments specified as `readonly` are not allowed to be set.
466
			if ( ! empty( $params['readonly'] ) ) {
467
				continue;
468
			}
469
470
			$endpoint_args[ $field_id ] = array(
471
				'validate_callback' => 'rest_validate_request_arg',
472
				'sanitize_callback' => 'rest_sanitize_request_arg',
473
			);
474
475
			if ( isset( $params['description'] ) ) {
476
				$endpoint_args[ $field_id ]['description'] = $params['description'];
477
			}
478
479
			if ( WP_REST_Server::CREATABLE === $method && isset( $params['default'] ) ) {
480
				$endpoint_args[ $field_id ]['default'] = $params['default'];
481
			}
482
483
			if ( WP_REST_Server::CREATABLE === $method && ! empty( $params['required'] ) ) {
484
				$endpoint_args[ $field_id ]['required'] = true;
485
			}
486
487
			foreach ( array( 'type', 'format', 'enum' ) as $schema_prop ) {
488
				if ( isset( $params[ $schema_prop ] ) ) {
489
					$endpoint_args[ $field_id ][ $schema_prop ] = $params[ $schema_prop ];
490
				}
491
			}
492
493
			// Merge in any options provided by the schema property.
494
			if ( isset( $params['arg_options'] ) ) {
495
496
				// Only use required / default from arg_options on CREATABLE endpoints.
497
				if ( WP_REST_Server::CREATABLE !== $method ) {
498
					$params['arg_options'] = array_diff_key( $params['arg_options'], array(
499
						'required' => '',
500
						'default' => '',
501
					) );
502
				}
503
504
				$endpoint_args[ $field_id ] = array_merge( $endpoint_args[ $field_id ], $params['arg_options'] );
505
			}
506
		}// End foreach().
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
507
508
		return $endpoint_args;
509
	}
510
511
	/**
512
	 * Retrieves post data given a post ID or post object.
513
	 *
514
	 * This is a subset of the functionality of the `get_post()` function, with
515
	 * the additional functionality of having `the_post` action done on the
516
	 * resultant post object. This is done so that plugins may manipulate the
517
	 * post that is used in the REST API.
518
	 *
519
	 * @see get_post()
520
	 * @global WP_Query $wp_query
521
	 *
522
	 * @param int|WP_Post $post Post ID or post object. Defaults to global $post.
523
	 * @return WP_Post|null A `WP_Post` object when successful.
524
	 */
525
	public function get_post( $post ) {
526
		$post_obj = get_post( $post );
527
528
		/**
529
		 * Filter the post.
530
		 *
531
		 * Allows plugins to filter the post object as returned by `\WP_REST_Controller::get_post()`.
532
		 *
533
		 * @param WP_Post|null $post_obj  The post object as returned by `get_post()`.
534
		 * @param int|WP_Post  $post      The original value used to obtain the post object.
535
		 */
536
		$post = apply_filters( 'rest_the_post', $post_obj, $post );
537
538
		return $post;
539
	}
540
}
541