Completed
Push — milestone/2_0/react-ui ( c9a76c...950db8 )
by
unknown
02:58
created

Complex_Field::set_hierarchy()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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