ContainerQuery::desc()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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

156
					if (property_exists($item, /** @scrutinizer ignore-type */ $this->pluck)) {
Loading history...
157 1
						return $item->{$this->pluck};
158
					}
159 22
					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

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