Completed
Push — develop ( 92f117...50ff57 )
by Zack
17:06
created

GVLogic_Shortcode   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 437
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 3

Test Coverage

Coverage 4.8%

Importance

Changes 0
Metric Value
dl 0
loc 437
ccs 6
cts 125
cp 0.048
rs 8.8798
c 0
b 0
f 0
wmc 44
lcom 2
cbo 3

13 Methods

Rating   Name   Duplication   Size   Complexity  
A get_instance() 0 8 2
A __construct() 0 3 1
A add_hooks() 0 4 1
A get_operators() 0 14 3
A set_operation() 0 12 2
A setup_operation_and_comparison() 0 18 6
B shortcode() 0 50 6
A reset() 0 6 1
A set_is_match() 0 18 4
A get_output() 0 26 3
A set_content_and_else_content() 0 21 5
B process_elseif() 0 42 5
B parse_atts() 0 39 5

How to fix   Complexity   

Complex Class

Complex classes like GVLogic_Shortcode often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use GVLogic_Shortcode, and based on these observations, apply Extract Interface, too.

1
<?php
0 ignored issues
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 6 and the first side effect is on line 444.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
2
3
/**
4
 * Shortcode to handle showing/hiding content in merge tags. Works great with GravityView Custom Content fields
5
 */
6
class GVLogic_Shortcode {
0 ignored issues
show
Coding Style introduced by
Since you have declared the constructor as private, maybe you should also declare the class as final.
Loading history...
7
8
	private static $SUPPORTED_SCALAR_OPERATORS = array( 'is', 'isnot', 'contains', 'starts_with', 'ends_with' );
9
10
	private static $SUPPORTED_NUMERIC_OPERATORS = array( 'greater_than', 'less_than' );
11
12
	private static $SUPPORTED_ARRAY_OPERATORS = array( 'in', 'not_in', 'isnot', 'contains' );
13
14
	private static $SUPPORTED_CUSTOM_OPERATORS = array( 'equals', 'greater_than_or_is', 'greater_than_or_equals', 'less_than_or_is', 'less_than_or_equals', 'not_contains' );
15
16
	/**
17
	 * Attributes passed to the shortcode
18
	 * @var array
19
	 */
20
	var $passed_atts;
0 ignored issues
show
Coding Style introduced by
The visibility should be declared for property $passed_atts.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
21
22
	/**
23
	 * Content inside the shortcode, displayed if matched
24
	 * @var string
25
	 */
26
	var $passed_content;
0 ignored issues
show
Coding Style introduced by
The visibility should be declared for property $passed_content.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
27
28
	/**
29
	 * Parsed attributes
30
	 * @var array
31
	 */
32
	var $atts = array();
0 ignored issues
show
Coding Style introduced by
The visibility should be declared for property $atts.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
33
34
	/**
35
	 * Parsed content, shown if matched
36
	 * @var string
37
	 */
38
	var $content = '';
0 ignored issues
show
Coding Style introduced by
The visibility should be declared for property $content.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
39
40
	/**
41
	 * Content shown if not matched
42
	 * This is set by having `[else]` inside the $content block
43
	 * @var string
44
	 */
45
	var $else_content = '';
0 ignored issues
show
Coding Style introduced by
The visibility should be declared for property $else_content.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
46
47
	/**
48
	 * The current shortcode name being processed
49
	 * @var string
50
	 */
51
	var $shortcode = 'gvlogic';
0 ignored issues
show
Coding Style introduced by
The visibility should be declared for property $shortcode.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
52
53
	/**
54
	 * The left side of the comparison
55
	 * @var string
56
	 */
57
	var $if = '';
0 ignored issues
show
Coding Style introduced by
The visibility should be declared for property $if.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
58
59
	/**
60
	 * Special logged_in condition.
61
	 * @since 2.3
62
	 * @var bool
63
	 */
64
	var $logged_in = null;
0 ignored issues
show
Coding Style introduced by
The visibility should be declared for property $logged_in.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
65
66
	/**
67
	 * The right side of the comparison
68
	 * @var string
69
	 */
70
	var $comparison = '';
0 ignored issues
show
Coding Style introduced by
The visibility should be declared for property $comparison.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
71
72
	/**
73
	 * The comparison operator
74
	 * @since 1.21.5
75
	 * @since 2.0 Changed default from "is" to "isnot"
76
	 * @var string
77
	 */
78
	var $operation = 'isnot';
0 ignored issues
show
Coding Style introduced by
The visibility should be declared for property $operation.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
79
80
	/**
81
	 * Does the comparison pass?
82
	 * @var bool
83
	 */
84
	var $is_match = false;
0 ignored issues
show
Coding Style introduced by
The visibility should be declared for property $is_match.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
85
86
	/**
87
	 * @var GVLogic_Shortcode
88
	 */
89
	private static $instance;
90
91
	/**
92
	 * Instantiate!
93
	 * @return GVLogic_Shortcode
94
	 */
95 1
	public static function get_instance() {
96
97 1
		if( empty( self::$instance ) ) {
98
			self::$instance = new self;
99
		}
100
101 1
		return self::$instance;
102
	}
103
104
	/**
105
	 * Add the WordPress hooks
106
	 * @return void
0 ignored issues
show
Comprehensibility Best Practice introduced by
Adding a @return annotation to constructors is generally not recommended as a constructor does not have a meaningful return value.

Adding a @return annotation to a constructor is not recommended, since a constructor does not have a meaningful return value.

Please refer to the PHP core documentation on constructors.

Loading history...
107
	 */
108
	private function __construct() {
109
		$this->add_hooks();
110
	}
111
112
	/**
113
	 * Register the shortcode
114
	 * @return void
115
	 */
116
	private function add_hooks() {
117
		add_shortcode( 'gvlogic', array( $this, 'shortcode' ) );
118
		add_shortcode( 'gvlogicelse', array( $this, 'shortcode' ) );
119
	}
120
121
	/**
122
	 * Get array of supported operators
123
	 * @param bool $with_values
124
	 *
125
	 * @return array
126
	 */
127
	private function get_operators( $with_values = false ) {
128
129
		$operators = array_merge( self::$SUPPORTED_ARRAY_OPERATORS, self::$SUPPORTED_NUMERIC_OPERATORS, self::$SUPPORTED_SCALAR_OPERATORS, self::$SUPPORTED_CUSTOM_OPERATORS );
130
131
		if( $with_values ) {
132
			$operators_with_values = array();
133
			foreach( $operators as $key ) {
134
				$operators_with_values[ $key ] = '';
135
			}
136
			return $operators_with_values;
137
		} else {
138
			return $operators;
139
		}
140
	}
141
142
	/**
143
	 * Set the operation for the shortcode.
144
	 * @param string $operation
145
	 *
146
	 * @return bool True: it's an allowed operation type and was added. False: invalid operation type
147
	 */
148
	private function set_operation( $operation = 'isnot' ) {
149
150
		$operators = $this->get_operators( false );
151
152
		if( !in_array( $operation, $operators ) ) {
0 ignored issues
show
introduced by
Expected 1 space after "!"; 0 found
Loading history...
153
			gravityview()->log->debug( ' Attempted to add invalid operation type. {operation}', array( 'operation' => $operation ) );
154
			return false;
155
		}
156
157
		$this->operation = $operation;
158
		return true;
159
	}
160
161
	/**
162
	 * Set the operation and comparison for the shortcode
163
	 *
164
	 * Loop through each attribute passed to the shortcode and see if it's a valid operator. If so, set it.
165
	 * Example: [gvlogic if="{example}" greater_than="5"]
166
	 * `greater_than` will be set as the operator
167
	 * `5` will be set as the comparison value
168
	 *
169
	 * @return bool True: we've got an operation and comparison value; False: no, we don't
170
	 */
171
	private function setup_operation_and_comparison() {
172
173
		if ( empty( $this->atts ) ) {
174
			return true;
175
		}
176
177
		foreach ( $this->atts as $key => $value ) {
178
179
			$valid = $this->set_operation( $key == 'else' ? 'isnot' : $key );
180
181
			if ( $valid ) {
182
				$this->comparison = $key == 'else' ? '' : $value;
183
				return true;
184
			}
185
		}
186
187
		return false;
188
	}
189
190
	/**
191
	 * @param array $atts User defined attributes in shortcode tag.
192
	 * @param null $content
193
	 * @param string $shortcode_tag
194
	 *
195
	 * @return string|null
196
	 */
197 5
	public function shortcode( $atts = array(), $content = NULL, $shortcode_tag = '' ) {
0 ignored issues
show
Coding Style introduced by
TRUE, FALSE and NULL must be lowercase; expected null, but found NULL.
Loading history...
198
199
		// Don't process except on frontend
200 5
		if ( gravityview()->request->is_admin() ) {
201 5
			return null;
202
		}
203
204
		if( empty( $atts ) ) {
205
			gravityview()->log->error( '$atts are empty.', array( 'data' => $atts ) );
206
			return null;
207
		}
208
209
		$this->passed_atts = $atts;
210
		$this->passed_content = $content;
211
		$this->content = '';
212
		$this->else_content = '';
213
		$this->atts = array();
214
		$this->shortcode = $shortcode_tag;
215
216
		$this->parse_atts();
217
218
		// Logged in operation
219
		if ( ! is_null( $this->logged_in ) ) {
220
			$this->setup_operation_and_comparison();
221
		} else if ( false === $this->if ) {
222
			gravityview()->log->error( '$atts->if is empty.', array( 'data' => $this->passed_atts ) );
223
			return null;
224
		} else {
225
			$setup = $this->setup_operation_and_comparison();
226
227
			// We need an operation and comparison value
228
			if( ! $setup ) {
229
				gravityview()->log->error( 'No valid operators were passed.', array( 'data' => $this->atts ) );
230
				return null;
231
			}
232
		}
233
234
		// Check if it's a match
235
		$this->set_is_match();
236
237
		// Set the content and else_content
238
		$this->set_content_and_else_content();
239
240
		// Return the value!
241
		$output = $this->get_output();
242
243
		$this->reset();
244
245
		return $output;
246
	}
247
248
	/**
249
	 * Restore the original settings for the shortcode
250
	 *
251
	 * @since 2.0 Needed because $atts can now be empty
252
	 *
253
	 * @return void
254
	 */
255
	private function reset() {
256
		$this->operation = 'isnot';
257
		$this->comparison = '';
258
		$this->passed_atts = array();
259
		$this->passed_content = '';
260
	}
261
262
	/**
263
	 * Does the if and the comparison match?
264
	 * @uses GVCommon::matches_operation
265
	 *
266
	 * @return void
267
	 */
268
	private function set_is_match() {
269
270
		$comparison_match = GVCommon::matches_operation( $this->if, $this->comparison, $this->operation );
271
272
		if ( is_null( $this->logged_in ) ) {
273
			$this->is_match = $comparison_match;
274
			return;
275
		}
276
277
		$logged_in_match = ! $this->logged_in ^ is_user_logged_in(); // XNOR
278
279
		// Only logged-in match
280
		if( 1 === sizeof( $this->passed_atts ) ) {
281
			$this->is_match = $logged_in_match;
282
		} else {
283
			$this->is_match = $logged_in_match && $comparison_match;
284
		}
285
	}
286
287
	/**
288
	 * Get the output for the shortcode, based on whether there's a matched value
289
	 *
290
	 * @return string HTML/Text output of the shortcode
291
	 */
292
	private function get_output() {
293
294
		if( $this->is_match ) {
295
			$output = $this->content;
296
		} else {
297
			$output = $this->else_content;
298
		}
299
300
		// Get recursive!
301
		$output = do_shortcode( $output );
302
303
		if ( class_exists( 'GFCommon' ) ) {
304
			$output = GFCommon::replace_variables( $output, array(), array(), false, true, false );
305
		}
306
307
		/**
308
		 * @filter `gravityview/gvlogic/output` Modify the [gvlogic] output
309
		 * @param string $output HTML/text output
310
		 * @param GVLogic_Shortcode $this This class
311
		 */
312
		$output = apply_filters('gravityview/gvlogic/output', $output, $this );
0 ignored issues
show
Coding Style introduced by
Expected 1 spaces after opening bracket; 0 found
Loading history...
313
314
		gravityview()->log->debug( 'Output: ', array( 'data' => $output ) );
315
316
		return $output;
317
	}
318
319
	/**
320
	 * Check for `[else]` tag inside the shortcode content. If exists, set the else_content variable.
321
	 * If not, use the `else` attribute passed by the shortcode, if exists.
322
	 *
323
	 * @return void
324
	 */
325
	private function set_content_and_else_content() {
326
327
		$passed_content = $this->passed_content;
328
329
		list( $before_else, $after_else ) = array_pad( explode( '[else]', $passed_content ), 2, NULL );
0 ignored issues
show
Coding Style introduced by
TRUE, FALSE and NULL must be lowercase; expected null, but found NULL.
Loading history...
330
		list( $before_else_if, $after_else_if ) = array_pad( explode( '[else', $passed_content ), 2, NULL );
0 ignored issues
show
Coding Style introduced by
TRUE, FALSE and NULL must be lowercase; expected null, but found NULL.
Loading history...
331
332
		$else_attr = isset( $this->atts['else'] ) ? $this->atts['else'] : NULL;
0 ignored issues
show
Coding Style introduced by
TRUE, FALSE and NULL must be lowercase; expected null, but found NULL.
Loading history...
333
		$else_content = isset( $after_else ) ? $after_else : $else_attr;
334
335
		// The content is everything OTHER than the [else]
336
		$this->content = $before_else_if;
337
338
		if ( ! $this->is_match ) {
339
			if( $elseif_content = $this->process_elseif( $before_else ) ) {
340
				$this->else_content = $elseif_content;
341
			} else {
342
				$this->else_content = $else_content;
343
			}
344
		}
345
	}
346
347
	/**
348
	 * Handle additional conditional logic inside the [else] pseudo-shortcode
349
	 *
350
	 * @since 1.21.2
351
	 *
352
	 * @param string $before_else Shortcode content before the [else] tag (if it exists)
353
	 *
354
	 * @return bool|string False: No [else if] statements found. Otherwise, return the matched content.
355
	 */
356
	private function process_elseif( $before_else ) {
357
358
		$regex = get_shortcode_regex( array( 'else' ) );
359
360
		// 2. Check if there are any ELSE IF statements
361
		preg_match_all( '/' . $regex . '/', $before_else . '[/else]', $else_if_matches, PREG_SET_ORDER );
362
363
		// 3. The ELSE IF statements that remain should be processed to see if they are valid
364
		foreach ( $else_if_matches as $key => $else_if_match ) {
365
366
			// If $else_if_match[5] exists and has content, check for more shortcodes
367
			preg_match_all( '/' . $regex . '/', $else_if_match[5] . '[/else]', $recursive_matches, PREG_SET_ORDER );
368
369
			// If the logic passes, this is the value that should be used for $this->else_content
370
			$else_if_value = $else_if_match[5];
371
			$check_elseif_match = $else_if_match[0];
372
373
			// Retrieve the value of the match that is currently being checked, without any other [else] tags
374
			if( ! empty( $recursive_matches[0][0] ) ) {
375
				$else_if_value = str_replace( $recursive_matches[0][0], '', $else_if_value );
376
				$check_elseif_match = str_replace( $recursive_matches[0][0], '', $check_elseif_match );
377
			}
378
379
			$check_elseif_match = str_replace( '[else', '[gvlogicelse', $check_elseif_match );
380
			$check_elseif_match = str_replace( '[/else', '[/gvlogicelse', $check_elseif_match );
381
382
			// Make sure to close the tag
383
			if ( '[/gvlogicelse]' !== substr( $check_elseif_match, -14, 14 ) ) {
384
				$check_elseif_match .= '[/gvlogicelse]';
385
			}
386
387
			// The shortcode returned a value; it was a match
388
			if ( $result = do_shortcode( $check_elseif_match ) ) {
389
				return $else_if_value;
390
			}
391
392
			// Process any remaining [else] tags
393
			return $this->process_elseif( $else_if_match[5] );
394
		}
395
396
		return false;
397
	}
398
399
	/**
400
	 * Process the attributes passed to the shortcode. Make sure they're valid
401
	 * @return void
402
	 */
403
	private function parse_atts() {
404
405
		$supported = array(
406
			'if' => false,
407
			'else' => false,
408
			'logged_in' => null,
409
		);
410
411
		$supported_args = $supported + $this->get_operators( true );
412
413
		// Whittle down the attributes to only valid pairs
414
		$this->atts = shortcode_atts( $supported_args, $this->passed_atts, $this->shortcode );
415
416
		// Only keep the passed attributes after making sure that they're valid pairs
417
		$this->atts = array_intersect_key( $this->passed_atts, $this->atts );
418
419
		// Strip whitespace if it's not default false
420
		$this->if = ( isset( $this->atts['if'] ) && is_string( $this->atts['if'] ) ) ? trim( $this->atts['if'] ) : false;
421
422
		if ( isset( $this->atts['logged_in'] ) ) {
423
			// Truthy
424
			if ( in_array( strtolower( $this->atts['logged_in'] ), array( '0', 'false', 'no' ) ) ) {
425
				$this->logged_in = false;
426
			} else {
427
				$this->logged_in = true;
428
			}
429
		}
430
431
		/**
432
		 * @action `gravityview/gvlogic/parse_atts/after` Modify shortcode attributes after it's been parsed
433
		 * @see https://gist.github.com/zackkatz/def9b295b80c4ae109760ffba200f498 for an example
434
		 * @since 1.21.5
435
		 * @param GVLogic_Shortcode $this The GVLogic_Shortcode instance
436
		 */
437
		do_action( 'gravityview/gvlogic/parse_atts/after', $this );
438
439
		// Make sure the "if" isn't processed in self::setup_operation_and_comparison()
440
		unset( $this->atts['if'] );
441
	}
442
}
443
444
GVLogic_Shortcode::get_instance();
445