Completed
Push — milestone/2.0 ( 27edeb...debc3a )
by
unknown
02:20
created

Container::verify_unique_field_name()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 7
Ratio 100 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 1
dl 7
loc 7
ccs 0
cts 6
cp 0
crap 6
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace Carbon_Fields\Container;
4
5
use Carbon_Fields\App;
6
use Carbon_Fields\Field\Field;
7
use Carbon_Fields\Field\Group_Field;
8
use Carbon_Fields\Datastore\Datastore_Interface;
9
use Carbon_Fields\Datastore\Datastore_Holder_Interface;
10
use Carbon_Fields\Exception\Incorrect_Syntax_Exception;
11
12
/**
13
 * Base container class.
14
 * Defines the key container methods and their default implementations.
15
 */
16
abstract class Container implements Datastore_Holder_Interface {
17
	/**
18
	 * Where to put a particular tab -- at the head or the tail. Tail by default
19
	 */
20
	const TABS_TAIL = 1;
21
	const TABS_HEAD = 2;
22
23
	/**
24
	 * Stores if the container is active on the current page
25
	 *
26
	 * @see activate()
27
	 * @var bool
28
	 */
29
	protected $active = false;
30
31
	/**
32
	 * List of registered unique field names for this container instance
33
	 *
34
	 * @see verify_unique_field_name()
35
	 * @var array
36
	 */
37
	protected $registered_field_names = array();
38
39
	/**
40
	 * Stores all the container Backbone templates
41
	 *
42
	 * @see factory()
43
	 * @see add_template()
44
	 * @var array
45
	 */
46
	protected $templates = array();
47
48
	/**
49
	 * Tabs available
50
	 */
51
	protected $tabs = array();
52
53
	/**
54
	 * List of default container settings
55
	 *
56
	 * @see init()
57
	 * @var array
58
	 */
59
	public $settings = array();
60
61
	/**
62
	 * Title of the container
63
	 *
64
	 * @var string
65
	 */
66
	public $title = '';
67
68
	/**
69
	 * List of notification messages to be displayed on the front-end
70
	 *
71
	 * @var array
72
	 */
73
	protected $notifications = array();
74
75
	/**
76
	 * List of error messages to be displayed on the front-end
77
	 *
78
	 * @var array
79
	 */
80
	protected $errors = array();
81
82
	/**
83
	 * List of container fields
84
	 *
85
	 * @see add_fields()
86
	 * @var array
87
	 */
88
	protected $fields = array();
89
90
	/**
91
	 * Container datastores. Propagated to all container fields
92
	 *
93
	 * @see set_datastore()
94
	 * @see get_datastore()
95
	 * @var object
96
	 */
97
	protected $datastore;
98
99
	/**
100
	 * Flag whether the datastore is the default one or replaced with a custom one
101
	 *
102
	 * @see set_datastore()
103
	 * @see get_datastore()
104
	 * @var boolean
105
	 */
106
	protected $has_default_datastore = true;
107
108
	/**
109
	 * Normalizes a container type string to an expected format
110
	 *
111
	 * @param string $type
112
	 * @return string $normalized_type
113
	 **/
114
	protected static function normalize_container_type( $type ) {
115
		// backward compatibility: post_meta container used to be called custom_fields
116
		if ( $type === 'custom_fields' ) {
117
			$type = 'post_meta';
118
		}
119
120
		$normalized_type = str_replace( ' ', '_', ucwords( str_replace( '_', ' ', $type ) ) );
121
		return $normalized_type;
122
	}
123
124
	/**
125
	 * Resolves a string-based type to a fully qualified container class name
126
	 *
127
	 * @param string $type
128
	 * @return string $class_name
129
	 **/
130
	protected static function container_type_to_class( $type ) {
131
		$class = __NAMESPACE__ . '\\' . $type . '_Container';
132 View Code Duplication
		if ( ! class_exists( $class ) ) {
133
			Incorrect_Syntax_Exception::raise( 'Unknown container "' . $type . '".' );
134
			$class = __NAMESPACE__ . '\\Broken_Container';
135
		}
136
		return $class;
137
	}
138
139
	/**
140
	 * Create a new container of type $type and name $name.
141
	 *
142
	 * @param string $type
143
	 * @param string $name Human-readable name of the container
144
	 * @return object $container
145
	 **/
146 9
	public static function factory( $type, $name ) {
147 9
		$repository = App::resolve( 'container_repository' );
148 9
		$unique_id = $repository->get_unique_panel_id( $name );
149
		
150 9
		$normalized_type = static::normalize_container_type( $type );
151 9
		$class = static::container_type_to_class( $normalized_type );
152 7
		$container = new $class( $unique_id, $name, $normalized_type );
153 7
		$repository->register_container( $container );
154
155 7
		return $container;
156
	}
157
158
	/**
159
	 * An alias of factory().
160
	 *
161
	 * @see Container::factory()
162
	 **/
163
	public static function make( $type, $name ) {
164
		return static::factory( $type, $name );
165
	}
166
167
	/**
168
	 * Create a new container
169
	 *
170
	 * @param string $unique_id Unique id of the container
171
	 * @param string $title title of the container
172
	 * @param string $type Type of the container
173
	 **/
174 2
	public function __construct( $unique_id, $title, $type ) {
175 2
		if ( empty( $title ) ) {
176 1
			Incorrect_Syntax_Exception::raise( 'Empty container title is not supported' );
177
		}
178
179 1
		$this->id = $unique_id;
180 1
		$this->title = $title;
181 1
		$this->type = $type;
182 1
	}
183
184
	/**
185
	 * Return whether the container is active
186
	 **/
187
	public function active() {
188
		return $this->active;
189
	}
190
191
	/**
192
	 * Activate the container and trigger an action
193
	 **/
194
	protected function activate() {
195
		$this->active = true;
196
		$this->boot();
197
		do_action( 'crb_container_activated', $this );
198
	}
199
200
	/**
201
	 * Activates and boots a field recursively
202
	 **/
203
	protected function activate_field( $field ) {
204
		if ( method_exists( $field, 'get_fields' ) ) {
205
			$fields = $field->get_fields();
206
207
			foreach ( $fields as $inner_field ) {
208
				$this->activate_field( $inner_field );
209
			}
210
		}
211
212
		$field->boot();
213
214
		do_action( 'crb_field_activated', $field );
215
	}
216
217
	/**
218
	 * Perform instance initialization
219
	 **/
220
	abstract public function init();
221
222
	/**
223
	 * Prints the container Underscore template
224
	 **/
225
	public function template() {
226
		?>
227
		<div class="{{{ classes.join(' ') }}}">
228
			<# _.each(fields, function(field) { #>
229
				<div class="{{{ field.classes.join(' ') }}}">
230
					<label for="{{{ field.id }}}">
231
						{{ field.label }}
232
233
						<# if (field.required) { #>
234
							 <span class="carbon-required">*</span>
235
						<# } #>
236
					</label>
237
238
					<div class="field-holder {{{ field.id }}}"></div>
239
240
					<# if (field.help_text) { #>
241
						<em class="help-text">
242
							{{{ field.help_text }}}
243
						</em>
244
					<# } #>
245
246
					<em class="carbon-error"></em>
247
				</div>
248
			<# }); #>
249
		</div>
250
		<?php
251
	}
252
253
	/**
254
	 * Boot the container once it's attached.
255
	 **/
256
	protected function boot() {
257
		$this->add_template( $this->type, array( $this, 'template' ) );
258
259
		add_action( 'admin_footer', array( get_class(), 'admin_hook_scripts' ), 5 );
260
		add_action( 'admin_footer', array( get_class(), 'admin_hook_styles' ), 5 );
261
	}
262
263
	/**
264
	 * Called first as part of the container save procedure.
265
	 * Responsible for checking the request validity and
266
	 * calling the container-specific save() method
267
	 *
268
	 * @see save()
269
	 * @see is_valid_save()
270
	 **/
271
	public function _save() {
272
		$param = func_get_args();
273
		if ( call_user_func_array( array( $this, 'is_valid_save' ), $param ) ) {
274
			call_user_func_array( array( $this, 'save' ), $param );
275
		}
276
	}
277
278
	/**
279
	 * Load submitted data and save each field in the container
280
	 *
281
	 * @see is_valid_save()
282
	 **/
283
	public function save( $data ) {
284
		foreach ( $this->fields as $field ) {
285
			$field->set_value_from_input();
286
			$field->save();
287
		}
288
	}
289
290
	/**
291
	 * Checks whether the current request is valid
292
	 *
293
	 * @return bool
294
	 **/
295
	public function is_valid_save() {
296
		return false;
297
	}
298
299
	/**
300
	 * Load the value for each field in the container.
301
	 * Could be used internally during container rendering
302
	 **/
303
	public function load() {
304
		foreach ( $this->fields as $field ) {
305
			$field->load();
306
		}
307
	}
308
309
	/**
310
	 * Called first as part of the container attachment procedure.
311
	 * Responsible for checking it's OK to attach the container
312
	 * and if it is, calling the container-specific attach() method
313
	 *
314
	 * @see attach()
315
	 * @see is_valid_attach()
316
	 **/
317
	public function _attach() {
318
		$param = func_get_args();
319
		if ( call_user_func_array( array( $this, 'is_valid_attach' ), $param ) ) {
320
			call_user_func_array( array( $this, 'attach' ), $param );
321
322
			if ( call_user_func_array( array( $this, 'is_active' ), $param ) ) {
323
				$this->activate();
324
325
				$fields = $this->get_fields();
326
				foreach ( $fields as $field ) {
327
					$this->activate_field( $field );
328
				}
329
			}
330
		}
331
	}
332
333
	/**
334
	 * Returns all the Backbone templates
335
	 *
336
	 * @return array
337
	 **/
338
	public function get_templates() {
339
		return $this->templates;
340
	}
341
342
	/**
343
	 * Adds a new Backbone template
344
	 **/
345
	protected function add_template( $name, $callback ) {
346
		$this->templates[ $name ] = $callback;
347
	}
348
349
	/**
350
	 * Attach the container rendering and helping methods
351
	 * to concrete WordPress Action hooks
352
	 **/
353
	public function attach() {}
354
355
	/**
356
	 * Perform checks whether the container should be attached during the current request
357
	 *
358
	 * @return bool True if the container is allowed to be attached
359
	 **/
360
	final public function is_valid_attach() {
361
		$is_valid_attach = $this->_is_valid_attach();
362
		return apply_filters( 'carbon_container_is_valid_attach', $is_valid_attach, $this );
363
	}
364
365
	/**
366
	 * Require extending containers to define their own attach rules
367
	 *
368
	 * @return bool True if the container is allowed to be attached
369
	 **/
370
	abstract protected function _is_valid_attach();
371
372
	/**
373
	 * Whether this container is currently viewed.
374
	 **/
375
	public function is_active() {
376
		return $this->is_valid_attach();
377
	}
378
379
	/**
380
	 * Perform a check whether the current container has fields
381
	 *
382
	 * @return bool
383
	 **/
384
	public function has_fields() {
385
		return (bool) $this->fields;
386
	}
387
388
	/**
389
	 * Returns the private container array of fields.
390
	 * Use only if you are completely aware of what you are doing.
391
	 *
392
	 * @return array
393
	 **/
394
	public function get_fields() {
395
		return $this->fields;
396
	}
397
398
	/**
399
	 * Return root field from container with specified name
400
	 * 
401
	 * @example crb_complex
402
	 * 
403
	 * @param string $field_name
404
	 * @return Field
405
	 **/
406
	public function get_root_field_by_name( $field_name ) {
407
		$fields = $this->get_fields();
408
		foreach ( $fields as $field ) {
409
			if ( $field->get_base_name() === $field_name ) {
410
				return $field;
411
			}
412
		}
413
		return null;
414
	}
415
416
	/**
417
	 * Return field from container with specified name
418
	 * 
419
	 * @example crb_complex/text_field
420
	 * @example crb_complex/complex_2
421
	 * @example crb_complex/complex_2:text_group/text_field
422
	 * 
423
	 * @param string $field_name Can specify a field inside a complex with a / (slash) separator
424
	 * @return Field
425
	 **/
426
	public function get_field_by_name( $field_name ) {
427
		$hierarchy = array_filter( explode( '/', $field_name ) );
428
		$field = null;
429
430
		$field_group = $this->get_fields();
431
		$hierarchy_left = $hierarchy;
432
433
		while ( ! empty( $hierarchy_left ) ) {
434
			$segment = array_shift( $hierarchy_left );
435
			$segment_pieces = explode( ':', $segment, 2 );
436
			$field_name = $segment_pieces[0];
437
			$group_name = isset( $segment_pieces[1] ) ? $segment_pieces[1] : Group_Field::DEFAULT_GROUP_NAME;
438
439
			foreach ( $field_group as $f ) {
440
				if ( $f->get_base_name() === $field_name ) {
441
					if ( empty( $hierarchy_left ) ) {
442
						$field = $f;
443
					} else {
444
						if ( is_a( $f, '\\Carbon_Fields\\Field\\Complex_Field' ) ) {
445
							$group = $f->get_group_by_name( $group_name );
446
							if ( ! $group ) {
447
								Incorrect_Syntax_Exception::raise( 'Unknown group name specified when fetching a value inside a complex field: "' . $group_name . '".' );
448
							}
449
							$field_group = $group->get_fields();
450
						} else {
451
							Incorrect_Syntax_Exception::raise( 'Attempted to look for a nested field inside a non-complex field.' );
452
						}
453
					}
454
					break;
455
				}
456
			}
457
		}
458
459
		return $field;
460
	}
461
462
	/**
463
	 * Perform checks whether there is a field registered with the name $name.
464
	 * If not, the field name is recorded.
465
	 *
466
	 * @param string $name
467
	 **/
468 View Code Duplication
	public function verify_unique_field_name( $name ) {
469
		if ( in_array( $name, $this->registered_field_names ) ) {
470
			Incorrect_Syntax_Exception::raise( 'Field name "' . $name . '" already registered' );
471
		}
472
473
		$this->registered_field_names[] = $name;
474
	}
475
476
	/**
477
	 * Remove field name $name from the list of unique field names
478
	 *
479
	 * @param string $name
480
	 **/
481
	public function drop_unique_field_name( $name ) {
482
		$index = array_search( $name, $this->registered_field_names );
483
484
		if ( $index !== false ) {
485
			unset( $this->registered_field_names[ $index ] );
486
		}
487
	}
488
489
	/**
490
	 * Return whether the datastore instance is the default one or has been overriden
491
	 *
492
	 * @return boolean
493
	 **/
494 6
	public function has_default_datastore() {
495 6
		return $this->has_default_datastore;
496
	}
497
498
	/**
499
	 * Set datastore instance
500
	 *
501
	 * @param Datastore_Interface $datastore
502
	 * @return object $this
503
	 **/
504 6 View Code Duplication
	public function set_datastore( Datastore_Interface $datastore, $set_as_default = false ) {
1 ignored issue
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...
505 6
		if ( $set_as_default && ! $this->has_default_datastore() ) {
506 1
			return $this; // datastore has been overriden with a custom one - abort changing to a default one
507
		}
508 6
		$this->datastore = $datastore;
509 6
		$this->has_default_datastore = $set_as_default;
510
511 6
		foreach ( $this->fields as $field ) {
512
			$field->set_datastore( $this->get_datastore(), true );
513 6
		}
514 6
		return $this;
515
	}
516
517
	/**
518
	 * Get the DataStore instance
519
	 *
520
	 * @return Datastore_Interface $datastore
521
	 **/
522 6
	public function get_datastore() {
523 6
		return $this->datastore;
524
	}
525
526
	/**
527
	 * Return WordPress nonce name used to identify the current container instance
528
	 *
529
	 * @return string
530
	 **/
531
	public function get_nonce_name() {
532
		return 'carbon_panel_' . $this->id . '_nonce';
533
	}
534
535
	/**
536
	 * Return WordPress nonce field
537
	 *
538
	 * @return string
539
	 **/
540
	public function get_nonce_field() {
541
		return wp_nonce_field( $this->get_nonce_name(), $this->get_nonce_name(), /*referer?*/ false, /*echo?*/ false );
542
	}
543
544
	/**
545
	 * Internal function that creates the tab and associates it with particular field set
546
	 *
547
	 * @param string $tab_name
548
	 * @param array $fields
549
	 * @param int $queue_end
550
	 * @return object $this
551
	 */
552
	private function create_tab( $tab_name, $fields, $queue_end = self::TABS_TAIL ) {
553
		if ( isset( $this->tabs[ $tab_name ] ) ) {
554
			Incorrect_Syntax_Exception::raise( "Tab name duplication for $tab_name" );
555
		}
556
557
		if ( $queue_end === static::TABS_TAIL ) {
558
			$this->tabs[ $tab_name ] = array();
559
		} else if ( $queue_end === static::TABS_HEAD ) {
560
			$this->tabs = array_merge(
561
				array( $tab_name => array() ),
562
				$this->tabs
563
			);
564
		}
565
566
		foreach ( $fields as $field ) {
567
			$field_name = $field->get_name();
568
			$this->tabs[ $tab_name ][ $field_name ] = $field;
569
		}
570
571
		$this->settings['tabs'] = $this->get_tabs_json();
572
	}
573
574
	/**
575
	 * Whether the container is tabbed or not
576
	 *
577
	 * @return bool
578
	 */
579
	public function is_tabbed() {
580
		return (bool) $this->tabs;
581
	}
582
583
	/**
584
	 * Retrieve all fields that are not defined under a specific tab
585
	 *
586
	 * @return array
587
	 */
588
	protected function get_untabbed_fields() {
589
		$tabbed_fields_names = array();
590
		foreach ( $this->tabs as $tab_fields ) {
591
			$tabbed_fields_names = array_merge( $tabbed_fields_names, array_keys( $tab_fields ) );
592
		}
593
594
		$all_fields_names = array();
595
		foreach ( $this->fields as $field ) {
596
			$all_fields_names[] = $field->get_name();
597
		}
598
599
		$fields_not_in_tabs = array_diff( $all_fields_names, $tabbed_fields_names );
600
601
		$untabbed_fields = array();
602
		foreach ( $this->fields as $field ) {
603
			if ( in_array( $field->get_name(), $fields_not_in_tabs ) ) {
604
				$untabbed_fields[] = $field;
605
			}
606
		}
607
608
		return $untabbed_fields;
609
	}
610
611
	/**
612
	 * Retrieve all tabs.
613
	 * Create a default tab if there are any untabbed fields.
614
	 *
615
	 * @return array
616
	 */
617
	protected function get_tabs() {
618
		$untabbed_fields = $this->get_untabbed_fields();
619
620
		if ( ! empty( $untabbed_fields ) ) {
621
			$this->create_tab( __( 'General', \Carbon_Fields\TEXT_DOMAIN ), $untabbed_fields, static::TABS_HEAD );
622
		}
623
624
		return $this->tabs;
625
	}
626
627
	/**
628
	 * Build the tabs JSON
629
	 *
630
	 * @return array
631
	 */
632
	protected function get_tabs_json() {
633
		$tabs_json = array();
634
		$tabs = $this->get_tabs();
635
636
		foreach ( $tabs as $tab_name => $fields ) {
637
			foreach ( $fields as $field_name => $field ) {
638
				$tabs_json[ $tab_name ][] = $field_name;
639
			}
640
		}
641
642
		return $tabs_json;
643
	}
644
645
	/**
646
	 * Underscore template for tabs
647
	 */
648
	public function template_tabs() {
649
		?>
650
		<div class="carbon-tabs">
651
			<ul class="carbon-tabs-nav">
652
				<# _.each(tabs, function (tab, tabName) { #>
653
					<li><a href="#" data-id="{{{ tab.id }}}">{{{ tabName }}}</a></li>
654
				<# }); #>
655
			</ul>
656
657
			<div class="carbon-tabs-body">
658
				<# _.each(tabs, function (tab) { #>
659
					<div class="carbon-fields-collection carbon-tab">
660
						{{{ tab.html }}}
661
					</div>
662
				<# }); #>
663
			</div>
664
		</div>
665
		<?php
666
	}
667
668
	/**
669
	 * Returns an array that holds the container data, suitable for JSON representation.
670
	 * This data will be available in the Underscore template and the Backbone Model.
671
	 *
672
	 * @param bool $load  Should the value be loaded from the database or use the value from the current instance.
673
	 * @return array
674
	 */
675
	public function to_json( $load ) {
676
		$container_data = array(
677
			'id' => $this->id,
678
			'type' => $this->type,
679
			'title' => $this->title,
680
			'settings' => $this->settings,
681
			'fields' => array(),
682
		);
683
684
		$fields = $this->get_fields();
685
		foreach ( $fields as $field ) {
686
			$field_data = $field->to_json( $load );
687
			$container_data['fields'][] = $field_data;
688
		}
689
690
		return $container_data;
691
	}
692
693
	/**
694
	 * Enqueue admin scripts
695
	 */
696
	public static function admin_hook_scripts() {
697
		wp_enqueue_script( 'carbon-containers', \Carbon_Fields\URL . '/assets/js/containers.js', array( 'carbon-app' ), \Carbon_Fields\VERSION );
698
699
		wp_localize_script( 'carbon-containers', 'carbon_containers_l10n',
700
			array(
701
				'please_fill_the_required_fields' => __( 'Please fill out all required fields highlighted below.', \Carbon_Fields\TEXT_DOMAIN ),
702
				'changes_made_save_alert' => __( 'The changes you made will be lost if you navigate away from this page.', \Carbon_Fields\TEXT_DOMAIN ),
703
			)
704
		);
705
	}
706
707
	/**
708
	 * Enqueue admin styles
709
	 */
710
	public static function admin_hook_styles() {
711
		wp_enqueue_style( 'carbon-main', \Carbon_Fields\URL . '/assets/bundle.css', array(), \Carbon_Fields\VERSION );
712
	}
713
714
	/**
715
	 * COMMON USAGE METHODS
716
	 */
717
718
	/**
719
	 * Append array of fields to the current fields set. All items of the array
720
	 * must be instances of Field and their names should be unique for all
721
	 * Carbon containers.
722
	 * If a field does not have DataStore already, the container datastore is
723
	 * assigned to them instead.
724
	 *
725
	 * @param array $fields
726
	 * @return object $this
727
	 **/
728
	public function add_fields( $fields ) {
729
		foreach ( $fields as $field ) {
730
			if ( ! is_a( $field, 'Carbon_Fields\\Field\\Field' ) ) {
731
				Incorrect_Syntax_Exception::raise( 'Object must be of type Carbon_Fields\\Field\\Field' );
732
			}
733
734
			$this->verify_unique_field_name( $field->get_name() );
735
736
			$field->set_context( $this->type );
737
			if ( ! $field->get_datastore() ) {
738
				$field->set_datastore( $this->get_datastore(), $this->has_default_datastore() );
739
			}
740
		}
741
742
		$this->fields = array_merge( $this->fields, $fields );
743
744
		return $this;
745
	}
746
747
	/**
748
	 * Configuration function for adding tab with fields
749
	 *
750
	 * @param string $tab_name
751
	 * @param array $fields
752
	 * @return object $this
753
	 */
754
	public function add_tab( $tab_name, $fields ) {
755
		$this->add_template( 'tabs', array( $this, 'template_tabs' ) );
756
757
		$this->add_fields( $fields );
758
		$this->create_tab( $tab_name, $fields );
759
760
		return $this;
761
	}
762
}
763