Completed
Push — master ( a3db22...aec616 )
by Marin
53:38 queued 51:58
created

core/Container/Container.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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