Completed
Push — trunk ( 0cdb75...7dd985 )
by Justin
13:23 queued 07:40
created

CMB2::render_group_row()   B

Complexity

Conditions 6
Paths 24

Size

Total Lines 48
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 6.1474

Importance

Changes 2
Bugs 0 Features 1
Metric Value
c 2
b 0
f 1
dl 0
loc 48
ccs 21
cts 25
cp 0.84
rs 8.551
cc 6
eloc 22
nc 24
nop 2
crap 6.1474
1
<?php
2
/**
3
 * CMB2 - The core metabox object
4
 *
5
 * @category  WordPress_Plugin
6
 * @package   CMB2
7
 * @author    WebDevStudios
8
 * @license   GPL-2.0+
9
 * @link      http://webdevstudios.com
10
 *
11
 * @property-read string $cmb_id
12
 * @property-read array $meta_box
13
 * @property-read array $updated
14
 */
15
class CMB2 extends CMB2_Base {
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...
16
17
	/**
18
	 * The object properties name.
19
	 * @var   string
20
	 * @since 2.2.3
21
	 */
22
	protected $properties_name = 'meta_box';
23
24
	/**
25
	 * Metabox Config array
26
	 * @var   array
27
	 * @since 0.9.0
28
	 */
29
	protected $meta_box = array();
30
31
	/**
32
	 * Type of object registered for metabox. (e.g., post, user, or comment)
33
	 * @var   string
34
	 * @since 1.0.0
35
	 */
36
	protected $mb_object_type = null;
37
38
	/**
39
	 * List of fields that are changed/updated on save
40
	 * @var   array
41
	 * @since 1.1.0
42
	 */
43
	protected $updated = array();
44
45
	/**
46
	 * Metabox Defaults
47
	 * @var   array
48
	 * @since 1.0.1
49
	 */
50
	protected $mb_defaults = array(
51
		'id'               => '',
52
		'title'            => '',
53
		'type'             => '',
54
		'object_types'     => array(), // Post type
55
		'context'          => 'normal',
56
		'priority'         => 'high',
57
		'show_names'       => true, // Show field names on the left
58
		'show_on_cb'       => null, // Callback to determine if metabox should display.
59
		'show_on'          => array(), // Post IDs or page templates to display this metabox. overrides 'show_on_cb'
60
		'cmb_styles'       => true, // Include CMB2 stylesheet
61
		'enqueue_js'       => true, // Include CMB2 JS
62
		'fields'           => array(),
63
		'hookup'           => true,
64
		'save_fields'      => true, // Will not save during hookup if false
65
		'closed'           => false, // Default to metabox being closed?
66
		'taxonomies'       => array(),
67
		'new_user_section' => 'add-new-user', // or 'add-existing-user'
68
		'new_term_section' => true,
69
	);
70
71
	/**
72
	 * Metabox field objects
73
	 * @var   array
74
	 * @since 2.0.3
75
	 */
76
	protected $fields = array();
77
78
	/**
79
	 * An array of hidden fields to output at the end of the form
80
	 * @var   array
81
	 * @since 2.0.0
82
	 */
83
	protected $hidden_fields = array();
84
85
	/**
86
	 * Array of key => value data for saving. Likely $_POST data.
87
	 * @var   string
88
	 * @since 2.0.0
89
	 */
90
	protected $generated_nonce = '';
91
92
	/**
93
	 * Whether there are fields to be shown in columns. Set in CMB2::add_field().
94
	 * @var   bool
95
	 * @since 2.2.2
96
	 */
97
	protected $has_columns = false;
98
99
	/**
100
	 * Get started
101
	 * @since 0.4.0
102
	 * @param array   $config    Metabox config array
103
	 * @param integer $object_id Optional object id
104
	 */
105 44
	public function __construct( $config, $object_id = 0 ) {
106
107 44
		if ( empty( $config['id'] ) ) {
108 1
			wp_die( __( 'Metabox configuration is required to have an ID parameter', 'cmb2' ) );
109
		}
110
111 44
		$this->meta_box = wp_parse_args( $config, $this->mb_defaults );
112 44
		$this->meta_box['fields'] = array();
113
114 44
		$this->object_id( $object_id );
115 44
		$this->mb_object_type();
116 44
		$this->cmb_id = $config['id'];
117
118 44
		if ( ! empty( $config['fields'] ) && is_array( $config['fields'] ) ) {
119 41
			$this->add_fields( $config['fields'] );
120 41
		}
121
122 44
		CMB2_Boxes::add( $this );
123
124
		/**
125
		 * Hook during initiation of CMB2 object
126
		 *
127
		 * The dynamic portion of the hook name, $this->cmb_id, is this meta_box id.
128
		 *
129
		 * @param array $cmb This CMB2 object
130
		 */
131 44
		do_action( "cmb2_init_{$this->cmb_id}", $this );
132 44
	}
133
134
	/**
135
	 * Loops through and displays fields
136
	 * @since 1.0.0
137
	 * @param int    $object_id   Object ID
138
	 * @param string $object_type Type of object being saved. (e.g., post, user, or comment)
139
	 */
140 1
	public function show_form( $object_id = 0, $object_type = '' ) {
141 1
		$this->render_form_open( $object_id, $object_type );
142
143 1
		foreach ( $this->prop( 'fields' ) as $field_args ) {
144 1
			$this->render_field( $field_args );
145 1
		}
146
147 1
		$this->render_form_close( $object_id, $object_type );
148 1
	}
149
150
	/**
151
	 * Outputs the opening form markup and runs corresponding hooks:
152
	 * 'cmb2_before_form' and "cmb2_before_{$object_type}_form_{$this->cmb_id}"
153
	 * @since  2.2.0
154
	 * @param  integer $object_id   Object ID
155
	 * @param  string  $object_type Object type
156
	 * @return void
157
	 */
158 1
	public function render_form_open( $object_id = 0, $object_type = '' ) {
159 1
		$object_type = $this->object_type( $object_type );
160 1
		$object_id = $this->object_id( $object_id );
161
162 1
		$this->nonce_field();
163
164 1
		echo "\n<!-- Begin CMB2 Fields -->\n";
165
166
		/**
167
		 * Hook before form table begins
168
		 *
169
		 * @param array  $cmb_id      The current box ID
170
		 * @param int    $object_id   The ID of the current object
171
		 * @param string $object_type The type of object you are working with.
172
		 *	                           Usually `post` (this applies to all post-types).
173
		 *	                           Could also be `comment`, `user` or `options-page`.
174
		 * @param array  $cmb         This CMB2 object
175
		 */
176 1
		do_action( 'cmb2_before_form', $this->cmb_id, $object_id, $object_type, $this );
177
178
		/**
179
		 * Hook before form table begins
180
		 *
181
		 * The first dynamic portion of the hook name, $object_type, is the type of object
182
		 * you are working with. Usually `post` (this applies to all post-types).
183
		 * Could also be `comment`, `user` or `options-page`.
184
		 *
185
		 * The second dynamic portion of the hook name, $this->cmb_id, is the meta_box id.
186
		 *
187
		 * @param array  $cmb_id      The current box ID
188
		 * @param int    $object_id   The ID of the current object
189
		 * @param array  $cmb         This CMB2 object
190
		 */
191 1
		do_action( "cmb2_before_{$object_type}_form_{$this->cmb_id}", $object_id, $this );
192
193 1
		echo '<div class="', $this->box_classes(), '"><div id="cmb2-metabox-', sanitize_html_class( $this->cmb_id ), '" class="cmb2-metabox cmb-field-list">';
194
195 1
	}
196
197
	/**
198
	 * Defines the classes for the CMB2 form/wrap.
199
	 *
200
	 * @since  2.0.0
201
	 * @return string Space concatenated list of classes
202
	 */
203 1
	public function box_classes() {
204
205 1
		$classes = array( 'cmb2-wrap', 'form-table' );
206
207
		// Use the callback to fetch classes.
208 1 View Code Duplication
		if ( $added_classes = $this->get_param_callback_result( 'classes_cb' ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
209 1
			$added_classes = is_array( $added_classes ) ? $added_classes : array( $added_classes );
210 1
			$classes = array_merge( $classes, $added_classes );
211 1
		}
212
213 1 View Code Duplication
		if ( $added_classes = $this->prop( 'classes' ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
214 1
			$added_classes = is_array( $added_classes ) ? $added_classes : array( $added_classes );
215 1
			$classes = array_merge( $classes, $added_classes );
216 1
		}
217
218
		/**
219
		 * Globally filter box wrap classes
220
		 *
221
		 * @since 2.2.2
222
		 *
223
		 * @param string $classes Array of classes for the cmb2-wrap.
224
		 * @param CMB2   $cmb     This CMB2 object.
225
		 */
226 1
		$classes = apply_filters( 'cmb2_wrap_classes', $classes, $this );
227
228
		// Clean up.
229 1
		$classes = array_map( 'strip_tags', array_filter( $classes ) );
230
231
		// Make a string.
232 1
		return implode( ' ', $classes );
233
	}
234
235
	/**
236
	 * Outputs the closing form markup and runs corresponding hooks:
237
	 * 'cmb2_after_form' and "cmb2_after_{$object_type}_form_{$this->cmb_id}"
238
	 * @since  2.2.0
239
	 * @param  integer $object_id   Object ID
240
	 * @param  string  $object_type Object type
241
	 * @return void
242
	 */
243 1
	public function render_form_close( $object_id = 0, $object_type = '' ) {
244 1
		$object_type = $this->object_type( $object_type );
245 1
		$object_id = $this->object_id( $object_id );
246
247 1
		echo '</div></div>';
248
249 1
		$this->render_hidden_fields();
250
251
		/**
252
		 * Hook after form form has been rendered
253
		 *
254
		 * @param array  $cmb_id      The current box ID
255
		 * @param int    $object_id   The ID of the current object
256
		 * @param string $object_type The type of object you are working with.
257
		 *	                           Usually `post` (this applies to all post-types).
258
		 *	                           Could also be `comment`, `user` or `options-page`.
259
		 * @param array  $cmb         This CMB2 object
260
		 */
261 1
		do_action( 'cmb2_after_form', $this->cmb_id, $object_id, $object_type, $this );
262
263
		/**
264
		 * Hook after form form has been rendered
265
		 *
266
		 * The dynamic portion of the hook name, $this->cmb_id, is the meta_box id.
267
		 *
268
		 * The first dynamic portion of the hook name, $object_type, is the type of object
269
		 * you are working with. Usually `post` (this applies to all post-types).
270
		 * Could also be `comment`, `user` or `options-page`.
271
		 *
272
		 * @param int    $object_id   The ID of the current object
273
		 * @param array  $cmb         This CMB2 object
274
		 */
275 1
		do_action( "cmb2_after_{$object_type}_form_{$this->cmb_id}", $object_id, $this );
276
277 1
		echo "\n<!-- End CMB2 Fields -->\n";
278
279 1
	}
280
281
	/**
282
	 * Renders a field based on the field type
283
	 * @since  2.2.0
284
	 * @param  array $field_args A field configuration array.
285
	 * @return mixed CMB2_Field object if successful.
286
	 */
287 1
	public function render_field( $field_args ) {
288 1
		$field_args['context'] = $this->prop( 'context' );
289
290 1
		if ( 'group' == $field_args['type'] ) {
291
292
			if ( ! isset( $field_args['show_names'] ) ) {
293
				$field_args['show_names'] = $this->prop( 'show_names' );
294
			}
295
			$field = $this->render_group( $field_args );
296
297 1
		} elseif ( 'hidden' == $field_args['type'] && $this->get_field( $field_args )->should_show() ) {
298
			// Save rendering for after the metabox
299
			$field = $this->add_hidden_field( $field_args );
300
301
		} else {
302
303 1
			$field_args['show_names'] = $this->prop( 'show_names' );
304
305
			// Render default fields
306 1
			$field = $this->get_field( $field_args )->render_field();
307
		}
308
309 1
		return $field;
310
	}
311
312
	/**
313
	 * Render a repeatable group.
314
	 * @param array $args Array of field arguments for a group field parent.
315
	 * @return CMB2_Field|null Group field object.
316
	 */
317 2
	public function render_group( $args ) {
318
319 2
		if ( ! isset( $args['id'], $args['fields'] ) || ! is_array( $args['fields'] ) ) {
320
			return;
321
		}
322
323 2
		$field_group = $this->get_field( $args );
324
325
		// If field is requesting to be conditionally shown
326 2
		if ( ! $field_group || ! $field_group->should_show() ) {
327
			return;
328
		}
329
330 2
		$desc            = $field_group->args( 'description' );
331 2
		$label           = $field_group->args( 'name' );
332 2
		$sortable        = $field_group->options( 'sortable' ) ? ' sortable' : ' non-sortable';
333 2
		$repeat_class    = $field_group->args( 'repeatable' ) ? ' repeatable' : ' non-repeatable';
334 2
		$group_val       = (array) $field_group->value();
335 2
		$nrows           = count( $group_val );
336 2
		$remove_disabled = $nrows <= 1 ? 'disabled="disabled" ' : '';
337 2
		$field_group->index = 0;
338
339 2
		$field_group->peform_param_callback( 'before_group' );
340
341 2
		echo '<div class="cmb-row cmb-repeat-group-wrap ', $field_group->row_classes(), '" data-fieldtype="group"><div class="cmb-td"><div data-groupid="', $field_group->id(), '" id="', $field_group->id(), '_repeat" class="cmb-nested cmb-field-list cmb-repeatable-group', $sortable, $repeat_class, '" style="width:100%;">';
342
343 2
		if ( $desc || $label ) {
344 2
			$class = $desc ? ' cmb-group-description' : '';
345 2
			echo '<div class="cmb-row', $class, '"><div class="cmb-th">';
346 2
				if ( $label ) {
347 2
					echo '<h2 class="cmb-group-name">', $label, '</h2>';
348 2
				}
349 2
				if ( $desc ) {
350 1
					echo '<p class="cmb2-metabox-description">', $desc, '</p>';
351 1
				}
352 2
			echo '</div></div>';
353 2
		}
354
355 2
		if ( ! empty( $group_val ) ) {
356
357
			foreach ( $group_val as $group_key => $field_id ) {
358
				$this->render_group_row( $field_group, $remove_disabled );
359
				$field_group->index++;
360
			}
361
		} else {
362 2
			$this->render_group_row( $field_group, $remove_disabled );
363
		}
364
365 2
		if ( $field_group->args( 'repeatable' ) ) {
366 1
			echo '<div class="cmb-row"><div class="cmb-td"><p class="cmb-add-row"><button type="button" data-selector="', $field_group->id(), '_repeat" data-grouptitle="', $field_group->options( 'group_title' ), '" class="cmb-add-group-row button">', $field_group->options( 'add_button' ), '</button></p></div></div>';
367 1
		}
368
369 2
		echo '</div></div></div>';
370
371 2
		$field_group->peform_param_callback( 'after_group' );
372
373 2
		return $field_group;
374
	}
375
376
	/**
377
	 * Render a repeatable group row
378
	 * @since  1.0.2
379
	 * @param  CMB2_Field $field_group  CMB2_Field group field object
380
	 * @param  string  $remove_disabled Attribute string to disable the remove button
381
	 */
382 2
	public function render_group_row( $field_group, $remove_disabled ) {
383
384 2
		$field_group->peform_param_callback( 'before_group_row' );
385 2
		$closed_class = $field_group->options( 'closed' ) ? ' closed' : '';
386
387
		echo '
388 2
		<div class="postbox cmb-row cmb-repeatable-grouping', $closed_class, '" data-iterator="', $field_group->index, '">';
389
390 2
			if ( $field_group->args( 'repeatable' ) ) {
391 1
				echo '<button type="button" ', $remove_disabled, 'data-selector="', $field_group->id(), '_repeat" class="dashicons-before dashicons-no-alt cmb-remove-group-row"></button>';
392 1
			}
393
394
			echo '
395 2
			<div class="cmbhandle" title="' , __( 'Click to toggle', 'cmb2' ), '"><br></div>
396 2
			<h3 class="cmb-group-title cmbhandle-title"><span>', $field_group->replace_hash( $field_group->options( 'group_title' ) ), '</span></h3>
0 ignored issues
show
Documentation introduced by
$field_group->options('group_title') is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
397
398
			<div class="inside cmb-td cmb-nested cmb-field-list">';
399
				// Loop and render repeatable group fields
400 2
				foreach ( array_values( $field_group->args( 'fields' ) ) as $field_args ) {
401 2
					if ( 'hidden' == $field_args['type'] ) {
402
403
						// Save rendering for after the metabox
404
						$this->add_hidden_field( $field_args, $field_group );
405
406
					} else {
407
408 2
						$field_args['show_names'] = $field_group->args( 'show_names' );
409 2
						$field_args['context']    = $field_group->args( 'context' );
410
411 2
						$field = $this->get_field( $field_args, $field_group )->render_field();
0 ignored issues
show
Unused Code introduced by
$field is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
412
					}
413 2
				}
414 2
				if ( $field_group->args( 'repeatable' ) ) {
415
					echo '
416
					<div class="cmb-row cmb-remove-field-row">
417
						<div class="cmb-remove-row">
418 1
							<button type="button" ', $remove_disabled, 'data-selector="', $field_group->id(), '_repeat" class="button cmb-remove-group-row alignright">', $field_group->options( 'remove_button' ), '</button>
419
						</div>
420
					</div>
421
					';
422 1
				}
423
			echo '
424
			</div>
425
		</div>
426 2
		';
427
428 2
		$field_group->peform_param_callback( 'after_group_row' );
429 2
	}
430
431
	/**
432
	 * Add a hidden field to the list of hidden fields to be rendered later
433
	 * @since 2.0.0
434
	 * @param array  $field_args Array of field arguments to be passed to CMB2_Field
435
	 */
436
	public function add_hidden_field( $field_args, $field_group = null ) {
437
		if ( isset( $field_args['field_args'] ) ) {
438
			// For back-compatibility.
439
			$field = new CMB2_Field( $field_args );
440
		} else {
441
			$field = $this->get_new_field( $field_args, $field_group );
442
		}
443
444
		$type = new CMB2_Types( $field );
445
446
		if ( $field_group ) {
447
			$type->iterator = $field_group->index;
448
		}
449
450
		$this->hidden_fields[] = $type;
451
452
		return $field;
453
	}
454
455
	/**
456
	 * Loop through and output hidden fields
457
	 * @since  2.0.0
458
	 */
459 1
	public function render_hidden_fields() {
460 1
		if ( ! empty( $this->hidden_fields ) ) {
461
			foreach ( $this->hidden_fields as $hidden ) {
462
				$hidden->render();
463
			}
464
		}
465 1
	}
466
467
	/**
468
	 * Returns array of sanitized field values (without saving them)
469
	 * @since  2.0.3
470
	 * @param  array  $data_to_sanitize Array of field_id => value data for sanitizing (likely $_POST data).
471
	 */
472 2
	public function get_sanitized_values( array $data_to_sanitize ) {
473 2
		$this->data_to_save = $data_to_sanitize;
474 2
		$stored_id          = $this->object_id();
475
476
		// We do this So CMB will sanitize our data for us, but not save it
477 2
		$this->object_id( '_' );
478
479
		// Ensure temp. data store is empty
480 2
		cmb2_options( 0 )->set();
481
482
		// Process/save fields
483 2
		$this->process_fields();
484
485
		// Get data from temp. data store
486 2
		$sanitized_values = cmb2_options( 0 )->get_options();
487
488
		// Empty out temp. data store again
489 2
		cmb2_options( 0 )->set();
490
491
		// Reset the object id
492 2
		$this->object_id( $stored_id );
493
494 2
		return $sanitized_values;
495
	}
496
497
	/**
498
	 * Loops through and saves field data
499
	 * @since  1.0.0
500
	 * @param  int    $object_id    Object ID
501
	 * @param  string $object_type  Type of object being saved. (e.g., post, user, or comment)
502
	 * @param  array  $data_to_save Array of key => value data for saving. Likely $_POST data.
503
	 */
504 1
	public function save_fields( $object_id = 0, $object_type = '', $data_to_save = array() ) {
0 ignored issues
show
Coding Style introduced by
save_fields uses the super-global variable $_POST which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

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

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
505
506
		// Fall-back to $_POST data
507 1
		$this->data_to_save = ! empty( $data_to_save ) ? $data_to_save : $_POST;
508 1
		$object_id = $this->object_id( $object_id );
509 1
		$object_type = $this->object_type( $object_type );
510
511 1
		$this->process_fields();
512
513
		// If options page, save the updated options
514 1
		if ( 'options-page' == $object_type ) {
515 1
			cmb2_options( $object_id )->set();
516 1
		}
517
518 1
		$this->after_save();
519 1
	}
520
521
	/**
522
	 * Process and save form fields
523
	 * @since  2.0.0
524
	 */
525 3
	public function process_fields() {
526
527 3
		$this->pre_process();
528
529
		// Remove the show_on properties so saving works
530 3
		$this->prop( 'show_on', array() );
531
532
		// save field ids of those that are updated
533 3
		$this->updated = array();
534
535 3
		foreach ( $this->prop( 'fields' ) as $field_args ) {
536 3
			$this->process_field( $field_args );
537 3
		}
538 3
	}
539
540
	/**
541
	 * Process and save a field
542
	 * @since  2.0.0
543
	 * @param  array  $field_args Array of field arguments
544
	 */
545 3
	public function process_field( $field_args ) {
546
547 3
		switch ( $field_args['type'] ) {
548
549 3
			case 'group':
550 1
				if ( $this->save_group( $field_args ) ) {
551 1
					$this->updated[] = $field_args['id'];
552 1
				}
553
554 1
				break;
555
556 2
			case 'title':
557
				// Don't process title fields
558
				break;
559
560 2
			default:
0 ignored issues
show
Coding Style introduced by
The default body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a default statement must start on the line immediately following the statement.

switch ($expr) {
    default:
        doSomething(); //right
        break;
}


switch ($expr) {
    default:

        doSomething(); //wrong
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
561
562 2
				$field = $this->get_new_field( $field_args );
563
564 2
				if ( $field->save_field_from_data( $this->data_to_save ) ) {
565 2
					$this->updated[] = $field->id();
566 2
				}
567
568 2
				break;
569 3
		}
570
571 3
	}
572
573 3
	public function pre_process() {
574
		/**
575
		 * Fires before fields have been processed/saved.
576
		 *
577
		 * The dynamic portion of the hook name, $this->cmb_id, is the meta_box id.
578
		 *
579
		 * The dynamic portion of the hook name, $object_type, refers to the metabox/form's object type
580
		 * 	Usually `post` (this applies to all post-types).
581
		 *  	Could also be `comment`, `user` or `options-page`.
582
		 *
583
		 * @param array $cmb       This CMB2 object
584
		 * @param int   $object_id The ID of the current object
585
		 */
586 3
		do_action( "cmb2_{$this->object_type()}_process_fields_{$this->cmb_id}", $this, $this->object_id() );
587 3
	}
588
589 1
	public function after_save() {
590 1
		$object_type = $this->object_type();
591 1
		$object_id   = $this->object_id();
592
593
		/**
594
		 * Fires after all fields have been saved.
595
		 *
596
		 * The dynamic portion of the hook name, $object_type, refers to the metabox/form's object type
597
		 * 	Usually `post` (this applies to all post-types).
598
		 *  	Could also be `comment`, `user` or `options-page`.
599
		 *
600
		 * @param int    $object_id   The ID of the current object
601
		 * @param array  $cmb_id      The current box ID
602
		 * @param string $updated     Array of field ids that were updated.
603
		 *                            Will only include field ids that had values change.
604
		 * @param array  $cmb         This CMB2 object
605
		 */
606 1
		do_action( "cmb2_save_{$object_type}_fields", $object_id, $this->cmb_id, $this->updated, $this );
607
608
		/**
609
		 * Fires after all fields have been saved.
610
		 *
611
		 * The dynamic portion of the hook name, $this->cmb_id, is the meta_box id.
612
		 *
613
		 * The dynamic portion of the hook name, $object_type, refers to the metabox/form's object type
614
		 * 	Usually `post` (this applies to all post-types).
615
		 *  	Could also be `comment`, `user` or `options-page`.
616
		 *
617
		 * @param int    $object_id   The ID of the current object
618
		 * @param string $updated     Array of field ids that were updated.
619
		 *                            Will only include field ids that had values change.
620
		 * @param array  $cmb         This CMB2 object
621
		 */
622 1
		do_action( "cmb2_save_{$object_type}_fields_{$this->cmb_id}", $object_id, $this->updated, $this );
623 1
	}
624
625
	/**
626
	 * Save a repeatable group
627
	 * @since  1.x.x
628
	 * @param  array  $args Field arguments array
629
	 * @return mixed        Return of CMB2_Field::update_data()
630
	 */
631 1
	public function save_group( $args ) {
632 1
		if ( ! isset( $args['id'], $args['fields'] ) || ! is_array( $args['fields'] ) ) {
633
			return;
634
		}
635
636 1
		return $this->save_group_field( $this->get_new_field( $args ) );
0 ignored issues
show
Documentation introduced by
$this->get_new_field($args) is of type object<CMB2_Field>, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
637
	}
638
639
	/**
640
	 * Save a repeatable group
641
	 * @since  1.x.x
642
	 * @param  array $field_group CMB2_Field group field object
643
	 * @return mixed              Return of CMB2_Field::update_data()
644
	 */
645 1
	public function save_group_field( $field_group ) {
646 1
		$base_id = $field_group->id();
0 ignored issues
show
Bug introduced by
The method id cannot be called on $field_group (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
647
648 1
		if ( ! isset( $this->data_to_save[ $base_id ] ) ) {
649
			return;
650
		}
651
652 1
		$old        = $field_group->get_data();
0 ignored issues
show
Bug introduced by
The method get_data cannot be called on $field_group (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
653
		// Check if group field has sanitization_cb
654 1
		$group_vals = $field_group->sanitization_cb( $this->data_to_save[ $base_id ] );
0 ignored issues
show
Bug introduced by
The method sanitization_cb cannot be called on $field_group (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
655 1
		$saved      = array();
656
657 1
		$field_group->index = 0;
658 1
		$field_group->data_to_save = $this->data_to_save;
659
660 1
		foreach ( array_values( $field_group->fields() ) as $field_args ) {
0 ignored issues
show
Bug introduced by
The method fields cannot be called on $field_group (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
661
662 1
			$field  = $this->get_new_field( $field_args, $field_group );
0 ignored issues
show
Documentation introduced by
$field_group is of type array, but the function expects a object<CMB2_Field>|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
663 1
			$sub_id = $field->id( true );
664
665 1
			foreach ( (array) $group_vals as $field_group->index => $post_vals ) {
666
667
				// Get value
668 1
				$new_val = isset( $group_vals[ $field_group->index ][ $sub_id ] )
669 1
					? $group_vals[ $field_group->index ][ $sub_id ]
670 1
					: false;
671
672
				// Sanitize
673 1
				$new_val = $field->sanitization_cb( $new_val );
674
675 1
				if ( is_array( $new_val ) && $field->args( 'has_supporting_data' ) ) {
676 1
					if ( $field->args( 'repeatable' ) ) {
677 1
						$_new_val = array();
678 1
						foreach ( $new_val as $group_index => $grouped_data ) {
679
							// Add the supporting data to the $saved array stack
680 1
							$saved[ $field_group->index ][ $grouped_data['supporting_field_id'] ][] = $grouped_data['supporting_field_value'];
681
							// Reset var to the actual value
682 1
							$_new_val[ $group_index ] = $grouped_data['value'];
683 1
						}
684 1
						$new_val = $_new_val;
685 1
					} else {
686
						// Add the supporting data to the $saved array stack
687 1
						$saved[ $field_group->index ][ $new_val['supporting_field_id'] ] = $new_val['supporting_field_value'];
688
						// Reset var to the actual value
689 1
						$new_val = $new_val['value'];
690
					}
691 1
				}
692
693
				// Get old value
694 1
				$old_val = is_array( $old ) && isset( $old[ $field_group->index ][ $sub_id ] )
695 1
					? $old[ $field_group->index ][ $sub_id ]
696 1
					: false;
697
698 1
				$is_updated = ( ! cmb2_utils()->isempty( $new_val ) && $new_val !== $old_val );
699 1
				$is_removed = ( cmb2_utils()->isempty( $new_val ) && ! cmb2_utils()->isempty( $old_val ) );
700
701
				// Compare values and add to `$updated` array
702 1
				if ( $is_updated || $is_removed ) {
703 1
					$this->updated[] = $base_id . '::' . $field_group->index . '::' . $sub_id;
704 1
				}
705
706
				// Add to `$saved` array
0 ignored issues
show
Unused Code Comprehensibility introduced by
40% 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...
707 1
				$saved[ $field_group->index ][ $sub_id ] = $new_val;
708
709 1
			}
710
711 1
			$saved[ $field_group->index ] = cmb2_utils()->filter_empty( $saved[ $field_group->index ] );
712 1
		}
713
714 1
		$saved = cmb2_utils()->filter_empty( $saved );
715
716 1
		return $field_group->update_data( $saved, true );
0 ignored issues
show
Bug introduced by
The method update_data cannot be called on $field_group (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
717
	}
718
719
	/**
720
	 * Get object id from global space if no id is provided
721
	 * @since  1.0.0
722
	 * @param  integer $object_id Object ID
723
	 * @return integer $object_id Object ID
724
	 */
725 48
	public function object_id( $object_id = 0 ) {
0 ignored issues
show
Coding Style introduced by
object_id uses the super-global variable $_REQUEST which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

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

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
Coding Style introduced by
object_id uses the super-global variable $GLOBALS which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

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

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
726 48
		global $pagenow;
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...
727
728 48
		if ( $object_id ) {
729 18
			$this->object_id = $object_id;
730 18
			return $this->object_id;
731
		}
732
733 45
		if ( $this->object_id ) {
734 13
			return $this->object_id;
735
		}
736
737
		// Try to get our object ID from the global space
738 42
		switch ( $this->object_type() ) {
739 42
			case 'user':
740
				$object_id = isset( $_REQUEST['user_id'] ) ? $_REQUEST['user_id'] : $object_id;
741
				$object_id = ! $object_id && 'user-new.php' != $pagenow && isset( $GLOBALS['user_ID'] ) ? $GLOBALS['user_ID'] : $object_id;
742
				break;
743
744 42
			case 'comment':
745
				$object_id = isset( $_REQUEST['c'] ) ? $_REQUEST['c'] : $object_id;
746
				$object_id = ! $object_id && isset( $GLOBALS['comments']->comment_ID ) ? $GLOBALS['comments']->comment_ID : $object_id;
747
				break;
748
749 42
			case 'term':
750
				$object_id = isset( $_REQUEST['tag_ID'] ) ? $_REQUEST['tag_ID'] : $object_id;
751
				break;
752
753 42
			default:
754 42
				$object_id = isset( $GLOBALS['post']->ID ) ? $GLOBALS['post']->ID : $object_id;
755 42
				$object_id = isset( $_REQUEST['post'] ) ? $_REQUEST['post'] : $object_id;
756 42
				break;
757 42
		}
758
759
		// reset to id or 0
760 42
		$this->object_id = $object_id ? $object_id : 0;
761
762 42
		return $this->object_id;
763
	}
764
765
	/**
766
	 * Sets the $object_type based on metabox settings
767
	 * @since  1.0.0
768
	 * @return string Object type
769
	 */
770 44
	public function mb_object_type() {
771 44
		if ( null !== $this->mb_object_type ) {
772 12
			return $this->mb_object_type;
773
		}
774
775 44
		if ( $this->is_options_page_mb() ) {
776 36
			$this->mb_object_type = 'options-page';
777 36
			return $this->mb_object_type;
778
		}
779
780 43
		$registered_types = $this->prop( 'object_types' );
781
782 43
		if ( ! $registered_types ) {
783 40
			$this->mb_object_type = 'post';
784 40
			return $this->mb_object_type;
785
		}
786
787 4
		$type = false;
788
789
		// check if 'object_types' is a string
790 4
		if ( is_string( $registered_types ) ) {
791
			$type = $registered_types;
792
		}
793
794
		// if it's an array of one, extract it
795 4
		elseif ( is_array( $registered_types ) && 1 === count( $registered_types ) ) {
796 4
			$last = end( $registered_types );
797 4
			if ( is_string( $last ) ) {
798 4
				$type = $last;
799 4
			}
800 4
		} elseif ( is_array( $registered_types ) ) {
801
			$page_type = $this->current_object_type();
802
803
			if ( in_array( $page_type, $registered_types, true ) ) {
804
				$type = $page_type;
805
			}
806
		}
807
808
		// Get our object type
809
		switch ( $type ) {
810
811 4
			case 'user':
812 4
			case 'comment':
813 4
			case 'term':
814 1
				$this->mb_object_type = $type;
0 ignored issues
show
Documentation Bug introduced by
It seems like $type can also be of type false. However, the property $mb_object_type is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

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

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
815 1
				break;
816
817 3
			default:
818 3
				$this->mb_object_type = 'post';
819 3
				break;
820 3
		}
821
822 4
		return $this->mb_object_type;
823
	}
824
825
	/**
826
	 * Determines if metabox is for an options page
827
	 * @since  1.0.1
828
	 * @return boolean True/False
829
	 */
830 44
	public function is_options_page_mb() {
831 44
		return ( isset( $this->meta_box['show_on']['key'] ) && 'options-page' === $this->meta_box['show_on']['key'] || array_key_exists( 'options-page', $this->meta_box['show_on'] ) );
832
	}
833
834
	/**
835
	 * Returns the object type
836
	 * @since  1.0.0
837
	 * @return string Object type
838
	 */
839 48
	public function object_type( $object_type = '' ) {
840 48
		if ( $object_type ) {
841 18
			$this->object_type = $object_type;
842 18
			return $this->object_type;
843
		}
844
845 45
		if ( $this->object_type ) {
846 45
			return $this->object_type;
847
		}
848
849
		$this->object_type = $this->current_object_type();
850
851
		return $this->object_type;
852
	}
853
854
	/**
855
	 * Get the object type for the current page, based on the $pagenow global.
856
	 * @since  2.2.2
857
	 * @return string  Page object type name.
858
	 */
859 View Code Duplication
	public function current_object_type() {
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...
860
		global $pagenow;
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...
861
		$type = 'post';
862
863
		if ( in_array( $pagenow, array( 'user-edit.php', 'profile.php', 'user-new.php' ), true ) ) {
864
			$type = 'user';
865
		}
866
867
		if ( in_array( $pagenow, array( 'edit-comments.php', 'comment.php' ), true ) ) {
868
			$type = 'comment';
869
		}
870
871
		if ( in_array( $pagenow, array( 'edit-tags.php', 'term.php' ), true ) ) {
872
			$type = 'term';
873
		}
874
875
		return $type;
876
	}
877
878
	/**
879
	 * Set metabox property.
880
	 * @since  2.2.2
881
	 * @param  string $property Metabox config property to retrieve
882
	 * @param  mixed  $value    Value to set if no value found
883
	 * @return mixed            Metabox config property value or false
884
	 */
885 1
	public function set_prop( $property, $value ) {
886 1
		$this->meta_box[ $property ] = $value;
887
888 1
		return $this->prop( $property );
889
	}
890
891
	/**
892
	 * Get metabox property and optionally set a fallback
893
	 * @since  2.0.0
894
	 * @param  string $property Metabox config property to retrieve
895
	 * @param  mixed  $fallback Fallback value to set if no value found
896
	 * @return mixed            Metabox config property value or false
897
	 */
898 44
	public function prop( $property, $fallback = null ) {
899 44
		if ( array_key_exists( $property, $this->meta_box ) ) {
900 44
			return $this->meta_box[ $property ];
901 1
		} elseif ( $fallback ) {
902 1
			return $this->meta_box[ $property ] = $fallback;
903
		}
904
	}
905
906
	/**
907
	 * Get a field object
908
	 * @since  2.0.3
909
	 * @param  string|array|CMB2_Field $field       Metabox field id or field config array or CMB2_Field object
910
	 * @param  CMB2_Field              $field_group (optional) CMB2_Field object (group parent)
911
	 * @return CMB2_Field|false CMB2_Field object (or false)
912
	 */
913 15
	public function get_field( $field, $field_group = null ) {
914 15
		if ( is_a( $field, 'CMB2_Field' ) ) {
915
			return $field;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $field; (string|array|CMB2_Field) is incompatible with the return type documented by CMB2::get_field of type CMB2_Field|false.

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...
916
		}
917
918 15
		$field_id = is_string( $field ) ? $field : $field['id'];
919
920 15
		$parent_field_id = ! empty( $field_group ) ? $field_group->id() : '';
921 15
		$ids = $this->get_field_ids( $field_id, $parent_field_id, true );
0 ignored issues
show
Unused Code introduced by
The call to CMB2::get_field_ids() has too many arguments starting with true.

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.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
922
923 15
		if ( ! $ids ) {
924
			return false;
925
		}
926
927 15
		list( $field_id, $sub_field_id ) = $ids;
928
929 15
		$index = implode( '', $ids ) . ( $field_group ? $field_group->index : '' );
930 15
		if ( array_key_exists( $index, $this->fields ) ) {
931 3
			return $this->fields[ $index ];
932
		}
933
934 13
		$this->fields[ $index ] = new CMB2_Field( $this->get_field_args( $field_id, $field, $sub_field_id, $field_group ) );
935
936 13
		return $this->fields[ $index ];
937
	}
938
939
	/**
940
	 * Handles determining which type of arguments to pass to CMB2_Field
941
	 * @since  2.0.7
942
	 * @param  mixed  $field_id     Field (or group field) ID
943
	 * @param  mixed  $field_args   Array of field arguments
944
	 * @param  mixed  $sub_field_id Sub field ID (if field_group exists)
945
	 * @param  mixed  $field_group  If a sub-field, will be the parent group CMB2_Field object
946
	 * @return array                Array of CMB2_Field arguments
947
	 */
948 13
	public function get_field_args( $field_id, $field_args, $sub_field_id, $field_group ) {
949
950
		// Check if group is passed and if fields were added in the old-school fields array
951 13
		if ( $field_group && ( $sub_field_id || 0 === $sub_field_id ) ) {
952
953
			// Update the fields array w/ any modified properties inherited from the group field
954 2
			$this->meta_box['fields'][ $field_id ]['fields'][ $sub_field_id ] = $field_args;
955
956 2
			return $this->get_default_args( $field_args, $field_group );
957
		}
958
959 13
		if ( is_array( $field_args ) ) {
960 2
			$this->meta_box['fields'][ $field_id ] = array_merge( $field_args, $this->meta_box['fields'][ $field_id ] );
961 2
		}
962
963 13
		return $this->get_default_args( $this->meta_box['fields'][ $field_id ] );
964
	}
965
966
	/**
967
	 * Get default field arguments specific to this CMB2 object.
968
	 * @since  2.2.0
969
	 * @param  array      $field_args  Metabox field config array.
970
	 * @param  CMB2_Field $field_group (optional) CMB2_Field object (group parent)
971
	 * @return array                   Array of field arguments.
972
	 */
973 17 View Code Duplication
	protected function get_default_args( $field_args, $field_group = null ) {
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...
974 17
		if ( $field_group ) {
975
			$args = array(
976 3
				'field_args'  => $field_args,
977 3
				'group_field' => $field_group,
978 3
			);
979 3
		} else {
980
			$args = array(
981 17
				'field_args'  => $field_args,
982 17
				'object_type' => $this->object_type(),
983 17
				'object_id'   => $this->object_id(),
984 17
				'cmb_id'      => $this->cmb_id,
985 17
			);
986
		}
987
988 17
		return $args;
989
	}
990
991
	/**
992
	 * Get a new field object specific to this CMB2 object.
993
	 * @since  2.2.0
994
	 * @param  array      $field_args  Metabox field config array.
995
	 * @param  CMB2_Field $field_group (optional) CMB2_Field object (group parent)
996
	 * @return CMB2_Field CMB2_Field object
997
	 */
998 5
	protected function get_new_field( $field_args, $field_group = null ) {
999 5
		return new CMB2_Field( $this->get_default_args( $field_args, $field_group ) );
1000
	}
1001
1002
	/**
1003
	 * When fields are added in the old-school way, intitate them as they should be
1004
	 * @since 2.1.0
1005
	 * @param array $fields          Array of fields to add
1006
	 * @param mixed $parent_field_id Parent field id or null
1007
	 */
1008 41
	protected function add_fields( $fields, $parent_field_id = null ) {
1009 41
		foreach ( $fields as $field ) {
1010
1011 41
			$sub_fields = false;
1012 41
			if ( array_key_exists( 'fields', $field ) ) {
1013
				$sub_fields = $field['fields'];
1014
				unset( $field['fields'] );
1015
			}
1016
1017
			$field_id = $parent_field_id
1018 41
				? $this->add_group_field( $parent_field_id, $field )
1019 41
				: $this->add_field( $field );
1020
1021 41
			if ( $sub_fields ) {
1022
				$this->add_fields( $sub_fields, $field_id );
1023
			}
1024 41
		}
1025 41
	}
1026
1027
	/**
1028
	 * Add a field to the metabox
1029
	 * @since  2.0.0
1030
	 * @param  array  $field           Metabox field config array
1031
	 * @param  int    $position        (optional) Position of metabox. 1 for first, etc
1032
	 * @return mixed                   Field id or false
1033
	 */
1034 43
	public function add_field( array $field, $position = 0 ) {
1035 43
		if ( ! is_array( $field ) || ! array_key_exists( 'id', $field ) ) {
1036
			return false;
1037
		}
1038
1039 43
		if ( 'oembed' === $field['type'] ) {
1040
			// Initiate oembed Ajax hooks
1041 1
			cmb2_ajax();
1042 1
		}
1043
1044 43
		if ( isset( $field['column'] ) && false !== $field['column'] ) {
1045
			$this->has_columns = true;
1046
1047
			$column = is_array( $field['column'] ) ? $field['column'] : array();
1048
1049
			$field['column'] = wp_parse_args( $column, array(
1050
				'name'     => isset( $field['name'] ) ? $field['name'] : '',
1051
				'position' => false,
1052
			) );
1053
		}
1054
1055 43
		$this->_add_field_to_array(
1056 43
			$field,
1057 43
			$this->meta_box['fields'],
1058
			$position
1059 43
		);
1060
1061 43
		return $field['id'];
1062
	}
1063
1064
	/**
1065
	 * Add a field to a group
1066
	 * @since  2.0.0
1067
	 * @param  string $parent_field_id The field id of the group field to add the field
1068
	 * @param  array  $field           Metabox field config array
1069
	 * @param  int    $position        (optional) Position of metabox. 1 for first, etc
1070
	 * @return mixed                   Array of parent/field ids or false
1071
	 */
1072 3
	public function add_group_field( $parent_field_id, array $field, $position = 0 ) {
1073 3
		if ( ! array_key_exists( $parent_field_id, $this->meta_box['fields'] ) ) {
1074
			return false;
1075
		}
1076
1077 3
		$parent_field = $this->meta_box['fields'][ $parent_field_id ];
1078
1079 3
		if ( 'group' !== $parent_field['type'] ) {
1080
			return false;
1081
		}
1082
1083 3
		if ( ! isset( $parent_field['fields'] ) ) {
1084 3
			$this->meta_box['fields'][ $parent_field_id ]['fields'] = array();
1085 3
		}
1086
1087 3
		$this->_add_field_to_array(
1088 3
			$field,
1089 3
			$this->meta_box['fields'][ $parent_field_id ]['fields'],
1090
			$position
1091 3
		);
1092
1093 3
		return array( $parent_field_id, $field['id'] );
1094
	}
1095
1096
	/**
1097
	 * Add a field array to a fields array in desired position
1098
	 * @since 2.0.2
1099
	 * @param array   $field    Metabox field config array
1100
	 * @param array   &$fields  Array (passed by reference) to append the field (array) to
1101
	 * @param integer $position Optionally specify a position in the array to be inserted
1102
	 */
1103 43
	protected function _add_field_to_array( $field, &$fields, $position = 0 ) {
1104 43
		if ( $position ) {
1105 1
			cmb2_utils()->array_insert( $fields, array( $field['id'] => $field ), $position );
1106 1
		} else {
1107 43
			$fields[ $field['id'] ] = $field;
1108
		}
1109 43
	}
1110
1111
	/**
1112
	 * Remove a field from the metabox
1113
	 * @since 2.0.0
1114
	 * @param  string $field_id        The field id of the field to remove
1115
	 * @param  string $parent_field_id (optional) The field id of the group field to remove field from
1116
	 * @return bool                    True if field was removed
1117
	 */
1118 2
	public function remove_field( $field_id, $parent_field_id = '' ) {
1119 2
		$ids = $this->get_field_ids( $field_id, $parent_field_id );
1120
1121 2
		if ( ! $ids ) {
1122
			return false;
1123
		}
1124
1125 2
		list( $field_id, $sub_field_id ) = $ids;
1126
1127 2
		unset( $this->fields[ implode( '', $ids ) ] );
1128
1129 2
		if ( ! $sub_field_id ) {
1130 1
			unset( $this->meta_box['fields'][ $field_id ] );
1131 1
			return true;
1132
		}
1133
1134 1
		if ( isset( $this->fields[ $field_id ]->args['fields'][ $sub_field_id ] ) ) {
1135 1
			unset( $this->fields[ $field_id ]->args['fields'][ $sub_field_id ] );
1136 1
		}
1137 1
		if ( isset( $this->meta_box['fields'][ $field_id ]['fields'][ $sub_field_id ] ) ) {
1138 1
			unset( $this->meta_box['fields'][ $field_id ]['fields'][ $sub_field_id ] );
1139 1
		}
1140 1
		return true;
1141
	}
1142
1143
	/**
1144
	 * Update or add a property to a field
1145
	 * @since  2.0.0
1146
	 * @param  string $field_id        Field id
1147
	 * @param  string $property        Field property to set/update
1148
	 * @param  mixed  $value           Value to set the field property
1149
	 * @param  string $parent_field_id (optional) The field id of the group field to remove field from
1150
	 * @return mixed                   Field id. Strict compare to false, as success can return a falsey value (like 0)
1151
	 */
1152 4
	public function update_field_property( $field_id, $property, $value, $parent_field_id = '' ) {
1153 4
		$ids = $this->get_field_ids( $field_id, $parent_field_id );
1154
1155 4
		if ( ! $ids ) {
1156 2
			return false;
1157
		}
1158
1159 2
		list( $field_id, $sub_field_id ) = $ids;
1160
1161 2
		if ( ! $sub_field_id ) {
1162 2
			$this->meta_box['fields'][ $field_id ][ $property ] = $value;
1163 2
			return $field_id;
1164
		}
1165
1166
		$this->meta_box['fields'][ $field_id ]['fields'][ $sub_field_id ][ $property ] = $value;
1167
		return $field_id;
1168
	}
1169
1170
	/**
1171
	 * Check if field ids match a field and return the index/field id
1172
	 * @since  2.0.2
1173
	 * @param  string  $field_id        Field id
1174
	 * @param  string  $parent_field_id (optional) Parent field id
1175
	 * @return mixed                    Array of field/parent ids, or false
1176
	 */
1177 19
	public function get_field_ids( $field_id, $parent_field_id = '' ) {
1178 19
		$sub_field_id = $parent_field_id ? $field_id : '';
1179 19
		$field_id     = $parent_field_id ? $parent_field_id : $field_id;
1180 19
		$fields       =& $this->meta_box['fields'];
1181
1182 19
		if ( ! array_key_exists( $field_id, $fields ) ) {
1183 2
			$field_id = $this->search_old_school_array( $field_id, $fields );
1184 2
		}
1185
1186 19
		if ( false === $field_id ) {
1187 2
			return false;
1188
		}
1189
1190 17
		if ( ! $sub_field_id ) {
1191 17
			return array( $field_id, $sub_field_id );
1192
		}
1193
1194 3
		if ( 'group' !== $fields[ $field_id ]['type'] ) {
1195
			return false;
1196
		}
1197
1198 3
		if ( ! array_key_exists( $sub_field_id, $fields[ $field_id ]['fields'] ) ) {
1199
			$sub_field_id = $this->search_old_school_array( $sub_field_id, $fields[ $field_id ]['fields'] );
1200
		}
1201
1202 3
		return false === $sub_field_id ? false : array( $field_id, $sub_field_id );
1203
	}
1204
1205
	/**
1206
	 * When using the old array filter, it is unlikely field array indexes will be the field id
1207
	 * @since  2.0.2
1208
	 * @param  string $field_id The field id
1209
	 * @param  array  $fields   Array of fields to search
1210
	 * @return mixed            Field index or false
1211
	 */
1212 2
	public function search_old_school_array( $field_id, $fields ) {
1213 2
		$ids = wp_list_pluck( $fields, 'id' );
1214 2
		$index = array_search( $field_id, $ids );
1215 2
		return false !== $index ? $index : false;
1216
	}
1217
1218
	/**
1219
	 * Handles metabox property callbacks, and passes this $cmb object as property.
1220
	 * @since  2.2.3
1221
	 * @param  callable $cb The callback method/function/closure
1222
	 * @return mixed        Return of the callback function.
1223
	 */
1224 1
	protected function do_callback( $cb ) {
1225 1
		return call_user_func( $cb, $this );
1226
	}
1227
1228
	/**
1229
	 * Generate a unique nonce field for each registered meta_box
1230
	 * @since  2.0.0
1231
	 * @return string unique nonce hidden input
1232
	 */
1233 1
	public function nonce_field() {
1234 1
		wp_nonce_field( $this->nonce(), $this->nonce(), false, true );
1235 1
	}
1236
1237
	/**
1238
	 * Generate a unique nonce for each registered meta_box
1239
	 * @since  2.0.0
1240
	 * @return string unique nonce string
1241
	 */
1242 1
	public function nonce() {
1243 1
		if ( $this->generated_nonce ) {
1244 1
			return $this->generated_nonce;
1245
		}
1246 1
		$this->generated_nonce = sanitize_html_class( 'nonce_' . basename( __FILE__ ) . $this->cmb_id );
1247 1
		return $this->generated_nonce;
1248
	}
1249
1250
	/**
1251
	 * Magic getter for our object.
1252
	 * @param string $field
1253
	 * @throws Exception Throws an exception if the field is invalid.
1254
	 * @return mixed
1255
	 */
1256 44
	public function __get( $field ) {
1257
		switch ( $field ) {
1258 44
			case 'updated':
1259 44
			case 'has_columns':
1260 1
				return $this->{$field};
1261 44
			default:
1262 44
				return parent::__get( $field );
1263 44
		}
1264
	}
1265
1266
}
1267