Completed
Pull Request — develop (#1487)
by Gennady
07:54
created

GravityView_Field::field_options()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 6
dl 0
loc 7
ccs 0
cts 4
cp 0
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * @file class-gravityview-field.php
4
 * @package GravityView
5
 * @subpackage includes\fields
6
 */
7
8
/**
9
 * Modify field settings by extending this class.
10
 */
11
abstract class GravityView_Field {
12
13
	/**
14
	 * The name of the GravityView field type
15
	 * Example: `created_by`, `text`, `fileupload`, `address`, `entry_link`
16
	 * @var string
17
	 */
18
	public $name;
19
20
	/**
21
	 * @internal Not yet implemented
22
	 * @since 1.15.2
23
	 * @type string The description of the field in the field picker
24
	 */
25
	public $description;
26
27
	/**
28
	 * @since 1.15.2
29
	 * @type string The label of the field in the field picker
30
	 */
31
	public $label;
32
33
	/**
34
	 * @var string The default search label used by the search widget, if not set
35
	 */
36
	public $default_search_label;
37
38
	/**
39
	 * `standard`, `advanced`, `post`, `pricing`, `meta`, `gravityview`
40
	 * @since 1.15.2
41
	 * @type string The group belongs to this field in the field picker
42
	 */
43
	public $group;
44
45
	/**
46
	 * @internal Not yet implemented
47
	 * @type boolean Can the field be searched?
48
	 * @since 1.15.2
49
	 */
50
	public $is_searchable = true;
51
52
	/**
53
	 * @internal Not yet implemented
54
	 * @type array $search_operators The type of search operators available for this field
55
	 * @since 1.15.2
56
	 */
57
	public $search_operators;
58
59
	/**
60
	 * @type boolean Can the field be sorted in search?
61
	 * @since 1.15.2
62
	 */
63
	public $is_sortable = true;
64
65
	/**
66
	 * @type boolean Is field content number-based?
67
	 * @since 1.15.2
68
	 */
69
	public $is_numeric;
70
71
	/**
72
	 * @var null|string The key used to search and sort entry meta in Gravity Forms. Used if the field stores data as custom entry meta.
73
	 * @see https://www.gravityhelp.com/documentation/article/gform_entry_meta/
74
	 * @since 1.19
75
	 */
76
	public $entry_meta_key = null;
77
78
	/**
79
	 * @var string|array Optional. The callback function after entry meta is updated, only used if $entry_meta_key is set.
80
	 * @see https://www.gravityhelp.com/documentation/article/gform_entry_meta/
81
	 * @since 1.19
82
	 */
83
	var $entry_meta_update_callback = null;
84
85
	/**
86
	 * @var bool Whether to show meta when set to true automatically adds the column to the entry list, without having to edit and add the column for display
87
	 * @since 1.19
88
	 */
89
	var $entry_meta_is_default_column = false;
90
91
	/**
92
	 * @internal Not yet implemented
93
	 * @todo implement supports_context() method
94
	 * The contexts in which a field is available. Some fields aren't editable, for example.
95
	 * - `singular` is an alias for both `single` and `edit`
96
	 * - `multiple` is an alias for `directory` (backward compatibility)
97
	 * @type array
98
	 * @since 1.15.2
99
	 */
100
	public $contexts = array( 'single', 'multiple', 'edit', 'export' );
101
102
	/**
103
	 * @since 1.15.2
104
	 * @since 1.16.2.2 Changed access to public (previously, protected)
105
	 * @type string The name of a corresponding Gravity Forms GF_Field class, if exists
106
	 */
107
	public $_gf_field_class_name;
108
109
	/**
110
	 * @var string The field ID being requested
111
	 * @since 1.14
112
	 */
113
	protected $_field_id = '';
114
115
	/**
116
	 * @var string Field options to be rendered
117
	 * @since 1.14
118
	 */
119
	protected $_field_options = array();
120
121
	/**
122
	 * @var bool|string Name of merge tag (without curly brackets), if the field has custom GravityView merge tags to add. Otherwise, false.
123
	 * @since 1.16
124
	 */
125
	protected $_custom_merge_tag = false;
126
127
	/**
128
	 * GravityView_Field constructor.
129
	 */
130
	public function __construct() {
131
132
		// Modify the field options based on the name of the field type
133
		add_filter( sprintf( 'gravityview_template_%s_options', $this->name ), array( &$this, 'field_options' ), 10, 6 );
134
135
		add_filter( 'gravityview/sortable/field_blacklist', array( $this, '_filter_sortable_fields' ), 1 );
136
137
		if( $this->entry_meta_key ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->entry_meta_key of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
138
			add_filter( 'gform_entry_meta', array( $this, 'add_entry_meta' ) );
139
			add_filter( 'gravityview/common/sortable_fields', array( $this, 'add_sortable_field' ), 10, 2 );
140
		}
141
142
		if( $this->_custom_merge_tag ) {
143
			add_filter( 'gform_custom_merge_tags', array( $this, '_filter_gform_custom_merge_tags' ), 10, 4 );
144
			add_filter( 'gform_replace_merge_tags', array( $this, '_filter_gform_replace_merge_tags' ), 10, 7 );
145
		}
146
147
		if( 'meta' === $this->group || '' !== $this->default_search_label ) {
148
			add_filter( 'gravityview_search_field_label', array( $this, 'set_default_search_label' ), 10, 3 );
149
		}
150
151
		/**
152
		 * Auto-assign label from Gravity Forms label, if exists
153
		 * @since 1.20
154
		 */
155
		if( empty( $this->label ) && ! empty( $this->_gf_field_class_name ) && class_exists( $this->_gf_field_class_name ) ) {
156
			$this->label = ucfirst( GF_Fields::get( $this->name )->get_form_editor_field_title() );
157
		}
158
159
		try {
160
			GravityView_Fields::register( $this );
161
		} catch ( Exception $exception ) {
162
			gravityview()->log->critical( $exception->getMessage() );
163
		}
164
	}
165
166
	/**
167
	 * Add the field to the Filter & Sort available fields
168
	 *
169
	 * @since 1.19
170
	 *
171
	 * @param array $fields Sub-set of GF form fields that are sortable
172
	 *
173
	 * @return array Modified $fields array to include approval status in the sorting dropdown
174
	 */
175
	public function add_sortable_field( $fields ) {
176
177
		$added_field = array(
178
			'label' => $this->label,
179
			'type'  => $this->name
180
		);
181
182
		$fields["{$this->entry_meta_key}"] = $added_field;
183
184
		return $fields;
185
	}
186
187
	/**
188
	 * Allow setting a default search label for search fields based on the field type
189
	 *
190
	 * Useful for entry meta "fields" that don't have Gravity Forms labels, like `created_by`
191
	 *
192
	 * @since 1.17.3
193
	 *
194
	 * @param string $label Existing label text, sanitized.
195
	 * @param array $gf_field Gravity Forms field array, as returned by `GFFormsModel::get_field()`
196
	 * @param array $field Field setting as sent by the GV configuration - has `field`, `input` (input type), and `label` keys
197
	 *
198
	 * @return string
199
	 */
200 4
	function set_default_search_label( $label = '', $gf_field = null, $field = array() ) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
201
202 4
		if( $this->name === $field['field'] && '' === $label ) {
203
			$label = esc_html( $this->default_search_label );
204
		}
205
206 4
		return $label;
207
	}
208
209
	/**
210
	 * Match the merge tag in replacement text for the field.  DO NOT OVERRIDE.
211
	 *
212
	 * @see replace_merge_tag Override replace_merge_tag() to handle any matches
213
	 *
214
	 * @since 1.16
215
	 *
216
	 * @param string $text Text to replace
217
	 * @param array $form Gravity Forms form array
218
	 * @param array $entry Entry array
219
	 * @param bool $url_encode Whether to URL-encode output
220
	 *
221
	 * @return string Original text if {_custom_merge_tag} isn't found. Otherwise, replaced text.
222
	 */
223 49
	public function _filter_gform_replace_merge_tags( $text, $form = array(), $entry = array(), $url_encode = false, $esc_html = false  ) {
224
225
		// Is there is field merge tag? Strip whitespace off the ned, too.
226 49
		preg_match_all( '/{' . preg_quote( $this->_custom_merge_tag ) . ':?(.*?)(?:\s)?}/ism', $text, $matches, PREG_SET_ORDER );
227
228
		// If there are no matches, return original text
229 49
		if ( empty( $matches ) ) {
230 49
			return $text;
231
		}
232
233 2
		return $this->replace_merge_tag( $matches, $text, $form, $entry, $url_encode, $esc_html );
234
	}
235
236
	/**
237
	 * Run GravityView filters when using GFCommon::replace_variables()
238
	 *
239
	 * Instead of adding multiple hooks, add all hooks into this one method to improve speed
240
	 *
241
	 * @since 1.8.4
242
	 *
243
	 * @see GFCommon::replace_variables()
244
	 *
245
	 * @param array $matches Array of Merge Tag matches found in text by preg_match_all
246
	 * @param string $text Text to replace
247
	 * @param array|bool $form Gravity Forms form array. When called inside {@see GFCommon::replace_variables()} (now deprecated), `false`
248
	 * @param array|bool $entry Entry array.  When called inside {@see GFCommon::replace_variables()} (now deprecated), `false`
249
	 * @param bool $url_encode Whether to URL-encode output
250
	 * @param bool $esc_html Whether to apply `esc_html()` to output
251
	 *
252
	 * @return mixed
253
	 */
254 1
	public function replace_merge_tag( $matches = array(), $text = '', $form = array(), $entry = array(), $url_encode = false, $esc_html = false ) {
0 ignored issues
show
Unused Code introduced by
The parameter $form is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
255
256 1
		foreach( $matches as $match ) {
257
258 1
			$full_tag = $match[0];
259
260
			// Strip the Merge Tags
261 1
			$tag = str_replace( array( '{', '}'), '', $full_tag );
262
263
			// Replace the value from the entry, if exists
264 1
			if( isset( $entry[ $tag ] ) ) {
265
266 1
				$value = $entry[ $tag ];
267
268 1
				if( is_callable( array( $this, 'get_content') ) ) {
269 1
					$value = $this->get_content( $value );
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class GravityView_Field as the method get_content() does only exist in the following sub-classes of GravityView_Field: GravityView_Field_Date_Created, GravityView_Field_Date_Updated, GravityView_Field_Is_Fulfilled, GravityView_Field_Is_Starred, GravityView_Field_Payment_Amount, GravityView_Field_Payment_Date, GravityView_Field_Transaction_Type. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
270
				}
271
272 1
				$text = str_replace( $full_tag, $value, $text );
273
			}
274
		}
275
276 1
		unset( $value, $tag, $full_tag );
277
278 1
		return $text;
279
	}
280
281
	/**
282
	 * Add custom merge tags to merge tag options. DO NOT OVERRIDE.
283
	 *
284
	 * @internal Not to be overridden by fields
285
	 *
286
	 * @since 1.8.4
287
	 *
288
	 * @param array $custom_merge_tags
289
	 * @param int $form_id GF Form ID
290
	 * @param GF_Field[] $fields Array of fields in the form
291
	 * @param string $element_id The ID of the input that Merge Tags are being used on
292
	 *
293
	 * @return array Modified merge tags
294
	 */
295
	public function _filter_gform_custom_merge_tags( $custom_merge_tags = array(), $form_id, $fields = array(), $element_id = '' ) {
0 ignored issues
show
Unused Code introduced by
The parameter $element_id is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
296
297
		$form = GVCommon::get_form( $form_id );
298
299
		$field_merge_tags = $this->custom_merge_tags( $form, $fields );
300
301
		return array_merge( $custom_merge_tags, $field_merge_tags );
302
	}
303
304
	/**
305
	 * Add custom Merge Tags to Merge Tag options, if custom Merge Tags exist
306
	 *
307
	 * Should be overridden if there's more than one Merge Tag to add or if the Merge Tag isn't {_custom_merge_tag}
308
	 *
309
	 * @since 1.16
310
	 *
311
	 * @param array $form GF Form array
312
	 * @param GF_Field[] $fields Array of fields in the form
313
	 *
314
	 * @return array Merge tag array with `label` and `tag` keys based on class `label` and `_custom_merge_tag` variables
315
	 */
316
	protected function custom_merge_tags( $form = array(), $fields = array() ) {
0 ignored issues
show
Unused Code introduced by
The parameter $form is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $fields is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
317
318
		// Use variables to make it unnecessary for other fields to override
319
		$merge_tags = array(
320
			array(
321
				'label' => $this->label,
322
				'tag' => '{' . $this->_custom_merge_tag . '}',
323
			),
324
		);
325
326
		return $merge_tags;
327
	}
328
329
	/**
330
	 * Use field settings to modify whether a field is sortable
331
	 *
332
	 * @see GravityView_frontend::is_field_sortable
333
	 * @since 1.15.3
334
	 *
335
	 * @param array $not_sortable Existing field types that aren't sortable
336
	 *
337
	 * @return array
338
	 */
339 1
	public function _filter_sortable_fields( $not_sortable ) {
340
341 1
		if( ! $this->is_sortable ) {
342 1
			$not_sortable[] = $this->name;
343
		}
344
345 1
		return $not_sortable;
346
	}
347
348
	/**
349
	 * Add the custom entry meta key to make it searchable and sortable
350
	 *
351
	 * @see https://www.gravityhelp.com/documentation/article/gform_entry_meta/
352
	 *
353
	 * @param array $entry_meta Array of custom entry meta keys with associative arrays
354
	 *
355
	 * @return mixed
356
	 */
357 165
	function add_entry_meta( $entry_meta ) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
358
359 165
		if( ! isset( $entry_meta["{$this->entry_meta_key}"] ) ) {
360
361
			$added_meta = array(
362 165
				'label'             => $this->label,
363 165
				'is_numeric'        => $this->is_numeric,
364 165
				'is_default_column' => $this->entry_meta_is_default_column,
365
			);
366
367 165
			if ( $this->entry_meta_update_callback && is_callable( $this->entry_meta_update_callback ) ) {
368
				$added_meta['update_entry_meta_callback'] = $this->entry_meta_update_callback;
369
			}
370
371 165
			$entry_meta["{$this->entry_meta_key}"] = $added_meta;
372
373
		} else {
374
			gravityview()->log->error( 'Entry meta already set: {meta_key}', array( 'meta_key' => $this->entry_meta_key, 'data' =>  $entry_meta["{$this->entry_meta_key}"] ) );
375
		}
376
377 165
		return $entry_meta;
378
	}
379
380
	private function field_support_options() {
381
		$options = array(
382
			'link_to_post' => array(
383
				'type' => 'checkbox',
384
				'label' => __( 'Link to the post', 'gravityview' ),
385
				'desc' => __( 'Link to the post created by the entry.', 'gravityview' ),
386
				'value' => false,
387
			),
388
			'link_to_term' => array(
389
				'type' => 'checkbox',
390
				'label' => __( 'Link to the category or tag', 'gravityview' ),
391
				'desc' => __( 'Link to the current category or tag. "Link to single entry" must be unchecked.', 'gravityview' ),
392
				'value' => false,
393
			),
394
			'dynamic_data' => array(
395
				'type' => 'checkbox',
396
				'label' => __( 'Use the live post data', 'gravityview' ),
397
				'desc' => __( 'Instead of using the entry data, instead use the current post data.', 'gravityview' ),
398
				'value' => true,
399
			),
400
			'date_display' => array(
401
				'type' => 'text',
402
				'label' => __( 'Override Date Format', 'gravityview' ),
403
				'desc' => sprintf( __( 'Define how the date is displayed (using %sthe PHP date format%s)', 'gravityview'), '<a href="https://wordpress.org/support/article/formatting-date-and-time/" rel="external">', '</a>' ),
404
				/**
405
				 * @filter `gravityview_date_format` Override the date format with a [PHP date format](https://codex.wordpress.org/Formatting_Date_and_Time)
406
				 * @param[in,out] null|string $date_format Date Format (default: null)
407
				 */
408
				'value' => apply_filters( 'gravityview_date_format', null ),
409
				'class' => 'code',
410
			),
411
			'new_window' => array(
412
				'type' => 'checkbox',
413
				'label' => __( 'Open link in a new tab or window?', 'gravityview' ),
414
				'value' => false,
415
			),
416
			'is_numeric' => array(
417
				'type'  => 'checkbox',
418
				'label' => __( 'This content is numeric', 'gravityview' ),
419
				'desc'  => __( 'Treat field values as numbers when sorting. By default, this field will be sorted alphabetically.', 'gravityview' ),
420
				'value' => false,
421
			),
422
		);
423
424
		/**
425
		 * @filter `gravityview_field_support_options` Modify the settings that a field supports
426
		 * @param array $options Options multidimensional array with each key being the input name, with each array setting having `type`, `label`, `desc` and `value` (default values) keys
427
		 */
428
		return apply_filters( 'gravityview_field_support_options', $options );
429
	}
430
431
	function add_field_support( $key, &$field_options ) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
432
433
		$options = $this->field_support_options();
434
435
		if( isset( $options[ $key ] ) ) {
436
			$field_options[ $key ] = $options[ $key ];
437
		}
438
439
		return $field_options;
440
	}
441
442
	/**
443
	 * Tap in here to modify field options.
444
	 *
445
	 * Here's an example:
446
	 *
447
	 * <pre>
448
	 * $field_options['name_display'] = array(
449
	 * 	'type' => 'select',
450
	 * 	'label' => __( 'User Format', 'gravityview' ),
451
	 * 	'desc' => __( 'How should the User information be displayed?', 'gravityview'),
452
	 * 	'choices' => array(
453
	 * 		array(
454
	 *		 	'value' => 'display_name',
455
	 *		  	'label' => __('Display Name (Example: "Ellen Ripley")', 'gravityview'),
456
	 *		),
457
	 *  	array(
458
	 *			'value' => 'user_login',
459
	 *			'label' => __('Username (Example: "nostromo")', 'gravityview')
460
	 *		),
461
	 * 	 'value' => 'display_name'
462
	 * );
463
	 * </pre>
464
	 *
465
	 * @param  array      $field_options [description]
466
	 * @param  string      $template_id   [description]
467
	 * @param  string      $field_id      [description]
468
	 * @param  string      $context       [description]
469
	 * @param  string      $input_type    [description]
470
	 * @return array                     [description]
471
	 */
472
	public function field_options( $field_options, $template_id, $field_id, $context, $input_type, $form_id ) {
473
474
		$this->_field_options = $field_options;
475
		$this->_field_id = $field_id;
476
477
		return $field_options;
478
	}
479
480
	/**
481
	 * Check whether the `enableChoiceValue` flag is set for a GF field
482
	 *
483
	 * Gets the current form ID, gets the field at that ID, then checks for the enableChoiceValue value.
484
	 *
485
	 * @access protected
486
	 *
487
	 * @uses GFAPI::get_form
488
	 *
489
	 * @since 1.17
490
	 *
491
	 * @return bool True: Enable Choice Value is active for field; False: not active, or form invalid, or form not found.
492
	 */
493
	protected function is_choice_value_enabled() {
494
495
		// If "Add Field" button is processing, get the Form ID
496
		$connected_form = \GV\Utils::_POST( 'form_id' );
497
498
		// Otherwise, get the Form ID from the Post page
499
		if( empty( $connected_form ) ) {
500
			$connected_form = gravityview_get_form_id( get_the_ID() );
501
		}
502
503
		if( empty( $connected_form ) ) {
504
			gravityview()->log->error( 'Form not found for form ID "{form_id}"', array( 'form_id' => $connected_form ) );
505
			return false;
506
		}
507
508
		$form = GFAPI::get_form( $connected_form );
509
510
		if ( ! $form ) {
511
			gravityview()->log->error( 'Form not found for field ID of "{field_id}", when checking for a form with ID of "{form_id}"', array( 'field_id' => $this->_field_id, 'form_id' => $connected_form ) );
512
			return false;
513
		}
514
515
		$field = gravityview_get_field( $form, $this->_field_id );
516
517
		return ! empty( $field->enableChoiceValue );
518
	}
519
520
}
521