Completed
Push — milestone/2_0/react-ui ( 42c3e4...a147b7 )
by
unknown
03:18
created

Association_Field::get_item_label()   B

Complexity

Conditions 4
Paths 6

Size

Total Lines 22
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

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