Completed
Push — milestone/2_0/react-ui ( 3dbb9a...65728f )
by
unknown
03:10
created

Complex_Field::set_value()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
cc 3
eloc 7
nc 3
nop 1
dl 0
loc 9
ccs 0
cts 9
cp 0
crap 12
rs 9.6666
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
		$reserved_names = array( Value_Set::VALUE_PROPERTY, static::TYPE_PROPERTY );
222
		foreach ( $fields as $field ) {
223
			if ( in_array( $field->get_base_name(), $reserved_names ) ) {
224
				Incorrect_Syntax_Exception::raise( '"' . $field->get_base_name() . '" is a reserved keyword for Complex fields and cannot be used for a field name.' );
225
				return $this;
226
			}
227
		}
228
229
		$group = new Group_Field( $name, $label, $fields );
230
		$this->groups[ $group->get_name() ] = $group;
231
232
		$this->update_child_hierarchy();
233
		if ( $this->get_datastore() !== null ) {
234
			$this->update_child_datastore( $this->get_datastore(), true );
235
		}
236
237
		return $this;
238
	}
239
240
	/**
241
	 * Retrieve the groups of this field.
242
	 *
243
	 * @return array
244
	 */
245
	public function get_group_names() {
246
		return array_keys( $this->groups );
247
	}
248
249
	/**
250
	 * Retrieve a group by its name.
251
	 *
252
	 * @param  string $group_name        Group name
253
	 * @return Group_Field $group_object Group object
254
	 */
255
	public function get_group_by_name( $group_name ) {
256
		$group_object = null;
257
258
		foreach ( $this->groups as $group ) {
259
			if ( $group->get_name() == $group_name ) {
260
				$group_object = $group;
261
			}
262
		}
263
264
		return $group_object;
265
	}
266
267
	/**
268
	 * Set the group label Underscore template.
269
	 *
270
	 * @param  string|callable $template
271
	 * @return $this
272
	 */
273
	public function set_header_template( $template ) {
274
		$template = is_callable( $template ) ? call_user_func( $template ) : $template;
275
276
		// Assign the template to the group that was added last
277
		$values = array_values( $this->groups );
278
		$group = end( $values );
279
280
		if ( $group ) {
281
			$group->set_label_template( $template );
282
283
			$this->groups[ $group->get_name() ] = $group;
284
		}
285
286
		return $this;
287
	}
288
289
	/**
290
	 * Set the field labels.
291
	 * Currently supported values:
292
	 *  - singular_name - the singular entry label
293
	 *  - plural_name - the plural entries label
294
	 *
295
	 * @param  array $labels Labels
296
	 */
297
	public function setup_labels( $labels ) {
298
		$this->labels = array_merge( $this->labels, $labels );
299
		return $this;
300
	}
301
302
	/**
303
	 * Return a clone of a field with hierarchy settings applied
304
	 *
305
	 * @param Field $field
306
	 * @param Field $parent_field
307
	 * @param int $group_index
308
	 * @return Field
309
	 */
310
	public function get_clone_under_field_in_hierarchy( $field, $parent_field, $group_index = 0 ) {
311
		$clone = clone $field;
312
		$clone->set_hierarchy( array_merge( $parent_field->get_hierarchy(), array( $parent_field->get_base_name() ) ) );
313
		$clone->set_hierarchy_index( array_merge( $parent_field->get_hierarchy_index(), array( $group_index ) ) );
314
		return $clone;
315
	}
316
317
	protected function get_prefilled_group_fields( $group_fields, $group_values, $group_index ) {
318
		$fields = array();
319
320
		foreach ( $group_fields as $field ) {
321
			$clone = $this->get_clone_under_field_in_hierarchy( $field, $this, $group_index );
322
			if ( isset( $group_values[ $clone->get_base_name() ] ) ) {
323
				$clone->set_value( $group_values[ $clone->get_base_name() ] );
324
			}
325
			$fields[] = $clone;
326
		}
327
328
		return $fields;
329
	}
330
331
	protected function get_prefilled_groups( $value_tree ) {
332
		$fields = array();
333
334
		foreach ( $value_tree as $group_index => $value ) {
335
			$group_name = $value[ Value_Set::VALUE_PROPERTY ];
336
			$group = $this->get_group_by_name( $group_name );
337
			if ( ! $group ) {
338
				// Failed to find group - sombody has been messing with the database or group definitions
339
				continue;
340
			}
341
			$group_fields = $group->get_fields();
342
			$group_values = array();
343
			if ( isset( $value_tree[ $group_index ] ) ) {
344
				$group_values = $value_tree[ $group_index ];
345
			}
346
			$fields[ $group_index ] = array( Value_Set::VALUE_PROPERTY => $group->get_name() ) + $this->get_prefilled_group_fields( $group_fields, $group_values, $group_index );
347
		}
348
349
		return $fields;
350
	}
351
352
	/**
353
	 * Load the field value from an input array based on it's name.
354
	 *
355
	 * @param array $input Array of field names and values.
356
	 */
357
	public function set_value_from_input( $input ) {
358
		if ( ! isset( $input[ $this->get_name() ] ) ) {
359
			return;
360
		}
361
362
		$value_tree = array();
363
		$input_groups = $input[ $this->get_name() ];
364
		$input_group_index = 0;
365
		foreach ( $input_groups as $values ) {
366
			if ( ! isset( $values[ Value_Set::VALUE_PROPERTY ] ) || ! isset( $this->groups[ $values[ Value_Set::VALUE_PROPERTY ] ] ) ) {
367
				continue;
368
			}
369
370
			$group = $this->get_group_by_name( $values[ Value_Set::VALUE_PROPERTY ] );
371
			$group_fields = $group->get_fields();
372
			$group_field_names = array_flip( $group->get_field_names() );
373
374
			$value_group = array( Value_Set::VALUE_PROPERTY => $values[ Value_Set::VALUE_PROPERTY ] );
375
			unset( $values[ Value_Set::VALUE_PROPERTY ] );
376
377
			// trim input values to those used by the field
378
			$values = array_intersect_key( $values, $group_field_names );
379
380
			foreach ( $group_fields as $field ) {
381
				$tmp_field = $this->get_clone_under_field_in_hierarchy( $field, $this, $input_group_index );
382
383
				$tmp_field->set_value_from_input( $values );
384
				if ( is_a( $tmp_field, get_class() ) ) {
385
					$value_group[ $tmp_field->get_base_name() ] = $tmp_field->get_value_tree();
386
				} else {
387
					$value_group[ $tmp_field->get_base_name() ] = $tmp_field->get_full_value();
388
				}
389
			}
390
			$value_tree[] = $value_group;
391
			$input_group_index++;
392
		}
393
394
		$this->set_value( $value_tree );
395
	}
396
397
	/**
398
	 * Save all contained groups of fields.
399
	 */
400
	public function save() {
401
		// Only delete root field values as nested field values should be deleted in a cascading manner by the datastore
402
		$hierarchy = $this->get_hierarchy();
403
		$delete_on_save = empty( $hierarchy );
404
		$delete_on_save = apply_filters( 'carbon_fields_should_delete_field_value_on_save', $delete_on_save, $this );
405
		if ( $delete_on_save ) {
406
			$this->delete();
407
		}
408
409
		$save = apply_filters( 'carbon_fields_should_save_field_value', true, $this->get_value(), $this );
410
		if ( $save ) {
411
			$this->get_datastore()->save( $this );
412
			$field_groups = $this->get_prefilled_groups( $this->get_value_tree() );
413
			foreach ( $field_groups as $group_index => $fields ) {
414
				foreach ( $fields as $field ) {
415
					if ( ! is_a( $field, __NAMESPACE__ . '\\Field' ) ) {
416
						continue;
417
					}
418
					$field->save();
419
				}
420
			}
421
		}
422
	}
423
424
	/**
425
	 * {@inheritDoc}
426
	 */
427
	public function get_formatted_value() {
428
		$field_groups = $this->get_prefilled_groups( $this->get_value_tree() );
429
430
		$value = array();
431
		foreach ( $field_groups as $group_index => $field_group ) {
432
			$value[ $group_index ] = array();
433
			foreach ( $field_group as $key => $field ) {
434
				if ( is_a( $field, __NAMESPACE__ . '\\Field' ) ) {
435
					$value[ $group_index ][ $field->get_base_name() ] = $field->get_formatted_value();
436
				} else {
437
					if ( $key === Value_Set::VALUE_PROPERTY ) {
438
						$value[ $group_index ][ static::TYPE_PROPERTY ] = $field;
439
					} else {
440
						$value[ $group_index ][ $key ] = $field;
441
					}
442
				}
443
			}
444
		}
445
		return $value;
446
	}
447
448
	/**
449
	 * Convert an externally-keyed value array ('_type' => ...)
450
	 * to an internally-keyed one ('value' => ...)
451
	 * 
452
	 * @param  mixed $value
453
	 * @return mixed
454
	 */
455
	protected function external_to_internal_value( $value ) {
456
		if ( ! is_array( $value ) ) {
457
			return $value;
458
		}
459
		if ( ! isset( $value[ static::TYPE_PROPERTY ] ) ) {
460
			return $value;
461
		}
462
		$value = array_map( array( $this, 'external_to_internal_value' ), $value );
463
		$value[ Value_Set::VALUE_PROPERTY ] = $value[ static::TYPE_PROPERTY ];
464
		unset( $value[ static::TYPE_PROPERTY ] );
465
		return $value;
466
	}
467
468
	/**
469
	 * {@inheritDoc}
470
	 */
471
	public function set_value( $value ) {
472
		$value = array_map( array( $this, 'external_to_internal_value' ), $value );
473
		$groups = array();
474
		foreach ( $value as $values ) {
475
			$groups[] = isset( $values[ Value_Set::VALUE_PROPERTY ] ) ? $values[ Value_Set::VALUE_PROPERTY ] : Group_Field::DEFAULT_GROUP_NAME;
476
		}
477
		parent::set_value( $groups );
478
		$this->set_value_tree( $value );
479
	}
480
481
	/**
482
	 * {@inheritDoc}
483
	 */
484
	public function set_default_value( $default_value ) {
485
		foreach ( $default_value as $index => $group ) {
486
			if ( ! isset( $group[ static::TYPE_PROPERTY ] ) ) {
487
				$default_value[ $index ][ static::TYPE_PROPERTY ] = Group_Field::DEFAULT_GROUP_NAME;
488
			}
489
		}
490
		$this->default_value = $default_value;
491
		return $this;
492
	}
493
494
	/**
495
	 * Return the full value tree of all groups and their fields
496
	 *
497
	 * @return mixed
498
	 */
499
	public function get_value_tree() {
500
		return (array) $this->value_tree;
501
	}
502
503
	/**
504
	 * Set the full value tree of all groups and their fields
505
	 *
506
	 * @see  Internal Glossary in DEVELOPMENT.MD
507
	 */
508
	public function set_value_tree( $value_tree ) {
509
		$this->value_tree = $value_tree;
510
	}
511
512
	/**
513
	 * Returns an array that holds the field data, suitable for JSON representation.
514
	 *
515
	 * @param bool $load  Should the value be loaded from the database or use the value from the current instance.
516
	 * @return array
517
	 */
518
	public function to_json( $load ) {
519
		$complex_data = parent::to_json( $load );
520
521
		$groups_data = array();
522
		foreach ( $this->groups as $group ) {
523
			$group_data = $group->to_json( false );
524
			$group_data['collapsed'] = $this->get_collapsed();
525
526
			$groups_data[] = $group_data;
527
		}
528
529
		$field_groups = $this->get_prefilled_groups( $this->get_value_tree() );
530
		$value_data = array();
531
		foreach ( $field_groups as $group_index => $fields ) {
532
			$group = $this->get_group_by_name( $fields[ Value_Set::VALUE_PROPERTY ] );
533
534
			$data = array(
535
				'name' => $group->get_name(),
536
				'label' => $group->get_label(),
537
				'label_template' => $group->get_label_template(),
538
				'group_id' => $group->get_group_id(),
539
				'collapsed' => $this->get_collapsed(),
540
				'fields' => array(),
541
			);
542
543
			foreach ( $fields as $field ) {
544
				if ( ! is_a( $field, __NAMESPACE__ . '\\Field' ) ) {
545
					continue;
546
				}
547
				$data['fields'][] = $field->to_json( false );
548
			}
549
550
			$value_data[] = $data;
551
		}
552
553
		$complex_data = array_merge( $complex_data, array(
554
			'layout' => $this->layout,
555
			'labels' => $this->labels,
556
			'min' => $this->get_min(),
557
			'max' => $this->get_max(),
558
			'multiple_groups' => count( $groups_data ) > 1,
559
			'groups' => $groups_data,
560
			'value' => $value_data,
561
			'collapsed' => $this->get_collapsed(),
562
		) );
563
		return $complex_data;
564
	}
565
566
	/**
567
	 * Modify the layout of this field.
568
	 *
569
	 * @param string $layout
570
	 */
571
	public function set_layout( $layout ) {
572
		$available_layouts = array(
573
			static::LAYOUT_GRID,
574
			static::LAYOUT_TABBED_HORIZONTAL,
575
			static::LAYOUT_TABBED_VERTICAL,
576
		);
577
578 View Code Duplication
		if ( ! in_array( $layout,  $available_layouts ) ) {
579
			$error_message = 'Incorrect layout ``' . $layout . '" specified. ' .
580
				'Available layouts: ' . implode( ', ', $available_layouts );
581
582
			Incorrect_Syntax_Exception::raise( $error_message );
583
			return $this;
584
		}
585
586
		$this->layout = $layout;
587
588
		return $this;
589
	}
590
591
	/**
592
	 * Get the minimum number of entries.
593
	 *
594
	 * @return int $min
595
	 */
596
	public function get_min() {
597
		return $this->values_min;
598
	}
599
600
	/**
601
	 * Set the minimum number of entries.
602
	 *
603
	 * @param int $min
604
	 */
605
	public function set_min( $min ) {
606
		$this->values_min = intval( $min );
607
		return $this;
608
	}
609
610
	/**
611
	 * Get the maximum number of entries.
612
	 *
613
	 * @return int $max
614
	 */
615
	public function get_max() {
616
		return $this->values_max;
617
	}
618
619
	/**
620
	 * Set the maximum number of entries.
621
	 *
622
	 * @param int $max
623
	 */
624
	public function set_max( $max ) {
625
		$this->values_max = intval( $max );
626
		return $this;
627
	}
628
629
	/**
630
	 * Get collapsed state
631
	 *
632
	 * @return bool
633
	 */
634
	public function get_collapsed() {
635
		return $this->collapsed;
636
	}
637
638
	/**
639
	 * Change the groups initial collapse state.
640
	 * This state relates to the state of which the groups are rendered.
641
	 *
642
	 * @param bool $collapsed
643
	 */
644
	public function set_collapsed( $collapsed = true ) {
645
		$this->collapsed = $collapsed;
646
		return $this;
647
	}
648
}
649