Completed
Push — milestone/2_0/react-ui ( 5abf86...69be13 )
by
unknown
32:35
created

Complex_Field::set_layout()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 19
Code Lines 12

Duplication

Lines 7
Ratio 36.84 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 12
nc 2
nop 1
dl 7
loc 19
ccs 0
cts 12
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
	/**
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
	 * @param  boolean             $set_as_default
145
	 * @return object              $this
146
	 */
147 View Code Duplication
	public function set_datastore( Datastore_Interface $datastore, $set_as_default = false ) {
148
		if ( $set_as_default && ! $this->has_default_datastore() ) {
149
			return $this; // datastore has been overriden with a custom one - abort changing to a default one
150
		}
151
		$this->datastore = $datastore;
152
		$this->has_default_datastore = $set_as_default;
153
154
		$this->update_child_datastore( $this->get_datastore(), true );
155
		return $this;
156
	}
157
158
	/**
159
	 * Propagate the datastore down the hierarchy
160
	 *
161
	 * @param Datastore_Interface $datastore
162
	 * @param boolean             $set_as_default
163
	 */
164
	protected function update_child_datastore( Datastore_Interface $datastore, $set_as_default = false ) {
0 ignored issues
show
Unused Code introduced by
The parameter $datastore is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
165
		foreach ( $this->groups as $group ) {
166
			$group->set_datastore( $this->get_datastore(), $set_as_default );
167
		}
168
	}
169
170
	/**
171
	 * Retrieve all groups of fields.
172
	 *
173
	 * @return array $fields
174
	 */
175
	public function get_fields() {
176
		$fields = array();
177
178
		foreach ( $this->groups as $group ) {
179
			$group_fields = $group->get_fields();
180
181
			$fields = array_merge( $fields, $group_fields );
182
		}
183
184
		return $fields;
185
	}
186
187
	/**
188
	 * Add a set/group of fields.
189
	 *
190
	 * Accepted param variations:
191
	 *   - array<Field> $fields
192
	 *   - string $group_name, array<Field> $fields
193
	 *   - string $group_name, string $group_label, array<Field> $fields
194
	 *
195
	 * @return $this
196
	 */
197
	public function add_fields() {
198
		$argv = func_get_args();
199
		$argc = count( $argv );
200
		$fields = $argv[ $argc - 1 ];
201
		$name = '';
202
		$label = null;
203
204
		if ( $argc >= 2 ) {
205
			$name = $argv[0];
206
		}
207
208
		if ( $argc >= 3 ) {
209
			$label = $argv[1];
210
		}
211
212
		$name = ! empty( $name ) ? $name : Group_Field::DEFAULT_GROUP_NAME;
213
214
		if ( array_key_exists( $name, $this->groups ) ) {
215
			Incorrect_Syntax_Exception::raise( 'Group with name "' . $name . '" in Complex Field "' . $this->get_label() . '" already exists.' );
216
			return $this;
217
		}
218
219
		foreach ( $fields as $field ) {
220
			if ( $field->get_base_name() === Value_Set::VALUE_PROPERTY ) {
221
				Incorrect_Syntax_Exception::raise( '"' . Value_Set::VALUE_PROPERTY . '" is a reserved keyword for Complex fields and cannot be used for a field name.' );
222
				return $this;
223
			}
224
		}
225
226
		$group = new Group_Field( $name, $label, $fields );
227
		$this->groups[ $group->get_name() ] = $group;
228
229
		$this->update_child_hierarchy();
230
		if ( $this->get_datastore() !== null ) {
231
			$this->update_child_datastore( $this->get_datastore(), true );
232
		}
233
234
		return $this;
235
	}
236
237
	/**
238
	 * Retrieve the groups of this field.
239
	 *
240
	 * @return array
241
	 */
242
	public function get_group_names() {
243
		return array_keys( $this->groups );
244
	}
245
246
	/**
247
	 * Retrieve a group by its name.
248
	 *
249
	 * @param  string $group_name        Group name
250
	 * @return Group_Field $group_object Group object
251
	 */
252
	public function get_group_by_name( $group_name ) {
253
		$group_object = null;
254
255
		foreach ( $this->groups as $group ) {
256
			if ( $group->get_name() == $group_name ) {
257
				$group_object = $group;
258
			}
259
		}
260
261
		return $group_object;
262
	}
263
264
	/**
265
	 * Set the group label Underscore template.
266
	 *
267
	 * @param  string|callable $template
268
	 * @return $this
269
	 */
270
	public function set_header_template( $template ) {
271
		$template = is_callable( $template ) ? call_user_func( $template ) : $template;
272
273
		// Assign the template to the group that was added last
274
		$values = array_values( $this->groups );
275
		$group = end( $values );
276
277
		if ( $group ) {
278
			$group->set_label_template( $template );
279
280
			$this->groups[ $group->get_name() ] = $group;
281
		}
282
283
		return $this;
284
	}
285
286
	/**
287
	 * Set the field labels.
288
	 * Currently supported values:
289
	 *  - singular_name - the singular entry label
290
	 *  - plural_name - the plural entries label
291
	 *
292
	 * @param  array $labels Labels
293
	 */
294
	public function setup_labels( $labels ) {
295
		$this->labels = array_merge( $this->labels, $labels );
296
		return $this;
297
	}
298
299
	/**
300
	 * Return a clone of a field with hierarchy settings applied
301
	 *
302
	 * @param Field $field
303
	 * @param Field $parent_field
304
	 * @param int $group_index
305
	 * @return Field
306
	 */
307
	public function get_clone_under_field_in_hierarchy( $field, $parent_field, $group_index = 0 ) {
308
		$clone = clone $field;
309
		$clone->set_hierarchy( array_merge( $parent_field->get_hierarchy(), array( $parent_field->get_base_name() ) ) );
310
		$clone->set_hierarchy_index( array_merge( $parent_field->get_hierarchy_index(), array( $group_index ) ) );
311
		return $clone;
312
	}
313
314
	protected function get_prefilled_group_fields( $group_fields, $group_values, $group_index ) {
315
		$fields = array();
316
317
		foreach ( $group_fields as $field ) {
318
			$clone = $this->get_clone_under_field_in_hierarchy( $field, $this, $group_index );
319
			if ( isset( $group_values[ $clone->get_base_name() ] ) ) {
320
				$clone->set_value( $group_values[ $clone->get_base_name() ] );
321
			}
322
			$fields[] = $clone;
323
		}
324
325
		return $fields;
326
	}
327
328
	protected function get_prefilled_groups( $value_tree ) {
329
		$fields = array();
330
331
		foreach ( $value_tree as $group_index => $value ) {
332
			$group_name = $value[ Value_Set::VALUE_PROPERTY ];
333
			$group = $this->get_group_by_name( $group_name );
334
			$group_fields = $group->get_fields();
335
			$group_values = array();
336
			if ( isset( $value_tree[ $group_index ] ) ) {
337
				$group_values = $value_tree[ $group_index ];
338
			}
339
			$fields[ $group_index ] = array( Value_Set::VALUE_PROPERTY => $group->get_name() ) + $this->get_prefilled_group_fields( $group_fields, $group_values, $group_index );
340
		}
341
342
		return $fields;
343
	}
344
345
	/**
346
	 * Load the field value from an input array based on it's name.
347
	 *
348
	 * @param array $input Array of field names and values.
349
	 */
350
	public function set_value_from_input( $input ) {
351
		if ( ! isset( $input[ $this->get_name() ] ) ) {
352
			return;
353
		}
354
355
		$value_tree = array();
356
		$input_groups = $input[ $this->get_name() ];
357
		$input_group_index = 0;
358
		foreach ( $input_groups as $values ) {
359
			if ( ! isset( $values[ Value_Set::VALUE_PROPERTY ] ) || ! isset( $this->groups[ $values[ Value_Set::VALUE_PROPERTY ] ] ) ) {
360
				continue;
361
			}
362
363
			$group = $this->get_group_by_name( $values[ Value_Set::VALUE_PROPERTY ] );
364
			$group_fields = $group->get_fields();
365
			$group_field_names = array_flip( $group->get_field_names() );
366
367
			$value_group = array( Value_Set::VALUE_PROPERTY => $values[ Value_Set::VALUE_PROPERTY ] );
368
			unset( $values[ Value_Set::VALUE_PROPERTY ] );
369
370
			// trim input values to those used by the field
371
			$values = array_intersect_key( $values, $group_field_names );
372
373
			foreach ( $group_fields as $field ) {
374
				$tmp_field = $this->get_clone_under_field_in_hierarchy( $field, $this, $input_group_index );
375
376
				$tmp_field->set_value_from_input( $values );
377
				if ( is_a( $tmp_field, get_class() ) ) {
378
					$value_group[ $tmp_field->get_base_name() ] = $tmp_field->get_value_tree();
379
				} else {
380
					$value_group[ $tmp_field->get_base_name() ] = $tmp_field->get_full_value();
381
				}
382
			}
383
			$value_tree[] = $value_group;
384
			$input_group_index++;
385
		}
386
387
		$this->set_value( $value_tree );
388
	}
389
390
	/**
391
	 * Save all contained groups of fields.
392
	 */
393
	public function save() {
394
		// Only delete root field values as nested field values should be deleted in a cascading manner by the datastore
395
		$hierarchy = $this->get_hierarchy();
396
		$delete_on_save = empty( $hierarchy );
397
		$delete_on_save = apply_filters( 'carbon_fields_should_delete_field_value_on_save', $delete_on_save, $this );
398
		if ( $delete_on_save ) {
399
			$this->delete();
400
		}
401
402
		$save = apply_filters( 'carbon_fields_should_save_field_value', true, $this->get_value(), $this );
403
		if ( $save ) {
404
			$this->get_datastore()->save( $this );
405
			$field_groups = $this->get_prefilled_groups( $this->get_value_tree() );
406
			foreach ( $field_groups as $group_index => $fields ) {
407
				foreach ( $fields as $field ) {
408
					if ( ! is_a( $field, __NAMESPACE__ . '\\Field' ) ) {
409
						continue;
410
					}
411
					// $field->save();
0 ignored issues
show
Unused Code Comprehensibility introduced by
72% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
412
					$this->get_datastore()->save( $field );
413
				}
414
			}
415
		}
416
	}
417
418
	/**
419
	 * {@inheritDoc}
420
	 */
421
	public function set_value( $value ) {
422
		parent::set_value( wp_list_pluck( $value, Value_Set::VALUE_PROPERTY ) );
423
		$this->set_value_tree( $value );
424
	}
425
426
	/**
427
	 * Return the full value tree of all groups and their fields
428
	 *
429
	 * @return mixed
430
	 */
431
	public function get_value_tree() {
432
		return (array) $this->value_tree;
433
	}
434
435
	/**
436
	 * Set the full value tree of all groups and their fields
437
	 *
438
	 * @see  Internal Glossary in DEVELOPMENT.MD
439
	 */
440
	public function set_value_tree( $value_tree ) {
441
		$this->value_tree = $value_tree;
442
	}
443
444
	/**
445
	 * Return a differently formatted value for end-users
446
	 *
447
	 * @return mixed
448
	 */
449
	public function get_formatted_value() {
450
		$field_groups = $this->get_prefilled_groups( $this->get_value_tree() );
451
452
		$value = array();
453
		foreach ( $field_groups as $group_index => $field_group ) {
454
			$value[ $group_index ] = array();
455
			foreach ( $field_group as $key => $field ) {
456
				if ( is_a( $field, __NAMESPACE__ . '\\Field' ) ) {
457
					$value[ $group_index ][ $field->get_base_name() ] = $field->get_formatted_value();
458
				} else {
459
					if ( $key === Value_Set::VALUE_PROPERTY ) {
460
						$value[ $group_index ]['_type'] = $field;
461
					} else {
462
						$value[ $group_index ][ $key ] = $field;
463
					}
464
				}
465
			}
466
		}
467
		return $value;
468
	}
469
470
	/**
471
	 * Returns an array that holds the field data, suitable for JSON representation.
472
	 *
473
	 * @param bool $load  Should the value be loaded from the database or use the value from the current instance.
474
	 * @return array
475
	 */
476
	public function to_json( $load ) {
477
		$complex_data = parent::to_json( $load );
478
479
		$groups_data = array();
480
		foreach ( $this->groups as $group ) {
481
			$group_data = $group->to_json( false );
482
			$group_data['collapsed'] = $this->get_collapsed();
483
484
			$groups_data[] = $group_data;
485
		}
486
487
		$field_groups = $this->get_prefilled_groups( $this->get_value_tree() );
488
		$value_data = array();
489
		foreach ( $field_groups as $group_index => $fields ) {
490
			$group = $this->get_group_by_name( $fields[ Value_Set::VALUE_PROPERTY ] );
491
492
			$data = array(
493
				'name' => $group->get_name(),
494
				'label' => $group->get_label(),
495
				'label_template' => $group->get_label_template(),
496
				'group_id' => $group->get_group_id(),
497
				'collapsed' => $this->get_collapsed(),
498
				'fields' => array(),
499
			);
500
501
			foreach ( $fields as $field ) {
502
				if ( ! is_a( $field, __NAMESPACE__ . '\\Field' ) ) {
503
					continue;
504
				}
505
				$data['fields'][] = $field->to_json( false );
506
			}
507
508
			$value_data[] = $data;
509
		}
510
511
		$complex_data = array_merge( $complex_data, array(
512
			'layout' => $this->layout,
513
			'labels' => $this->labels,
514
			'min' => $this->get_min(),
515
			'max' => $this->get_max(),
516
			'multiple_groups' => count( $groups_data ) > 1,
517
			'groups' => $groups_data,
518
			'value' => $value_data,
519
			'collapsed' => $this->get_collapsed(),
520
		) );
521
		return $complex_data;
522
	}
523
524
	/**
525
	 * Modify the layout of this field.
526
	 *
527
	 * @param string $layout
528
	 */
529
	public function set_layout( $layout ) {
530
		$available_layouts = array(
531
			static::LAYOUT_GRID,
532
			static::LAYOUT_TABBED_HORIZONTAL,
533
			static::LAYOUT_TABBED_VERTICAL,
534
		);
535
536 View Code Duplication
		if ( ! in_array( $layout,  $available_layouts ) ) {
537
			$error_message = 'Incorrect layout ``' . $layout . '" specified. ' .
538
				'Available layouts: ' . implode( ', ', $available_layouts );
539
540
			Incorrect_Syntax_Exception::raise( $error_message );
541
			return $this;
542
		}
543
544
		$this->layout = $layout;
545
546
		return $this;
547
	}
548
549
	/**
550
	 * Get the minimum number of entries.
551
	 *
552
	 * @return int $min
553
	 */
554
	public function get_min() {
555
		return $this->values_min;
556
	}
557
558
	/**
559
	 * Set the minimum number of entries.
560
	 *
561
	 * @param int $min
562
	 */
563
	public function set_min( $min ) {
564
		$this->values_min = intval( $min );
565
		return $this;
566
	}
567
568
	/**
569
	 * Get the maximum number of entries.
570
	 *
571
	 * @return int $max
572
	 */
573
	public function get_max() {
574
		return $this->values_max;
575
	}
576
577
	/**
578
	 * Set the maximum number of entries.
579
	 *
580
	 * @param int $max
581
	 */
582
	public function set_max( $max ) {
583
		$this->values_max = intval( $max );
584
		return $this;
585
	}
586
587
	/**
588
	 * Get collapsed state
589
	 *
590
	 * @return bool
591
	 */
592
	public function get_collapsed() {
593
		return $this->collapsed;
594
	}
595
596
	/**
597
	 * Change the groups initial collapse state.
598
	 * This state relates to the state of which the groups are rendered.
599
	 *
600
	 * @param bool $collapsed
601
	 */
602
	public function set_collapsed( $collapsed = true ) {
603
		$this->collapsed = $collapsed;
604
		return $this;
605
	}
606
}
607