Fulfillable_Collection   B
last analyzed

Complexity

Total Complexity 44

Size/Duplication

Total Lines 370
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 56.16%

Importance

Changes 0
Metric Value
dl 0
loc 370
ccs 82
cts 146
cp 0.5616
rs 8.8798
c 0
b 0
f 0
wmc 44
lcom 1
cbo 4

17 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A create_collection() 0 3 1
A get_fulfillables() 0 3 1
A get_condition_type_list() 0 3 1
A set_condition_type_list() 0 6 1
A is_condition_type_list_whitelist() 0 3 1
A is_condition_type_allowed() 0 7 2
A or_where() 0 4 1
A where_array() 0 5 1
A where_collection() 0 7 1
A add_fulfillable() 0 11 2
A propagate_condition_type_list() 0 9 3
A remove_fulfillable() 0 12 3
A filter() 0 27 5
B evaluate() 0 39 8
A where() 0 26 5
B is_fulfilled() 0 29 7

How to fix   Complexity   

Complex Class

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

1
<?php
2
3
namespace Carbon_Fields\Container\Fulfillable;
4
5
use Carbon_Fields\Container\Condition\Factory;
6
use Carbon_Fields\Container\Fulfillable\Translator\Array_Translator;
7
use Carbon_Fields\Exception\Incorrect_Syntax_Exception;
8
9
class Fulfillable_Collection implements Fulfillable {
10
11
	/**
12
	 * Condition factory used to translated condition types
13
	 *
14
	 * @var Factory
15
	 */
16
	protected $condition_factory;
17
18
	/**
19
	 * Array translator used to support array representations of fulfillables
20
	 *
21
	 * @var Array_Translator
22
	 */
23
	protected $array_translator;
24
25
	/**
26
	 * Array of fulfillables in this collection
27
	 *
28
	 * @var array<array>
29
	 */
30
	protected $fulfillables = array();
31
32
	/**
33
	 * Array of supported fulfillable comparisons
34
	 *
35
	 * @var array<string>
36
	 */
37
	protected $supported_fulfillable_comparisons = array( 'AND', 'OR' );
38
39
	/**
40
	 * Array of allowed condition types which propagate to child collections
41
	 *
42
	 * @var array<string>
43
	 */
44
	protected $condition_type_list = array();
45
46
	/**
47
	 * Whether the condition type list is a whitelist or a blacklist
48
	 *
49
	 * @var bool
50
	 */
51
	protected $condition_type_list_whitelist = false;
52
53
	/**
54
	 * Constructor
55
	 *
56
	 * @param Factory          $condition_factory
57
	 * @param Array_Translator $array_translator
58
	 */
59
	public function __construct( Factory $condition_factory, Array_Translator $array_translator ) {
60
		$this->condition_factory = $condition_factory;
61
		$this->array_translator = $array_translator;
62
	}
63
64
	/**
65
	 * Create a new collection
66
	 *
67
	 * @return Fulfillable_Collection
68
	 */
69
	protected function create_collection() {
70
		return \Carbon_Fields\Carbon_Fields::resolve( 'container_condition_fulfillable_collection' );
71
	}
72
73
	/**
74
	 * Get an array of the fulfillables in this collection
75
	 *
76
	 * @return array<Fulfillable>
77
	 */
78 1
	public function get_fulfillables() {
79 1
		return $this->fulfillables;
80
	}
81
82
	/**
83
	 * Get array of allowed condition types
84
	 *
85
	 * @return array<string>
86
	 */
87
	public function get_condition_type_list() {
88
		return $this->condition_type_list;
89
	}
90
91
	/**
92
	 * Set array of allowed condition types
93
	 * WARNING: this will NOT remove already added conditions which are no longer allowed
94
	 *
95
	 * @param  array<string>          $condition_type_list
96
	 * @param  bool                   $whitelist
97
	 * @return Fulfillable_Collection $this
98
	 */
99
	public function set_condition_type_list( $condition_type_list, $whitelist ) {
100
		$this->condition_type_list_whitelist = $whitelist;
101
		$this->condition_type_list = $condition_type_list;
102
		$this->propagate_condition_type_list();
103
		return $this;
104
	}
105
106
	/**
107
	 * Check if conditions types list is a whitelist
108
	 *
109
	 * @return bool
110
	 */
111
	public function is_condition_type_list_whitelist() {
112
		return $this->condition_type_list_whitelist;
113
	}
114
115
	/**
116
	 * Check if condition type is allowed
117
	 *
118
	 * @param  string $condition_type
119
	 * @return bool
120
	 */
121
	public function is_condition_type_allowed( $condition_type ) {
122
		$in_list = in_array( $condition_type, $this->get_condition_type_list() );
123
		if ( $this->is_condition_type_list_whitelist() ) {
124
			return $in_list;
125
		}
126
		return ! $in_list;
127
	}
128
129
	/**
130
	 * Propagate allowed condition types to child collections
131
	 */
132
	protected function propagate_condition_type_list() {
133
		$condition_type_list = $this->get_condition_type_list();
134
		$fulfillables = $this->get_fulfillables();
135
		foreach ( $fulfillables as $fulfillable ) {
136
			if ( $fulfillable['fulfillable'] instanceof Fulfillable_Collection ) {
137
				$fulfillable['fulfillable']->set_condition_type_list( $condition_type_list, $this->is_condition_type_list_whitelist() );
138
			}
139
		}
140
	}
141
142
	/**
143
	 * Shorthand for where with OR comparison
144
	 *
145
	 * @param  string|array|callable  $condition_type
146
	 * @param  string                 $comparison_operator Can be skipped. Defaults to "="
147
	 * @param  mixed                  $value
148
	 * @return Fulfillable_Collection $this
149
	 */
150
	public function or_where( $condition_type, $comparison_operator = '=', $value = null ) {
151
		$this->where( $condition_type, $comparison_operator, $value, 'OR' );
152
		return $this;
153
	}
154
155
	/**
156
	 * Add fulfillable with optional comparison_operator
157
	 * This method assumes there is no fulfillable that can be compared with literal NULL
158
	 *
159
	 * @param  string|array|callable  $condition_type
160
	 * @param  string                 $comparison_operator Can be skipped. Defaults to "="
161
	 * @param  mixed                  $value
162
	 * @param  string                 $fulfillable_comparison
163
	 * @return Fulfillable_Collection $this
164
	 */
165 9
	public function where( $condition_type, $comparison_operator = '=', $value = null, $fulfillable_comparison = 'AND' ) {
166 9
		if ( is_array( $condition_type ) ) {
167 2
			return $this->where_array( $condition_type, $fulfillable_comparison );
168
		}
169
170 7
		if ( $condition_type instanceof \Closure ) {
171 2
			return $this->where_collection( $condition_type, $fulfillable_comparison );
172
		}
173
174 7
		if ( ! $this->is_condition_type_allowed( $condition_type ) ) {
175
			Incorrect_Syntax_Exception::raise( 'Unsupported container condition used: ' . $condition_type );
176
			return $this;
177
		}
178
179 7
		if ( $value === null ) {
180
			// We do not have a supplied comparison_operator so we default to "="
181 5
			$value = $comparison_operator;
182 5
			$comparison_operator = '=';
183 5
		}
184
185 7
		$condition = $this->condition_factory->make( $condition_type );
186 6
		$condition->set_comparison_operator( $comparison_operator );
187 6
		$condition->set_value( $value );
188 6
		$this->add_fulfillable( $condition, $fulfillable_comparison );
189 6
		return $this;
190
	}
191
192
	/**
193
	 * Add a Fulfillable through array representation
194
	 *
195
	 * @param  array                  $fulfillable_as_array
196
	 * @param  string                 $fulfillable_comparison
197
	 * @return Fulfillable_Collection $this
198
	 */
199
	protected function where_array( $fulfillable_as_array, $fulfillable_comparison) {
200
		$fulfillable = $this->array_translator->foreign_to_fulfillable( $fulfillable_as_array );
201
		$this->add_fulfillable( $fulfillable, $fulfillable_comparison );
202
		return $this;
203
	}
204
205
	/**
206
	 * Add a Fulfillable_Collection for nested logic
207
	 *
208
	 * @param  callable               $collection_callable
209
	 * @param  string                 $fulfillable_comparison
210
	 * @return Fulfillable_Collection $this
211
	 */
212
	protected function where_collection( $collection_callable, $fulfillable_comparison) {
213
		$collection = $this->create_collection();
214
		$collection->set_condition_type_list( $this->get_condition_type_list(), $this->is_condition_type_list_whitelist() );
215
		$collection_callable( $collection );
216
		$this->add_fulfillable( $collection, $fulfillable_comparison );
217
		return $this;
218
	}
219
220
	/**
221
	 * Add fulfillable to collection
222
	 *
223
	 * @param Fulfillable $fulfillable
224
	 * @param string      $fulfillable_comparison See static::$supported_fulfillable_comparisons
225
	 */
226 2
	public function add_fulfillable( Fulfillable $fulfillable, $fulfillable_comparison ) {
227 2
		if ( ! in_array( $fulfillable_comparison, $this->supported_fulfillable_comparisons ) ) {
228 1
			Incorrect_Syntax_Exception::raise( 'Invalid fulfillable comparison passed: ' . $fulfillable_comparison );
229
			return;
230
		}
231
232 1
		$this->fulfillables[] = array(
233 1
			'fulfillable_comparison' => $fulfillable_comparison,
234 1
			'fulfillable' => $fulfillable,
235
		);
236 1
	}
237
238
	/**
239
	 * Remove fulfillable from collection
240
	 *
241
	 * @param Fulfillable $fulfillable
242
	 * @return bool Fulfillable found and removed
243
	 */
244
	public function remove_fulfillable( Fulfillable $fulfillable ) {
245
		$fulfillables = $this->get_fulfillables();
246
		foreach ( $fulfillables as $index => $fulfillable_tuple ) {
247
			if ( $fulfillable_tuple['fulfillable'] === $fulfillable ) {
248
				$fulfillables_copy = $fulfillables; // introduce a copy array to highlight array_splice mutation
249
				array_splice( $fulfillables_copy, $index, 1 );
250
				$this->fulfillables = array_values( $fulfillables_copy ); // make sure our array is indexed cleanly
251
				return true;
252
			}
253
		}
254
		return false;
255
	}
256
257
	/**
258
	 * Get a copy of the collection with conditions not in the whitelist filtered out
259
	 *
260
	 * @param  array<string>          $condition_whitelist
261
	 * @return Fulfillable_Collection
262
	 */
263 2
	public function filter( $condition_whitelist ) {
264 2
		$fulfillables = $this->get_fulfillables();
265
266 2
		$collection = $this->create_collection();
267 2
		foreach ( $fulfillables as $fulfillable_tuple ) {
268 2
			$fulfillable = $fulfillable_tuple['fulfillable'];
269 2
			$fulfillable_comparison = $fulfillable_tuple['fulfillable_comparison'];
270
271 2
			if ( $fulfillable instanceof Fulfillable_Collection ) {
272 1
				$filtered_collection = $fulfillable->filter( $condition_whitelist );
273 1
				$filtered_collection_fulfillables = $filtered_collection->get_fulfillables();
274 1
				if ( empty( $filtered_collection_fulfillables ) ) {
275 1
					continue; // skip empty collections to reduce clutter
276
				}
277
				$collection->add_fulfillable( $filtered_collection, $fulfillable_comparison );
278
			} else {
279 2
				$type = $this->condition_factory->get_type( get_class( $fulfillable ) );
280 2
				if ( ! in_array( $type, $condition_whitelist ) ) {
281 2
					continue;
282
				}
283
284 2
				$fulfillable_clone = clone $fulfillable;
285 2
				$collection->add_fulfillable( $fulfillable_clone, $fulfillable_comparison );
286
			}
287 2
		}
288 2
		return $collection;
289
	}
290
291
	/**
292
	 * Get a copy of the collection with passed conditions evaluated into boolean conditions
293
	 * Useful when evaluating only certain condition types but preserving the rest
294
	 * or when passing dynamic conditions to the front-end
295
	 *
296
	 * @param  array<string>          $condition_types
297
	 * @param  array|boolean          $environment Environment array or a boolean value to force on conditions
298
	 * @param  array<string>          $comparison_operators Array of comparison operators to evaluate regardless of condition type
299
	 * @param  boolean                $condition_types_blacklist Whether the condition list should act as a blacklist
300
	 * @param  boolean                $comparison_operators_blacklist Whether the comparison operators list should act as a blacklist
301
	 * @return Fulfillable_Collection
302
	 */
303 3
	public function evaluate( $condition_types, $environment, $comparison_operators = array(), $condition_types_blacklist = false, $comparison_operators_blacklist = false ) {
304 3
		$fulfillables = $this->get_fulfillables();
305
306 3
		$collection = $this->create_collection();
307 3
		foreach ( $fulfillables as $fulfillable_tuple ) {
308 3
			$fulfillable = $fulfillable_tuple['fulfillable'];
309 3
			$fulfillable_comparison = $fulfillable_tuple['fulfillable_comparison'];
310
311 3
			if ( $fulfillable instanceof Fulfillable_Collection ) {
312
				$evaluated_collection = $fulfillable->evaluate( $condition_types, $environment, $comparison_operators, $condition_types_blacklist, $comparison_operators_blacklist );
313
				$collection->add_fulfillable( $evaluated_collection, $fulfillable_comparison );
314
			} else {
315 3
				$type = $this->condition_factory->get_type( get_class( $fulfillable ) );
316 3
				$comparison_operator = $fulfillable->get_comparison_operator();
317
318 3
				$condition_type_match = in_array( $type, $condition_types );
319 3
				if ( $condition_types_blacklist ) {
320
					$condition_type_match = ! $condition_type_match;
321
				}
322
323 3
				$comparison_operator_match = in_array( $comparison_operator, $comparison_operators );
324 3
				if ( $comparison_operators_blacklist ) {
325
					$comparison_operator_match = ! $comparison_operator_match;
326
				}
327
328 3
				if ( $condition_type_match || $comparison_operator_match ) {
329 3
					$boolean_condition = $this->condition_factory->make( 'boolean' );
330 3
					$boolean_condition->set_comparison_operator( '=' );
331
332 3
					$value = is_bool( $environment ) ? $environment : $fulfillable->is_fulfilled( $environment );
333 3
					$boolean_condition->set_value( $value );
334 3
					$collection->add_fulfillable( $boolean_condition, $fulfillable_comparison );
335 3
				} else {
336 3
					$collection->add_fulfillable( clone $fulfillable, $fulfillable_comparison );
337
				}
338
			}
339 3
		}
340 3
		return $collection;
341
	}
342
343
	/**
344
	 * Check if all fulfillables are fulfilled taking into account their fulfillable comparison
345
	 *
346
	 * @param  array $environment
347
	 * @return bool
348
	 */
349 14
	public function is_fulfilled( $environment ) {
350 14
		$fulfilled = true; // return true for empty collections
351 14
		$fulfillables = $this->get_fulfillables();
352
353 14
		foreach ( $fulfillables as $i => $fulfillable_tuple ) {
354 13
			$fulfillable = $fulfillable_tuple['fulfillable'];
355 13
			$fulfillable_comparison = $fulfillable_tuple['fulfillable_comparison'];
356
357 13
			if ( $i === 0 ) {
358
				// Ignore first comparison as we need a base fulfillment value
359 13
				$fulfilled = $fulfillable->is_fulfilled( $environment );
360 13
				continue;
361
			}
362
363
			// minor optimization - avoid unnecessary AND check if $fulfilled is currently false
364
			// false && whatever is always false
365 4
			if ( $fulfillable_comparison == 'AND' && $fulfilled ) {
366 2
				$fulfilled = $fulfillable->is_fulfilled( $environment );
367 2
			}
368
369
			// minor optimization - avoid unnecessary OR check if $fulfilled is currently true
370
			// true || whatever is always true
371 4
			if ( $fulfillable_comparison == 'OR' && ! $fulfilled ) {
372 1
				$fulfilled = $fulfillable->is_fulfilled( $environment );
373 1
			}
374 14
		}
375
376 14
		return $fulfilled;
377
	}
378
}
379