Completed
Push — add/contact-form-gutenblock-sd... ( 93c213...30deca )
by
unknown
17:44 queued 10:24
created

WPCOM_REST_API_V2_Field_Controller   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 327
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 0

Importance

Changes 0
Metric Value
dl 0
loc 327
rs 8.96
c 0
b 0
f 0
wmc 43
lcom 1
cbo 0

13 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 15 3
A register_fields() 0 13 2
A prepare_for_response() 0 11 3
B get_default_value() 0 23 10
A get_for_response() 0 17 3
A update_from_request() 0 20 4
A get_permission_check() 0 4 1
A get() 0 4 1
A update_permission_check() 0 4 1
A update() 0 4 1
A get_schema() 0 4 1
A is_valid_for_context() 0 3 2
C filter_response_by_context() 0 56 11

How to fix   Complexity   

Complex Class

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

1
<?php
2
3
// @todo - nicer API for array values?
4
5
/**
6
 * `WP_REST_Controller` is basically a wrapper for `register_rest_route()`
7
 * `WPCOM_REST_API_V2_Field_Controller` is a mostly-analogous wrapper for `register_rest_field()`
8
 */
9
abstract class WPCOM_REST_API_V2_Field_Controller {
10
	/**
11
	 * @var string|string[] $object_type The REST Object Type(s) to which the field should be added.
12
	 */
13
	protected $object_type;
14
15
	/**
16
	 * @var string $field_name The name of the REST API field to add.
17
	 */
18
	protected $field_name;
19
20
	public function __construct() {
21
		if ( ! $this->object_type ) {
22
			/* translators: %s: object_type */
23
			_doing_it_wrong( 'WPCOM_REST_API_V2_Field_Controller::$object_type', sprintf( __( "Property '%s' must be overridden.", 'jetpack' ), 'object_type' ), 'Jetpack 6.8' );
24
			return;
25
		}
26
27
		if ( ! $this->field_name ) {
28
			/* translators: %s: field_name */
29
			_doing_it_wrong( 'WPCOM_REST_API_V2_Field_Controller::$field_name', sprintf( __( "Property '%s' must be overridden.", 'jetpack' ), 'field_name' ), 'Jetpack 6.8' );
30
			return;
31
		}
32
33
		add_action( 'rest_api_init', array( $this, 'register_fields' ) );
34
	}
35
36
	/**
37
	 * Registers the field with the appropriate schema and callbacks.
38
	 */
39
	public function register_fields() {
40
		foreach ( (array) $this->object_type as $object_type ) {
41
			register_rest_field(
42
				$object_type,
43
				$this->field_name,
44
				array(
45
					'get_callback'    => array( $this, 'get_for_response' ),
46
					'update_callback' => array( $this, 'update_from_request' ),
47
					'schema'          => $this->get_schema(),
48
				)
49
			);
50
		}
51
	}
52
53
	/**
54
	 * Ensures the response matches the schema and request context.
55
	 *
56
	 * @param mixed           $value
57
	 * @param WP_REST_Request $request
58
	 * @return mixed
59
	 */
60
	private function prepare_for_response( $value, $request ) {
61
		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
62
		$schema  = $this->get_schema();
63
64
		$is_valid = rest_validate_value_from_schema( $value, $schema, $this->field_name );
65
		if ( is_wp_error( $is_valid ) ) {
66
			return $is_valid;
67
		}
68
69
		return $this->filter_response_by_context( $value, $schema, $context );
0 ignored issues
show
Bug introduced by
It seems like $schema defined by $this->get_schema() on line 62 can also be of type null; however, WPCOM_REST_API_V2_Field_...r_response_by_context() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
70
	}
71
72
	/**
73
	 * Returns the schema's default value
74
	 *
75
	 * If there is no default, returns the type's falsey value.
76
	 *
77
	 * @param array $schema
78
	 * @return mixed
79
	 */
80
	final public function get_default_value( $schema ) {
81
		if ( isset( $schema['default'] ) ) {
82
			return $schema['default'];
83
		}
84
85
		// If you have something more complicated, use $schema['default'];
86
		switch ( isset( $schema['type'] ) ? $schema['type'] : 'null' ) {
87
			case 'string':
88
				return '';
89
			case 'integer':
90
			case 'number':
91
				return 0;
92
			case 'object':
93
				return (object) array();
94
			case 'array':
95
				return array();
96
			case 'boolean':
97
				return false;
98
			case 'null':
99
			default:
100
				return null;
101
		}
102
	}
103
104
	/**
105
	 * The field's wrapped getter. Does permission checks and output preparation.
106
	 *
107
	 * This cannot be extended: implement `->get()` instead.
108
	 *
109
	 * @param mixed           $object_data Probably an array. Whatever the endpoint returns.
110
	 * @param string          $field_name Should always match `->field_name`
111
	 * @param WP_REST_Request $request
112
	 * @param string          $object_type Should always match `->object_type`
113
	 * @return mixed
114
	 */
115
	final public function get_for_response( $object_data, $field_name, $request, $object_type ) {
116
		$permission_check = $this->get_permission_check( $object_data, $request );
117
118
		if ( ! $permission_check ) {
119
			/* translators: %s: get_permission_check() */
120
			_doing_it_wrong( 'WPCOM_REST_API_V2_Field_Controller::get_permission_check', sprintf( __( "Method '%s' must return either true or WP_Error.", 'jetpack' ), 'get_permission_check' ), 'Jetpack 6.8' );
121
			return $this->get_default_value( $this->get_schema() );
0 ignored issues
show
Bug introduced by
It seems like $this->get_schema() targeting WPCOM_REST_API_V2_Field_Controller::get_schema() can also be of type null; however, WPCOM_REST_API_V2_Field_...er::get_default_value() does only seem to accept array, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
122
		}
123
124
		if ( is_wp_error( $permission_check ) ) {
125
			return $this->get_default_value( $this->get_schema() );
0 ignored issues
show
Bug introduced by
It seems like $this->get_schema() targeting WPCOM_REST_API_V2_Field_Controller::get_schema() can also be of type null; however, WPCOM_REST_API_V2_Field_...er::get_default_value() does only seem to accept array, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
126
		}
127
128
		$value = $this->get( $object_data, $request );
129
130
		return $this->prepare_for_response( $value, $request );
131
	}
132
133
	/**
134
	 * The field's wrapped setter. Does permission checks.
135
	 *
136
	 * This cannot be extended: implement `->update()` instead.
137
	 *
138
	 * @param mixed           $value The new value for the field.
139
	 * @param mixed           $object_data Probably a WordPress object (e.g., WP_Post)
140
	 * @param string          $field_name Should always match `->field_name`
141
	 * @param WP_REST_Request $request
142
	 * @param string          $object_type Should always match `->object_type`
143
	 * @return void|WP_Error
144
	 */
145
	final public function update_from_request( $value, $object_data, $field_name, $request, $object_type ) {
146
		$permission_check = $this->update_permission_check( $value, $object_data, $request );
147
148
		if ( ! $permission_check ) {
149
			/* translators: %s: update_permission_check() */
150
			_doing_it_wrong( 'WPCOM_REST_API_V2_Field_Controller::update_permission_check', sprintf( __( "Method '%s' must return either true or WP_Error.", 'jetpack' ), 'update_permission_check' ), 'Jetpack 6.8' );
151
			/* translators: %s: the name of an API response field */
152
			return new WP_Error( 'invalid_user_permission', sprintf( __( "You are not allowed to access the '%s' field.", 'jetpack' ), $this->field_name ) );
153
		}
154
155
		if ( is_wp_error( $permission_check ) ) {
156
			return $permission_check;
157
		}
158
159
		$updated = $this->update( $value, $object_data, $request );
160
161
		if ( is_wp_error( $updated ) ) {
162
			return $updated;
163
		}
164
	}
165
166
	/**
167
	 * Permission Check for the field's getter. Must be implemented in the inheriting class.
168
	 *
169
	 * @param mixed           $object_data Whatever the endpoint would return for its response.
170
	 * @param WP_REST_Request $request
171
	 * @return true|WP_Error
172
	 */
173
	public function get_permission_check( $object_data, $request ) {
174
		/* translators: %s: get_permission_check() */
175
		_doing_it_wrong( 'WPCOM_REST_API_V2_Field_Controller::get_permission_check', sprintf( __( "Method '%s' must be overridden.", 'jetpack' ), __METHOD__ ), 'Jetpack 6.8' );
176
	}
177
178
	/**
179
	 * The field's "raw" getter. Must be implemented in the inheriting class.
180
	 *
181
	 * @param mixed           $object_data Whatever the endpoint would return for its response.
182
	 * @param WP_REST_Request $request
183
	 * @return mixed
184
	 */
185
	public function get( $object_data, $request ) {
186
		/* translators: %s: get() */
187
		_doing_it_wrong( 'WPCOM_REST_API_V2_Field_Controller::get', sprintf( __( "Method '%s' must be overridden.", 'jetpack' ), __METHOD__ ), 'Jetpack 6.8' );
188
	}
189
190
	/**
191
	 * Permission Check for the field's setter. Must be implemented in the inheriting class.
192
	 *
193
	 * @param mixed           $value The new value for the field.
194
	 * @param mixed           $object_data Probably a WordPress object (e.g., WP_Post)
195
	 * @param WP_REST_Request $request
196
	 * @return true|WP_Error
197
	 */
198
	public function update_permission_check( $value, $object_data, $request ) {
199
		/* translators: %s: update_permission_check() */
200
		_doing_it_wrong( 'WPCOM_REST_API_V2_Field_Controller::update_permission_check', sprintf( __( "Method '%s' must be overridden.", 'jetpack' ), __METHOD__ ), 'Jetpack 6.8' );
201
	}
202
203
	/**
204
	 * The field's "raw" setter. Must be implemented in the inheriting class.
205
	 *
206
	 * @param mixed           $value The new value for the field.
207
	 * @param mixed           $object_data Probably a WordPress object (e.g., WP_Post)
208
	 * @param WP_REST_Request $request
209
	 * @return mixed
210
	 */
211
	public function update( $value, $object_data, $request ) {
212
		/* translators: %s: update() */
213
		_doing_it_wrong( 'WPCOM_REST_API_V2_Field_Controller::update', sprintf( __( "Method '%s' must be overridden.", 'jetpack' ), __METHOD__ ), 'Jetpack 6.8' );
214
	}
215
216
	/**
217
	 * The JSON Schema for the field
218
	 *
219
	 * @link https://json-schema.org/understanding-json-schema/
220
	 * As of WordPress 5.0, Core currently understands:
221
	 * * type
222
	 *   * string - not minLength, not maxLength, not pattern
223
	 *   * integer - minimum, maximum, exclusiveMinimum, exclusiveMaximum, not multipleOf
224
	 *   * number  - minimum, maximum, exclusiveMinimum, exclusiveMaximum, not multipleOf
225
	 *   * boolean
226
	 *   * null
227
	 *   * object - properties, additionalProperties, not propertyNames, not dependencies, not patternProperties, not required
228
	 *   * array: only lists, not tuples - items, not minItems, not maxItems, not uniqueItems, not contains
229
	 * * enum
230
	 * * format
231
	 *   * date-time
232
	 *   * email
233
	 *   * ip
234
	 *   * uri
235
	 * As of WordPress 5.0, Core does not support:
236
	 * * Multiple type: `type: [ 'string', 'integer' ]`
237
	 * * $ref, allOf, anyOf, oneOf, not, const
238
	 *
239
	 * @return array
240
	 */
241
	public function get_schema() {
242
		/* translators: %s: get_schema() */
243
		_doing_it_wrong( 'WPCOM_REST_API_V2_Field_Controller::get_schema', sprintf( __( "Method '%s' must be overridden.", 'jetpack' ), __METHOD__ ), 'Jetpack 6.8' );
244
	}
245
246
	/**
247
	 * @param array  $schema
248
	 * @param string $context REST API Request context
249
	 * @return bool
250
	 */
251
	private function is_valid_for_context( $schema, $context ) {
252
		return empty( $schema['context'] ) || in_array( $context, $schema['context'], true );
253
	}
254
255
	/**
256
	 * Removes properties that should not appear in the current
257
	 * request's context
258
	 *
259
	 * $context is a Core REST API Framework request attribute that is
260
	 * always one of:
261
	 * * view (what you see on the blog)
262
	 * * edit (what you see in an editor)
263
	 * * embed (what you see in, e.g., an oembed)
264
	 *
265
	 * Fields (and sub-fields, and sub-sub-...) can be flagged for a
266
	 * set of specific contexts via the field's schema.
267
	 *
268
	 * The Core API will filter out top-level fields with the wrong
269
	 * context, but will not recurse deeply enough into arrays/objects
270
	 * to remove all levels of sub-fields with the wrong context.
271
	 *
272
	 * This function handles that recursion.
273
	 *
274
	 * @param mixed  $value
275
	 * @param array  $schema
276
	 * @param string $context REST API Request context
277
	 * @return mixed Filtered $value
278
	 */
279
	final public function filter_response_by_context( $value, $schema, $context ) {
280
		if ( ! $this->is_valid_for_context( $schema, $context ) ) {
281
			// We use this intentionally odd looking WP_Error object
282
			// internally only in this recursive function (see below
283
			// in the `object` case). It will never be output by the REST API.
284
			// If we return this for the top level object, Core
285
			// correctly remove the top level object from the response
286
			// for us.
287
			return new WP_Error( '__wrong-context__' );
288
		}
289
290
		switch ( $schema['type'] ) {
291
			case 'array':
292
				if ( ! isset( $schema['items'] ) ) {
293
					return $value;
294
				}
295
296
				// Shortcircuit if we know none of the items are valid for this context.
297
				// This would only happen in a strangely written schema.
298
				if ( ! $this->is_valid_for_context( $schema['items'], $context ) ) {
299
					return array();
300
				}
301
302
				// Recurse to prune sub-properties of each item.
303
				$keys = array_keys( $value );
304
305
				$items = array_map(
306
					array( $this, 'filter_response_by_context' ),
307
					$value,
308
					array_fill( 0, count( $keys ), $schema['items'] ),
309
					array_fill( 0, count( $keys ), $context )
310
				);
311
312
				return array_combine( $keys, $items );
313
			case 'object':
314
				if ( ! isset( $schema['properties'] ) ) {
315
					return $value;
316
				}
317
318
				foreach ( $value as $field_name => $field_value ) {
319
					if ( isset( $schema['properties'][ $field_name ] ) ) {
320
						$field_value = $this->filter_response_by_context( $field_value, $schema['properties'][ $field_name ], $context );
321
						if ( is_wp_error( $field_value ) && '__wrong-context__' === $field_value->get_error_code() ) {
322
							unset( $value[ $field_name ] );
323
						} else {
324
							// Respect recursion that pruned sub-properties of each property.
325
							$value[ $field_name ] = $field_value;
326
						}
327
					}
328
				}
329
330
				return (object) $value;
331
		}
332
333
		return $value;
334
	}
335
}
336