Completed
Push — master ( cf672a...efc967 )
by
unknown
10:42 queued 03:29
created

Container::verify_unique_panel_id()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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