Completed
Push — development ( e7d969...cfc138 )
by
unknown
02:32
created

Container::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 2.0054

Importance

Changes 0
Metric Value
cc 2
eloc 7
nc 2
nop 1
dl 0
loc 11
ccs 8
cts 9
cp 0.8889
crap 2.0054
rs 9.4285
c 0
b 0
f 0
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 get_unique_panel_id()
24
	 * @var array
25
	 */
26
	public static $registered_panel_ids = array();
27
28
	/**
29
	 * List of containers created via factory that
30
	 * should be initialized
31
	 *
32
	 * @var array
33
	 */
34
	protected static $init_containers = array();
35
36
	/**
37
	 * List of containers attached to the current page view
38
	 *
39
	 * @see _attach()
40
	 * @var array
41
	 */
42
	public static $active_containers = array();
43
44
	/**
45
	 * List of fields attached to the current page view
46
	 *
47
	 * @see _attach()
48
	 * @var array
49
	 */
50
	protected static $active_fields = array();
51
52
	/**
53
	 * List of registered unique field names for this container instance
54
	 *
55
	 * @see verify_unique_field_name()
56
	 * @var array
57
	 */
58
	protected $registered_field_names = array();
1 ignored issue
show
Comprehensibility Naming introduced by
The variable name $registered_field_names exceeds the maximum configured length of 20.

Very long variable names usually make code harder to read. It is therefore recommended not to make variable names too verbose.

Loading history...
59
60
	/**
61
	 * Stores all the container Backbone templates
62
	 *
63
	 * @see factory()
64
	 * @see add_template()
65
	 * @var array
66
	 */
67
	protected $templates = array();
68
69
	/**
70
	 * Tabs available
71
	 */
72
	protected $tabs = array();
73
74
	/**
75
	 * List of default container settings
76
	 *
77
	 * @see init()
78
	 * @var array
79
	 */
80
	public $settings = array();
81
82
	/**
83
	 * Title of the container
84
	 *
85
	 * @var string
86
	 */
87
	public $title = '';
88
89
	/**
90
	 * Whether the container was setup
91
	 *
92
	 * @var bool
93
	 */
94
	public $setup_ready = false;
95
96
	/**
97
	 * List of notification messages to be displayed on the front-end
98
	 *
99
	 * @var array
100
	 */
101
	protected $notifications = array();
102
103
	/**
104
	 * List of error messages to be displayed on the front-end
105
	 *
106
	 * @var array
107
	 */
108
	protected $errors = array();
109
110
	/**
111
	 * List of container fields
112
	 *
113
	 * @see add_fields()
114
	 * @var array
115
	 */
116
	protected $fields = array();
117
118
	/**
119
	 * Container DataStore. Propagated to all container fields
120
	 *
121
	 * @see set_datastore()
122
	 * @see get_datastore()
123
	 * @var object
124
	 */
125
	protected $store;
126
127
	/**
128
	 * Create a new container of type $type and name $name and label $label.
129
	 *
130
	 * @param string $type
131
	 * @param string $name Human-readable name of the container
132
	 * @return object $container
133
	 **/
134 11 View Code Duplication
	public static function factory( $type, $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...
135
		// backward compatibility: post_meta container used to be called custom_fields
136 11
		if ( $type === 'custom_fields' ) {
137 1
			$type = 'post_meta';
138 1
		}
139
140 11
		$type = str_replace( ' ', '_', ucwords( str_replace( '_', ' ', $type ) ) );
141
142 11
		$class = __NAMESPACE__ . '\\' . $type . '_Container';
143
144 11
		if ( ! class_exists( $class ) ) {
145 3
			Incorrect_Syntax_Exception::raise( 'Unknown container "' . $type . '".' );
146 1
			$class = __NAMESPACE__ . '\\Broken_Container';
147 1
		}
148
149 9
		$container = new $class( $name );
150 8
		$container->type = $type;
151
152 8
		self::$init_containers[] = $container;
153
154 8
		return $container;
155
	}
156
157
	/**
158
	 * An alias of factory().
159
	 *
160
	 * @see Container::factory()
161
	 **/
162 11
	public static function make( $type, $name ) {
163 11
		return self::factory( $type, $name );
164
	}
165
166
	/**
167
	 * Initialize containers created via factory
168
	 *
169
	 * @return object
170
	 **/
171
	public static function init_containers() {
172
		while ( ( $container = array_shift( self::$init_containers ) ) ) {
173
			$container->init();
174
		}
175
176
		return $container;
177
	}
178
179
	/**
180
	 * Returns all the active containers created via factory
181
	 *
182
	 * @return array
183
	 **/
184
	public static function get_active_containers() {
185
		return self::$active_containers;
186
	}
187
188
	/**
189
	 * Adds a container to the active containers array and triggers an action
190
	 **/
191
	public static function activate_container( $container ) {
192
		self::$active_containers[] = $container;
193
194
		$container->boot();
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 activate_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::activate_field( $inner_field );
219
			}
220
		}
221
222
		$field->boot();
223
224
		do_action( 'crb_field_activated', $field );
225
	}
226
227
	/**
228
	 * Perform instance initialization after calling setup()
229
	 **/
230
	abstract public function init();
231
232
	/**
233
	 * Prints the container Underscore template
234
	 **/
235
	public function template() {
236
		?>
237
		<div class="{{{ classes.join(' ') }}}">
238
			<# _.each(fields, function(field) { #>
239
				<div class="{{{ field.classes.join(' ') }}}">
240
					<label for="{{{ field.id }}}">
241
						{{ field.label }}
242
243
						<# if (field.required) { #>
244
							 <span class="carbon-required">*</span>
245
						<# } #>
246
					</label>
247
248
					<div class="field-holder {{{ field.id }}}"></div>
249
250
					<# if (field.help_text) { #>
251
						<em class="help-text">
252
							{{{ field.help_text }}}
253
						</em>
254
					<# } #>
255
256
					<em class="carbon-error"></em>
257
				</div>
258
			<# }); #>
259
		</div>
260
		<?php
261
	}
262
263
	/**
264
	 * Create a new container
265
	 *
266
	 * @param string $title Unique title of the container
267
	 **/
268 9
	public function __construct( $title ) {
269 9
		if ( empty( $title ) ) {
270 1
			Incorrect_Syntax_Exception::raise( 'Empty container title is not supported' );
271
		}
272
273 8
		$this->title = $title;
274 8
		$this->id = preg_replace( '~\W~u', '', remove_accents( $title ) );
275 8
		$this->id = self::get_unique_panel_id( $this->id );
276
277 8
		self::$registered_panel_ids[] = $this->id;
278 8
	}
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 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...
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 get_unique_panel_id( $id ) {
621
		$base = $id;
622
		$suffix = 0;
623
624
		while ( in_array( $id, self::$registered_panel_ids ) ) {
625
			$suffix++;
626
			$id = $base . strval( $suffix );
627
		}
628
629
		return $id;
630
	}
631
632
633
	/**
634
	 * Remove container identificator $id from the list of unique container ids
635
	 *
636
	 * @param string $id
637
	 **/
638
	public static function drop_unique_panel_id( $id ) {
639
		if ( in_array( $id, self::$registered_panel_ids ) ) {
640
			unset( self::$registered_panel_ids[ array_search( $id, self::$registered_panel_ids ) ] );
641
		}
642
	}
643
644
	/**
645
	 * Perform checks whether there is a field registered with the name $name.
646
	 * If not, the field name is recorded.
647
	 *
648
	 * @param string $name
649
	 **/
650 View Code Duplication
	public function verify_unique_field_name( $name ) {
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...
651
		if ( in_array( $name, $this->registered_field_names ) ) {
652
			Incorrect_Syntax_Exception::raise( 'Field name "' . $name . '" already registered' );
653
		}
654
655
		$this->registered_field_names[] = $name;
656
	}
657
658
	/**
659
	 * Remove field name $name from the list of unique field names
660
	 *
661
	 * @param string $name
662
	 **/
663
	public function drop_unique_field_name( $name ) {
664
		$index = array_search( $name, $this->registered_field_names );
665
666
		if ( $index !== false ) {
667
			unset( $this->registered_field_names[ $index ] );
668
		}
669
	}
670
671
	/**
672
	 * Assign DataStore instance for use by the container fields
673
	 *
674
	 * @param object $store
675
	 * @return object $this
676
	 **/
677
	public function set_datastore( $store ) {
678
		$this->store = $store;
679
680
		foreach ( $this->fields as $field ) {
681
			$field->set_datastore( $this->store );
682
		}
683
684
		return $this;
685
	}
686
687
	/**
688
	 * Return the DataStore instance used by container fields
689
	 *
690
	 * @return object $store
691
	 **/
692
	public function get_datastore() {
693
		return $this->store;
694
	}
695
696
	/**
697
	 * Return WordPress nonce name used to identify the current container instance
698
	 *
699
	 * @return string
700
	 **/
701
	public function get_nonce_name() {
702
		return 'carbon_panel_' . $this->id . '_nonce';
703
	}
704
705
	/**
706
	 * Return WordPress nonce field
707
	 *
708
	 * @return string
709
	 **/
710
	public function get_nonce_field() {
711
		return wp_nonce_field( $this->get_nonce_name(), $this->get_nonce_name(), /*referer?*/ false, /*echo?*/ false );
712
	}
713
714
	/**
715
	 * Returns an array that holds the container data, suitable for JSON representation.
716
	 * This data will be available in the Underscore template and the Backbone Model.
717
	 *
718
	 * @param bool $load  Should the value be loaded from the database or use the value from the current instance.
719
	 * @return array
720
	 */
721
	public function to_json( $load ) {
722
		$container_data = array(
723
			'id' => $this->id,
724
			'type' => $this->type,
725
			'title' => $this->title,
726
			'settings' => $this->settings,
727
			'fields' => array(),
728
		);
729
730
		$fields = $this->get_fields();
731
		foreach ( $fields as $field ) {
732
			$field_data = $field->to_json( $load );
733
			$container_data['fields'][] = $field_data;
734
		}
735
736
		return $container_data;
737
	}
738
739
	/**
740
	 * Underscore template for tabs
741
	 */
742
	public function template_tabs() {
743
		?>
744
		<div class="carbon-tabs">
745
			<ul class="carbon-tabs-nav">
746
				<# _.each(tabs, function (tab, tabName) { #>
747
					<li><a href="#" data-id="{{{ tab.id }}}">{{{ tabName }}}</a></li>
748
				<# }); #>
749
			</ul>
750
751
			<div class="carbon-tabs-body">
752
				<# _.each(tabs, function (tab) { #>
753
					<div class="carbon-fields-collection carbon-tab">
754
						{{{ tab.html }}}
755
					</div>
756
				<# }); #>
757
			</div>
758
		</div>
759
		<?php
760
	}
761
762
	/**
763
	 * Enqueue admin scripts
764
	 */
765
	public static function admin_hook_scripts() {
766
		wp_enqueue_script( 'carbon-containers', \Carbon_Fields\URL . '/assets/js/containers.js', array( 'carbon-app' ) );
767
768
		wp_localize_script( 'carbon-containers', 'carbon_containers_l10n',
769
			array(
770
				'please_fill_the_required_fields' => __( 'Please fill out all required fields highlighted below.', 'carbon-fields' ),
771
				'changes_made_save_alert' => __( 'The changes you made will be lost if you navigate away from this page.', 'carbon-fields' ),
772
			)
773
		);
774
	}
775
776
	/**
777
	 * Enqueue admin styles
778
	 */
779
	public static function admin_hook_styles() {
780
		wp_enqueue_style( 'carbon-main', \Carbon_Fields\URL . '/assets/bundle.css' );
781
	}
782
} // END Container
783
784