Passed
Push — master ( c5a571...411ad7 )
by Tomáš
03:30
created

ContainerQuery::whereId()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 3
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 5
ccs 4
cts 4
cp 1
crap 2
rs 10
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
	 */
41 140
	public function __construct(BaseContainer $container, bool $topLevelOnly = false) {
42 140
		$this->container = $container;
43 140
		$this->topLevelOnly = $topLevelOnly;
44 140
	}
45
46
	/**
47
	 * Gets the first result of container query
48
	 *
49
	 * @return mixed|null
50
	 */
51 14
	public function getFirst() {
52 14
		$data = $this->get();
53 14
		if (count($data) === 0) {
54 3
			return null;
55
		}
56 14
		return reset($data);
57
	}
58
59
	/**
60
	 * Get the result
61
	 *
62
	 * @return array
63
	 */
64 139
	public function get() : array {
65
		// Get initial data
66 139
		if ($this->topLevelOnly) {
67 1
			$data = $this->container->getTopLevel();
68
		}
69
		else {
70 139
			$data = $this->container->get();
71
		}
72
73
		// Unique
74 139
		$this->filterUnique($data);
75
76
		// Filters
77 139
		$this->applyFilters($data);
78
79
		// Sorting
80 139
		$this->sortData($data);
81
82
		// Order reverse
83 139
		if ($this->desc) {
84 2
			$data = array_reverse($data, true);
85
		}
86
87
		// "Pluck" a specific value from an object
88 139
		$this->pluckData($data);
89
90 139
		return $data;
91
	}
92
93
	/**
94
	 *  Filter data to contain only unique values
95
	 *
96
	 * @param array $data
97
	 */
98 139
	protected function filterUnique(array &$data) : void {
99 139
		if ($this->uniqueOnly) {
100 123
			if (reset($data) instanceof Base) {
101 119
				$ids = [];
102 119
				foreach ($data as $key => $obj) {
103 119
					if (in_array($obj->getId(), $ids, true)) {
104 22
						unset($data[$key]);
105 22
						continue;
106
					}
107 119
					$ids[] = $obj->getId();
108
				}
109
			}
110
			else {
111 10
				$data = array_unique($data);
112
			}
113
		}
114 139
	}
115
116
	/**
117
	 * Apply predefined filters on data
118
	 *
119
	 * @param $data
120
	 */
121 139
	protected function applyFilters(&$data) : void {
122 139
		foreach ($this->filters as $filter) {
123 16
			$data = array_filter($data, $filter);
124
		}
125 139
		$data = array_values($data); // Reset array keys
126 139
	}
127
128
	/**
129
	 * Sort data using a predefined filters
130
	 *
131
	 * @param array $data
132
	 */
133 139
	protected function sortData(array &$data) : void {
134 139
		if (isset($this->sorter)) {
135 48
			$data = $this->sorter->sort($data);
136
		}
137 137
		elseif (isset($this->sortClosure)) {
138 1
			uasort($data, $this->sortClosure);
139
		}
140 137
		elseif (isset($this->sortProperty)) {
141 1
			uasort($data, [$this, 'sortByPropertyCallback']);
142
		}
143 139
	}
144
145
	/**
146
	 * Pluck a predefined value from data values
147
	 *
148
	 * @param $data
149
	 */
150 139
	protected function pluckData(&$data) : void {
151 139
		if (!empty($this->pluck)) {
152 18
			$data = array_map(function($item) {
153 18
				if (is_array($item) && isset($item[$this->pluck])) {
154 1
					return $item[$this->pluck];
155
				}
156 17
				if (is_object($item)) {
157 16
					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 1
						return $item->{$this->pluck};
159
					}
160 15
					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 15
						return $item->{$this->pluck}();
162
					}
163
				}
164 2
				return $item;
165 18
			}, $data);
166
		}
167 139
	}
168
169
	/**
170
	 * Add a filter callback
171
	 *
172
	 * @param Closure $callback
173
	 *
174
	 * @return $this
175
	 */
176 5
	public function filter(Closure $callback) : ContainerQuery {
177 5
		$this->filters[] = $callback;
178 5
		return $this;
179
	}
180
181
	/**
182
	 * Filter results to only contain those with a specific ID
183
	 *
184
	 * @param string|int $id
185
	 *
186
	 * @return ContainerQuery
187
	 */
188 11
	public function whereId($id) : ContainerQuery {
189 11
		$this->filters[] = static function($object) use ($id) {
190 11
			return $object InstanceOf WithId && $object->getId() === $id;
191
		};
192 11
		return $this;
193
	}
194
195
	/**
196
	 * Sort in descending order
197
	 *
198
	 * @return $this
199
	 */
200 2
	public function desc() : ContainerQuery {
201 2
		$this->desc = true;
202 2
		return $this;
203
	}
204
205
	/**
206
	 * Sort a result using a callback - maintaining the index association
207
	 *
208
	 * @param Closure $callback
209
	 *
210
	 * @return $this
211
	 */
212 1
	public function sort(Closure $callback) : ContainerQuery {
213 1
		$this->sortClosure = $callback;
214 1
		return $this;
215
	}
216
217
	/**
218
	 * Sort a result set by a given property
219
	 *
220
	 * @warning Sort callback has a priority.
221
	 *
222
	 * @param string $property
223
	 *
224
	 * @return $this
225
	 */
226 1
	public function sortBy(string $property) : ContainerQuery {
227 1
		$this->sortProperty = $property;
228 1
		return $this;
229
	}
230
231
	/**
232
	 * @param BaseSorter $sorter
233
	 *
234
	 * @return $this
235
	 */
236 48
	public function addSorter(BaseSorter $sorter) : ContainerQuery {
237 48
		$this->sorter = $sorter;
238 48
		return $this;
239
	}
240
241
	/**
242
	 * Get only unique values
243
	 *
244
	 * @return $this
245
	 */
246 123
	public function unique() : ContainerQuery {
247 123
		$this->uniqueOnly = true;
248 123
		return $this;
249
	}
250
251
	/**
252
	 * Get only the object's ids
253
	 *
254
	 * @return $this
255
	 * @throws Exception
256
	 */
257 14
	public function ids() : ContainerQuery {
258 14
		$this->only('getId');
259 14
		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 19
	public function only(string $property) : ContainerQuery {
271 19
		if (!empty($this->pluck)) {
272 1
			throw new Exception('only() can be only called once.');
273
		}
274 19
		$this->pluck = $property;
275 19
		return $this;
276
	}
277
278
	/**
279
	 * Sort function for sorting by a defined property
280
	 *
281
	 * @param array|object $value1
282
	 * @param array|object $value2
283
	 *
284
	 * @return int
285
	 */
286 1
	protected function sortByPropertyCallback($value1, $value2) : int {
287
		// Get values
288 1
		$property = $this->sortProperty ?? '';
289 1
		$property1 = null;
290 1
		$property2 = null;
291 1
		if (is_object($value1) && isset($value1->$property)) {
292 1
			$property1 = $value1->$property;
293
		}
294 1
		elseif (is_array($value1) && isset($value1[$property])) {
295 1
			$property1 = $value1[$property];
296
		}
297 1
		if (is_object($value2) && isset($value2->$property)) {
298 1
			$property2 = $value2->$property;
299
		}
300 1
		elseif (is_array($value2) && isset($value2[$property])) {
301 1
			$property2 = $value2[$property];
302
		}
303
304
		// Compare values
305 1
		if ($property1 === $property2) {
306 1
			return 0;
307
		}
308 1
		return $property1 < $property2 ? -1 : 1;
309
	}
310
311
}