Completed
Push — milestone/2_0/react-ui ( b00e8d...52106c )
by
unknown
02:51
created

Complex_Field::get_formatted_value()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 20
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

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