Completed
Push — milestone/2_0/react-ui ( 42c3e4...a147b7 )
by
unknown
03:18
created

Complex_Field::update_child_hierarchy()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 0
dl 0
loc 7
ccs 0
cts 7
cp 0
crap 6
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace Carbon_Fields\Field;
4
5
use Carbon_Fields\Datastore\Datastore_Interface;
6
use Carbon_Fields\Helper\Helper;
7
use Carbon_Fields\Field\Field;
8
use Carbon_Fields\Field\Group_Field;
9
use Carbon_Fields\Value_Set\Value_Set;
10
use Carbon_Fields\Exception\Incorrect_Syntax_Exception;
11
12
/**
13
 * Complex field class.
14
 * Allows nested repeaters with multiple field groups to be created.
15
 */
16
class Complex_Field extends Field {
17
18
	/**
19
	 * Visual layout type constants
20
	 */
21
	const LAYOUT_GRID = 'grid'; // default
22
23
	const LAYOUT_TABBED_HORIZONTAL = 'tabbed-horizontal';
24
25
	const LAYOUT_TABBED_VERTICAL = 'tabbed-vertical';
26
27
	const TYPE_PROPERTY = '_type';
28
29
	/**
30
	 * Default field value
31
	 *
32
	 * @var array
33
	 */
34
	protected $default_value = array();
35
36
	/**
37
	 * Complex field layout
38
	 *
39
	 * @var string static::LAYOUT_* constant
40
	 */
41
	protected $layout = self::LAYOUT_GRID;
42
43
	/**
44
	 * Value tree describing the complex values and all groups with their child fields
45
	 *
46
	 * @var array
47
	 */
48
	protected $value_tree = array();
49
50
	/**
51
	 * Array of groups registered for this complex field
52
	 *
53
	 * @var array
54
	 */
55
	protected $groups = array();
56
57
	/**
58
	 * Minimum number of entries. -1 for no limit
59
	 *
60
	 * @var integer
61
	 */
62
	protected $values_min = -1;
63
64
	/**
65
	 * Maximum number of entries. -1 for no limit
66
	 *
67
	 * @var integer
68
	 */
69
	protected $values_max = -1;
70
71
	/**
72
	 * Default entry state - collapsed or not
73
	 *
74
	 * @var boolean
75
	 */
76
	protected $collapsed = false;
77
78
	/**
79
	 * Defines whether duplicate groups are allowed or not
80
	 *
81
	 * @var boolean
82
	 */
83
	protected $duplicate_groups_allowed = true;
1 ignored issue
show
Comprehensibility Naming introduced by
The variable name $duplicate_groups_allowed 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...
84
85
	/**
86
	 * Entry labels
87
	 * These are translated in init()
88
	 *
89
	 * @var array
90
	 */
91
	public $labels = array(
92
		'singular_name' => 'Entry',
93
		'plural_name' => 'Entries',
94
	);
95
96
	/**
97
	 * Create a field from a certain type with the specified label.
98
	 *
99
	 * @param string $type  Field type
100
	 * @param string $name  Field name
101
	 * @param string $label Field label
102
	 */
103
	public function __construct( $type, $name, $label ) {
104
		$this->set_value_set( new Value_Set( Value_Set::TYPE_MULTIPLE_VALUES ) );
105
		parent::__construct( $type, $name, $label );
106
	}
107
108
	/**
109
	 * Initialization tasks.
110
	 */
111
	public function init() {
112
		$this->labels = array(
113
			'singular_name' => __( $this->labels['singular_name'], 'carbon-fields' ),
114
			'plural_name' => __( $this->labels['plural_name'], 'carbon-fields' ),
115
		);
116
		parent::init();
117
	}
118
119
	/**
120
	 * Set array of hierarchy field names
121
	 */
122
	public function set_hierarchy( $hierarchy ) {
123
		parent::set_hierarchy( $hierarchy );
124
		$this->update_child_hierarchy();
125
	}
126
127
	/**
128
	 * Propagate hierarchy to child fields
129
	 */
130
	public function update_child_hierarchy() {
131
		$hierarchy = array_merge( $this->get_hierarchy(), array( $this->get_base_name() ) );
132
		$fields = $this->get_fields();
133
		foreach ( $fields as $field ) {
134
			$field->set_hierarchy( $hierarchy );
135
		}
136
	}
137
138
	/**
139
	 * Activate the field once the container is attached.
140
	 */
141
	public function activate() {
142
		parent::activate();
143
		$fields = $this->get_fields();
144
		foreach ( $fields as $field ) {
145
			$field->activate();
146
		}
147
	}
148
149
	/**
150
	 * Set the datastore of this field and propagate it to children
151
	 *
152
	 * @param  Datastore_Interface $datastore
153
	 * @param  boolean             $set_as_default
154
	 * @return object              $this
155
	 */
156 View Code Duplication
	public function set_datastore( Datastore_Interface $datastore, $set_as_default = false ) {
157
		if ( $set_as_default && ! $this->has_default_datastore() ) {
158
			return $this; // datastore has been overriden with a custom one - abort changing to a default one
159
		}
160
		$this->datastore = $datastore;
161
		$this->has_default_datastore = $set_as_default;
162
163
		$this->update_child_datastore( $this->get_datastore(), true );
164
		return $this;
165
	}
166
167
	/**
168
	 * Propagate the datastore down the hierarchy
169
	 *
170
	 * @param Datastore_Interface $datastore
171
	 * @param boolean             $set_as_default
172
	 */
173
	protected function update_child_datastore( Datastore_Interface $datastore, $set_as_default = false ) {
174
		foreach ( $this->groups as $group ) {
175
			$group->set_datastore( $datastore, $set_as_default );
176
		}
177
	}
178
179
	/**
180
	 * Retrieve all groups of fields.
181
	 *
182
	 * @return array $fields
183
	 */
184
	public function get_fields() {
185
		$fields = array();
186
187
		foreach ( $this->groups as $group ) {
188
			$group_fields = $group->get_fields();
189
190
			$fields = array_merge( $fields, $group_fields );
191
		}
192
193
		return $fields;
194
	}
195
196
	/**
197
	 * Add a set/group of fields.
198
	 *
199
	 * Accepted param variations:
200
	 *   - array<Field> $fields
201
	 *   - string $group_name, array<Field> $fields
202
	 *   - string $group_name, string $group_label, array<Field> $fields
203
	 *
204
	 * @return $this
205
	 */
206
	public function add_fields() {
207
		$argv = func_get_args();
208
		$argc = count( $argv );
209
		$fields = $argv[ $argc - 1 ];
210
		$name = '';
211
		$label = null;
212
213
		if ( $argc >= 2 ) {
214
			$name = $argv[0];
215
		}
216
217
		if ( $argc >= 3 ) {
218
			$label = $argv[1];
219
		}
220
221
		$name = ! empty( $name ) ? $name : Group_Field::DEFAULT_GROUP_NAME;
222
223
		if ( array_key_exists( $name, $this->groups ) ) {
224
			Incorrect_Syntax_Exception::raise( 'Group with name "' . $name . '" in Complex Field "' . $this->get_label() . '" already exists.' );
225
			return $this;
226
		}
227
228
		$reserved_names = array( Value_Set::VALUE_PROPERTY, static::TYPE_PROPERTY );
229
		foreach ( $fields as $field ) {
230
			if ( in_array( $field->get_base_name(), $reserved_names ) ) {
231
				Incorrect_Syntax_Exception::raise( '"' . $field->get_base_name() . '" is a reserved keyword for Complex fields and cannot be used for a field name.' );
232
				return $this;
233
			}
234
		}
235
236
		$group = new Group_Field( $name, $label, $fields );
237
		$this->groups[ $group->get_name() ] = $group;
238
239
		$this->update_child_hierarchy();
240
		if ( $this->get_datastore() !== null ) {
241
			$this->update_child_datastore( $this->get_datastore(), true );
242
		}
243
244
		return $this;
245
	}
246
247
	/**
248
	 * Retrieve the groups of this field.
249
	 *
250
	 * @return array
251
	 */
252
	public function get_group_names() {
253
		return array_keys( $this->groups );
254
	}
255
256
	/**
257
	 * Retrieve a group by its name.
258
	 *
259
	 * @param  string $group_name        Group name
260
	 * @return Group_Field $group_object Group object
261
	 */
262
	public function get_group_by_name( $group_name ) {
263
		$group_object = null;
264
265
		foreach ( $this->groups as $group ) {
266
			if ( $group->get_name() == $group_name ) {
267
				$group_object = $group;
268
			}
269
		}
270
271
		return $group_object;
272
	}
273
274
	/**
275
	 * Set the group label Underscore template.
276
	 *
277
	 * @param  string|callable $template
278
	 * @return $this
279
	 */
280
	public function set_header_template( $template ) {
281
		$template = is_callable( $template ) ? call_user_func( $template ) : $template;
282
283
		// Assign the template to the group that was added last
284
		$values = array_values( $this->groups );
285
		$group = end( $values );
286
287
		if ( $group ) {
288
			$group->set_label_template( $template );
289
290
			$this->groups[ $group->get_name() ] = $group;
291
		}
292
293
		return $this;
294
	}
295
296
	/**
297
	 * Set the field labels.
298
	 * Currently supported values:
299
	 *  - singular_name - the singular entry label
300
	 *  - plural_name - the plural entries label
301
	 *
302
	 * @param  array $labels Labels
303
	 */
304
	public function setup_labels( $labels ) {
305
		$this->labels = array_merge( $this->labels, $labels );
306
		return $this;
307
	}
308
309
	/**
310
	 * Return a clone of a field with hierarchy settings applied
311
	 *
312
	 * @param Field $field
313
	 * @param Field $parent_field
314
	 * @param int $group_index
315
	 * @return Field
316
	 */
317
	public function get_clone_under_field_in_hierarchy( $field, $parent_field, $group_index = 0 ) {
318
		$clone = clone $field;
319
		$clone->set_hierarchy( array_merge( $parent_field->get_hierarchy(), array( $parent_field->get_base_name() ) ) );
320
		$clone->set_hierarchy_index( array_merge( $parent_field->get_hierarchy_index(), array( $group_index ) ) );
321
		return $clone;
322
	}
323
324
	protected function get_prefilled_group_fields( $group_fields, $group_values, $group_index ) {
325
		$fields = array();
326
327
		foreach ( $group_fields as $field ) {
328
			$clone = $this->get_clone_under_field_in_hierarchy( $field, $this, $group_index );
329
			if ( isset( $group_values[ $clone->get_base_name() ] ) ) {
330
				$clone->set_value( $group_values[ $clone->get_base_name() ] );
331
			}
332
			$fields[] = $clone;
333
		}
334
335
		return $fields;
336
	}
337
338
	protected function get_prefilled_groups( $value_tree ) {
339
		$fields = array();
340
341
		foreach ( $value_tree as $group_index => $value ) {
342
			$group_name = $value[ Value_Set::VALUE_PROPERTY ];
343
			$group = $this->get_group_by_name( $group_name );
344
			if ( ! $group ) {
345
				// Failed to find group - sombody has been messing with the database or group definitions
346
				continue;
347
			}
348
			$group_fields = $group->get_fields();
349
			$group_values = array();
350
			if ( isset( $value_tree[ $group_index ] ) ) {
351
				$group_values = $value_tree[ $group_index ];
352
			}
353
			$fields[ $group_index ] = array( Value_Set::VALUE_PROPERTY => $group->get_name() ) + $this->get_prefilled_group_fields( $group_fields, $group_values, $group_index );
354
		}
355
356
		return $fields;
357
	}
358
359
	/**
360
	 * Load the field value from an input array based on it's name.
361
	 *
362
	 * @param array $input Array of field names and values.
363
	 */
364
	public function set_value_from_input( $input ) {
365
		if ( ! isset( $input[ $this->get_name() ] ) ) {
366
			return;
367
		}
368
369
		$value_tree = array();
370
		$input_groups = $input[ $this->get_name() ];
371
		$input_group_index = 0;
372
		foreach ( $input_groups as $values ) {
373
			if ( ! isset( $values[ Value_Set::VALUE_PROPERTY ] ) || ! isset( $this->groups[ $values[ Value_Set::VALUE_PROPERTY ] ] ) ) {
374
				continue;
375
			}
376
377
			$group = $this->get_group_by_name( $values[ Value_Set::VALUE_PROPERTY ] );
378
			$group_fields = $group->get_fields();
379
			$group_field_names = array_flip( $group->get_field_names() );
380
381
			$value_group = array( Value_Set::VALUE_PROPERTY => $values[ Value_Set::VALUE_PROPERTY ] );
382
			unset( $values[ Value_Set::VALUE_PROPERTY ] );
383
384
			// trim input values to those used by the field
385
			$values = array_intersect_key( $values, $group_field_names );
386
387
			foreach ( $group_fields as $field ) {
388
				$tmp_field = $this->get_clone_under_field_in_hierarchy( $field, $this, $input_group_index );
389
390
				$tmp_field->set_value_from_input( $values );
391
				if ( is_a( $tmp_field, get_class() ) ) {
392
					$value_group[ $tmp_field->get_base_name() ] = $tmp_field->get_value_tree();
393
				} else {
394
					$value_group[ $tmp_field->get_base_name() ] = $tmp_field->get_full_value();
395
				}
396
			}
397
			$value_tree[] = $value_group;
398
			$input_group_index++;
399
		}
400
401
		$this->set_value( $value_tree );
402
	}
403
404
	/**
405
	 * Save all contained groups of fields.
406
	 */
407
	public function save() {
408
		// Only delete root field values as nested field values should be deleted in a cascading manner by the datastore
409
		$hierarchy = $this->get_hierarchy();
410
		$delete_on_save = empty( $hierarchy );
411
		$delete_on_save = apply_filters( 'carbon_fields_should_delete_field_value_on_save', $delete_on_save, $this );
412
		if ( $delete_on_save ) {
413
			$this->delete();
414
		}
415
416
		$save = apply_filters( 'carbon_fields_should_save_field_value', true, $this->get_value(), $this );
417
		if ( $save ) {
418
			$this->get_datastore()->save( $this );
419
			$field_groups = $this->get_prefilled_groups( $this->get_value_tree() );
420
			foreach ( $field_groups as $group_index => $fields ) {
421
				foreach ( $fields as $field ) {
422
					if ( ! is_a( $field, __NAMESPACE__ . '\\Field' ) ) {
423
						continue;
424
					}
425
					$field->save();
426
				}
427
			}
428
		}
429
	}
430
431
	/**
432
	 * {@inheritDoc}
433
	 */
434
	public function get_formatted_value() {
435
		$field_groups = $this->get_prefilled_groups( $this->get_value_tree() );
436
437
		$value = array();
438
		foreach ( $field_groups as $group_index => $field_group ) {
439
			$value[ $group_index ] = array();
440
			foreach ( $field_group as $key => $field ) {
441
				if ( is_a( $field, __NAMESPACE__ . '\\Field' ) ) {
442
					$value[ $group_index ][ $field->get_base_name() ] = $field->get_formatted_value();
443
				} else {
444
					if ( $key === Value_Set::VALUE_PROPERTY ) {
445
						$value[ $group_index ][ static::TYPE_PROPERTY ] = $field;
446
					} else {
447
						$value[ $group_index ][ $key ] = $field;
448
					}
449
				}
450
			}
451
		}
452
		return $value;
453
	}
454
455
	/**
456
	 * Convert an externally-keyed value array ('_type' => ...)
457
	 * to an internally-keyed one ('value' => ...)
458
	 * 
459
	 * @param  mixed $value
460
	 * @return mixed
461
	 */
462
	protected function external_to_internal_value( $value ) {
463
		if ( ! is_array( $value ) ) {
464
			return $value;
465
		}
466
		if ( ! isset( $value[ static::TYPE_PROPERTY ] ) ) {
467
			return $value;
468
		}
469
		$value = array_map( array( $this, 'external_to_internal_value' ), $value );
470
		$value[ Value_Set::VALUE_PROPERTY ] = $value[ static::TYPE_PROPERTY ];
471
		unset( $value[ static::TYPE_PROPERTY ] );
472
		return $value;
473
	}
474
475
	/**
476
	 * {@inheritDoc}
477
	 */
478
	public function set_value( $value ) {
479
		$value = array_map( array( $this, 'external_to_internal_value' ), $value );
480
		$groups = array();
481
		foreach ( $value as $values ) {
482
			$groups[] = isset( $values[ Value_Set::VALUE_PROPERTY ] ) ? $values[ Value_Set::VALUE_PROPERTY ] : Group_Field::DEFAULT_GROUP_NAME;
483
		}
484
		parent::set_value( $groups );
485
		$this->set_value_tree( $value );
486
	}
487
488
	/**
489
	 * {@inheritDoc}
490
	 */
491
	public function set_default_value( $default_value ) {
492
		foreach ( $default_value as $index => $group ) {
493
			if ( ! isset( $group[ static::TYPE_PROPERTY ] ) ) {
494
				$default_value[ $index ][ static::TYPE_PROPERTY ] = Group_Field::DEFAULT_GROUP_NAME;
495
			}
496
		}
497
		$this->default_value = $default_value;
498
		return $this;
499
	}
500
501
	/**
502
	 * Return the full value tree of all groups and their fields
503
	 *
504
	 * @return mixed
505
	 */
506
	public function get_value_tree() {
507
		return (array) $this->value_tree;
508
	}
509
510
	/**
511
	 * Set the full value tree of all groups and their fields
512
	 *
513
	 * @see  Internal Glossary in DEVELOPMENT.MD
514
	 */
515
	public function set_value_tree( $value_tree ) {
516
		$this->value_tree = $value_tree;
517
	}
518
519
	/**
520
	 * Returns an array that holds the field data, suitable for JSON representation.
521
	 *
522
	 * @param bool $load  Should the value be loaded from the database or use the value from the current instance.
523
	 * @return array
524
	 */
525
	public function to_json( $load ) {
526
		$complex_data = parent::to_json( $load );
527
528
		$groups_data = array();
529
		foreach ( $this->groups as $group ) {
530
			$group_data = $group->to_json( false );
531
			$group_data['collapsed'] = $this->get_collapsed();
532
533
			$groups_data[] = $group_data;
534
		}
535
536
		$field_groups = $this->get_prefilled_groups( $this->get_value_tree() );
537
		$value_data = array();
538
		foreach ( $field_groups as $group_index => $fields ) {
539
			$group = $this->get_group_by_name( $fields[ Value_Set::VALUE_PROPERTY ] );
540
541
			$data = array(
542
				'name' => $group->get_name(),
543
				'label' => $group->get_label(),
544
				'label_template' => $group->get_label_template(),
545
				'group_id' => $group->get_group_id(),
546
				'collapsed' => $this->get_collapsed(),
547
				'fields' => array(),
548
			);
549
550
			foreach ( $fields as $field ) {
551
				if ( ! is_a( $field, __NAMESPACE__ . '\\Field' ) ) {
552
					continue;
553
				}
554
				$data['fields'][] = $field->to_json( false );
555
			}
556
557
			$value_data[] = $data;
558
		}
559
560
		$group_types = array();
561
		foreach ( $this->groups as $group ) {
562
			$group_types[] = array(
563
				'name' => $group->get_name(),
564
				'label' => $group->get_label(),
565
			);
566
		}
567
568
		$complex_data = array_merge( $complex_data, array(
569
			'duplicate_groups_allowed' => $this->get_duplicate_groups_allowed(),
570
			'group_types' => $group_types,
571
			'layout' => $this->layout,
572
			'labels' => $this->labels,
573
			'min' => $this->get_min(),
574
			'max' => $this->get_max(),
575
			'multiple_groups' => count( $groups_data ) > 1,
576
			'groups' => $groups_data,
577
			'value' => $value_data,
578
			'collapsed' => $this->get_collapsed(),
579
		) );
580
		return $complex_data;
581
	}
582
583
	/**
584
	 * Modify the layout of this field.
585
	 *
586
	 * @param string $layout
587
	 */
588
	public function set_layout( $layout ) {
589
		$available_layouts = array(
590
			static::LAYOUT_GRID,
591
			static::LAYOUT_TABBED_HORIZONTAL,
592
			static::LAYOUT_TABBED_VERTICAL,
593
		);
594
595 View Code Duplication
		if ( ! in_array( $layout,  $available_layouts ) ) {
596
			$error_message = 'Incorrect layout ``' . $layout . '" specified. ' .
597
				'Available layouts: ' . implode( ', ', $available_layouts );
598
599
			Incorrect_Syntax_Exception::raise( $error_message );
600
			return $this;
601
		}
602
603
		$this->layout = $layout;
604
605
		return $this;
606
	}
607
608
	/**
609
	 * Get the minimum number of entries.
610
	 *
611
	 * @return int $min
612
	 */
613
	public function get_min() {
614
		return $this->values_min;
615
	}
616
617
	/**
618
	 * Set the minimum number of entries.
619
	 *
620
	 * @param int $min
621
	 */
622
	public function set_min( $min ) {
623
		$this->values_min = intval( $min );
624
		return $this;
625
	}
626
627
	/**
628
	 * Get the maximum number of entries.
629
	 *
630
	 * @return int $max
631
	 */
632
	public function get_max() {
633
		return $this->values_max;
634
	}
635
636
	/**
637
	 * Set the maximum number of entries.
638
	 *
639
	 * @param int $max
640
	 */
641
	public function set_max( $max ) {
642
		$this->values_max = intval( $max );
643
		return $this;
644
	}
645
646
	/**
647
	 * Get collapsed state
648
	 *
649
	 * @return bool
650
	 */
651
	public function get_collapsed() {
652
		return $this->collapsed;
653
	}
654
655
	/**
656
	 * Change the groups initial collapse state.
657
	 * This state relates to the state of which the groups are rendered.
658
	 *
659
	 * @param bool $collapsed
660
	 */
661
	public function set_collapsed( $collapsed = true ) {
662
		$this->collapsed = $collapsed;
663
		return $this;
664
	}
665
666
	/**
667
	 * Get whether duplicate groups are allowed.
668
	 *
669
	 * @return bool
670
	 */
671
	public function get_duplicate_groups_allowed() {
672
		return $this->duplicate_groups_allowed;
673
	}
674
675
	/**
676
	 * Set whether duplicate groups are allowed.
677
	 *
678
	 * @param bool $allowed
679
	 */
680
	public function set_duplicate_groups_allowed( $allowed ) {
681
		$this->duplicate_groups_allowed = $allowed;
682
		return $this;
683
	}
684
}
685