Completed
Push — development ( 5ed9d2...646735 )
by
unknown
04:14
created

Complex_Field::add_fields()   B

Complexity

Conditions 8
Paths 48

Size

Total Lines 40

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 72

Importance

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