Completed
Pull Request — master (#10)
by Nicolas
02:35
created

Container::attach()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 1
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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