Passed
Push — master ( d023d7...4e8d12 )
by Ron
01:40
created

MySQLRunnableSelect   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 323
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 55
eloc 130
c 1
b 0
f 0
dl 0
loc 323
rs 6

22 Methods

Rating   Name   Duplication   Size   Complexity  
A fetch() 0 19 6
A createStatement() 0 9 2
A bindValue() 0 3 1
A getFoundRows() 0 2 1
A setPreserveTypes() 0 3 1
A clearValues() 0 3 1
A fetchArray() 0 6 2
A fetchAll() 0 25 6
A fetchRowsLazy() 0 3 1
A fetchRows() 0 2 1
A createTempStatement() 0 6 1
A fetchGroups() 0 16 4
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 fetchLazy() 0 23 5
A fetchRow() 0 4 1
A getIterator() 0 2 1
A fetchKeyValue() 0 12 3
A fetchObject() 0 4 2

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