Container   F
last analyzed

Complexity

Total Complexity 85

Size/Duplication

Total Lines 861
Duplicated Lines 2.9 %

Coupling/Cohesion

Components 1
Dependencies 8

Test Coverage

Coverage 16.26%

Importance

Changes 0
Metric Value
dl 25
loc 861
ccs 47
cts 289
cp 0.1626
rs 1.739
c 0
b 0
f 0
wmc 85
lcom 1
cbo 8

48 Methods

Rating   Name   Duplication   Size   Complexity  
A make() 0 3 1
A get_id() 0 3 1
A get_condition_types() 0 10 2
A is_active() 0 3 1
init() 0 1 ?
A boot() 0 3 1
A _is_valid_save() 0 5 1
is_valid_save() 0 1 ?
A attach() 0 1 1
A is_valid_attach() 0 4 1
get_environment_for_request() 0 1 ?
is_valid_attach_for_request() 0 1 ?
A static_conditions_pass() 0 8 1
get_environment_for_object() 0 1 ?
is_valid_attach_for_object() 0 1 ?
A all_conditions_pass() 0 4 1
A should_activate() 0 3 1
A has_fields() 0 3 1
A get_fields() 0 3 1
A register_field_name() 9 9 2
A has_default_datastore() 0 3 1
A get_datastore() 0 3 1
A get_nonce_name() 0 3 1
A get_nonce_value() 0 3 1
A verified_nonce_in_request() 0 6 2
A is_tabbed() 0 3 1
A get_classes() 0 3 1
A set_classes() 0 4 1
A add_tab() 0 5 1
A where() 0 4 1
A or_where() 0 4 1
B factory() 4 46 7
A __construct() 0 17 2
A activate() 0 10 2
A load() 0 5 2
A _save() 0 6 2
A save() 0 6 2
A _attach() 0 13 3
A get_root_field_by_name() 0 9 3
A get_field_pattern_regex() 0 17 1
B get_field_by_name() 0 46 10
A set_datastore() 12 12 4
A create_tab() 0 21 5
A get_untabbed_fields() 0 12 2
A get_tabs() 0 13 2
A get_tabs_json() 0 12 3
A to_json() 0 26 2
A add_fields() 0 22 5

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

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

1
<?php
2
3
namespace Carbon_Fields\Container;
4
5
use Carbon_Fields\Carbon_Fields;
6
use Carbon_Fields\Helper\Helper;
7
use Carbon_Fields\Field\Field;
8
use Carbon_Fields\Field\Group_Field;
9
use Carbon_Fields\Container\Fulfillable\Fulfillable_Collection;
10
use Carbon_Fields\Datastore\Datastore_Interface;
11
use Carbon_Fields\Datastore\Datastore_Holder_Interface;
12
use Carbon_Fields\Exception\Incorrect_Syntax_Exception;
13
14
/**
15
 * Base container class.
16
 * Defines the key container methods and their default implementations.
17
 */
18
abstract class Container implements Datastore_Holder_Interface {
19
	/**
20
	 * Where to put a particular tab -- at the head or the tail. Tail by default
21
	 */
22
	const TABS_TAIL = 1;
23
	const TABS_HEAD = 2;
24
25
	/**
26
	 * Separator signifying field hierarchy relation
27
	 * Used when searching for fields in a specific complex field
28
	 */
29
	const HIERARCHY_FIELD_SEPARATOR = '/';
30
31
	/**
32
	 * Separator signifying complex_field->group relation
33
	 * Used when searching for fields in a specific complex field group
34
	 */
35
	const HIERARCHY_GROUP_SEPARATOR = ':';
36
37
	/**
38
	 * Stores if the container is active on the current page
39
	 *
40
	 * @see activate()
41
	 * @var bool
42
	 */
43
	protected $active = false;
44
45
	/**
46
	 * List of registered unique field names for this container instance
47
	 *
48
	 * @see register_field_name()
49
	 * @var array
50
	 */
51
	protected $registered_field_names = array();
52
53
	/**
54
	 * Tabs available
55
	 */
56
	protected $tabs = array();
57
58
	/**
59
	 * List of default container settings
60
	 *
61
	 * @see init()
62
	 * @var array
63
	 */
64
	public $settings = array();
65
66
	/**
67
	 * Unique ID of the container
68
	 *
69
	 * @var string
70
	 */
71
	public $id;
72
73
	/**
74
	 * Title of the container
75
	 *
76
	 * @var string
77
	 */
78
	public $title = '';
79
80
	/**
81
	 * Type of the container
82
	 *
83
	 * @var string
84
	 */
85
	public $type;
86
87
	/**
88
	 * List of notification messages to be displayed on the front-end
89
	 *
90
	 * @var array
91
	 */
92
	protected $notifications = array();
93
94
	/**
95
	 * List of error messages to be displayed on the front-end
96
	 *
97
	 * @var array
98
	 */
99
	protected $errors = array();
100
101
	/**
102
	 * List of container fields
103
	 *
104
	 * @see add_fields()
105
	 * @var array
106
	 */
107
	protected $fields = array();
108
109
	/**
110
	 * Array of custom CSS classes.
111
	 *
112
	 * @see set_classes()
113
	 * @see get_classes()
114
	 * @var array<string>
115
	 */
116
	protected $classes = array();
117
118
	/**
119
	 * Container datastores. Propagated to all container fields
120
	 *
121
	 * @see set_datastore()
122
	 * @see get_datastore()
123
	 * @var Datastore_Interface
124
	 */
125
	protected $datastore;
126
127
	/**
128
	 * Flag whether the datastore is the default one or replaced with a custom one
129
	 *
130
	 * @see set_datastore()
131
	 * @see get_datastore()
132
	 * @var boolean
133
	 */
134
	protected $has_default_datastore = true;
135
136
	/**
137
	 * Fulfillable_Collection to use when checking attachment/saving conditions
138
	 *
139
	 * @var Fulfillable_Collection
140
	 */
141
	protected $condition_collection;
142
143
	/**
144
	 * Translator to use when translating conditions to json
145
	 *
146
	 * @var \Carbon_Fields\Container\Fulfillable\Translator\Translator
147
	 */
148
	protected $condition_translator;
149
150
	/**
151
	 * Create a new container of type $type and name $name.
152
	 *
153
	 * @param  string    $raw_type
154
	 * @param  string    $id        Unique id for the container. Optional
155
	 * @param  string    $name      Human-readable name of the container
156
	 * @return Container $container
157
	 */
158 8
	public static function factory( $raw_type, $id, $name = '' ) {
159
		// no name provided - switch input around as the id is optionally generated based on the name
160 8
		if ( $name === '' ) {
161 8
			$name = $id;
162 8
			$id = '';
163 8
	}
164
165 8
		$type = Helper::normalize_type( $raw_type );
166 8
		$repository = Carbon_Fields::resolve( 'container_repository' );
167 8
		$id = $repository->get_unique_container_id( ( $id !== '' ) ? $id : $name );
168
169 8
		if ( ! Helper::is_valid_entity_id( $id ) ) {
170
			Incorrect_Syntax_Exception::raise( 'Container IDs can only contain lowercase alphanumeric characters, dashes and underscores ("' . $id . '" passed).' );
171
			return null;
172
		}
173
174 8
		if ( ! $repository->is_unique_container_id( $id ) ) {
175
			Incorrect_Syntax_Exception::raise( 'The passed container id is already taken ("' . $id . '" passed).' );
176
			return null;
177
		}
178
179 8
		$container = null;
180 8
		if ( Carbon_Fields::has( $type, 'containers' ) ) {
181
			$container = Carbon_Fields::resolve_with_arguments( $type, array(
182
				'id' => $id,
183
				'name' => $name,
184
				'type' => $type,
185
			), 'containers' );
186
		} else {
187
			// Fallback to class name-based resolution
188 8
			$class = Helper::type_to_class( $type, __NAMESPACE__, '_Container' );
189
190 8 View Code Duplication
			if ( ! class_exists( $class ) ) {
191 3
				Incorrect_Syntax_Exception::raise( 'Unknown container "' . $raw_type . '".' );
192 1
				$class = __NAMESPACE__ . '\\Broken_Container';
193 1
			}
194
195 6
			$fulfillable_collection = Carbon_Fields::resolve( 'container_condition_fulfillable_collection' );
196 6
			$condition_translator = Carbon_Fields::resolve( 'container_condition_translator_json' );
197 6
			$container = new $class( $id, $name, $type, $fulfillable_collection, $condition_translator );
198
		}
199
200 6
		$repository->register_container( $container );
201
202 6
		return $container;
203
	}
204
205
	/**
206
	 * An alias of factory().
207
	 *
208
	 * @see    Container::factory()
209
	 * @return Container
210
	 */
211
	public static function make() {
212
		return call_user_func_array( array( get_class(), 'factory' ), func_get_args() );
213
	}
214
215
	/**
216
	 * Create a new container
217
	 *
218
	 * @param string                 $id                   Unique id of the container
219
	 * @param string                 $title                Title of the container
220
	 * @param string                 $type                 Type of the container
221
	 * @param Fulfillable_Collection $condition_collection
222
	 * @param \Carbon_Fields\Container\Fulfillable\Translator\Translator $condition_translator
223
	 */
224 2
	public function __construct( $id, $title, $type, $condition_collection, $condition_translator ) {
225 2
		Carbon_Fields::verify_boot();
226
227 2
		if ( empty( $title ) ) {
228 1
			Incorrect_Syntax_Exception::raise( 'Empty container title is not supported' );
229
		}
230
231 1
		$this->id = $id;
232 1
		$this->title = $title;
233 1
		$this->type = $type;
234 1
		$this->condition_collection = $condition_collection;
235 1
		$this->condition_collection->set_condition_type_list(
236 1
			array_merge( $this->get_condition_types( true ), $this->get_condition_types( false ) ),
237
			true
238 1
		);
239 1
		$this->condition_translator = $condition_translator;
240 1
	}
241
242
	/**
243
	 * Return the container id
244
	 *
245
	 * @return string
246
	 */
247
	public function get_id() {
248
		return $this->id;
249
	}
250
251
	/**
252
	 * Get array of all static condition types
253
	 *
254
	 * @param  boolean       $static
255
	 * @return array<string>
256
	 */
257
	protected function get_condition_types( $static ) {
258
		$group = $static ? 'static' : 'dynamic';
259
		$container_type = Helper::class_to_type( get_class( $this ), '_Container' );
260
261
		$condition_types = array();
262
		$condition_types = apply_filters( 'carbon_fields_' . $container_type . '_container_' . $group . '_condition_types', $condition_types, $container_type, $this );
263
		$condition_types = apply_filters( 'carbon_fields_container_' . $group . '_condition_types', $condition_types, $container_type, $this );
264
265
		return $condition_types;
266
	}
267
268
	/**
269
	 * Return whether the container is active
270
	 */
271
	public function is_active() {
272
		return $this->active;
273
	}
274
275
	/**
276
	 * Activate the container and trigger an action
277
	 */
278
	protected function activate() {
279
		$this->active = true;
280
		$this->boot();
281
		do_action( 'carbon_fields_container_activated', $this );
282
283
		$fields = $this->get_fields();
284
		foreach ( $fields as $field ) {
285
			$field->activate();
286
		}
287
	}
288
289
	/**
290
	 * Perform instance initialization
291
	 */
292
	abstract public function init();
293
294
	/**
295
	 * Boot the container once it's attached.
296
	 */
297
	protected function boot() {
298
299
	}
300
301
	/**
302
	 * Load the value for each field in the container.
303
	 * Could be used internally during container rendering
304
	 */
305
	public function load() {
306
		foreach ( $this->fields as $field ) {
307
			$field->load();
308
		}
309
	}
310
311
	/**
312
	 * Called first as part of the container save procedure.
313
	 * Responsible for checking the request validity and
314
	 * calling the container-specific save() method
315
	 *
316
	 * @see save()
317
	 * @see is_valid_save()
318
	 */
319
	public function _save() {
320
		$param = func_get_args();
321
		if ( call_user_func_array( array( $this, '_is_valid_save' ), $param ) ) {
322
			call_user_func_array( array( $this, 'save' ), $param );
323
		}
324
	}
325
326
	/**
327
	 * Load submitted data and save each field in the container
328
	 *
329
	 * @see is_valid_save()
330
	 */
331
	public function save( $data = null ) {
332
		foreach ( $this->fields as $field ) {
333
			$field->set_value_from_input( Helper::input() );
334
			$field->save();
335
		}
336
	}
337
338
	/**
339
	 * Checks whether the current save request is valid
340
	 *
341
	 * @return bool
342
	 */
343
	final protected function _is_valid_save() {
344
		$params = func_get_args();
345
		$is_valid_save = call_user_func_array( array( $this, 'is_valid_save' ), $params );
346
		return apply_filters( 'carbon_fields_container_is_valid_save', $is_valid_save, $this );
347
	}
348
349
	/**
350
	 * Checks whether the current save request is valid
351
	 *
352
	 * @return bool
353
	 */
354
	abstract protected function is_valid_save();
355
356
	/**
357
	 * Called first as part of the container attachment procedure.
358
	 * Responsible for checking it's OK to attach the container
359
	 * and if it is, calling the container-specific attach() method
360
	 *
361
	 * @see attach()
362
	 * @see is_valid_attach()
363
	 */
364
	public function _attach() {
365
		if ( ! $this->is_valid_attach() ) {
366
			return;
367
		}
368
369
		$param = func_get_args();
370
		call_user_func_array( array( $this, 'attach' ), $param );
371
372
		// Allow containers to initialize but not activate (useful in cases such as theme options)
373
		if ( $this->should_activate() ) {
374
			$this->activate();
375
		}
376
	}
377
378
	/**
379
	 * Attach the container rendering and helping methods
380
	 * to concrete WordPress Action hooks
381
	 */
382
	public function attach() {}
383
384
	/**
385
	 * Perform checks whether the container should be attached during the current request
386
	 *
387
	 * @return bool True if the container is allowed to be attached
388
	 */
389
	final public function is_valid_attach() {
390
		$is_valid_attach = $this->is_valid_attach_for_request();
391
		return apply_filters( 'carbon_fields_container_is_valid_attach', $is_valid_attach, $this );
392
	}
393
394
	/**
395
	 * Get environment array for page request (in admin)
396
	 *
397
	 * @return array
398
	 */
399
	abstract protected function get_environment_for_request();
400
401
	/**
402
	 * Check container attachment rules against current page request (in admin)
403
	 *
404
	 * @return bool
405
	 */
406
	abstract protected function is_valid_attach_for_request();
407
408
	/**
409
	 * Check if conditions pass for request
410
	 *
411
	 * @return bool
412
	 */
413
	protected function static_conditions_pass() {
414
		$environment = $this->get_environment_for_request();
415
		$static_condition_collection = $this->condition_collection->evaluate(
416
			$this->get_condition_types( false ),
417
			true
418
		);
419
		return $static_condition_collection->is_fulfilled( $environment );
420
	}
421
422
	/**
423
	 * Get environment array for object id
424
	 *
425
	 * @param integer $object_id
426
	 * @return array
427
	 */
428
	abstract protected function get_environment_for_object( $object_id );
429
430
	/**
431
	 * Check container attachment rules against object id
432
	 *
433
	 * @param int $object_id
434
	 * @return bool
435
	 */
436
	abstract public function is_valid_attach_for_object( $object_id );
437
438
	/**
439
	 * Check if all conditions pass for object
440
	 *
441
	 * @param  integer $object_id
442
	 * @return bool
443
	 */
444
	protected function all_conditions_pass( $object_id ) {
445
		$environment = $this->get_environment_for_object( $object_id );
446
		return $this->condition_collection->is_fulfilled( $environment );
447
	}
448
449
	/**
450
	 * Whether this container is currently viewed.
451
	 */
452
	public function should_activate() {
453
		return true;
454
	}
455
456
	/**
457
	 * Perform a check whether the current container has fields
458
	 *
459
	 * @return bool
460
	 */
461
	public function has_fields() {
462
		return (bool) $this->fields;
463
	}
464
465
	/**
466
	 * Returns the private container array of fields.
467
	 * Use only if you are completely aware of what you are doing.
468
	 *
469
	 * @return Field[]
470
	 */
471
	public function get_fields() {
472
		return $this->fields;
473
	}
474
475
	/**
476
	 * Return root field from container with specified name
477
	 *
478
	 * @example crb_complex
479
	 *
480
	 * @param string $field_name
481
	 * @return Field
482
	 */
483
	public function get_root_field_by_name( $field_name ) {
484
		$fields = $this->get_fields();
485
		foreach ( $fields as $field ) {
486
			if ( $field->get_base_name() === $field_name ) {
487
				return $field;
488
			}
489
		}
490
		return null;
491
	}
492
493
	/**
494
	 * Get a regex to match field name patterns used to fetch specific fields
495
	 *
496
	 * @return string
497
	 */
498
	protected function get_field_pattern_regex() {
499
		$field_name_characters = Helper::get_field_name_characters_pattern();
500
501
		// matches:
502
		// field_name
503
		// field_name[0]
504
		// field_name[0]:group_name
505
		// field_name:group_name
506
		$regex = '/
507
			\A
508
			(?P<field_name>[' . $field_name_characters . ']+)
509
			(?:\[(?P<group_index>\d+)\])?
510
			(?:' .  preg_quote( static::HIERARCHY_GROUP_SEPARATOR, '/' ). '(?P<group_name>[' . $field_name_characters . ']+))?
511
			\z
512
		/x';
513
		return $regex;
514
	}
515
516
	/**
517
	 * Return field from container with specified name
518
	 *
519
	 * @example $field_name = 'crb_complex/text_field'
520
	 * @example $field_name = 'crb_complex/complex_2'
521
	 * @example $field_name = 'crb_complex/complex_2:text_group/text_field'
522
	 * @example $field_name = 'crb_complex[3]/complex_2[1]:text_group/text_field'
523
	 *
524
	 * @param string $field_name
525
	 * @return Field
526
	 */
527
	public function get_field_by_name( $field_name ) {
528
		$hierarchy = array_filter( explode( static::HIERARCHY_FIELD_SEPARATOR, $field_name ) );
529
		$field = null;
530
531
		$field_group = $this->get_fields();
532
		$hierarchy_left = $hierarchy;
533
		$field_pattern_regex = $this->get_field_pattern_regex();
534
		$hierarchy_index = array();
535
536
		while ( ! empty( $hierarchy_left ) ) {
537
			$segment = array_shift( $hierarchy_left );
538
			$segment_pieces = array();
539
			if ( ! preg_match( $field_pattern_regex, $segment, $segment_pieces ) ) {
540
				return null;
541
			}
542
543
			$segment_field_name = $segment_pieces['field_name'];
544
			$segment_group_index = isset( $segment_pieces['group_index'] ) ? $segment_pieces['group_index'] : 0;
545
			$segment_group_name = isset( $segment_pieces['group_name'] ) ? $segment_pieces['group_name'] : Group_Field::DEFAULT_GROUP_NAME;
546
547
			foreach ( $field_group as $f ) {
548
				if ( $f->get_base_name() !== $segment_field_name ) {
549
					continue;
550
				}
551
552
				if ( empty( $hierarchy_left ) ) {
553
					$field = clone $f;
554
					$field->set_hierarchy_index( $hierarchy_index );
555
				} else {
556
					if ( ! ( $f instanceof \Carbon_Fields\Field\Complex_Field ) ) {
557
						return null;
558
					}
559
560
					$group = $f->get_group_by_name( $segment_group_name );
561
					if ( ! $group ) {
562
						return null;
563
					}
564
					$field_group = $group->get_fields();
565
					$hierarchy_index[] = $segment_group_index;
566
				}
567
				break;
568
			}
569
		}
570
571
		return $field;
572
	}
573
574
	/**
575
	 * Perform checks whether there is a field registered with the name $name.
576
	 * If not, the field name is recorded.
577
	 *
578
	 * @param string $name
579
	 * @return boolean
580
	 */
581 View Code Duplication
	protected function register_field_name( $name ) {
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...
582
		if ( in_array( $name, $this->registered_field_names ) ) {
583
			Incorrect_Syntax_Exception::raise( 'Field name "' . $name . '" already registered' );
584
			return false;
585
		}
586
587
		$this->registered_field_names[] = $name;
588
		return true;
589
	}
590
591
	/**
592
	 * Return whether the datastore instance is the default one or has been overriden
593
	 *
594
	 * @return boolean
595
	 */
596 6
	public function has_default_datastore() {
597 6
		return $this->has_default_datastore;
598
	}
599
600
	/**
601
	 * Set datastore instance
602
	 *
603
	 * @param Datastore_Interface $datastore
604
	 * @param bool                $set_as_default (optional)
605
	 * @return Container $this
606
	 */
607 6 View Code Duplication
	public function set_datastore( Datastore_Interface $datastore, $set_as_default = false ) {
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...
608 6
		if ( $set_as_default && ! $this->has_default_datastore() ) {
609 1
			return $this; // datastore has been overriden with a custom one - abort changing to a default one
610
		}
611 6
		$this->datastore = $datastore;
612 6
		$this->has_default_datastore = $set_as_default;
613
614 6
		foreach ( $this->fields as $field ) {
615
			$field->set_datastore( $this->get_datastore(), true );
616 6
		}
617 6
		return $this;
618
	}
619
620
	/**
621
	 * Get the DataStore instance
622
	 *
623
	 * @return Datastore_Interface $datastore
624
	 */
625 6
	public function get_datastore() {
626 6
		return $this->datastore;
627
	}
628
629
	/**
630
	 * Return WordPress nonce name used to identify the current container instance
631
	 *
632
	 * @return string
633
	 */
634
	protected function get_nonce_name() {
635
		return $this->get_id() . '_nonce';
636
	}
637
638
	/**
639
	 * Return WordPress nonce name used to identify the current container instance
640
	 *
641
	 * @return string
642
	 */
643
	protected function get_nonce_value() {
644
		return wp_create_nonce( $this->get_nonce_name() );
645
	}
646
647
	/**
648
	 * Check if the nonce is present in the request and that it is verified
649
	 *
650
	 * @return bool
651
	 */
652
	protected function verified_nonce_in_request() {
653
		$input = Helper::input();
654
		$nonce_name = $this->get_nonce_name();
655
		$nonce_value = isset( $input[ $nonce_name ] ) ? $input[ $nonce_name ] : '';
656
		return wp_verify_nonce( $nonce_value, $nonce_name );
657
	}
658
659
	/**
660
	 * Internal function that creates the tab and associates it with particular field set
661
	 *
662
	 * @param string $tab_name
663
	 * @param array $fields
664
	 * @param int $queue_end
665
	 * @return object $this
666
	 */
667
	private function create_tab( $tab_name, $fields, $queue_end = self::TABS_TAIL ) {
668
		if ( isset( $this->tabs[ $tab_name ] ) ) {
669
			Incorrect_Syntax_Exception::raise( "Tab name duplication for $tab_name" );
670
		}
671
672
		if ( $queue_end === static::TABS_TAIL ) {
673
			$this->tabs[ $tab_name ] = array();
674
		} else if ( $queue_end === static::TABS_HEAD ) {
675
			$this->tabs = array_merge(
676
				array( $tab_name => array() ),
677
				$this->tabs
678
			);
679
		}
680
681
		foreach ( $fields as $field ) {
682
			$field_name = $field->get_name();
683
			$this->tabs[ $tab_name ][ $field_name ] = $field;
684
		}
685
686
		$this->settings['tabs'] = $this->get_tabs_json();
687
	}
688
689
	/**
690
	 * Whether the container is tabbed or not
691
	 *
692
	 * @return bool
693
	 */
694
	public function is_tabbed() {
695
		return (bool) $this->tabs;
696
	}
697
698
	/**
699
	 * Retrieve all fields that are not defined under a specific tab
700
	 *
701
	 * @return array
702
	 */
703
	protected function get_untabbed_fields() {
704
		$tabbed_fields_names = array();
705
		foreach ( $this->tabs as $tab_fields ) {
706
			$tabbed_fields_names = array_merge( $tabbed_fields_names, array_keys( $tab_fields ) );
707
		}
708
709
		$untabbed_fields = array_filter( $this->fields, function( $field ) use ( $tabbed_fields_names ) {
710
			return ! in_array( $field->get_name(), $tabbed_fields_names );
711
		} );
712
713
		return $untabbed_fields;
714
	}
715
716
	/**
717
	 * Retrieve all tabs.
718
	 * Create a default tab if there are any untabbed fields.
719
	 *
720
	 * @return array
721
	 */
722
	protected function get_tabs() {
723
		$untabbed_fields = $this->get_untabbed_fields();
724
725
		if ( ! empty( $untabbed_fields ) ) {
726
			$this->create_tab(
727
				apply_filters( 'carbon_fields_untabbed_fields_tab_title', __( 'General', 'carbon-fields' ), $this ),
728
				$untabbed_fields,
729
				static::TABS_HEAD
730
			);
731
		}
732
733
		return $this->tabs;
734
	}
735
736
	/**
737
	 * Build the tabs JSON
738
	 *
739
	 * @return array
740
	 */
741
	protected function get_tabs_json() {
742
		$tabs_json = array();
743
		$tabs = $this->get_tabs();
744
745
		foreach ( $tabs as $tab_name => $fields ) {
746
			foreach ( $fields as $field_name => $field ) {
747
				$tabs_json[ $tab_name ][] = $field_name;
748
			}
749
		}
750
751
		return $tabs_json;
752
	}
753
754
	/**
755
	 * Get custom CSS classes.
756
	 *
757
	 * @return array<string>
758
	 */
759
	public function get_classes() {
760
		return $this->classes;
761
	}
762
763
	/**
764
	 * Set CSS classes that the container should use.
765
	 *
766
	 * @param string|array<string> $classes
767
	 * @return Container $this
768
	 */
769
	public function set_classes( $classes ) {
770
		$this->classes = Helper::sanitize_classes( $classes );
771
		return $this;
772
	}
773
774
	/**
775
	 * Returns an array that holds the container data, suitable for JSON representation.
776
	 *
777
	 * @param bool $load  Should the value be loaded from the database or use the value from the current instance.
778
	 * @return array
779
	 */
780
	public function to_json( $load ) {
781
		$conditions = $this->condition_collection->evaluate( $this->get_condition_types( true ), $this->get_environment_for_request(), array( 'CUSTOM' ) );
782
		$conditions = $this->condition_translator->fulfillable_to_foreign( $conditions );
783
784
		$container_data = array(
785
			'id' => $this->get_id(),
786
			'type' => $this->type,
787
			'title' => $this->title,
788
			'classes' => $this->get_classes(),
789
			'settings' => $this->settings,
790
			'conditions' => $conditions,
791
			'fields' => array(),
792
			'nonce' => array(
793
				'name' => $this->get_nonce_name(),
794
				'value' => $this->get_nonce_value(),
795
			),
796
		);
797
798
		$fields = $this->get_fields();
799
		foreach ( $fields as $field ) {
800
			$field_data = $field->to_json( $load );
801
			$container_data['fields'][] = $field_data;
802
		}
803
804
		return $container_data;
805
	}
806
807
	/**
808
	 * COMMON USAGE METHODS
809
	 */
810
811
	/**
812
	 * Append array of fields to the current fields set. All items of the array
813
	 * must be instances of Field and their names should be unique for all
814
	 * Carbon containers.
815
	 * If a field does not have DataStore already, the container datastore is
816
	 * assigned to them instead.
817
	 *
818
	 * @param array $fields
819
	 * @return Container $this
820
	 */
821
	public function add_fields( $fields ) {
822
		foreach ( $fields as $field ) {
823
			if ( ! ( $field instanceof Field ) ) {
824
				Incorrect_Syntax_Exception::raise( 'Object must be of type Carbon_Fields\\Field\\Field' );
825
				return $this;
826
			}
827
828
			$unique = $this->register_field_name( $field->get_name() );
829
			if ( ! $unique ) {
830
				return $this;
831
			}
832
833
			$field->set_context( $this->type );
834
			if ( ! $field->get_datastore() ) {
835
				$field->set_datastore( $this->get_datastore(), $this->has_default_datastore() );
836
			}
837
		}
838
839
		$this->fields = array_merge( $this->fields, $fields );
840
841
		return $this;
842
	}
843
844
	/**
845
	 * Configuration function for adding tab with fields
846
	 *
847
	 * @param string $tab_name
848
	 * @param array $fields
849
	 * @return Container $this
850
	 */
851
	public function add_tab( $tab_name, $fields ) {
852
		$this->add_fields( $fields );
853
		$this->create_tab( $tab_name, $fields );
854
		return $this;
855
	}
856
857
	/**
858
	 * Proxy function to set attachment conditions
859
	 *
860
	 * @see    Fulfillable_Collection::where()
861
	 * @return Container $this
862
	 */
863
	public function where() {
864
		call_user_func_array( array( $this->condition_collection, 'where' ), func_get_args() );
865
		return $this;
866
	}
867
868
	/**
869
	 * Proxy function to set attachment conditions
870
	 *
871
	 * @see    Fulfillable_Collection::or_where()
872
	 * @return Container $this
873
	 */
874
	public function or_where() {
875
		call_user_func_array( array( $this->condition_collection, 'or_where' ), func_get_args() );
876
		return $this;
877
	}
878
}
879