MySQLRunnableSelect   C
last analyzed

Complexity

Total Complexity 56

Size/Duplication

Total Lines 330
Duplicated Lines 0 %

Importance

Changes 4
Bugs 1 Features 0
Metric Value
wmc 56
eloc 133
c 4
b 1
f 0
dl 0
loc 330
rs 5.5199

23 Methods

Rating   Name   Duplication   Size   Complexity  
A bindValue() 0 3 1
A createStatement() 0 9 2
A getFoundRows() 0 2 1
A setPreserveTypes() 0 3 1
A clearValues() 0 3 1
A fetchArray() 0 6 2
A fetchRowsLazy() 0 3 1
A fetchRows() 0 2 1
A createTempStatement() 0 6 1
A fetchGroups() 0 16 4
A fetchIndexedRows() 0 2 1
A fetchObjects() 0 21 6
A __construct() 0 4 3
A fetchObjectsLazy() 0 3 2
A bindValues() 0 3 1
A fetchValue() 0 7 4
A fetchRow() 0 5 1
A getIterator() 0 2 1
A fetchKeyValue() 0 12 3
A fetchObject() 0 5 2
A fetch() 0 19 6
A fetchAll() 0 25 6
A fetchLazy() 0 23 5

How to fix   Complexity   

Complex Class

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

1
<?php
2
namespace Kir\MySQL\Databases\MySQL;
3
4
use Closure;
5
use Generator;
6
use Kir\MySQL\Builder\Helpers\DBIgnoreRow;
7
use Kir\MySQL\Builder\Helpers\FieldTypeProvider;
8
use Kir\MySQL\Builder\Helpers\FieldValueConverter;
9
use Kir\MySQL\Builder\QueryStatement;
10
use Kir\MySQL\Databases\MySQL;
11
use PDO;
12
use stdClass;
13
use Throwable;
14
use Traversable;
15
16
/**
17
 */
18
class MySQLRunnableSelect extends MySQLSelect {
19
	/** @var array<string, mixed> */
20
	private array $values = [];
21
	private bool $preserveTypes;
22
	private string $defaultClassName;
23
	private int $foundRows = 0;
24
25
	/**
26
	 * @param MySQL $db
27
	 * @param array<string, mixed> $options
28
	 */
29
	public function __construct($db, array $options = []) {
30
		parent::__construct($db);
31
		$this->preserveTypes = array_key_exists('preserve-types-default', $options) ? $options['preserve-types-default'] : false;
32
		$this->defaultClassName = array_key_exists('fetch-object-class-default', $options) ? $options['fetch-object-class-default'] : stdClass::class;
33
	}
34
35
	/**
36
	 * @inheritDoc
37
	 */
38
	public function bindValues(array $values) {
39
		$this->values = array_merge($this->values, $values);
40
		return $this;
41
	}
42
43
	/**
44
	 * @inheritDoc
45
	 */
46
	public function bindValue(string $key, $value) {
47
		$this->values[$key] = $value;
48
		return $this;
49
	}
50
51
	/**
52
	 * @inheritDoc
53
	 */
54
	public function clearValues() {
55
		$this->values = [];
56
		return $this;
57
	}
58
59
	/**
60
	 * @inheritDoc
61
	 */
62
	public function setPreserveTypes(bool $preserveTypes = true) {
63
		$this->preserveTypes = $preserveTypes;
64
		return $this;
65
	}
66
67
	/**
68
	 * @inheritDoc
69
	 */
70
	public function fetchIndexedRows($callback = null): array {
71
		return $this->fetchAll($callback, PDO::FETCH_NUM);
72
	}
73
74
	/**
75
	 * @inheritDoc
76
	 */
77
	public function fetchRows($callback = null): array {
78
		return $this->fetchAll($callback, PDO::FETCH_ASSOC);
79
	}
80
81
	/**
82
	 * @inheritDoc
83
	 */
84
	public function fetchRowsLazy($callback = null) {
85
		$callback ??= static fn($row) => $row;
86
		yield from $this->fetchLazy($callback, PDO::FETCH_ASSOC);
87
	}
88
89
	/**
90
	 * @inheritDoc
91
	 */
92
	public function fetchRow($callback = null): array {
93
		$callback ??= static fn($row) => $row;
94
		return $this->fetch($callback, PDO::FETCH_ASSOC, null, static fn($row) => [
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->fetch($cal...ion(...) { /* ... */ }) could return the type Kir\MySQL\Databases\MySQ...MySQL\Databases\MySQL\U which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
95
			'valid' => is_array($row),
96
			'default' => []
97
		]);
98
	}
99
100
	/**
101
	 * @inheritDoc
102
	 */
103
	public function fetchObjects(string $className = 'stdClass', $callback = null): array {
104
		return $this->createTempStatement(function (QueryStatement $statement) use ($className, $callback) {
105
			$data = $statement->fetchAll(PDO::FETCH_CLASS, $className);
106
			if($this->preserveTypes) {
107
				$columnDefinitions = FieldTypeProvider::getFieldTypes($statement);
108
				$data = array_map(static fn($row) => FieldValueConverter::convertValues($row, $columnDefinitions), $data);
109
			}
110
			if($callback !== null) {
111
				return call_user_func(static function ($resultData = []) use ($data, $callback) {
112
					foreach($data as $row) {
113
						$result = $callback($row);
114
						if($result !== null && !($result instanceof DBIgnoreRow)) {
115
							$resultData[] = $result;
116
						} else {
117
							$resultData[] = $row;
118
						}
119
					}
120
					return $resultData;
121
				});
122
			}
123
			return $data;
124
		});
125
	}
126
127
	/**
128
	 * @inheritDoc
129
	 */
130
	public function fetchObjectsLazy($className = null, $callback = null) {
131
		$callback ??= static fn($row) => $row;
132
		yield from $this->fetchLazy($callback, PDO::FETCH_CLASS, $className ?: $this->defaultClassName);
133
	}
134
135
	/**
136
	 * @inheritDoc
137
	 */
138
	public function fetchObject($className = null, $callback = null) {
139
		$callback ??= static fn($row) => $row;
140
		return $this->fetch($callback, PDO::FETCH_CLASS, $className ?: $this->defaultClassName, static fn($row) => [
141
			'valid' => is_object($row),
142
			'default' => null
143
		]);
144
	}
145
146
	/**
147
	 * @inheritDoc
148
	 */
149
	public function fetchKeyValue($treatValueAsArray = false): array {
150
		return $this->createTempStatement(static function (QueryStatement $statement) use ($treatValueAsArray) {
151
			if($treatValueAsArray) {
152
				$rows = $statement->fetchAll(PDO::FETCH_ASSOC);
153
				$result = [];
154
				foreach($rows as $row) {
155
					[$key] = array_values($row);
156
					$result[$key] = $row;
157
				}
158
				return $result;
159
			}
160
			return $statement->fetchAll(PDO::FETCH_KEY_PAIR);
161
		});
162
	}
163
164
	/**
165
	 * @inheritDoc
166
	 */
167
	public function fetchGroups(array $fields): array {
168
		$rows = $this->fetchRows();
169
		$result = [];
170
		foreach($rows as $row) {
171
			/** @var array<string, mixed> $tmp */
172
			$tmp = &$result;
173
			foreach($fields as $field) {
174
				$value = (string) $row[$field];
175
				if(!array_key_exists($value, $tmp)) {
176
					$tmp[$value] = [];
177
				}
178
				$tmp = &$tmp[$value];
179
			}
180
			$tmp[] = $row;
181
		}
182
		return $result;
183
	}
184
185
	/**
186
	 * @inheritDoc
187
	 */
188
	public function fetchArray(?callable $fn = null): array {
189
		return $this->createTempStatement(static function (QueryStatement $stmt) use ($fn) {
190
			if($fn !== null) {
191
				return $stmt->fetchAll(PDO::FETCH_FUNC, $fn);
192
			}
193
			return $stmt->fetchAll(PDO::FETCH_COLUMN);
194
		});
195
	}
196
197
	/**
198
	 * @inheritDoc
199
	 */
200
	public function fetchValue($default = null, ?callable $fn = null) {
201
		return $this->createTempStatement(static function (QueryStatement $stmt) use ($default, $fn) {
202
			$result = $stmt->fetchAll(PDO::FETCH_COLUMN);
203
			if($result !== false && array_key_exists(0, $result)) {
204
				return $fn !== null ? $fn($result[0]) : $result[0];
205
			}
206
			return $default;
207
		});
208
	}
209
210
	/**
211
	 * @inheritDoc
212
	 */
213
	public function getFoundRows(): int {
214
		return $this->foundRows;
215
	}
216
217
	/**
218
	 * @param callable $fn
219
	 * @return mixed
220
	 */
221
	private function createTempStatement(callable $fn) {
222
		$stmt = $this->createStatement();
223
		try {
224
			return $fn($stmt);
225
		} finally {
226
			$stmt->closeCursor();
227
		}
228
	}
229
230
	/**
231
	 * @return QueryStatement
232
	 */
233
	private function createStatement(): QueryStatement {
234
		$db = $this->db();
235
		$query = $this->__toString();
236
		$statement = $db->prepare($query);
237
		$statement->execute($this->values);
238
		if($this->getCalcFoundRows()) {
239
			$this->foundRows = (int) $db->query('SELECT FOUND_ROWS()')->fetchColumn();
240
		}
241
		return $statement;
242
	}
243
244
	/**
245
	 * @return Traversable<int, array<string, bool|float|int|string|null>>
246
	 */
247
	public function getIterator(): Traversable {
248
		yield from $this->fetchRowsLazy();
249
	}
250
251
	/**
252
	 * @template TFnReturnType of array<int|string, mixed>
253
	 * @param null|(callable(array<string, null|scalar>): (TFnReturnType|DBIgnoreRow|null|void)) $callback
0 ignored issues
show
Documentation Bug introduced by
The doc comment null|(callable(array<str...DBIgnoreRow|null|void)) at position 3 could not be parsed: Expected ')' at position 3, but found 'callable'.
Loading history...
254
	 * @param int $mode
255
	 * @param mixed $arg0
256
	 * @return ($callback is null ? array<int, ($mode is PDO::FETCH_NUM ? array<int, null|scalar> : array<string, null|scalar>)> : array<int, TFnReturnType>)
0 ignored issues
show
Documentation Bug introduced by
The doc comment ($callback at position 1 could not be parsed: Unknown type name '$callback' at position 1 in ($callback.
Loading history...
257
	 */
258
	private function fetchAll($callback = null, int $mode = 0, $arg0 = null) {
259
		return $this->createTempStatement(function (QueryStatement $statement) use ($callback, $mode, $arg0) {
260
			$statement->setFetchMode($mode, $arg0);
261
			$data = $statement->fetchAll();
262
			if($this->preserveTypes) {
263
				$columnDefinitions = FieldTypeProvider::getFieldTypes($statement);
264
				$data = array_map(static fn($row) => FieldValueConverter::convertValues($row, $columnDefinitions), $data);
265
			}
266
			if($callback !== null) {
267
				return call_user_func(static function ($resultData = []) use ($data, $callback) {
268
					foreach($data as $row) {
269
						$result = $callback($row);
270
						if($result instanceof DBIgnoreRow) {
271
							continue;
272
						}
273
						if($result !== null) {
274
							$resultData[] = $result;
275
						} else {
276
							$resultData[] = $row;
277
						}
278
					}
279
					return $resultData;
280
				});
281
			}
282
			return $data;
283
		});
284
	}
285
286
	/**
287
	 * @template T
288
	 * @template U
289
	 * @param null|callable(T): U $callback
290
	 * @param int $mode
291
	 * @param mixed $arg0
292
	 * @return ($callback is null ? Generator<int, T> : Generator<int, U>)
0 ignored issues
show
Documentation Bug introduced by
The doc comment ($callback at position 1 could not be parsed: Unknown type name '$callback' at position 1 in ($callback.
Loading history...
293
	 */
294
	private function fetchLazy($callback = null, int $mode = PDO::FETCH_ASSOC, $arg0 = null): Generator {
295
		$statement = $this->createStatement();
296
		$statement->setFetchMode($mode, $arg0);
297
		try {
298
			while($row = $statement->fetch()) {
299
				/** @var T $row */
300
//				if($this->preserveTypes) {
301
//					$columnDefinitions = FieldTypeProvider::getFieldTypes($statement);
302
//					$row = FieldValueConverter::convertValues($row, $columnDefinitions);
303
//				}
304
				$result = $callback($row);
305
				if($result instanceof DBIgnoreRow) {
306
					// Skip row in this case
307
					continue;
308
				}
309
				if($result !== null) {
310
					yield $result;
311
				} else {
312
					yield $row;
313
				}
314
			}
315
		} finally {
316
			try { $statement->closeCursor(); } catch (Throwable $e) {}
0 ignored issues
show
introduced by
Consider moving this CATCH statement to a new line.
Loading history...
317
		}
318
	}
319
320
	/**
321
	 * @template T
322
	 * @template U
323
	 * @param callable(T): U $callback
324
	 * @param int $mode
325
	 * @param mixed $arg0
326
	 * @param Closure|null $resultValidator
327
	 * @return T|U|array<string, mixed>
0 ignored issues
show
Bug introduced by
The type Kir\MySQL\Databases\MySQL\U was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
Bug introduced by
The type Kir\MySQL\Databases\MySQL\T was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
328
	 */
329
	private function fetch($callback, int $mode = PDO::FETCH_ASSOC, $arg0 = null, ?Closure $resultValidator = null) {
330
		return $this->createTempStatement(function (QueryStatement $statement) use ($callback, $mode, $arg0, $resultValidator) {
331
			$statement->setFetchMode($mode, $arg0);
332
			$row = $statement->fetch();
333
			$result = $resultValidator === null ? ['valid' => true] : $resultValidator($row);
334
			if(!$result['valid']) {
335
				return $result['default'];
336
			}
337
			if($this->preserveTypes) {
338
				$columnDefinitions = FieldTypeProvider::getFieldTypes($statement);
339
				$row = FieldValueConverter::convertValues($row, $columnDefinitions);
0 ignored issues
show
Bug introduced by
$row of type Kir\MySQL\Builder\T is incompatible with the type array expected by parameter $row of Kir\MySQL\Builder\Helper...verter::convertValues(). ( Ignorable by Annotation )

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

339
				$row = FieldValueConverter::convertValues(/** @scrutinizer ignore-type */ $row, $columnDefinitions);
Loading history...
340
			}
341
			if($callback !== null) {
0 ignored issues
show
introduced by
The condition $callback !== null is always true.
Loading history...
342
				$result = $callback($row);
343
				if($result !== null) {
344
					$row = $result;
345
				}
346
			}
347
			return $row;
348
		});
349
	}
350
}
351