Completed
Push — develop ( 78a124...956be6 )
by Aristeides
03:10
created

Kirki_Output   C

Complexity

Total Complexity 59

Size/Duplication

Total Lines 270
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 1

Importance

Changes 0
Metric Value
dl 0
loc 270
rs 6.1904
c 0
b 0
f 0
wmc 59
lcom 1
cbo 1

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 1
A apply_sanitize_callback() 0 14 4
D apply_value_pattern() 0 47 23
C parse_output() 0 50 16
C process_output() 0 24 10
A process_property_value() 0 13 2
A process_value() 0 6 2
A get_styles() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like Kirki_Output 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 Kirki_Output, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Handles CSS output for fields.
4
 *
5
 * @package     Kirki
6
 * @subpackage  Controls
7
 * @copyright   Copyright (c) 2017, Aristeides Stathopoulos
8
 * @license     http://opensource.org/licenses/https://opensource.org/licenses/MIT
9
 * @since       2.2.0
10
 */
11
12
/**
13
 * Handles field CSS output.
14
 */
15
class Kirki_Output {
16
17
	/**
18
	 * The Kirki configuration used in the field.
19
	 *
20
	 * @access protected
21
	 * @var string
22
	 */
23
	protected $config_id = 'global';
24
25
	/**
26
	 * The field's `output` argument.
27
	 *
28
	 * @access protected
29
	 * @var array
30
	 */
31
	protected $output = array();
32
33
	/**
34
	 * An array of the generated styles.
35
	 *
36
	 * @access protected
37
	 * @var array
38
	 */
39
	protected $styles = array();
40
41
	/**
42
	 * The value.
43
	 *
44
	 * @access protected
45
	 * @var string|array
46
	 */
47
	protected $value;
48
49
	/**
50
	 * The class constructor.
51
	 *
52
	 * @access public
53
	 * @param string       $config_id The config ID.
54
	 * @param array        $output    The output argument.
55
	 * @param string|array $value     The value.
56
	 */
57
	public function __construct( $config_id, $output, $value ) {
58
59
		$this->config_id = $config_id;
60
		$this->value     = $value;
61
		$this->output    = $output;
62
63
		$this->parse_output();
64
	}
65
66
	/**
67
	 * If we have a sanitize_callback defined, apply it to the value.
68
	 *
69
	 * @param array        $output The output args.
70
	 * @param string|array $value  The value.
71
	 *
72
	 * @return string|array
73
	 */
74
	protected function apply_sanitize_callback( $output, $value ) {
75
76
		if ( isset( $output['sanitize_callback'] ) && null !== $output['sanitize_callback'] ) {
77
78
			// If the sanitize_callback is invalid, return the value.
79
			if ( ! is_callable( $output['sanitize_callback'] ) ) {
80
				return $value;
81
			}
82
			return call_user_func( $output['sanitize_callback'], $this->value );
83
		}
84
85
		return $value;
86
87
	}
88
89
	/**
90
	 * If we have a value_pattern defined, apply it to the value.
91
	 *
92
	 * @param array        $output The output args.
93
	 * @param string|array $value  The value.
94
	 *
95
	 * @return string|array
96
	 */
97
	protected function apply_value_pattern( $output, $value ) {
98
99
		if ( isset( $output['value_pattern'] ) && ! empty( $output['value_pattern'] ) ) {
100
			if ( is_string( $output['value_pattern'] ) ) {
101
				$value = str_replace( '$', $value, $output['value_pattern'] );
102
				if ( isset( $output['pattern_replace'] ) && is_array( $output['pattern_replace'] ) ) {
103
					$option_type = 'theme_mod';
104
					$option_name = false;
105
					if ( isset( Kirki::$config[ $this->config_id ] ) ) {
106
						$config = Kirki::$config[ $this->config_id ];
107
						$option_type = ( isset( $config['option_type'] ) ) ? $config['option_type'] : 'theme_mod';
108
						if ( 'option' === $option_type || 'site_option' === $option_type ) {
109
							$option_name = ( isset( $config['option_name'] ) ) ? $config['option_name'] : false;
110
						}
111
					}
112
					if ( $option_name ) {
113
						$options = ( 'site_option' === $option_type ) ? get_site_option( $option_name ) : get_option( $option_name );
114
					}
115
					foreach ( $output['pattern_replace'] as $search => $replace ) {
116
						$replacement = '';
117
						switch ( $option_type ) {
118
							case 'option':
119
								$replacement = ( is_array( $options ) && isset( $options[ $replace ] ) ) ? $options[ $replace ] : get_option( $replace );
0 ignored issues
show
Bug introduced by
The variable $options does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
120
								break;
121
							case 'site_option':
122
								$replacement = ( is_array( $options ) && isset( $options[ $replace ] ) ) ? $options[ $replace ] : get_site_option( $replace );
123
								break;
124
							case 'user_meta':
125
								$user_id = get_current_user_id();
126
								if ( $user_id ) {
127
									// @codingStandardsIgnoreLine
128
									$replacement = get_user_meta( $user_id, $replace, true );
129
								}
130
								break;
131
							default:
132
								$replacement = get_theme_mod( $replace );
133
						}
134
						$replacement = ( false === $replacement ) ? '' : $replacement;
135
						$value = str_replace( $search, $replacement, $value );
136
					}
137
				} // End if().
138
			} // End if().
139
		} // End if().
140
141
		return $value;
142
143
	}
144
145
	/**
146
	 * Parses the output arguments.
147
	 * Calls the process_output method for each of them.
148
	 *
149
	 * @access protected
150
	 */
151
	protected function parse_output() {
152
		foreach ( $this->output as $output ) {
153
			$skip = false;
154
155
			// Apply any sanitization callbacks defined.
156
			$value = $this->apply_sanitize_callback( $output, $this->value );
157
158
			// Skip if value is empty.
159
			if ( '' === $this->value ) {
160
				$skip = true;
161
			}
162
163
			// No need to proceed this if the current value is the same as in the "exclude" value.
164
			if ( isset( $output['exclude'] ) && false !== $output['exclude'] && is_array( $output['exclude'] ) ) {
165
				foreach ( $output['exclude'] as $exclude ) {
166
					if ( is_array( $value ) && is_array( $exclude ) ) {
167
						$diff1 = array_diff( $value, $exclude );
168
						$diff2 = array_diff( $exclude, $value );
169
170
						if ( empty( $diff1 ) && empty( $diff2 ) ) {
171
							$skip = true;
172
						}
173
					}
174
					if ( $skip ) {
175
						continue;
176
					}
177
178
					// Skip if value is defined as excluded.
179
					if ( $exclude === $value ) {
180
						$skip = true;
181
					}
182
				}
183
			}
184
			if ( $skip ) {
185
				continue;
186
			}
187
188
			// Apply any value patterns defined.
189
			$value = $this->apply_value_pattern( $output, $value );
190
191
			if ( isset( $output['element'] ) && is_array( $output['element'] ) ) {
192
				$output['element'] = array_unique( $output['element'] );
193
				sort( $output['element'] );
194
				$output['element'] = implode( ',', $output['element'] );
195
			}
196
197
			$value = $this->process_value( $value, $output );
198
			$this->process_output( $output, $value );
0 ignored issues
show
Bug introduced by
It seems like $value defined by $this->process_value($value, $output) on line 197 can also be of type array; however, Kirki_Output::process_output() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
199
		} // End foreach().
200
	}
201
202
	/**
203
	 * Parses an output and creates the styles array for it.
204
	 *
205
	 * @access protected
206
	 * @param array  $output The field output.
207
	 * @param string $value  The value.
208
	 *
209
	 * @return void
210
	 */
211
	protected function process_output( $output, $value ) {
212
		if ( ! isset( $output['element'] ) || ! isset( $output['property'] ) ) {
213
			return;
214
		}
215
		$output['media_query'] = ( isset( $output['media_query'] ) ) ? $output['media_query'] : 'global';
216
		$output['prefix']      = ( isset( $output['prefix'] ) )      ? $output['prefix']      : '';
217
		$output['units']       = ( isset( $output['units'] ) )       ? $output['units']       : '';
218
		$output['suffix']      = ( isset( $output['suffix'] ) )      ? $output['suffix']      : '';
219
220
		// Properties that can accept multiple values.
221
		// Useful for example for gradients where all browsers use the "background-image" property
222
		// and the browser prefixes go in the value_pattern arg.
223
		$accepts_multiple = array(
224
			'background-image',
225
		);
226
		if ( in_array( $output['property'], $accepts_multiple ) ) {
227
			if ( isset( $this->styles[ $output['media_query'] ][ $output['element'] ][ $output['property'] ] ) && ! is_array( $this->styles[ $output['media_query'] ][ $output['element'] ][ $output['property'] ] ) ) {
228
				$this->styles[ $output['media_query'] ][ $output['element'] ][ $output['property'] ] = (array) $this->styles[ $output['media_query'] ][ $output['element'] ][ $output['property'] ];
229
			}
230
			$this->styles[ $output['media_query'] ][ $output['element'] ][ $output['property'] ][] = $output['prefix'] . $value . $output['units'] . $output['suffix'];
231
			return;
232
		}
233
		$this->styles[ $output['media_query'] ][ $output['element'] ][ $output['property'] ] = $output['prefix'] . $value . $output['units'] . $output['suffix'];
234
	}
235
236
	/**
237
	 * Some CSS properties are unique.
238
	 * We need to tweak the value to make everything works as expected.
239
	 *
240
	 * @access protected
241
	 * @param string $property The CSS property.
242
	 * @param string $value    The value.
243
	 *
244
	 * @return array
245
	 */
246
	protected function process_property_value( $property, $value ) {
247
		$properties = apply_filters( "kirki/{$this->config_id}/output/property-classnames", array(
248
			'font-family'         => 'Kirki_Output_Property_Font_Family',
249
			'background-image'    => 'Kirki_Output_Property_Background_Image',
250
			'background-position' => 'Kirki_Output_Property_Background_Position',
251
		) );
252
		if ( array_key_exists( $property, $properties ) ) {
253
			$classname = $properties[ $property ];
254
			$obj = new $classname( $property, $value );
255
			return $obj->get_value();
256
		}
257
		return $value;
258
	}
259
260
	/**
261
	 * Returns the value.
262
	 *
263
	 * @access protected
264
	 * @param string|array $value The value.
265
	 * @param array        $output The field "output".
266
	 * @return string|array
267
	 */
268
	protected function process_value( $value, $output ) {
269
		if ( isset( $output['property'] ) ) {
270
			return $this->process_property_value( $output['property'], $value );
0 ignored issues
show
Bug introduced by
It seems like $value defined by parameter $value on line 268 can also be of type array; however, Kirki_Output::process_property_value() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
271
		}
272
		return $value;
273
	}
274
275
	/**
276
	 * Exploses the private $styles property to the world
277
	 *
278
	 * @access protected
279
	 * @return array
280
	 */
281
	public function get_styles() {
282
		return $this->styles;
283
	}
284
}
285