Completed
Push — master ( 0683e2...fa03b3 )
by Marin
02:37
created

Container::create_tab()   B

Complexity

Conditions 5
Paths 12

Size

Total Lines 21
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 3
Bugs 1 Features 0
Metric Value
c 3
b 1
f 0
dl 0
loc 21
rs 8.7624
ccs 0
cts 18
cp 0
cc 5
eloc 13
nc 12
nop 3
crap 30
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 Datastore_Interface
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";
0 ignored issues
show
Coding Style Comprehensibility introduced by
The string literal \\Broken_Container does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
148 1
		}
149
150 9
		$container = new $class( $name );
151 8
		$container->type = $type;
152 8
		$container->add_template( $type, array( $container, 'template' ) );
153
154 8
		self::$init_containers[] = $container;
155
156 8
		return $container;
157
	}
158
159
	/**
160
	 * An alias of factory().
161
	 *
162
	 * @see Container::factory()
163
	 **/
164 11
	public static function make( $type, $name ) {
165 11
		return self::factory( $type, $name );
166
	}
167
168
	/**
169
	 * Initialize containers created via factory
170
	 *
171
	 * @return object
172
	 **/
173
	public static function init_containers() {
174
		while ( ( $container = array_shift( self::$init_containers ) ) ) {
175
			$container->init();
176
		}
177
178
		return $container;
179
	}
180
181
	/**
182
	 * Returns all the active containers created via factory
183
	 *
184
	 * @return array
185
	 **/
186
	public static function get_active_containers() {
187
		return self::$active_containers;
188
	}
189
190
	/**
191
	 * Adds a container to the active containers array and triggers an action
192
	 **/
193
	public static function add_active_container( $container ) {
194
		self::$active_containers[] = $container;
195
196
		do_action( 'crb_container_activated', $container );
197
	}
198
199
	/**
200
	 * Returns all the active fields created via factory
201
	 *
202
	 * @return array
203
	 **/
204
	public static function get_active_fields() {
205
		return self::$active_fields;
206
	}
207
208
	/**
209
	 * Adds a field to the active fields array and triggers an action
210
	 **/
211
	public static function add_active_field( $field ) {
212
		self::$active_fields[] = $field;
213
214
		if ( method_exists( $field, 'get_fields' ) ) {
215
			$fields = $field->get_fields();
216
217
			foreach ( $fields as $inner_field ) {
218
				self::add_active_field( $inner_field );
219
			}
220
		}
221
222
		do_action( 'crb_field_activated', $field );
223
	}
224
225
	/**
226
	 * Perform instance initialization after calling setup()
227
	 **/
228
	public abstract function init();
0 ignored issues
show
Coding Style introduced by
The abstract declaration must precede the visibility declaration
Loading history...
229
230
	/**
231
	 * Prints the container Underscore template
232
	 **/
233
	public function template() {
234
		?>
235
		<div class="{{{ classes.join(' ') }}}">
236
			<# _.each(fields, function(field) { #>
237
				<div class="{{{ field.classes.join(' ') }}}">
238
					<label for="{{{ field.id }}}">
239
						{{ field.label }}
240
241
						<# if (field.required) { #>
242
							 <span class="carbon-required">*</span>
243
						<# } #>
244
					</label>
245
246
					<div class="field-holder {{{ field.id }}}"></div>
247
248
					<# if (field.help_text) { #>
249
						<em class="help-text">
250
							{{{ field.help_text }}}
251
						</em>
252
					<# } #>
253
254
					<em class="carbon-error"></em>
255
				</div>
256
			<# }); #>
257
		</div>
258
		<?php
259
	}
260
261
	/**
262
	 * Create a new container
263
	 *
264
	 * @param string $title Unique title of the container
265
	 **/
266 9
	public function __construct( $title ) {
267 9
		if ( empty($title) ) {
268 1
			Incorrect_Syntax_Exception::raise( 'Empty container title is not supported' );
269
		}
270
271 8
		$this->title = $title;
272 8
		$this->id = preg_replace( '~\W~u', '', remove_accents( $title ) );
273
274 8
		$this->verify_unique_panel_id( $this->id );
275
276 8
		add_action( 'admin_print_scripts', array( $this, 'admin_hook_scripts' ) );
277 8
		add_action( 'admin_print_styles', array( $this, 'admin_hook_styles' ) );
278 8
	}
279
280
	/**
281
	 * Update container settings and begin initialization
282
	 *
283
	 * @see init()
284
	 * @param array $settings
285
	 **/
286
	public function setup( $settings = array() ) {
287
		if ( $this->setup_ready ) {
288
			Incorrect_Syntax_Exception::raise( 'Panel "' . $this->title . '" already setup' );
289
		}
290
291
		$this->check_setup_settings( $settings );
292
293
		$this->settings = array_merge( $this->settings, $settings );
294
295
		foreach ( $this->settings as $key => $value ) {
296
			if ( is_null( $value ) ) {
297
				unset( $this->settings[ $key ] );
298
			}
299
		}
300
301
		$this->setup_ready = true;
302
303
		return $this;
304
	}
305
306
	/**
307
	 * Check if all required container settings have been specified
308
	 *
309
	 * @param array $settings Container settings
310
	 **/
311
	public function check_setup_settings( &$settings = array() ) {
312
		$invalid_settings = array_diff_key( $settings, $this->settings );
313 View Code Duplication
		if ( ! empty( $invalid_settings ) ) {
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...
314
			Incorrect_Syntax_Exception::raise( 'Invalid settings supplied to setup(): "' . implode( '", "', array_keys( $invalid_settings ) ) . '"' );
315
		}
316
	}
317
318
	/**
319
	 * Called first as part of the container save procedure.
320
	 * Responsible for checking the request validity and 
321
	 * calling the container-specific save() method
322
	 *
323
	 * @see save()
324
	 * @see is_valid_save()
325
	 **/
326
	public function _save() {
327
		$param = func_get_args();
328
		if ( call_user_func_array( array( $this, 'is_valid_save' ), $param ) ) {
329
			call_user_func_array( array( $this, 'save' ), $param );
330
		}
331
	}
332
333
	/**
334
	 * Load submitted data and save each field in the container
335
	 *
336
	 * @see is_valid_save()
337
	 **/
338
	public function save( $user_data ) {
339
		foreach ( $this->fields as $field ) {
340
			$field->set_value_from_input();
341
			$field->save();
342
		}
343
	}
344
345
	/**
346
	 * Checks whether the current request is valid
347
	 *
348
	 * @return bool
349
	 **/
350
	public function is_valid_save() {
351
		return false;
352
	}
353
354
	/**
355
	 * Load the value for each field in the container.
356
	 * Could be used internally during container rendering
357
	 **/
358
	public function load() {
359
		foreach ( $this->fields as $field ) {
360
			$field->load();
361
		}
362
	}
363
	
364
365
	/**
366
	 * Called first as part of the container attachment procedure.
367
	 * Responsible for checking  it's ok to attach the container 
368
	 * and if it is, calling the container-specific attach() method
369
	 *
370
	 * @see attach()
371
	 * @see is_valid_attach()
372
	 **/
373
	public function _attach() {
374
		$param = func_get_args();
375
		if ( call_user_func_array( array( $this, 'is_valid_attach' ), $param ) ) {
376
			call_user_func_array( array( $this, 'attach' ), $param );
377
378
			if ( call_user_func_array( array( $this, 'is_active' ), $param ) ) {
379
				self::add_active_container( $this );
380
381
				$fields = $this->get_fields(); 
382
				foreach ( $fields as $field ) {
383
					self::add_active_field( $field );
384
				}
385
			}
386
		}
387
	}
388
389
	/**
390
	 * Returns all the backbone templates
391
	 *
392
	 * @return array
393
	 **/
394
	public function get_templates() {
395
		return $this->templates;
396
	}
397
398
	/**
399
	 * Adds a new backbone template
400
	 **/
401 7
	public function add_template( $name, $callback ) {
402 7
		$this->templates[ $name ] = $callback;
403 7
	}
404
405
	/**
406
	 * Attach the container rendering and helping methods 
407
	 * to concrete WordPress Action hooks
408
	 **/
409
	public function attach() {}
410
411
	/**
412
	 * Perform checks whether the container is active for current request
413
	 *
414
	 * @return bool True if the container is active
415
	 **/
416
	public function is_active() {
417
		return $this->is_valid_attach();
418
	}
419
420
	/**
421
	 * Perform checks whether the container should be attached during the current request
422
	 *
423
	 * @return bool True if the container is allowed to be attached
424
	 **/
425
	public function is_valid_attach() {
426
		return true;
427
	}
428
429
	/**
430
	 * Revert the result of attach()
431
	 **/
432
	public function detach() {
433
		$this->drop_unique_panel_id( $this->id );
434
435
		// unregister field names
436
		foreach ( $this->fields as $field ) {
437
			$this->drop_unique_field_name( $field->get_name() );
438
		}
439
	}
440
441
	/**
442
	 * Append array of fields to the current fields set. All items of the array
443
	 * must be instances of Field and their names should be unique for all
444
	 * Carbon containers.
445
	 * If a field does not have DataStore already, the container data store is 
446
	 * assigned to them instead.
447
	 *
448
	 * @param array $fields
449
	 **/
450
	public function add_fields( $fields ) {
451
		foreach ( $fields as $field ) {
452
			if ( ! is_a( $field, 'Carbon_Fields\\Field\\Field' ) ) {
453
				Incorrect_Syntax_Exception::raise( 'Object must be of type Carbon_Fields\\Field\\Field' );
454
			}
455
456
			$this->verify_unique_field_name( $field->get_name() );
457
458
			$field->set_context( $this->type );
459
			if ( ! $field->get_datastore() ) {
460
				$field->set_datastore( $this->store );
461
			}
462
		}
463
464
		$this->fields = array_merge( $this->fields, $fields );
465
466
		return $this;
467
	}
468
469
	/**
470
	 * Configuration function for adding tab with fields
471
	 */
472
	public function add_tab( $tab_name, $fields ) {
473
		$this->add_template( 'tabs', array( $this, 'template_tabs' ) );
474
		
475
		$this->add_fields( $fields );
476
		$this->create_tab( $tab_name, $fields );
477
478
		return $this;
479
	}
480
481
	/**
482
	 * Internal function that creates the tab and associates it with particular field set
483
	 */
484
	private function create_tab( $tab_name, $fields, $queue_end = self::TABS_TAIL ) {
485
		if ( isset( $this->tabs[ $tab_name ] ) ) {
486
			Incorrect_Syntax_Exception::raise( "Tab name duplication for $tab_name" );
487
		}
488
489
		if ( $queue_end === self::TABS_TAIL ) {
490
			$this->tabs[ $tab_name ] = array();
491
		} else if ( $queue_end === self::TABS_HEAD ) {
492
			$this->tabs = array_merge(
493
				array( $tab_name => array() ),
494
				$this->tabs
495
			);
496
		}
497
498
		foreach ( $fields as $field ) {
499
			$field_name = $field->get_name();
500
			$this->tabs[ $tab_name ][ $field_name ] = $field;
501
		}
502
503
		$this->settings['tabs'] = $this->get_tabs_json();
504
	}
505
506
	/**
507
	 * Whether the container is tabbed or not
508
	 */
509
	public function is_tabbed() {
510
		return (bool) $this->tabs;
511
	}
512
513
	/**
514
	 * Retrieve all fields that are not defined under a specific tab
515
	 */
516
	public function get_untabbed_fields() {
517
		$tabbed_fields_names = array();
518
		foreach ( $this->tabs as $tab_fields ) {
519
			$tabbed_fields_names = array_merge( $tabbed_fields_names, array_keys( $tab_fields ) );
520
		}
521
522
		$all_fields_names = array();
523
		foreach ( $this->fields as $field ) {
524
			$all_fields_names[] = $field->get_name();
525
		}
526
527
		$fields_not_in_tabs = array_diff( $all_fields_names, $tabbed_fields_names );
528
529
		$untabbed_fields = array();
530
		foreach ( $this->fields as $field ) {
531
			if ( in_array( $field->get_name(), $fields_not_in_tabs ) ) {
532
				$untabbed_fields[] = $field;
533
			}
534
		}
535
536
		return $untabbed_fields;
537
	}
538
539
	/**
540
	 * Retrieve all tabs.
541
	 * Create a default tab if there are any untabbed fields.
542
	 */
543
	public function get_tabs() {
544
		$untabbed_fields = $this->get_untabbed_fields();
545
546
		if ( ! empty( $untabbed_fields ) ) {
547
			$this->create_tab( __( 'General', 'carbon_fields' ), $untabbed_fields, self::TABS_HEAD );
548
		}
549
550
		return $this->tabs;
551
	}
552
553
	/**
554
	 * Build the tabs JSON
555
	 */
556
	public function get_tabs_json() {
557
		$tabs_json = array();	
558
		$tabs = $this->get_tabs();
559
560
		foreach ( $tabs as $tab_name => $fields ) {
561
			foreach ( $fields as $field_name => $field ) {
562
				$tabs_json[ $tab_name ][] = $field_name;
563
			}
564
		}
565
566
		return $tabs_json;
567
	}
568
569
	/**
570
	 * Returns the private container array of fields.
571
	 * Use only if you are completely aware of what you are doing.
572
	 *
573
	 * @return array
574
	 **/
575
	public function get_fields() {
576
		return $this->fields;
577
	}
578
579
	/**
580
	 * Perform a check whether the current container has fields
581
	 *
582
	 * @return bool
583
	 **/
584
	public function has_fields() {
585
		return (bool) $this->fields;
586
	}
587
588
	/**
589
	 * Perform checks whether there is a container registered with identificator $id
590
	 */
591 8
	public function verify_unique_panel_id( $id ) {
592 8
		if ( in_array( $id, self::$registered_panel_ids ) ) {
593 2
			Incorrect_Syntax_Exception::raise( 'Panel ID "' . $id .'" already registered' );
594
		}
595
596 8
		self::$registered_panel_ids[] = $id;
597 8
	}
598
599
600
	/**
601
	 * Remove container identificator $id from the list of unique container ids
602
	 *
603
	 * @param string $id
604
	 **/
605
	public function drop_unique_panel_id( $id ) {
606
		if ( in_array( $id, self::$registered_panel_ids ) ) {
607
			unset( self::$registered_panel_ids[ array_search( $id, self::$registered_panel_ids ) ] );
608
		}
609
	}
610
611
	/**
612
	 * Perform checks whether there is a field registered with the name $name.
613
	 * If not, the field name is recorded.
614
	 *
615
	 * @param string $name
616
	 **/
617
	public function verify_unique_field_name( $name ) {
618
		if ( in_array( $name, self::$registered_field_names ) ) {
619
			Incorrect_Syntax_Exception::raise( 'Field name "' . $name . '" already registered' );
620
		}
621
622
		self::$registered_field_names[] = $name;
623
	}
624
625
	/**
626
	 * Remove field name $name from the list of unique field names
627
	 *
628
	 * @param string $name
629
	 **/
630
	public function drop_unique_field_name( $name ) {
631
		$index = array_search( $name, self::$registered_field_names );
632
		if ( $index !== false ) {
633
			unset( self::$registered_field_names[ $index ] );
634
		}
635
	}
636
637
	/**
638
	 * Assign DataStore instance for use by the container fields
639
	 *
640
	 * @param object $store
641
	 **/
642 7
	public function set_datastore( Datastore_Interface $store ) {
643 7
		$this->store = $store;
644
645 7
		foreach ( $this->fields as $field ) {
646
			$field->set_datastore( $this->store );
647 7
		}
648 7
	}
649
650
	/**
651
	 * Return the DataStore instance used by container fields
652
	 *
653
	 * @return object $store
654
	 **/
655 7
	public function get_datastore() {
656 7
		return $this->store;
657
	}
658
659
	/**
660
	 * Return WordPress nonce name used to identify the current container instance
661
	 *
662
	 * @return string
663
	 **/
664
	public function get_nonce_name() {
665
		return 'carbon_panel_' . $this->id . '_nonce';
666
	}
667
668
	/**
669
	 * Return WordPress nonce field
670
	 *
671
	 * @return string
672
	 **/
673
	public function get_nonce_field() {
674
		return wp_nonce_field( $this->get_nonce_name(), $this->get_nonce_name(), /*referer?*/ false, /*echo?*/ false );
675
	}
676
677
	/**
678
	 * Returns an array that holds the container data, suitable for JSON representation.
679
	 * This data will be available in the Underscore template and the Backbone Model.
680
	 * 
681
	 * @param bool $load  Should the value be loaded from the database or use the value from the current instance.
682
	 * @return array
683
	 */
684
	public function to_json( $load ) {
685
		$container_data = array(
686
			'id' => $this->id,
687
			'type' => $this->type,
688
			'title' => $this->title,
689
			'settings' => $this->settings,
690
			'fields' => array(),
691
		);
692
693
		$fields = $this->get_fields();
694
		foreach ( $fields as $field ) {
695
			$field_data = $field->to_json( $load );
696
			$container_data['fields'][] = $field_data;
697
		}
698
699
		return $container_data;
700
	}
701
702
	/**
703
	 * Underscore template for tabs
704
	 */
705
	public function template_tabs() {
706
		?>
707
		<div class="carbon-tabs">
708
			<ul class="carbon-tabs-nav">
709
				<# _.each(tabs, function (tab, tabName) { #>
710
					<li><a href="#">{{{ tabName }}}</a></li>
711
				<# }); #>
712
			</ul> 
713
714
			<div class="carbon-tabs-body">
715
				<# _.each(tabs, function (tab) { #>
716
					<div class="carbon-fields-collection carbon-tab">
717
						{{{ tab.html }}}
718
					</div>
719
				<# }); #>
720
			</div>
721
		</div>
722
		<?php
723
	}
724
725
	/**
726
	 * Enqueue admin scripts
727
	 */
728
	public function admin_hook_scripts() {
729
		wp_enqueue_script( 'carbon-containers', \Carbon_Fields\URL . '/assets/js/containers.js', array( 'carbon-app' ) );
730
731
		wp_localize_script( 'carbon-containers', 'carbon_containers_l10n',
732
			array(
733
				'please_fill_the_required_fields' => __( 'Please fill out all required fields highlighted below.', 'carbon_fields' ),
734
				'changes_made_save_alert' => __( 'The changes you made will be lost if you navigate away from this page.', 'carbon_fields' ),
735
			)
736
		);
737
	}
738
739
	/**
740
	 * Enqueue admin styles
741
	 */
742
	public function admin_hook_styles() {
743
		wp_enqueue_style( 'carbon-main', \Carbon_Fields\URL . '/assets/css/main.css' );
744
	}
745
746
} // END Container 
747
748