Completed
Push — milestone/2_0/react-ui ( feeecc...e46ba4 )
by
unknown
07:22
created

value_string_to_property_array()   B

Complexity

Conditions 5
Paths 16

Size

Total Lines 19
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

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