Completed
Push — milestone/2_0/react-ui ( d33541...5bf054 )
by
unknown
03:33
created

Association_Field::allow_duplicates()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 3
ccs 0
cts 2
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\Value_Set\Value_Set;
6
7
/**
8
 * Association field class.
9
 * Allows selecting and manually sorting entries from various types:
10
 *  - Posts
11
 *  - Terms
12
 *  - Users
13
 *  - Comments
14
 */
15
class Association_Field extends Field {
16
17
	/**
18
	 * WP_Toolset instance for WP data loading
19
	 *
20
	 * @var Carbon_Fields\Toolset\WP_Toolset
21
	 */
22
	protected $wp_toolset;
23
24
	/**
25
	 * Min number of selected items allowed. -1 for no limit
26
	 *
27
	 * @var integer
28
	 */
29
	protected $min = -1;
30
31
	/**
32
	 * Max number of selected items allowed. -1 for no limit
33
	 *
34
	 * @var integer
35
	 */
36
	protected $max = -1;
37
38
	/**
39
	 * Allow items to be added multiple times
40
	 *
41
	 * @var boolean
42
	 */
43
	protected $duplicates_allowed = false;
44
45
	/**
46
	 * Default field value
47
	 *
48
	 * @var array
49
	 */
50
	protected $default_value = array();
51
52
	/**
53
	 * Types of entries to associate with.
54
	 * @var array
55
	 */
56
	protected $types = array(
57
		array(
58
			'type' => 'post',
59
			'post_type' => 'post',
60
		),
61
	);
62
63
	/**
64
	 * Create a field from a certain type with the specified label.
65
	 *
66
	 * @param string $type  Field type
67
	 * @param string $name  Field name
68
	 * @param string $label Field label
69
	 */
70
	public function __construct( $type, $name, $label ) {
71
		$this->wp_toolset = \Carbon_Fields\Carbon_Fields::resolve( 'wp_toolset' );
72
		$this->set_value_set( new Value_Set( Value_Set::TYPE_VALUE_SET, array( 'type' => '', 'subtype' => '', 'id' => 0 ) ) );
73
		parent::__construct( $type, $name, $label );
74
	}
75
76
	/**
77
	 * Load the field value from an input array based on it's name
78
	 *
79
	 * @param array $input Array of field names and values.
80
	 */
81
	public function set_value_from_input( $input ) {
82
		$value = array();
83 View Code Duplication
		if ( isset( $input[ $this->get_name() ] ) ) {
84
			$value = stripslashes_deep( $input[ $this->get_name() ] );
85
			if ( is_array( $value ) ) {
86
				$value = array_values( $value );
87
			}
88
		}
89
		$this->set_value( $value );
90
	}
91
92
	/**
93
	 * Alias for $this->get_value_set()->set( $value );
94
	 */
95
	public function set_value( $value ) {
96
		$value = $this->value_string_array_to_value_set( $value );
97
		parent::set_value( $value );
98
	}
99
100
	/**
101
	 * Get value string for legacy value
102
	 *
103
	 * @return string
104
	 */
105
	protected function get_value_string_for_legacy_value( $legacy_value ) {
106
		$entry_type = 'post';
107
		$entry_subtype = 'post';
108
109
		// attempt to find a suitable type that is registered to this field as post type is not stored for legacy data
110
		foreach ( $this->types as $type ) {
111
			if ( $type['type'] === $entry_type ) {
112
				$entry_subtype = $type['post_type'];
113
				break;
114
			}
115
		}
116
117
		return $entry_type . ':' . $entry_subtype . ':' . $legacy_value;
118
	}
119
120
	/**
121
	 * Convert a colo:separated:string into it's expected components
122
	 * Used for backwards compatibility to CF 1.5
123
	 *
124
	 * @param string $value_string
125
	 * @return array
126
	 */
127
	protected function value_string_to_property_array( $value_string ) {
128
		if ( is_numeric( $value_string ) ) {
129
			// we are dealing with legacy data that only contains a post ID
130
			$value_string = $this->get_value_string_for_legacy_value( $value_string );
131
		}
132
133
		$value_pieces = explode( ':', $value_string );
134
		$type = isset( $value_pieces[0] ) ? $value_pieces[0] : 'post';
135
		$subtype = isset( $value_pieces[1] ) ? $value_pieces[1] : 'post';
136
		$id = isset( $value_pieces[2] ) ? $value_pieces[2] : 0;
137
138
		$property_array = array(
139
			Value_Set::VALUE_PROPERTY => $value_string,
140
			'type' => $type,
141
			'subtype' => $subtype,
142
			'id' => intval( $id ),
143
		);
144
		return $property_array;
145
	}
146
147
	/**
148
	 * Convert a colon:separated:string into it's expected components
149
	 * Used for backwards compatibility to CF 1.5
150
	 *
151
	 * @param array $value_string_array
152
	 * @return array<array>
153
	 */
154
	protected function value_string_array_to_value_set( $value_string_array ) {
155
		$value_set = array();
156
		foreach ( $value_string_array as $raw_value_entry ) {
157
			$value_string = $raw_value_entry;
158
159
			if ( is_array( $raw_value_entry ) ) {
160
				if ( isset( $raw_value_entry['type'] ) ) {
161
					// array is already in suitable format
162
					$value_set[] = $raw_value_entry;
163
					continue;
164
				}
165
				$value_string = $raw_value_entry[ Value_Set::VALUE_PROPERTY ];
166
			}
167
			$value_string = trim( $value_string );
168
			if ( empty( $value_string ) ) {
169
				continue;
170
			}
171
172
			$property_array = $this->value_string_to_property_array( $value_string );
173
			$value_set[] = $property_array;
174
		}
175
		return $value_set;
176
	}
177
178
	/**
179
	 * Used to get the title of an item.
180
	 *
181
	 * Can be overriden or extended by the `carbon_association_title` filter.
182
	 *
183
	 * @param int $id The database ID of the item.
184
	 * @param string $type Item type (post, term, user, comment, or a custom one).
185
	 * @param string $subtype The subtype - "page", "post", "category", etc.
186
	 * @return string $title The title of the item.
187
	 */
188
	protected function get_title_by_type( $id, $type, $subtype = '' ) {
189
		$title = '';
190
191
		$method = 'get_' . $type . '_title';
192
		$callable = array( $this->wp_toolset, $method );
193
		if ( is_callable( $callable ) ) {
194
			$title = call_user_func( $callable, $id, $subtype );
195
		}
196
197
		if ( $type === 'comment' ) {
198
			$max = apply_filters( 'carbon_fields_association_field_comment_length', 30, $this->get_base_name() );
199
			if ( strlen( $title ) > $max ) {
200
				$title = substr( $title, 0, $max ) . '...';
201
			}
202
		}
203
204
		/**
205
		 * Filter the title of the association item.
206
		 *
207
		 * @param string $title   The unfiltered item title.
208
		 * @param string $name    Name of the association field.
209
		 * @param int    $id      The database ID of the item.
210
		 * @param string $type    Item type (post, term, user, comment, or a custom one).
211
		 * @param string $subtype Subtype - "page", "post", "category", etc.
212
		 */
213
		$title = apply_filters( 'carbon_fields_association_field_title', $title, $this->get_base_name(), $id, $type, $subtype );
214
215
		if ( ! $title ) {
216
			$title = '(no title) - ID: ' . $id;
217
		}
218
219
		return $title;
220
	}
221
222
	/**
223
	 * Used to get the label of an item.
224
	 *
225
	 * Can be overriden or extended by the `carbon_association_item_label` filter.
226
	 *
227
	 * @param int     $id      The database ID of the item.
228
	 * @param string  $type    Item type (post, term, user, comment, or a custom one).
229
	 * @param string  $subtype Subtype - "page", "post", "category", etc.
230
	 * @return string $label The label of the item.
231
	 */
232
	protected function get_item_label( $id, $type, $subtype = '' ) {
233
		$label = $subtype ? $subtype : $type;
234
235
		if ( $type === 'post' ) {
236
			$post_type_object = get_post_type_object( $subtype );
237
			$label = $post_type_object->labels->singular_name;
238
		} elseif ( $type === 'term' ) {
239
			$taxonomy_object = get_taxonomy( $subtype );
240
			$label = $taxonomy_object->labels->singular_name;
241
		}
242
243
		/**
244
		 * Filter the label of the association item.
245
		 *
246
		 * @param string $label   The unfiltered item label.
247
		 * @param string $name    Name of the association field.
248
		 * @param int    $id      The database ID of the item.
249
		 * @param string $type    Item type (post, term, user, comment, or a custom one).
250
		 * @param string $subtype Subtype - "page", "post", "category", etc.
251
		 */
252
		return apply_filters( 'carbon_fields_association_field_item_label', $label, $this->get_base_name(), $id, $type, $subtype );
253
	}
254
255
	/**
256
	 * Get post options
257
	 *
258
	 * @return array $options
259
	 */
260
	protected function get_post_options( $type ) {
261
		/**
262
		 * Filter the default query when fetching posts for a particular field.
263
		 *
264
		 * @param array $args The parameters, passed to get_posts().
265
		 */
266
		$filter_name = 'carbon_fields_association_field_options_' . $this->get_base_name() . '_' . $type['type'] . '_' . $type['post_type'];
267
		$args = apply_filters( $filter_name, array(
268
			'post_type' => $type['post_type'],
269
			'posts_per_page' => -1,
270
			'fields' => 'ids',
271
			'suppress_filters' => false,
272
		) );
273
274
		// fetch and prepare posts as association items
275
		$posts = get_posts( $args );
276
		foreach ( $posts as &$p ) {
277
			$p = array(
278
				'id' => intval( $p ),
279
				'title' => $this->get_title_by_type( $p, $type['type'], $type['post_type'] ),
280
				'type' => $type['type'],
281
				'subtype' => $type['post_type'],
282
				'label' => $this->get_item_label( $p, $type['type'], $type['post_type'] ),
283
				'is_trashed' => ( get_post_status( $p ) == 'trash' ),
284
				'edit_link' => $this->get_object_edit_link( $type, $p ),
285
			);
286
		}
287
		return $posts;
288
	}
289
290
	/**
291
	 * Get term options
292
	 *
293
	 * @return array $options
294
	 */
295
	protected function get_term_options( $type ) {
296
		/**
297
		 * Filter the default parameters when fetching terms for a particular field.
298
		 *
299
		 * @param array $args The parameters, passed to get_terms().
300
		 */
301
		$filter_name = 'carbon_fields_association_field_options_' . $this->get_base_name() . '_' . $type['type'] . '_' . $type['taxonomy'];
302
		$args = apply_filters( $filter_name, array(
303
			'hide_empty' => 0,
304
			'fields' => 'id=>name',
305
		) );
306
307
		// fetch and prepare terms as association items
308
		$terms = get_terms( $type['taxonomy'], $args );
309
		foreach ( $terms as $term_id => &$term ) {
310
			$term = array(
311
				'id' => intval( $term_id ),
312
				'title' => $term,
313
				'type' => $type['type'],
314
				'subtype' => $type['taxonomy'],
315
				'label' => $this->get_item_label( $term_id, $type['type'], $type['taxonomy'] ),
316
				'is_trashed' => false,
317
				'edit_link' => $this->get_object_edit_link( $type, $term_id ),
318
			);
319
		}
320
		return $terms;
321
	}
322
323
	/**
324
	 * Get user options
325
	 *
326
	 * @return array $options
327
	 */
328 View Code Duplication
	protected function get_user_options( $type ) {
329
		/**
330
		 * Filter the default parameters when fetching users for a particular field.
331
		 *
332
		 * @param array $args The parameters, passed to get_users().
333
		 */
334
		$filter_name = 'carbon_fields_association_field_options_' . $this->get_base_name() . '_' . $type['type'];
335
		$args = apply_filters( $filter_name, array(
336
			'fields' => 'ID',
337
		) );
338
339
		// fetch and prepare users as association items
340
		$users = get_users( $args );
341
		foreach ( $users as &$u ) {
342
			$u = array(
343
				'id' => intval( $u ),
344
				'title' => $this->get_title_by_type( $u, $type['type'] ),
345
				'type' => $type['type'],
346
				'subtype' => 'user',
347
				'label' => $this->get_item_label( $u, $type['type'] ),
348
				'is_trashed' => false,
349
				'edit_link' => $this->get_object_edit_link( $type, $u ),
350
			);
351
		}
352
		return $users;
353
	}
354
355
	/**
356
	 * Get comment options
357
	 *
358
	 * @return array $options
359
	 */
360 View Code Duplication
	protected function get_comment_options( $type ) {
361
		/**
362
		 * Filter the default parameters when fetching comments for a particular field.
363
		 *
364
		 * @param array $args The parameters, passed to get_comments().
365
		 */
366
		$filter_name = 'carbon_fields_association_field_options_' . $this->get_base_name() . '_' . $type['type'];
367
		$args = apply_filters( $filter_name, array(
368
			'fields' => 'ids',
369
		) );
370
371
		// fetch and prepare comments as association items
372
		$comments = get_comments( $args );
373
		foreach ( $comments as &$c ) {
374
			$c = array(
375
				'id' => intval( $c ),
376
				'title' => $this->get_title_by_type( $c, $type['type'] ),
377
				'type' => $type['type'],
378
				'subtype' => 'comment',
379
				'label' => $this->get_item_label( $c, $type['type'] ),
380
				'is_trashed' => false,
381
				'edit_link' => $this->get_object_edit_link( $type, $c ),
382
			);
383
		}
384
		return $comments;
385
	}
386
387
	/**
388
	 * Generate the item options.
389
	 *
390
	 * @return array $options The selectable options of the association field.
391
	 */
392
	public function get_options() {
393
		$options = array();
394
395
		foreach ( $this->types as $type ) {
396
			$method = 'get_' . $type['type'] . '_options';
397
			$callable = array( $this, $method );
398
			if ( is_callable( $callable ) ) {
399
				$options = array_merge( $options, call_user_func( $callable, $type ) );
400
			}
401
		}
402
403
		/**
404
		 * Filter the final list of options, available to a certain association field.
405
		 *
406
		 * @param array $options Unfiltered options items.
407
		 * @param string $name Name of the association field.
408
		 */
409
		$options = apply_filters( 'carbon_fields_association_field_options', $options, $this->get_base_name() );
410
411
		return $options;
412
	}
413
414
	/**
415
	 * Retrieve the edit link of a particular object.
416
	 *
417
	 * @param  string $type Object type.
418
	 * @param  int $id      ID of the object.
419
	 * @return string       URL of the edit link.
420
	 */
421
	protected function get_object_edit_link( $type, $id ) {
422
		switch ( $type['type'] ) {
423
424
			case 'post':
425
				$edit_link = get_edit_post_link( $id );
426
				break;
427
428
			case 'term':
429
				$edit_link = get_edit_term_link( $id, $type['taxonomy'], $type['type'] );
430
				break;
431
432
			case 'comment':
433
				$edit_link = get_edit_comment_link( $id );
434
				break;
435
436
			case 'user':
437
				$edit_link = get_edit_user_link( $id );
438
				break;
439
440
			default:
441
				$edit_link = false;
442
443
		}
444
445
		return $edit_link;
446
	}
447
448
	/**
449
	 * Modify the types.
450
	 * @param array $types New types
451
	 */
452
	public function set_types( $types ) {
453
		$this->types = $types;
454
		return $this;
455
	}
456
457
	/**
458
	 * Get the minimum allowed number of selected entries.
459
	 *
460
	 * @return int
461
	 */
462
	public function get_min() {
463
		return $this->min;
464
	}
465
466
	/**
467
	 * Set the minimum allowed number of selected entries.
468
	 *
469
	 * @param int $min
470
	 */
471
	public function set_min( $min ) {
472
		$this->min = intval( $min );
473
		return $this;
474
	}
475
476
	/**
477
	 * Get the maximum allowed number of selected entries.
478
	 *
479
	 * @return int
480
	 */
481
	public function get_max() {
482
		return $this->max;
483
	}
484
485
	/**
486
	 * Set the maximum allowed number of selected entries.
487
	 *
488
	 * @param int $max
489
	 */
490
	public function set_max( $max ) {
491
		$this->max = intval( $max );
492
		return $this;
493
	}
494
495
	/**
496
	 * Get whether entry duplicates are allowed.
497
	 *
498
	 * @return boolean
499
	 */
500
	public function get_duplicates_allowed() {
501
		return $this->duplicates_allowed;
502
	}
503
504
	/**
505
	 * Set whether entry duplicates are allowed.
506
	 *
507
	 * @param boolean $allowed
508
	 */
509
	public function set_duplicates_allowed( $allowed ) {
510
		$this->duplicates_allowed = $allowed;
511
		return $this;
512
	}
513
514
	/**
515
	 * Specify whether to allow each entry to be selected multiple times.
516
	 * Backwards-compatibility alias.
517
	 *
518
	 * @param  boolean $allow
519
	 */
520
	public function allow_duplicates( $allow = true ) {
521
		return $this->set_duplicates_allowed( $allow );
522
	}
523
524
	/**
525
	 * Converts the field values into a usable associative array.
526
	 *
527
	 * The association data is saved in the database in the following format:
528
	 * 	array (
529
	 *		0 => 'post:page:4',
530
	 *		1 => 'term:category:2',
531
	 *		2 => 'user:user:1',
532
	 * 	)
533
	 * where the value of each array item contains:
534
	 * 	- Type of data (post, term, user or comment)
535
	 * 	- Subtype of data (the particular post type or taxonomy)
536
	 * 	- ID of the item (the database ID of the item)
537
	 */
538
	protected function value_to_json() {
539
		$value_set = $this->get_value();
540
		$value = array();
541
		foreach ( $value_set as $entry ) {
1 ignored issue
show
Bug introduced by
The expression $value_set of type string|array|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
542
			$item = array(
543
				'type' => $entry['type'],
544
				'subtype' => $entry['subtype'],
545
				'id' => intval( $entry['id'] ),
546
				'title' => $this->get_title_by_type( $entry['id'], $entry['type'], $entry['subtype'] ),
547
				'label' => $this->get_item_label( $entry['id'], $entry['type'], $entry['subtype'] ),
548
				'is_trashed' => ( $entry['type'] == 'post' && get_post_status( $entry['id'] ) === 'trash' ),
549
			);
550
			$value[] = $item;
551
		}
552
		return $value;
553
	}
554
555
	/**
556
	 * Convert the field data into JSON representation.
557
	 * @param  bool $load Whether to load data from the datastore.
558
	 * @return mixed      The JSON field data.
559
	 */
560
	public function to_json( $load ) {
561
		$field_data = parent::to_json( $load );
562
563
		$field_data = array_merge( $field_data, array(
564
			'value' => $this->value_to_json(),
565
			'options' => $this->get_options(),
566
			'min' => $this->get_min(),
567
			'max' => $this->get_max(),
568
			'duplicates_allowed' => $this->duplicates_allowed,
569
		) );
570
571
		return $field_data;
572
	}
573
}
574