Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
230 / 230 |
|
100.00% |
41 / 41 |
CRAP | |
100.00% |
1 / 1 |
Database | |
100.00% |
230 / 230 |
|
100.00% |
41 / 41 |
85 | |
100.00% |
1 / 1 |
__construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
__destruct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
log | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
makeConfig | |
100.00% |
29 / 29 |
|
100.00% |
1 / 1 |
1 | |||
connect | |
100.00% |
53 / 53 |
|
100.00% |
1 / 1 |
12 | |||
setCollations | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
setTimezone | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getConnection | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isOpen | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
close | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
ping | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
reconnect | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getConfig | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
warnings | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
errors | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
error | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
use | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
createSchema | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
dropSchema | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
alterSchema | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
createTable | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
dropTable | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
alterTable | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
delete | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
insert | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
loadData | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
replace | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
select | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
update | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
with | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
lastQuery | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
exec | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
4 | |||
query | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
4 | |||
prepare | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
transaction | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
3 | |||
insertId | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
protectIdentifier | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
quote | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
7 | |||
setDebugCollector | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
addToDebug | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
finalizeAddToDebug | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
2 |
1 | <?php declare(strict_types=1); |
2 | /* |
3 | * This file is part of Aplus Framework Database Library. |
4 | * |
5 | * (c) Natan Felles <natanfelles@gmail.com> |
6 | * |
7 | * For the full copyright and license information, please view the LICENSE |
8 | * file that was distributed with this source code. |
9 | */ |
10 | namespace Framework\Database; |
11 | |
12 | use Closure; |
13 | use Exception; |
14 | use Framework\Database\Debug\DatabaseCollector; |
15 | use Framework\Database\Definition\AlterSchema; |
16 | use Framework\Database\Definition\AlterTable; |
17 | use Framework\Database\Definition\CreateSchema; |
18 | use Framework\Database\Definition\CreateTable; |
19 | use Framework\Database\Definition\DropSchema; |
20 | use Framework\Database\Definition\DropTable; |
21 | use Framework\Database\Manipulation\Delete; |
22 | use Framework\Database\Manipulation\Insert; |
23 | use Framework\Database\Manipulation\LoadData; |
24 | use Framework\Database\Manipulation\Replace; |
25 | use Framework\Database\Manipulation\Select; |
26 | use Framework\Database\Manipulation\Update; |
27 | use Framework\Database\Manipulation\With; |
28 | use Framework\Log\Logger; |
29 | use Framework\Log\LogLevel; |
30 | use InvalidArgumentException; |
31 | use JetBrains\PhpStorm\ArrayShape; |
32 | use JetBrains\PhpStorm\Language; |
33 | use LogicException; |
34 | use mysqli; |
35 | use mysqli_sql_exception; |
36 | use RuntimeException; |
37 | use SensitiveParameter; |
38 | |
39 | /** |
40 | * Class Database. |
41 | * |
42 | * @package database |
43 | */ |
44 | class Database |
45 | { |
46 | protected ?mysqli $mysqli; |
47 | /** |
48 | * Connection configurations. |
49 | * |
50 | * Custom configs merged with the Base Connection configurations. |
51 | * |
52 | * @see Database::makeConfig() |
53 | * |
54 | * @var array<string,mixed> |
55 | */ |
56 | protected array $config = []; |
57 | /** |
58 | * The current $config failover index to be used in a connection. |
59 | * |
60 | * @see Database::connect() |
61 | * |
62 | * @var int|null Integer representing the array index or null for none |
63 | */ |
64 | protected ?int $failoverIndex = null; |
65 | /** |
66 | * @see Database::transaction() |
67 | */ |
68 | protected bool $inTransaction = false; |
69 | protected string $lastQuery = ''; |
70 | protected ?Logger $logger; |
71 | protected DatabaseCollector $debugCollector; |
72 | |
73 | /** |
74 | * Database constructor. |
75 | * |
76 | * @param array<string,mixed>|string $username |
77 | * @param string|null $password |
78 | * @param string|null $schema |
79 | * @param string $host |
80 | * @param int $port |
81 | * @param Logger|null $logger |
82 | * |
83 | * @see Database::makeConfig() |
84 | * |
85 | * @throws mysqli_sql_exception if connections fail |
86 | */ |
87 | public function __construct( |
88 | #[SensitiveParameter] array | string $username, |
89 | #[SensitiveParameter] string $password = null, |
90 | string $schema = null, |
91 | string $host = 'localhost', |
92 | int $port = 3306, |
93 | Logger $logger = null |
94 | ) { |
95 | $this->logger = $logger; |
96 | $this->connect($username, $password, $schema, $host, $port); |
97 | } |
98 | |
99 | public function __destruct() |
100 | { |
101 | $this->close(); |
102 | } |
103 | |
104 | protected function log(string $message, LogLevel $level = LogLevel::ERROR) : void |
105 | { |
106 | $this->logger?->log($level, $message); |
107 | } |
108 | |
109 | /** |
110 | * Make Base Connection configurations. |
111 | * |
112 | * @param array<string,mixed> $config |
113 | * |
114 | * @return array<string,mixed> |
115 | */ |
116 | #[ArrayShape([ |
117 | 'host' => 'string', |
118 | 'port' => 'int', |
119 | 'username' => 'string|null', |
120 | 'password' => 'string|null', |
121 | 'schema' => 'string|null', |
122 | 'socket' => 'string|null', |
123 | 'persistent' => 'bool', |
124 | 'engine' => 'string', |
125 | 'charset' => 'string', |
126 | 'collation' => 'string', |
127 | 'timezone' => 'string', |
128 | 'ssl' => 'array', |
129 | 'failover' => 'array', |
130 | 'options' => 'array', |
131 | 'report' => 'int', |
132 | ])] |
133 | protected function makeConfig(array $config) : array |
134 | { |
135 | return \array_replace_recursive([ |
136 | 'host' => 'localhost', |
137 | 'port' => 3306, |
138 | 'username' => null, |
139 | 'password' => null, |
140 | 'schema' => null, |
141 | 'socket' => null, |
142 | 'persistent' => false, |
143 | 'engine' => 'InnoDB', |
144 | 'charset' => 'utf8mb4', |
145 | 'collation' => 'utf8mb4_general_ci', |
146 | 'timezone' => '+00:00', |
147 | 'ssl' => [ |
148 | 'enabled' => false, |
149 | 'verify' => true, |
150 | 'key' => null, |
151 | 'cert' => null, |
152 | 'ca' => null, |
153 | 'capath' => null, |
154 | 'cipher' => null, |
155 | ], |
156 | 'failover' => [], |
157 | 'options' => [ |
158 | \MYSQLI_OPT_CONNECT_TIMEOUT => 10, |
159 | \MYSQLI_OPT_INT_AND_FLOAT_NATIVE => true, |
160 | \MYSQLI_OPT_LOCAL_INFILE => 1, |
161 | ], |
162 | 'report' => \MYSQLI_REPORT_ALL & ~\MYSQLI_REPORT_INDEX, |
163 | ], $config); |
164 | } |
165 | |
166 | /** |
167 | * @param array<string,mixed>|string $username |
168 | * @param string|null $password |
169 | * @param string|null $schema |
170 | * @param string $host |
171 | * @param int $port |
172 | * |
173 | * @throws mysqli_sql_exception if connection fail |
174 | * |
175 | * @return static |
176 | */ |
177 | protected function connect( |
178 | #[SensitiveParameter] array | string $username, |
179 | #[SensitiveParameter] string $password = null, |
180 | string $schema = null, |
181 | string $host = 'localhost', |
182 | int $port = 3306 |
183 | ) : static { |
184 | if ( ! \is_array($username)) { |
185 | $username = [ |
186 | 'host' => $host, |
187 | 'port' => $port, |
188 | 'username' => $username, |
189 | 'password' => $password, |
190 | 'schema' => $schema, |
191 | ]; |
192 | } |
193 | $config = $this->makeConfig($username); |
194 | if ($this->failoverIndex === null) { |
195 | $this->config = $config; |
196 | } |
197 | \mysqli_report($config['report']); |
198 | $this->mysqli = new mysqli(); |
199 | foreach ($config['options'] as $option => $value) { |
200 | $this->mysqli->options($option, $value); |
201 | } |
202 | try { |
203 | $flags = 0; |
204 | if ($config['ssl']['enabled'] === true) { |
205 | $this->mysqli->ssl_set( |
206 | $config['ssl']['key'], |
207 | $config['ssl']['cert'], |
208 | $config['ssl']['ca'], |
209 | $config['ssl']['capath'], |
210 | $config['ssl']['cipher'] |
211 | ); |
212 | $flags += \MYSQLI_CLIENT_SSL; |
213 | if ($config['ssl']['verify'] === false) { |
214 | $flags += \MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT; |
215 | } |
216 | } |
217 | $this->mysqli->real_connect( |
218 | ($config['persistent'] ? 'p:' : '') . $config['host'], |
219 | $config['username'], |
220 | $config['password'], |
221 | $config['schema'], |
222 | $config['port'] === null ? null : (int) $config['port'], |
223 | $config['socket'], |
224 | $flags |
225 | ); |
226 | } catch (mysqli_sql_exception $exception) { |
227 | $log = "Database: Connection failed for '{$config['username']}'@'{$config['host']}'"; |
228 | $log .= $this->failoverIndex !== null ? " (failover: {$this->failoverIndex})" : ''; |
229 | $this->log($log); |
230 | $this->failoverIndex = $this->failoverIndex === null |
231 | ? 0 |
232 | : $this->failoverIndex + 1; |
233 | if (empty($config['failover'][$this->failoverIndex])) { |
234 | throw $exception; |
235 | } |
236 | $config = \array_replace_recursive( |
237 | $config, |
238 | $config['failover'][$this->failoverIndex] |
239 | ); |
240 | return $this->connect($config); |
241 | } |
242 | $this->setCollations($config['charset'], $config['collation']); |
243 | $this->setTimezone($config['timezone']); |
244 | return $this; |
245 | } |
246 | |
247 | protected function setCollations(string $charset, string $collation) : bool |
248 | { |
249 | $this->mysqli->set_charset($charset); |
250 | $charset = $this->quote($charset); |
251 | $collation = $this->quote($collation); |
252 | return $this->mysqli->real_query("SET NAMES {$charset} COLLATE {$collation}"); |
253 | } |
254 | |
255 | protected function setTimezone(string $timezone) : bool |
256 | { |
257 | $timezone = $this->quote($timezone); |
258 | return $this->mysqli->real_query("SET time_zone = {$timezone}"); |
259 | } |
260 | |
261 | /** |
262 | * Gets the MySQLi connection. |
263 | * |
264 | * @return mysqli |
265 | */ |
266 | public function getConnection() : mysqli |
267 | { |
268 | return $this->mysqli; |
269 | } |
270 | |
271 | /** |
272 | * Tells if the connection is open. |
273 | * |
274 | * @return bool |
275 | */ |
276 | public function isOpen() : bool |
277 | { |
278 | return isset($this->mysqli); |
279 | } |
280 | |
281 | /** |
282 | * Closes the connection if it is open. |
283 | * |
284 | * @return bool |
285 | */ |
286 | public function close() : bool |
287 | { |
288 | if ( ! $this->isOpen()) { |
289 | return true; |
290 | } |
291 | $closed = $this->mysqli->close(); |
292 | if ($closed) { |
293 | $this->mysqli = null; |
294 | } |
295 | return $closed; |
296 | } |
297 | |
298 | /** |
299 | * Pings the server, or tries to reconnect if the connection has gone down. |
300 | * |
301 | * @return bool |
302 | */ |
303 | public function ping() : bool |
304 | { |
305 | return $this->mysqli->ping(); |
306 | } |
307 | |
308 | /** |
309 | * Closes the current and opens a new connection with the last config. |
310 | * |
311 | * @return static |
312 | */ |
313 | public function reconnect() : static |
314 | { |
315 | $this->close(); |
316 | return $this->connect($this->getConfig()); |
317 | } |
318 | |
319 | /** |
320 | * @return array<string,mixed> |
321 | */ |
322 | #[ArrayShape([ |
323 | 'host' => 'string', |
324 | 'port' => 'int', |
325 | 'username' => 'string|null', |
326 | 'password' => 'string|null', |
327 | 'schema' => 'string|null', |
328 | 'socket' => 'string|null', |
329 | 'persistent' => 'bool', |
330 | 'engine' => 'string', |
331 | 'charset' => 'string', |
332 | 'collation' => 'string', |
333 | 'timezone' => 'string', |
334 | 'ssl' => 'array', |
335 | 'failover' => 'array', |
336 | 'options' => 'array', |
337 | 'report' => 'int', |
338 | ])] |
339 | public function getConfig() : array |
340 | { |
341 | return $this->config; |
342 | } |
343 | |
344 | public function warnings() : int |
345 | { |
346 | return $this->mysqli->warning_count; |
347 | } |
348 | |
349 | /** |
350 | * Get a list of the latest errors. |
351 | * |
352 | * @return array<int,array<string,mixed>> |
353 | */ |
354 | public function errors() : array |
355 | { |
356 | return $this->mysqli->error_list; |
357 | } |
358 | |
359 | /** |
360 | * Get latest error. |
361 | * |
362 | * @return string|null |
363 | */ |
364 | public function error() : ?string |
365 | { |
366 | return $this->mysqli->error ?: null; |
367 | } |
368 | |
369 | /** |
370 | * @param string $schema |
371 | * |
372 | * @throws mysqli_sql_exception if schema is unknown |
373 | * |
374 | * @return static |
375 | */ |
376 | public function use(string $schema) : static |
377 | { |
378 | $this->mysqli->select_db($schema); |
379 | return $this; |
380 | } |
381 | |
382 | /** |
383 | * Call a CREATE SCHEMA statement. |
384 | * |
385 | * @param string|null $schemaName |
386 | * |
387 | * @return CreateSchema |
388 | */ |
389 | public function createSchema(string $schemaName = null) : CreateSchema |
390 | { |
391 | $instance = new CreateSchema($this); |
392 | if ($schemaName !== null) { |
393 | $instance->schema($schemaName); |
394 | } |
395 | return $instance; |
396 | } |
397 | |
398 | /** |
399 | * Call a DROP SCHEMA statement. |
400 | * |
401 | * @param string|null $schemaName |
402 | * |
403 | * @return DropSchema |
404 | */ |
405 | public function dropSchema(string $schemaName = null) : DropSchema |
406 | { |
407 | $instance = new DropSchema($this); |
408 | if ($schemaName !== null) { |
409 | $instance->schema($schemaName); |
410 | } |
411 | return $instance; |
412 | } |
413 | |
414 | /** |
415 | * Call a ALTER SCHEMA statement. |
416 | * |
417 | * @param string|null $schemaName |
418 | * |
419 | * @return AlterSchema |
420 | */ |
421 | public function alterSchema(string $schemaName = null) : AlterSchema |
422 | { |
423 | $instance = new AlterSchema($this); |
424 | if ($schemaName !== null) { |
425 | $instance->schema($schemaName); |
426 | } |
427 | return $instance; |
428 | } |
429 | |
430 | /** |
431 | * Call a CREATE TABLE statement. |
432 | * |
433 | * @param string|null $tableName |
434 | * |
435 | * @return CreateTable |
436 | */ |
437 | public function createTable(string $tableName = null) : CreateTable |
438 | { |
439 | $instance = new CreateTable($this); |
440 | if ($tableName !== null) { |
441 | $instance->table($tableName); |
442 | } |
443 | return $instance; |
444 | } |
445 | |
446 | /** |
447 | * Call a DROP TABLE statement. |
448 | * |
449 | * @param string|null $table |
450 | * @param string ...$tables |
451 | * |
452 | * @return DropTable |
453 | */ |
454 | public function dropTable(string $table = null, string ...$tables) : DropTable |
455 | { |
456 | $instance = new DropTable($this); |
457 | if ($table !== null) { |
458 | $instance->table($table, ...$tables); |
459 | } |
460 | return $instance; |
461 | } |
462 | |
463 | /** |
464 | * Call a ALTER TABLE statement. |
465 | * |
466 | * @param string|null $tableName |
467 | * |
468 | * @return AlterTable |
469 | */ |
470 | public function alterTable(string $tableName = null) : AlterTable |
471 | { |
472 | $instance = new AlterTable($this); |
473 | if ($tableName !== null) { |
474 | $instance->table($tableName); |
475 | } |
476 | return $instance; |
477 | } |
478 | |
479 | /** |
480 | * Call a DELETE statement. |
481 | * |
482 | * @param array<string,Closure|string>|Closure|string|null $reference |
483 | * @param array<string,Closure|string>|Closure|string ...$references |
484 | * |
485 | * @return Delete |
486 | */ |
487 | public function delete( |
488 | array | Closure | string $reference = null, |
489 | array | Closure | string ...$references |
490 | ) : Delete { |
491 | $instance = new Delete($this); |
492 | if ($reference !== null) { |
493 | $instance->table($reference, ...$references); |
494 | } |
495 | return $instance; |
496 | } |
497 | |
498 | /** |
499 | * Call a INSERT statement. |
500 | * |
501 | * @param string|null $intoTable |
502 | * |
503 | * @return Insert |
504 | */ |
505 | public function insert(string $intoTable = null) : Insert |
506 | { |
507 | $instance = new Insert($this); |
508 | if ($intoTable !== null) { |
509 | $instance->into($intoTable); |
510 | } |
511 | return $instance; |
512 | } |
513 | |
514 | /** |
515 | * Call a LOAD DATA statement. |
516 | * |
517 | * @param string|null $intoTable |
518 | * |
519 | * @return LoadData |
520 | */ |
521 | public function loadData(string $intoTable = null) : LoadData |
522 | { |
523 | $instance = new LoadData($this); |
524 | if ($intoTable !== null) { |
525 | $instance->intoTable($intoTable); |
526 | } |
527 | return $instance; |
528 | } |
529 | |
530 | /** |
531 | * Call a REPLACE statement. |
532 | * |
533 | * @param string|null $intoTable |
534 | * |
535 | * @return Replace |
536 | */ |
537 | public function replace(string $intoTable = null) : Replace |
538 | { |
539 | $instance = new Replace($this); |
540 | if ($intoTable !== null) { |
541 | $instance->into($intoTable); |
542 | } |
543 | return $instance; |
544 | } |
545 | |
546 | /** |
547 | * Call a SELECT statement. |
548 | * |
549 | * @param array<string,Closure|string>|Closure|string|null $reference |
550 | * @param array<string,Closure|string>|Closure|string ...$references |
551 | * |
552 | * @return Select |
553 | */ |
554 | public function select( |
555 | array | Closure | string $reference = null, |
556 | array | Closure | string ...$references |
557 | ) : Select { |
558 | $instance = new Select($this); |
559 | if ($reference !== null) { |
560 | $instance->from($reference, ...$references); |
561 | } |
562 | return $instance; |
563 | } |
564 | |
565 | /** |
566 | * Call a UPDATE statement. |
567 | * |
568 | * @param array<string,Closure|string>|Closure|string|null $reference |
569 | * @param array<string,Closure|string>|Closure|string ...$references |
570 | * |
571 | * @return Update |
572 | */ |
573 | public function update( |
574 | array | Closure | string $reference = null, |
575 | array | Closure | string ...$references |
576 | ) : Update { |
577 | $instance = new Update($this); |
578 | if ($reference !== null) { |
579 | $instance->table($reference, ...$references); |
580 | } |
581 | return $instance; |
582 | } |
583 | |
584 | /** |
585 | * Call a WITH statement. |
586 | * |
587 | * @return With |
588 | */ |
589 | public function with() : With |
590 | { |
591 | return new With($this); |
592 | } |
593 | |
594 | public function lastQuery() : string |
595 | { |
596 | return $this->lastQuery; |
597 | } |
598 | |
599 | /** |
600 | * Executes an SQL statement and return the number of affected rows. |
601 | * |
602 | * @param string $statement |
603 | * |
604 | * @return int|string |
605 | */ |
606 | public function exec(#[Language('SQL')] string $statement) : int | string |
607 | { |
608 | $this->lastQuery = $statement; |
609 | isset($this->debugCollector) |
610 | ? $this->addToDebug(fn () => $this->mysqli->real_query($statement)) |
611 | : $this->mysqli->real_query($statement); |
612 | if ($this->mysqli->field_count) { |
613 | $result = $this->mysqli->store_result(); |
614 | if ($result) { |
615 | $result->free(); |
616 | } |
617 | } |
618 | return $this->mysqli->affected_rows; |
619 | } |
620 | |
621 | /** |
622 | * Executes an SQL statement, returning a result set as a Result object. |
623 | * |
624 | * Must be: SELECT, SHOW, DESCRIBE or EXPLAIN |
625 | * |
626 | * @param string $statement |
627 | * @param bool $buffered |
628 | * |
629 | * @see https://www.php.net/manual/en/mysqlinfo.concepts.buffering.php |
630 | * |
631 | * @throws InvalidArgumentException if $statement does not return result |
632 | * |
633 | * @return Result |
634 | */ |
635 | public function query( |
636 | #[Language('SQL')] string $statement, |
637 | bool $buffered = true |
638 | ) : Result { |
639 | $this->lastQuery = $statement; |
640 | $resultMode = $buffered ? \MYSQLI_STORE_RESULT : \MYSQLI_USE_RESULT; |
641 | $result = isset($this->debugCollector) |
642 | ? $this->addToDebug(fn () => $this->mysqli->query($statement, $resultMode)) |
643 | : $this->mysqli->query($statement, $resultMode); |
644 | if (\is_bool($result)) { |
645 | throw new InvalidArgumentException( |
646 | "Statement does not return result: {$statement}" |
647 | ); |
648 | } |
649 | return new Result($result, $buffered); |
650 | } |
651 | |
652 | /** |
653 | * Prepares a statement for execution and returns a PreparedStatement object. |
654 | * |
655 | * @param string $statement |
656 | * |
657 | * @throws RuntimeException if prepared statement fail |
658 | * |
659 | * @return PreparedStatement |
660 | */ |
661 | public function prepare(#[Language('SQL')] string $statement) : PreparedStatement |
662 | { |
663 | $prepared = $this->mysqli->prepare($statement); |
664 | if ($prepared === false) { |
665 | throw new RuntimeException('Prepared statement failed: ' . $statement); |
666 | } |
667 | return new PreparedStatement($prepared); |
668 | } |
669 | |
670 | /** |
671 | * Run statements in a transaction. |
672 | * |
673 | * @param callable $statements |
674 | * |
675 | * @throws Exception if statements fail |
676 | * @throws LogicException if transaction already is active |
677 | * |
678 | * @return static |
679 | */ |
680 | public function transaction(callable $statements) : static |
681 | { |
682 | if ($this->inTransaction) { |
683 | throw new LogicException('Transaction already is active'); |
684 | } |
685 | $this->inTransaction = true; |
686 | $this->mysqli->autocommit(false); |
687 | $this->mysqli->begin_transaction(); |
688 | try { |
689 | $statements($this); |
690 | $this->mysqli->commit(); |
691 | } catch (Exception $exception) { |
692 | $this->mysqli->rollback(); |
693 | throw $exception; |
694 | } finally { |
695 | $this->inTransaction = false; |
696 | } |
697 | return $this; |
698 | } |
699 | |
700 | /** |
701 | * Gets the LAST_INSERT_ID(). |
702 | * |
703 | * Note: When an insert has many rows, this function returns the id of the |
704 | * first row inserted! |
705 | * That is default on MySQL. |
706 | * |
707 | * @return int|string |
708 | */ |
709 | public function insertId() : int | string |
710 | { |
711 | return $this->mysqli->insert_id; |
712 | } |
713 | |
714 | /** |
715 | * Protect identifier. |
716 | * |
717 | * @param string $identifier |
718 | * |
719 | * @see https://mariadb.com/kb/en/identifier-names/ |
720 | * |
721 | * @return string |
722 | */ |
723 | public function protectIdentifier(string $identifier) : string |
724 | { |
725 | if ($identifier === '*') { |
726 | return '*'; |
727 | } |
728 | $identifier = \strtr($identifier, ['`' => '``', '.' => '`.`']); |
729 | $identifier = "`{$identifier}`"; |
730 | return \strtr($identifier, ['`*`' => '*']); |
731 | } |
732 | |
733 | /** |
734 | * Quote SQL values. |
735 | * |
736 | * @param bool|float|int|string|null $value Value to be quoted |
737 | * |
738 | * @see https://mariadb.com/kb/en/quote/ |
739 | * |
740 | * @throws InvalidArgumentException For invalid value type |
741 | * |
742 | * @return float|int|string If the value is null, returns a string containing |
743 | * the word "NULL". If is false, "FALSE". If is true, "TRUE". If is a string, |
744 | * returns the quoted string. The types int or float returns the same input value. |
745 | */ |
746 | public function quote(float | bool | int | string | null $value) : float | int | string |
747 | { |
748 | $type = \gettype($value); |
749 | if ($type === 'string') { |
750 | // @phpstan-ignore-next-line |
751 | $value = $this->mysqli->real_escape_string($value); |
752 | return "'{$value}'"; |
753 | } |
754 | if ($type === 'integer' || $type === 'double') { |
755 | return $value; // @phpstan-ignore-line |
756 | } |
757 | if ($type === 'boolean') { |
758 | return $value ? 'TRUE' : 'FALSE'; |
759 | } |
760 | if ($value === null) { |
761 | return 'NULL'; |
762 | } |
763 | // @codeCoverageIgnoreStart |
764 | // Should never throw - all accepted types have been verified |
765 | throw new InvalidArgumentException("Invalid value type: {$type}"); |
766 | // @codeCoverageIgnoreEnd |
767 | } |
768 | |
769 | public function setDebugCollector(DatabaseCollector $collector) : static |
770 | { |
771 | $collector->setDatabase($this); |
772 | $this->debugCollector = $collector; |
773 | return $this; |
774 | } |
775 | |
776 | protected function addToDebug(Closure $function) : mixed |
777 | { |
778 | $start = \microtime(true); |
779 | try { |
780 | $result = $function(); |
781 | } catch (Exception $exception) { |
782 | $this->finalizeAddToDebug($start, $exception->getMessage()); |
783 | throw $exception; |
784 | } |
785 | $this->finalizeAddToDebug($start); |
786 | return $result; |
787 | } |
788 | |
789 | protected function finalizeAddToDebug( |
790 | float $start, |
791 | string|null $description = null |
792 | ) : void { |
793 | $end = \microtime(true); |
794 | $rows = $this->mysqli->affected_rows; |
795 | $rows = $rows < 0 ? 'error' : $rows; |
796 | $this->debugCollector->addData([ |
797 | 'start' => $start, |
798 | 'end' => $end, |
799 | 'statement' => $this->lastQuery(), |
800 | 'rows' => $rows, |
801 | 'description' => $description, |
802 | ]); |
803 | } |
804 | } |