Completed
Pull Request — master (#77)
by
unknown
02:25
created

Container::detach()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 0
dl 0
loc 8
ccs 0
cts 0
cp 0
crap 6
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 verify_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();
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 9
	public static function factory( $type, $name ) {
135
		// backward compatibility: post_meta container used to be called custom_fields
136 9
		if ( $type === 'custom_fields' ) {
137 1
			$type = 'post_meta';
138
		}
139
140
		$type = str_replace( ' ', '_', ucwords( str_replace( '_', ' ', $type ) ) );
141
142 9
		$class = __NAMESPACE__ . '\\' . $type . '_Container';
143
144
		if ( ! class_exists( $class ) ) {
145 1
			Incorrect_Syntax_Exception::raise( 'Unknown container "' . $type . '".' );
146 1
			$class = __NAMESPACE__ . '\\Broken_Container';
147
		}
148
149 1
		$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 3
	public static function make( $type, $name ) {
163 3
		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 8
	public function __construct( $title ) {
269 7
		if ( empty( $title ) ) {
270 1
			Incorrect_Syntax_Exception::raise( 'Empty container title is not supported' );
271
		}
272
273 6
		$this->title = $title;
274
		$this->id = preg_replace( '~\W~u', '', remove_accents( $title ) );
275
276
		self::verify_unique_panel_id( $this->id );
277 8
	}
278
279
	/**
280
	 * Boot the container once it's attached.
281
	 **/
282
	public function boot() {
283
		$this->add_template( $this->type, array( $this, 'template' ) );
284
285
		add_action( 'admin_footer', array( get_class(), 'admin_hook_scripts' ), 5 );
286
		add_action( 'admin_footer', array( get_class(), 'admin_hook_styles' ), 5 );
287
	}
288
289
	/**
290
	 * Update container settings and begin initialization
291
	 *
292
	 * @see init()
293
	 * @param array $settings
294
	 * @return object $this
295
	 **/
296
	public function setup( $settings = array() ) {
297
		if ( $this->setup_ready ) {
298
			Incorrect_Syntax_Exception::raise( 'Panel "' . $this->title . '" already setup' );
299
		}
300
301
		$this->check_setup_settings( $settings );
302
303
		$this->settings = array_merge( $this->settings, $settings );
304
305
		foreach ( $this->settings as $key => $value ) {
306
			if ( is_null( $value ) ) {
307
				unset( $this->settings[ $key ] );
308
			}
309
		}
310
311
		$this->setup_ready = true;
312
313
		return $this;
314
	}
315
316
	/**
317
	 * Check if all required container settings have been specified
318
	 *
319
	 * @param array $settings Container settings
320
	 **/
321
	public function check_setup_settings( &$settings = array() ) {
322
		$invalid_settings = array_diff_key( $settings, $this->settings );
323
		if ( ! empty( $invalid_settings ) ) {
324
			Incorrect_Syntax_Exception::raise( 'Invalid settings supplied to setup(): "' . implode( '", "', array_keys( $invalid_settings ) ) . '"' );
325
		}
326
	}
327
328
	/**
329
	 * Called first as part of the container save procedure.
330
	 * Responsible for checking the request validity and
331
	 * calling the container-specific save() method
332
	 *
333
	 * @see save()
334
	 * @see is_valid_save()
335
	 **/
336
	public function _save() {
337
		$param = func_get_args();
338
		if ( call_user_func_array( array( $this, 'is_valid_save' ), $param ) ) {
339
			call_user_func_array( array( $this, 'save' ), $param );
340
		}
341
	}
342
343
	/**
344
	 * Load submitted data and save each field in the container
345
	 *
346
	 * @see is_valid_save()
347
	 **/
348
	public function save( $data ) {
349
		foreach ( $this->fields as $field ) {
350
			$field->set_value_from_input();
351
			$field->save();
352
		}
353
	}
354
355
	/**
356
	 * Checks whether the current request is valid
357
	 *
358
	 * @return bool
359
	 **/
360
	public function is_valid_save() {
361
		return false;
362
	}
363
364
	/**
365
	 * Load the value for each field in the container.
366
	 * Could be used internally during container rendering
367
	 **/
368
	public function load() {
369
		foreach ( $this->fields as $field ) {
370
			$field->load();
371
		}
372
	}
373
374
375
	/**
376
	 * Called first as part of the container attachment procedure.
377
	 * Responsible for checking it's OK to attach the container
378
	 * and if it is, calling the container-specific attach() method
379
	 *
380
	 * @see attach()
381
	 * @see is_valid_attach()
382
	 **/
383
	public function _attach() {
384
		$param = func_get_args();
385
		if ( call_user_func_array( array( $this, 'is_valid_attach' ), $param ) ) {
386
			call_user_func_array( array( $this, 'attach' ), $param );
387
388
			if ( call_user_func_array( array( $this, 'is_active' ), $param ) ) {
389
				self::activate_container( $this );
390
391
				$fields = $this->get_fields();
392
				foreach ( $fields as $field ) {
393
					self::activate_field( $field );
394
				}
395
			}
396
		}
397
	}
398
399
	/**
400
	 * Returns all the Backbone templates
401
	 *
402
	 * @return array
403
	 **/
404
	public function get_templates() {
405
		return $this->templates;
406
	}
407
408
	/**
409
	 * Adds a new Backbone template
410
	 **/
411
	public function add_template( $name, $callback ) {
412
		$this->templates[ $name ] = $callback;
413
	}
414
415
	/**
416
	 * Attach the container rendering and helping methods
417
	 * to concrete WordPress Action hooks
418
	 **/
419
	public function attach() {}
420
421
	/**
422
	 * Perform checks whether the container is active for current request
423
	 *
424
	 * @return bool True if the container is active
425
	 **/
426
	public function is_active() {
427
		return $this->is_valid_attach();
428
	}
429
430
	/**
431
	 * Perform checks whether the container should be attached during the current request
432
	 *
433
	 * @return bool True if the container is allowed to be attached
434
	 **/
435
	public function is_valid_attach() {
436
		return true;
437
	}
438
439
	/**
440
	 * Revert the result of attach()
441
	 **/
442
	public function detach() {
443
		self::drop_unique_panel_id( $this->id );
444
445
		// unregister field names
446
		foreach ( $this->fields as $field ) {
447
			$this->drop_unique_field_name( $field->get_name() );
448
		}
449
	}
450
451
	/**
452
	 * Append array of fields to the current fields set. All items of the array
453
	 * must be instances of Field and their names should be unique for all
454
	 * Carbon containers.
455
	 * If a field does not have DataStore already, the container data store is
456
	 * assigned to them instead.
457
	 *
458
	 * @param array $fields
459
	 * @return object $this
460
	 **/
461
	public function add_fields( $fields ) {
462
		foreach ( $fields as $field ) {
463
			if ( ! is_a( $field, 'Carbon_Fields\\Field\\Field' ) ) {
464
				Incorrect_Syntax_Exception::raise( 'Object must be of type Carbon_Fields\\Field\\Field' );
465
			}
466
467
			$this->verify_unique_field_name( $field->get_name() );
468
469
			$field->set_context( $this->type );
470
			if ( ! $field->get_datastore() ) {
471
				$field->set_datastore( $this->store );
472
			}
473
		}
474
475
		$this->fields = array_merge( $this->fields, $fields );
476
477
		return $this;
478
	}
479
480
	/**
481
	 * Configuration function for adding tab with fields
482
	 *
483
	 * @param string $tab_name
484
	 * @param array $fields
485
	 * @return object $this
486
	 */
487
	public function add_tab( $tab_name, $fields ) {
488
		$this->add_template( 'tabs', array( $this, 'template_tabs' ) );
489
490
		$this->add_fields( $fields );
491
		$this->create_tab( $tab_name, $fields );
492
493
		return $this;
494
	}
495
496
	/**
497
	 * Internal function that creates the tab and associates it with particular field set
498
	 *
499
	 * @param string $tab_name
500
	 * @param array $fields
501
	 * @param int $queue_end
502
	 * @return object $this
503
	 */
504
	private function create_tab( $tab_name, $fields, $queue_end = self::TABS_TAIL ) {
505
		if ( isset( $this->tabs[ $tab_name ] ) ) {
506
			Incorrect_Syntax_Exception::raise( "Tab name duplication for $tab_name" );
507
		}
508
509
		if ( $queue_end === self::TABS_TAIL ) {
510
			$this->tabs[ $tab_name ] = array();
511
		} else if ( $queue_end === self::TABS_HEAD ) {
512
			$this->tabs = array_merge(
513
				array( $tab_name => array() ),
514
				$this->tabs
515
			);
516
		}
517
518
		foreach ( $fields as $field ) {
519
			$field_name = $field->get_name();
520
			$this->tabs[ $tab_name ][ $field_name ] = $field;
521
		}
522
523
		$this->settings['tabs'] = $this->get_tabs_json();
524
	}
525
526
	/**
527
	 * Whether the container is tabbed or not
528
	 *
529
	 * @return bool
530
	 */
531
	public function is_tabbed() {
532
		return (bool) $this->tabs;
533
	}
534
535
	/**
536
	 * Retrieve all fields that are not defined under a specific tab
537
	 *
538
	 * @return array
539
	 */
540
	public function get_untabbed_fields() {
541
		$tabbed_fields_names = array();
542
		foreach ( $this->tabs as $tab_fields ) {
543
			$tabbed_fields_names = array_merge( $tabbed_fields_names, array_keys( $tab_fields ) );
544
		}
545
546
		$all_fields_names = array();
547
		foreach ( $this->fields as $field ) {
548
			$all_fields_names[] = $field->get_name();
549
		}
550
551
		$fields_not_in_tabs = array_diff( $all_fields_names, $tabbed_fields_names );
552
553
		$untabbed_fields = array();
554
		foreach ( $this->fields as $field ) {
555
			if ( in_array( $field->get_name(), $fields_not_in_tabs ) ) {
556
				$untabbed_fields[] = $field;
557
			}
558
		}
559
560
		return $untabbed_fields;
561
	}
562
563
	/**
564
	 * Retrieve all tabs.
565
	 * Create a default tab if there are any untabbed fields.
566
	 *
567
	 * @return array
568
	 */
569
	public function get_tabs() {
570
		$untabbed_fields = $this->get_untabbed_fields();
571
572
		if ( ! empty( $untabbed_fields ) ) {
573
			$this->create_tab( __( 'General', 'carbon-fields' ), $untabbed_fields, self::TABS_HEAD );
574
		}
575
576
		return $this->tabs;
577
	}
578
579
	/**
580
	 * Build the tabs JSON
581
	 *
582
	 * @return array
583
	 */
584
	public function get_tabs_json() {
585
		$tabs_json = array();
586
		$tabs = $this->get_tabs();
587
588
		foreach ( $tabs as $tab_name => $fields ) {
589
			foreach ( $fields as $field_name => $field ) {
590
				$tabs_json[ $tab_name ][] = $field_name;
591
			}
592
		}
593
594
		return $tabs_json;
595
	}
596
597
	/**
598
	 * Returns the private container array of fields.
599
	 * Use only if you are completely aware of what you are doing.
600
	 *
601
	 * @return array
602
	 **/
603
	public function get_fields() {
604
		return $this->fields;
605
	}
606
607
	/**
608
	 * Perform a check whether the current container has fields
609
	 *
610
	 * @return bool
611
	 **/
612
	public function has_fields() {
613
		return (bool) $this->fields;
614
	}
615
616
	/**
617
	 * Perform checks whether there is a container registered with identificator $id
618
	 */
619
	public static function verify_unique_panel_id( $id ) {
620
		if ( in_array( $id, self::$registered_panel_ids ) ) {
621
			Incorrect_Syntax_Exception::raise( 'Panel ID "' . $id . '" already registered' );
622
		}
623
624
		self::$registered_panel_ids[] = $id;
625
	}
626
627
628
	/**
629
	 * Remove container identificator $id from the list of unique container ids
630
	 *
631
	 * @param string $id
632
	 **/
633
	public static function drop_unique_panel_id( $id ) {
634
		if ( in_array( $id, self::$registered_panel_ids ) ) {
635
			unset( self::$registered_panel_ids[ array_search( $id, self::$registered_panel_ids ) ] );
636
		}
637
	}
638
639
	/**
640
	 * Perform checks whether there is a field registered with the name $name.
641
	 * If not, the field name is recorded.
642
	 *
643
	 * @param string $name
644
	 **/
645 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...
646
		if ( in_array( $name, $this->registered_field_names ) ) {
647
			Incorrect_Syntax_Exception::raise( 'Field name "' . $name . '" already registered' );
648
		}
649
650
		$this->registered_field_names[] = $name;
651
	}
652
653
	/**
654
	 * Remove field name $name from the list of unique field names
655
	 *
656
	 * @param string $name
657
	 **/
658
	public function drop_unique_field_name( $name ) {
659
		$index = array_search( $name, $this->registered_field_names );
660
661
		if ( $index !== false ) {
662
			unset( $this->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/bundle.css' );
776
	}
777
} // END Container
778
779