Completed
Pull Request — trunk (#541)
by Justin
28:03 queued 25:22
created

CMB2_REST_Controller_Fields::update_field_value()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 6
nc 2
nop 1
dl 0
loc 11
ccs 6
cts 6
cp 1
crap 2
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * CMB2 objects/fields endpoint for WordPres REST API.
4
 * Allows access to fields registered to a specific box.
5
 *
6
 * @todo  Add better documentation.
7
 * @todo  Research proper schema.
8
 *
9
 * @since 2.2.4
10
 *
11
 * @category  WordPress_Plugin
12
 * @package   CMB2
13
 * @author    WebDevStudios
14
 * @license   GPL-2.0+
15
 * @link      http://webdevstudios.com
16
 */
17
class CMB2_REST_Controller_Fields extends CMB2_REST_Controller_Boxes {
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...
18
19
	/**
20
	 * Register the routes for the objects of the controller.
21
	 *
22
	 * @since 2.2.4
23
	 */
24 1
	public function register_routes() {
25
		$args = array(
26
			'_embed' => array(
27 1
				'description' => __( 'Includes the box object which the fields are registered to in the response.', 'cmb2' ),
28 1
			),
29
			'_rendered' => array(
30 1
				'description' => __( 'When the \'rendered\' argument is passed, the renderable field attributes will be returned fully rendered. By default, the names of the callback handers for the renderable attributes will be returned.', 'cmb2' ),
31 1
			),
32
			'object_id' => array(
33 1
				'description' => __( 'To view or modify the field\'s value, the \'object_id\' and \'object_type\' arguments are required.', 'cmb2' ),
34 1
			),
35
			'object_type' => array(
36 1
				'description' => __( 'To view or modify the field\'s value, the \'object_id\' and \'object_type\' arguments are required.', 'cmb2' ),
37 1
			),
38 1
		);
39
40
		// Returns specific box's fields.
41 1
		register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<cmb_id>[\w-]+)/fields/', array(
42
			array(
43 1
				'methods'             => WP_REST_Server::READABLE,
44 1
				'callback'            => array( $this, 'get_items' ),
45 1
				'permission_callback' => array( $this, 'get_items_permissions_check' ),
46 1
				'args'                => $args,
47 1
			),
48 1
			'schema' => array( $this, 'get_item_schema' ),
49 1
		) );
50
51 1
		$delete_args = $args;
52 1
		$delete_args['object_id']['required'] = true;
53 1
		$delete_args['object_type']['required'] = true;
54
55
		// Returns specific field data.
56 1
		register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<cmb_id>[\w-]+)/fields/(?P<field_id>[\w-]+)', array(
57
			array(
58 1
				'methods'             => WP_REST_Server::READABLE,
59 1
				'callback'            => array( $this, 'get_item' ),
60 1
				'permission_callback' => array( $this, 'get_item_permissions_check' ),
61 1
				'args'                => $args,
62 1
			),
63
			array(
64 1
				'methods'             => WP_REST_Server::EDITABLE,
65 1
				'callback'            => array( $this, 'update_field_value' ),
66 1
				'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
67 1
				'permission_callback' => array( $this, 'update_field_value_permissions_check' ),
68 1
				'args'                => $args,
69 1
			),
70
			array(
71 1
				'methods'             => WP_REST_Server::DELETABLE,
72 1
				'callback'            => array( $this, 'delete_field_value' ),
73 1
				'permission_callback' => array( $this, 'delete_field_value_permissions_check' ),
74 1
				'args'                => $delete_args,
75 1
			),
76 1
			'schema' => array( $this, 'get_item_schema' ),
77 1
		) );
78 1
	}
79
80
	/**
81
	 * Get all public CMB2 box fields.
82
	 *
83
	 * @since 2.2.4
84
	 *
85
	 * @param  WP_REST_Request $request Full data about the request.
86
	 * @return WP_Error|WP_REST_Response
87
	 */
88 1
	public function get_items( $request ) {
89 1
		$this->initiate_rest_read_box( $request, 'fields_read' );
90
91 1
		if ( is_wp_error( $this->rest_box ) ) {
92
			return $this->rest_box;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->rest_box; (CMB2_REST) is incompatible with the return type of the parent method CMB2_REST_Controller_Boxes::get_items of type WP_Error|WP_REST_Response.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
93
		}
94
95 1
		$fields = array();
96 1
		foreach ( $this->rest_box->cmb->prop( 'fields', array() ) as $field ) {
97 1
			$field_id = $field['id'];
98 1
			$rest_field = $this->get_rest_field( $field_id );
99
100 1
			if ( ! is_wp_error( $rest_field ) ) {
101 1
				$fields[ $field_id ] = $this->server->response_to_data( $rest_field, isset( $this->request['_embed'] ) );
102 1
			} else {
103
				$fields[ $field_id ] = array( 'error' => $rest_field->get_error_message() );
104
			}
105 1
		}
106
107 1
		return $this->prepare_item( $fields );
108
	}
109
110
	/**
111
	 * Get one CMB2 field from the collection.
112
	 *
113
	 * @since 2.2.4
114
	 *
115
	 * @param  WP_REST_Request $request Full data about the request.
116
	 * @return WP_Error|WP_REST_Response
117
	 */
118 2
	public function get_item( $request ) {
119 2
		$this->initiate_rest_read_box( $request, 'field_read' );
120
121 2
		if ( is_wp_error( $this->rest_box ) ) {
122
			return $this->rest_box;
123
		}
124
125 2
		$field = $this->get_rest_field( $this->request->get_param( 'field_id' ) );
126
127 2
		if ( is_wp_error( $field ) ) {
128 1
			return $field;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $field; (array|WP_Error) is incompatible with the return type of the parent method CMB2_REST_Controller_Boxes::get_item of type CMB2_REST|WP_REST_Response.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
129
		}
130
131 2
		return $this->prepare_item( $field );
132
	}
133
134
	/**
135
	 * Update CMB2 field value.
136
	 *
137
	 * @since 2.2.4
138
	 *
139
	 * @param  WP_REST_Request $request Full data about the request.
140
	 * @return WP_Error|WP_REST_Response
141
	 */
142 2
	public function update_field_value( $request ) {
143 2
		$this->initiate_rest_read_box( $request, 'field_value_update' );
144
145 2
		if ( ! $this->request['value'] ) {
146 1
			return new WP_Error( 'cmb2_rest_update_field_error', __( 'CMB2 Field value cannot be updated without the value parameter specified.', 'cmb2' ), array( 'status' => 400 ) );
147
		}
148
149 2
		$field = $this->rest_box->field_can_edit( $this->request->get_param( 'field_id' ), true );
150
151 2
		return $this->modify_field_value( 'updated', $field );
0 ignored issues
show
Bug introduced by
It seems like $field defined by $this->rest_box->field_c...aram('field_id'), true) on line 149 can also be of type boolean; however, CMB2_REST_Controller_Fields::modify_field_value() does only seem to accept object<CMB2_Field>, 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...
152
	}
153
154
	/**
155
	 * Delete CMB2 field value.
156
	 *
157
	 * @since 2.2.4
158
	 *
159
	 * @param  WP_REST_Request $request Full data about the request.
160
	 * @return WP_Error|WP_REST_Response
161
	 */
162 1
	public function delete_field_value( $request ) {
163 1
		$this->initiate_rest_read_box( $request, 'field_value_delete' );
164
165 1
		$field = $this->rest_box->field_can_edit( $this->request->get_param( 'field_id' ), true );
166
167 1
		return $this->modify_field_value( 'deleted', $field );
0 ignored issues
show
Bug introduced by
It seems like $field defined by $this->rest_box->field_c...aram('field_id'), true) on line 165 can also be of type boolean; however, CMB2_REST_Controller_Fields::modify_field_value() does only seem to accept object<CMB2_Field>, 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...
168
	}
169
170
	/**
171
	 * Modify CMB2 field value.
172
	 *
173
	 * @since 2.2.4
174
	 *
175
	 * @param  string     $activity The modification activity (updated or deleted).
176
	 * @param  CMB2_Field $field    The field object.
177
	 * @return WP_Error|WP_REST_Response
178
	 */
179 3
	public function modify_field_value( $activity, $field ) {
180
181 3
		if ( ! $this->request['object_id'] || ! $this->request['object_type'] ) {
182 1
			return new WP_Error( 'cmb2_rest_modify_field_value_error', __( 'CMB2 Field value cannot be modified without the object_id and object_type parameters specified.', 'cmb2' ), array( 'status' => 400 ) );
183
		}
184
185 2
		if ( is_wp_error( $this->rest_box ) ) {
186
			return $this->rest_box;
187
		}
188
189 2
		if ( ! $field ) {
190
			return new WP_Error( 'cmb2_rest_no_field_by_id_error', __( 'No field found by that id.', 'cmb2' ), array( 'status' => 403 ) );
191
		}
192
193 2
		$field->args["value_{$activity}"] = (bool) 'deleted' === $activity
194 2
			? $field->remove_data()
195 2
			: $field->save_field( $this->request['value'] );
196
197
		// If options page, save the $activity options
198 2
		if ( 'options-page' == $this->request['object_type'] ) {
199
			$field->args["value_{$activity}"] = cmb2_options( $this->request['object_id'] )->set();
200
		}
201
202 2
		$field_data = $this->get_rest_field( $field );
203
204 2
		if ( is_wp_error( $field_data ) ) {
205
			return $field_data;
206
		}
207
208 2
		return $this->prepare_item( $field_data );
209
	}
210
211
	/**
212
	 * Get a specific field
213
	 *
214
	 * @since 2.2.4
215
	 *
216
	 * @param  string Field id
217
	 * @return array|WP_Error
218
	 */
219 5
	public function get_rest_field( $field_id ) {
220 5
		$field = $this->rest_box->field_can_read( $field_id, true );
221
222 5
		if ( ! $field ) {
223 1
			return new WP_Error( 'cmb2_rest_no_field_by_id_error', __( 'No field found by that id.', 'cmb2' ), array( 'status' => 403 ) );
224
		}
225
226 5
		$field_data = $this->prepare_field_data( $field );
0 ignored issues
show
Bug introduced by
It seems like $field defined by $this->rest_box->field_can_read($field_id, true) on line 220 can also be of type boolean; however, CMB2_REST_Controller_Fields::prepare_field_data() does only seem to accept object<CMB2_Field>, 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...
227 5
		$response = rest_ensure_response( $field_data );
228
229 5
		$response->add_links( $this->prepare_links( $field ) );
0 ignored issues
show
Bug introduced by
It seems like $field defined by $this->rest_box->field_can_read($field_id, true) on line 220 can also be of type boolean; however, CMB2_REST_Controller_Fields::prepare_links() does only seem to accept object<CMB2_Field>, 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...
230
231 5
		return $response;
232
	}
233
234
	/**
235
	 * Prepare the field data array for JSON.
236
	 *
237
	 * @since  2.2.4
238
	 *
239
	 * @param  CMB2_Field $field field object.
240
	 *
241
	 * @return array             Array of field data.
242
	 */
243 6
	protected function prepare_field_data( CMB2_Field $field ) {
244 5
		$field_data = array();
245 5
		$params_to_ignore = array( 'show_in_rest', 'options' );
246
		$params_to_rename = array(
247 5
			'label_cb' => 'label',
248 5
			'options_cb' => 'options',
249 5
		);
250
251
		// Run this first so the js_dependencies arg is populated.
252 5
		$rendered = ( $cb = $field->maybe_callback( 'render_row_cb' ) )
253
			// Ok, callback is good, let's run it.
254 5
			? $this->get_cb_results( $cb, $field->args(), $field )
255 5
			: false;
256
257 5
		$field_args = $field->args();
258
259 5
		foreach ( $field_args as $key => $value ) {
260 5
			if ( in_array( $key, $params_to_ignore, true ) ) {
261 5
				continue;
262
			}
263
264 5
			if ( 'options_cb' === $key ) {
265 5
				$value = $field->options();
266 5
			} elseif ( in_array( $key, CMB2_Field::$callable_fields, true ) ) {
267
268 5
				if ( isset( $this->request['_rendered'] ) ) {
269
					$value = $key === 'render_row_cb' ? $rendered : $field->get_param_callback_result( $key );
270 5
				} elseif ( is_array( $value ) ) {
271
					// We need to rewrite callbacks as string as they will cause
272
					// JSON recursion errors.
273 5
					$class = is_string( $value[0] ) ? $value[0] : get_class( $value[0] );
274 5
					$value = $class . '::' . $value[1];
275 5
				}
276 5
			}
277
278 5
			$key = isset( $params_to_rename[ $key ] ) ? $params_to_rename[ $key ] : $key;
279
280 5
			if ( empty( $value ) || is_scalar( $value ) || is_array( $value ) ) {
281 5
				$field_data[ $key ] = $value;
282 5
			} else {
283
				$field_data[ $key ] = sprintf( __( 'Value Error for %s', 'cmb2' ), $key );
284
			}
285 5
		}
286
287 6
		if ( $this->request['object_id'] && $this->request['object_type'] ) {
288 3
			$field_data['value'] = $field->get_data();
289 3
		}
290
291 5
		return $field_data;
292
	}
293
294
	/**
295
	 * Return an array of contextual links for field/fields.
296
	 *
297
	 * @since  2.2.4
298
	 *
299
	 * @param  CMB2_Field $field Field object to build links from.
300
	 *
301
	 * @return array             Array of links
302
	 */
303 5
	protected function prepare_links( $field ) {
304 5
		$boxbase      = $this->namespace_base . '/' . $this->rest_box->cmb->cmb_id;
305 5
		$query_string = $this->get_query_string();
306
307
		$links = array(
308
			'self' => array(
309 5
				'href' => rest_url( trailingslashit( $boxbase ) . 'fields/' . $field->_id() . $query_string ),
310 5
			),
311
			'collection' => array(
312 5
				'href' => rest_url( trailingslashit( $boxbase ) . 'fields' . $query_string ),
313 5
			),
314
			'up' => array(
315 5
				'href' => rest_url( $boxbase . $query_string ),
316 5
			),
317 5
		);
318
319
		// Don't embed boxes when looking at boxes route.
320 5
		if ( '/cmb2/v1/boxes' !== CMB2_REST_Controller::get_intial_route() ) {
321
			$links['up']['embeddable'] = true;
322
		}
323
324 5
		return $links;
325
	}
326
327
}
328