Test Failed
Push — master ( 2fbbce...c5a571 )
by Tomáš
02:30
created

ContainerQuery   B

Complexity

Total Complexity 46

Size/Duplication

Total Lines 288
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 92
c 3
b 0
f 0
dl 0
loc 288
ccs 97
cts 97
cp 1
rs 8.72
wmc 46

17 Methods

Rating   Name   Duplication   Size   Complexity  
A ids() 0 3 1
B sortByPropertyCallback() 0 23 11
A desc() 0 3 1
A sortBy() 0 3 1
A get() 0 27 3
B pluckData() 0 16 7
A __construct() 0 3 1
A filterUnique() 0 14 5
A sort() 0 3 1
A applyFilters() 0 5 2
A getFirst() 0 6 2
A addSorter() 0 3 1
A sortData() 0 9 4
A filter() 0 3 1
A whereId() 0 5 2
A unique() 0 3 1
A only() 0 6 2

How to fix   Complexity   

Complex Class

Complex classes like ContainerQuery 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.

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 ContainerQuery, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
4
namespace TournamentGenerator\Containers;
5
6
use Closure;
7
use Exception;
8
use TournamentGenerator\Base;
9
use TournamentGenerator\Helpers\Sorter\BaseSorter;
10
use TournamentGenerator\Interfaces\WithId;
11
12
/**
13
 * Class ContainerQuery
14
 *
15
 * Container query is a helper class to filter, sort, etc. the values of the container hierarchy.
16
 *
17
 * @package TournamentGenerator\Containers
18
 * @author  Tomáš Vojík <[email protected]>
19
 * @since   0.4
20
 */
21
class ContainerQuery
22
{
23
24
	/** @var BaseContainer Queried container */
25
	protected BaseContainer $container;
26
	/** @var Closure[] Filter closures */
27
	protected array      $filters      = [];
28
	protected Closure    $sortClosure;
29
	protected string     $sortProperty;
30
	protected bool       $desc         = false;
31
	protected BaseSorter $sorter;
32
	protected bool       $topLevelOnly = false;
33
	protected bool       $uniqueOnly   = false;
34
	protected ?string    $pluck        = null;
35
36
	/**
37
	 * ContainerQuery constructor.
38
	 *
39
	 * @param BaseContainer $container Queried container
40 129
	 */
41 129
	public function __construct(BaseContainer $container, bool $topLevelOnly = false) {
42 129
		$this->container = $container;
43 129
		$this->topLevelOnly = $topLevelOnly;
44
	}
45
46
	/**
47
	 * Gets the first result of container query
48
	 *
49
	 * @return mixed|null
50 4
	 */
51 4
	public function getFirst() {
52 4
		$data = $this->get();
53 3
		if (count($data) === 0) {
54
			return null;
55 4
		}
56
		return reset($data);
57
	}
58
59
	/**
60
	 * Get the result
61
	 *
62
	 * @return array
63 128
	 */
64
	public function get() : array {
65 128
		// Get initial data
66 1
		if ($this->topLevelOnly) {
67
			$data = $this->container->getTopLevel();
68
		}
69 128
		else {
70
			$data = $this->container->get();
71
		}
72
73 128
		// Unique
74
		$this->filterUnique($data);
75
76 128
		// Filters
77
		$this->applyFilters($data);
78
79 128
		// Sorting
80
		$this->sortData($data);
81
82 128
		// Order reverse
83 2
		if ($this->desc) {
84
			$data = array_reverse($data, true);
85
		}
86
87 128
		// "Pluck" a specific value from an object
88
		$this->pluckData($data);
89 128
90
		return $data;
91
	}
92
93
	/**
94
	 *  Filter data to contain only unique values
95
	 *
96
	 * @param array $data
97 128
	 */
98 128
	protected function filterUnique(array &$data) : void {
99 123
		if ($this->uniqueOnly) {
100 119
			if (reset($data) instanceof Base) {
101 119
				$ids = [];
102 119
				foreach ($data as $key => $obj) {
103 22
					if (in_array($obj->getId(), $ids, true)) {
104 22
						unset($data[$key]);
105
						continue;
106 119
					}
107
					$ids[] = $obj->getId();
108
				}
109
			}
110 10
			else {
111
				$data = array_unique($data);
112
			}
113 128
		}
114
	}
115
116
	/**
117
	 * Apply predefined filters on data
118
	 *
119
	 * @param $data
120 128
	 */
121 128
	protected function applyFilters(&$data) : void {
122 5
		foreach ($this->filters as $filter) {
123
			$data = array_filter($data, $filter);
124 128
		}
125
		$data = array_values($data); // Reset array keys
126
	}
127
128
	/**
129
	 * Sort data using a predefined filters
130
	 *
131 128
	 * @param array $data
132 128
	 */
133 48
	protected function sortData(array &$data) : void {
134
		if (isset($this->sorter)) {
135 126
			$data = $this->sorter->sort($data);
136 1
		}
137
		elseif (isset($this->sortClosure)) {
138 126
			uasort($data, $this->sortClosure);
139 1
		}
140
		elseif (isset($this->sortProperty)) {
141 128
			uasort($data, [$this, 'sortByPropertyCallback']);
142
		}
143
	}
144
145
	/**
146
	 * Pluck a predefined value from data values
147
	 *
148 128
	 * @param $data
149 128
	 */
150 8
	protected function pluckData(&$data) : void {
151 8
		if (!empty($this->pluck)) {
152 1
			$data = array_map(function($item) {
153
				if (is_array($item) && isset($item[$this->pluck])) {
154 7
					return $item[$this->pluck];
155 6
				}
156 1
				if (is_object($item)) {
157
					if (property_exists($item, $this->pluck)) {
0 ignored issues
show
Bug introduced by
It seems like $this->pluck can also be of type null; however, parameter $property of property_exists() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

157
					if (property_exists($item, /** @scrutinizer ignore-type */ $this->pluck)) {
Loading history...
158 5
						return $item->{$this->pluck};
159 5
					}
160
					if (method_exists($item, $this->pluck)) {
0 ignored issues
show
Bug introduced by
It seems like $this->pluck can also be of type null; however, parameter $method of method_exists() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

160
					if (method_exists($item, /** @scrutinizer ignore-type */ $this->pluck)) {
Loading history...
161
						return $item->{$this->pluck}();
162 2
					}
163 8
				}
164
				return $item;
165 128
			}, $data);
166
		}
167
	}
168
169
	/**
170
	 * Add a filter callback
171
	 *
172
	 * @param Closure $callback
173
	 *
174 5
	 * @return $this
175 5
	 */
176 5
	public function filter(Closure $callback) : ContainerQuery {
177
		$this->filters[] = $callback;
178
		return $this;
179
	}
180
181
	/**
182
	 * Filter results to only contain those with a specific ID
183
	 *
184 2
	 * @param string|int $id
185 2
	 *
186 2
	 * @return ContainerQuery
187
	 */
188
	public function whereId($id) : ContainerQuery {
189
		$this->filters[] = static function($object) use ($id) {
190
			return $object InstanceOf WithId && $object->getId() === $id;
191
		};
192
		return $this;
193
	}
194
195
	/**
196 1
	 * Sort in descending order
197 1
	 *
198 1
	 * @return $this
199
	 */
200
	public function desc() : ContainerQuery {
201
		$this->desc = true;
202
		return $this;
203
	}
204
205
	/**
206
	 * Sort a result using a callback - maintaining the index association
207
	 *
208
	 * @param Closure $callback
209
	 *
210 1
	 * @return $this
211 1
	 */
212 1
	public function sort(Closure $callback) : ContainerQuery {
213
		$this->sortClosure = $callback;
214
		return $this;
215
	}
216
217
	/**
218
	 * Sort a result set by a given property
219
	 *
220 48
	 * @warning Sort callback has a priority.
221 48
	 *
222 48
	 * @param string $property
223
	 *
224
	 * @return $this
225
	 */
226
	public function sortBy(string $property) : ContainerQuery {
227
		$this->sortProperty = $property;
228
		return $this;
229
	}
230 123
231 123
	/**
232 123
	 * @param BaseSorter $sorter
233
	 *
234
	 * @return $this
235
	 */
236
	public function addSorter(BaseSorter $sorter) : ContainerQuery {
237
		$this->sorter = $sorter;
238
		return $this;
239
	}
240
241 4
	/**
242 4
	 * Get only unique values
243 4
	 *
244
	 * @return $this
245
	 */
246
	public function unique() : ContainerQuery {
247
		$this->uniqueOnly = true;
248
		return $this;
249
	}
250
251
	/**
252
	 * Get only the object's ids
253
	 *
254 9
	 * @return $this
255 9
	 * @throws Exception
256 1
	 */
257
	public function ids() : ContainerQuery {
258 9
		$this->only('getId');
259 9
		return $this;
260
	}
261
262
	/**
263
	 * Pluck a specific key from all values
264
	 *
265
	 * @param string $property Property, array key or method to extract from values
266
	 *
267
	 * @return ContainerQuery
268
	 * @throws Exception
269
	 */
270 1
	public function only(string $property) : ContainerQuery {
271
		if (!empty($this->pluck)) {
272 1
			throw new Exception('only() can be only called once.');
273 1
		}
274 1
		$this->pluck = $property;
275 1
		return $this;
276 1
	}
277
278 1
	/**
279 1
	 * Sort function for sorting by a defined property
280
	 *
281 1
	 * @param array|object $value1
282 1
	 * @param array|object $value2
283
	 *
284 1
	 * @return int
285 1
	 */
286
	protected function sortByPropertyCallback($value1, $value2) : int {
287
		// Get values
288
		$property = $this->sortProperty ?? '';
289 1
		$property1 = null;
290 1
		$property2 = null;
291
		if (is_object($value1) && isset($value1->$property)) {
292 1
			$property1 = $value1->$property;
293
		}
294
		elseif (is_array($value1) && isset($value1[$property])) {
295
			$property1 = $value1[$property];
296
		}
297
		if (is_object($value2) && isset($value2->$property)) {
298
			$property2 = $value2->$property;
299
		}
300
		elseif (is_array($value2) && isset($value2[$property])) {
301
			$property2 = $value2[$property];
302
		}
303
304
		// Compare values
305
		if ($property1 === $property2) {
306
			return 0;
307
		}
308
		return $property1 < $property2 ? -1 : 1;
309
	}
310
311
}