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