Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
79.12% |
144 / 182 |
|
71.43% |
15 / 21 |
CRAP | |
0.00% |
0 / 1 |
Mysql | |
79.12% |
144 / 182 |
|
71.43% |
15 / 21 |
148.22 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
connect | |
71.43% |
10 / 14 |
|
0.00% |
0 / 1 |
4.37 | |||
setOptions | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
5 | |||
hasOptions | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
3 | |||
beginTransaction | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
4 | |||
commit | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
4 | |||
rollback | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
4 | |||
isSuccess | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
3 | |||
query | |
66.67% |
12 / 18 |
|
0.00% |
0 / 1 |
8.81 | |||
prepare | |
61.54% |
8 / 13 |
|
0.00% |
0 / 1 |
6.42 | |||
bindParams | |
47.06% |
16 / 34 |
|
0.00% |
0 / 1 |
43.08 | |||
execute | |
66.67% |
8 / 12 |
|
0.00% |
0 / 1 |
7.33 | |||
fetch | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
8 | |||
fetchAll | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
disconnect | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
escape | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
getLastId | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getNumberOfRows | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
3.02 | |||
getNumberOfAffectedRows | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getVersion | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getTables | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 |
1 | <?php |
2 | /** |
3 | * Pop PHP Framework (http://www.popphp.org/) |
4 | * |
5 | * @link https://github.com/popphp/popphp-framework |
6 | * @author Nick Sagona, III <dev@nolainteractive.com> |
7 | * @copyright Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com) |
8 | * @license http://www.popphp.org/license New BSD License |
9 | */ |
10 | |
11 | /** |
12 | * @namespace |
13 | */ |
14 | namespace Pop\Db\Adapter; |
15 | |
16 | /** |
17 | * MySQL database adapter class |
18 | * |
19 | * @category Pop |
20 | * @package Pop\Db |
21 | * @author Nick Sagona, III <dev@nolainteractive.com> |
22 | * @copyright Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com) |
23 | * @license http://www.popphp.org/license New BSD License |
24 | * @version 6.5.0 |
25 | */ |
26 | class Mysql extends AbstractAdapter |
27 | { |
28 | |
29 | /** |
30 | * Statement result |
31 | * @var bool |
32 | */ |
33 | protected bool $statementResult = false; |
34 | |
35 | /** |
36 | * Constructor |
37 | * |
38 | * Instantiate the MySQL database connection object using mysqli |
39 | * |
40 | * @param array $options |
41 | */ |
42 | public function __construct(array $options = []) |
43 | { |
44 | if (!empty($options)) { |
45 | $this->connect($options); |
46 | } |
47 | } |
48 | |
49 | /** |
50 | * Connect to the database |
51 | * |
52 | * @param array $options |
53 | * @return Mysql |
54 | */ |
55 | public function connect(array $options = []): Mysql |
56 | { |
57 | if (!empty($options)) { |
58 | $this->setOptions($options); |
59 | } else if (!$this->hasOptions()) { |
60 | $this->throwError('Error: The proper database credentials were not passed.'); |
61 | } |
62 | |
63 | $this->connection = new \mysqli( |
64 | $this->options['host'], $this->options['username'], $this->options['password'], |
65 | $this->options['database'], $this->options['port'], $this->options['socket'] |
66 | ); |
67 | |
68 | if ($this->connection->connect_error != '') { |
69 | $this->throwError( |
70 | 'MySQL Connection Error: ' . $this->connection->connect_error . |
71 | ' (#' . $this->connection->connect_errno . ')' |
72 | ); |
73 | } |
74 | |
75 | return $this; |
76 | } |
77 | |
78 | /** |
79 | * Set database connection options |
80 | * |
81 | * @param array $options |
82 | * @return Mysql |
83 | */ |
84 | public function setOptions(array $options): Mysql |
85 | { |
86 | if (!isset($options['host'])) { |
87 | $options['host'] = 'localhost'; |
88 | } |
89 | if (!isset($options['port'])) { |
90 | $options['port'] = ini_get('mysqli.default_port'); |
91 | } |
92 | if (!isset($options['socket'])) { |
93 | $options['socket'] = ini_get('mysqli.default_socket'); |
94 | } |
95 | |
96 | $this->options = $options; |
97 | |
98 | if (!$this->hasOptions()) { |
99 | $this->throwError('Error: The proper database credentials were not passed.'); |
100 | } |
101 | |
102 | return $this; |
103 | } |
104 | |
105 | /** |
106 | * Has database connection options |
107 | * |
108 | * @return bool |
109 | */ |
110 | public function hasOptions(): bool |
111 | { |
112 | return (isset($this->options['database']) && isset($this->options['username']) && isset($this->options['password'])); |
113 | } |
114 | |
115 | /** |
116 | * Begin a transaction |
117 | * |
118 | * @param ?int $flags |
119 | * @param ?string $name |
120 | * @return Mysql |
121 | */ |
122 | public function beginTransaction(?int $flags = null, ?string $name = null): Mysql |
123 | { |
124 | $this->getTransactionManager()->enter( |
125 | beginFunc: function () use ($flags, $name) { |
126 | if (($flags !== null) && ($name !== null)) { |
127 | $this->connection->begin_transaction($flags, $name); |
128 | } else if ($flags !== null) { |
129 | $this->connection->begin_transaction($flags); |
130 | } else { |
131 | $this->connection->begin_transaction(); |
132 | } |
133 | }, |
134 | savepointFunc: function (string $sp) { $this->connection->savepoint($sp); }, |
135 | ); |
136 | |
137 | return $this; |
138 | } |
139 | |
140 | /** |
141 | * Commit a transaction |
142 | * |
143 | * @param ?int $flags |
144 | * @param ?string $name |
145 | * @return Mysql |
146 | */ |
147 | public function commit(?int $flags = null, ?string $name = null): Mysql |
148 | { |
149 | $this->getTransactionManager()->leave(true, |
150 | commitFunc: function () use ($flags, $name) { |
151 | if (($flags !== null) && ($name !== null)) { |
152 | $this->connection->commit($flags, $name); |
153 | } else if ($flags !== null) { |
154 | $this->connection->commit($flags); |
155 | } else { |
156 | $this->connection->commit(); |
157 | } |
158 | }, |
159 | savepointReleaseFunc: function (string $sp) { $this->connection->release_savepoint($sp); }, |
160 | ); |
161 | |
162 | return $this; |
163 | } |
164 | |
165 | /** |
166 | * Rollback a transaction |
167 | * |
168 | * @param ?int $flags |
169 | * @param ?string $name |
170 | * @return Mysql |
171 | */ |
172 | public function rollback(?int $flags = null, ?string $name = null): Mysql |
173 | { |
174 | $this->getTransactionManager()->leave(false, |
175 | rollbackFunc: function () use ($flags, $name) { |
176 | if (($flags !== null) && ($name !== null)) { |
177 | $this->connection->rollback($flags, $name); |
178 | } else if ($flags !== null) { |
179 | $this->connection->rollback($flags); |
180 | } else { |
181 | $this->connection->rollback(); |
182 | } |
183 | }, |
184 | savepointRollbackFunc: function (string $sp) { $this->query('ROLLBACK TO SAVEPOINT ' . $sp); }, |
185 | ); |
186 | |
187 | return $this; |
188 | } |
189 | |
190 | /** |
191 | * Check if transaction is success |
192 | * |
193 | * @return bool |
194 | */ |
195 | public function isSuccess(): bool |
196 | { |
197 | return ((($this->result) || ($this->statementResult)) && (!$this->hasError())); |
198 | } |
199 | |
200 | /** |
201 | * Execute a SQL query directly |
202 | * |
203 | * @param mixed $sql |
204 | * @return Mysql |
205 | */ |
206 | public function query(mixed $sql): Mysql |
207 | { |
208 | $this->statement = null; |
209 | $this->statementResult = false; |
210 | |
211 | if ($sql instanceof \Pop\Db\Sql\AbstractSql) { |
212 | $sql = (string)$sql; |
213 | } |
214 | |
215 | if (!($this->result = $this->connection->query($sql))) { |
216 | if ($this->profiler !== null) { |
217 | $this->profiler->addStep(); |
218 | $this->profiler->current->setQuery($sql); |
219 | $this->profiler->current->addError($this->connection->error, $this->connection->errno); |
220 | } |
221 | $this->throwError('Error: ' . $this->connection->errno . ' => ' . $this->connection->error); |
222 | } else if ($this->profiler !== null) { |
223 | $this->profiler->addStep(); |
224 | $this->profiler->current->setQuery($sql); |
225 | } |
226 | |
227 | if ($this->profiler !== null) { |
228 | $this->profiler->current->finish(); |
229 | if ($this->profiler->hasDebugger()) { |
230 | $this->profiler->debugger()->save(); |
231 | } |
232 | } |
233 | |
234 | return $this; |
235 | } |
236 | |
237 | /** |
238 | * Prepare a SQL query |
239 | * |
240 | * @param mixed $sql |
241 | * @return Mysql |
242 | */ |
243 | public function prepare(mixed $sql): Mysql |
244 | { |
245 | if ($sql instanceof \Pop\Db\Sql\AbstractSql) { |
246 | $sql = (string)$sql; |
247 | } |
248 | |
249 | $this->statement = $this->connection->stmt_init(); |
250 | if (!$this->statement->prepare($sql)) { |
251 | if ($this->profiler !== null) { |
252 | $this->profiler->addStep(); |
253 | $this->profiler->current->setQuery($sql); |
254 | $this->profiler->current->addError($this->statement->error, $this->statement->errno); |
255 | } |
256 | $this->throwError('MySQL Statement Error: ' . $this->statement->errno . ' (#' . $this->statement->error . ')'); |
257 | } else if ($this->profiler !== null) { |
258 | $this->profiler->addStep(); |
259 | $this->profiler->current->setQuery($sql); |
260 | } |
261 | |
262 | return $this; |
263 | } |
264 | |
265 | /** |
266 | * Bind parameters to a prepared SQL query |
267 | * |
268 | * @param array $params |
269 | * @return Mysql |
270 | */ |
271 | public function bindParams(array $params): Mysql |
272 | { |
273 | $bindParams = ['']; |
274 | |
275 | if ($this->profiler !== null) { |
276 | $this->profiler->current->addParams($params); |
277 | } |
278 | |
279 | $i = 1; |
280 | foreach ($params as $dbColumnName => $dbColumnValue) { |
281 | if (is_array($dbColumnValue)) { |
282 | foreach ($dbColumnValue as $dbColumnVal) { |
283 | ${$dbColumnName . $i} = $dbColumnVal; |
284 | |
285 | if (is_int($dbColumnVal)) { |
286 | $bindParams[0] .= 'i'; |
287 | } else if (is_double($dbColumnVal)) { |
288 | $bindParams[0] .= 'd'; |
289 | } else if (is_string($dbColumnVal)) { |
290 | $bindParams[0] .= 's'; |
291 | } else if (is_null($dbColumnVal)) { |
292 | $bindParams[0] .= 's'; |
293 | } else { |
294 | $bindParams[0] .= 'b'; |
295 | } |
296 | |
297 | $bindParams[] = &${$dbColumnName . $i}; |
298 | $i++; |
299 | } |
300 | } else { |
301 | ${$dbColumnName . $i} = $dbColumnValue; |
302 | |
303 | if (is_int($dbColumnValue)) { |
304 | $bindParams[0] .= 'i'; |
305 | } else if (is_double($dbColumnValue)) { |
306 | $bindParams[0] .= 'd'; |
307 | } else if (is_string($dbColumnValue)) { |
308 | $bindParams[0] .= 's'; |
309 | } else if (is_null($dbColumnValue)) { |
310 | $bindParams[0] .= 's'; |
311 | } else { |
312 | $bindParams[0] .= 'b'; |
313 | } |
314 | |
315 | $bindParams[] = &${$dbColumnName . $i}; |
316 | $i++; |
317 | } |
318 | |
319 | } |
320 | |
321 | if (call_user_func_array([$this->statement, 'bind_param'], $bindParams) === false) { |
322 | $this->throwError('Error: There was an error binding the parameters'); |
323 | } |
324 | |
325 | return $this; |
326 | } |
327 | |
328 | /** |
329 | * Execute a prepared SQL query |
330 | * |
331 | * @throws Exception |
332 | * @return Mysql |
333 | */ |
334 | public function execute(): Mysql |
335 | { |
336 | if ($this->statement === null) { |
337 | $this->throwError('Error: The database statement resource is not currently set'); |
338 | } |
339 | |
340 | $this->statementResult = $this->statement->execute(); |
341 | |
342 | if (!empty($this->statement->error)) { |
343 | if ($this->profiler !== null) { |
344 | $this->profiler->current->addError($this->statement->error, $this->statement->errno); |
345 | } |
346 | $this->throwError('MySQL Statement Error: ' . $this->statement->errno . ' (#' . $this->statement->error . ')'); |
347 | } |
348 | |
349 | if ($this->profiler !== null) { |
350 | $this->profiler->current->finish(); |
351 | if ($this->profiler->hasDebugger()) { |
352 | $this->profiler->debugger()->save(); |
353 | } |
354 | } |
355 | |
356 | return $this; |
357 | } |
358 | |
359 | /** |
360 | * Fetch and return a row from the result |
361 | * |
362 | * @throws Exception |
363 | * @return mixed |
364 | */ |
365 | public function fetch(): mixed |
366 | { |
367 | if (($this->statement !== null) && ($this->statementResult !== false)) { |
368 | $params = []; |
369 | $bindParams = []; |
370 | $row = false; |
371 | |
372 | $metaData = $this->statement->result_metadata(); |
373 | if ($metaData !== false) { |
374 | foreach ($metaData->fetch_fields() as $col) { |
375 | ${$col->name} = null; |
376 | $bindParams[] = &${$col->name}; |
377 | $params[] = $col->name; |
378 | } |
379 | |
380 | call_user_func_array([$this->statement, 'bind_result'], $bindParams); |
381 | |
382 | if (($r = $this->statement->fetch()) != false) { |
383 | $row = []; |
384 | foreach ($bindParams as $dbColumnName => $dbColumnValue) { |
385 | $row[$params[$dbColumnName]] = $dbColumnValue; |
386 | } |
387 | } |
388 | } |
389 | |
390 | return $row; |
391 | } else { |
392 | if ($this->result === null) { |
393 | $this->throwError('Error: The database result resource is not currently set.'); |
394 | } |
395 | return $this->result->fetch_array(MYSQLI_ASSOC); |
396 | } |
397 | } |
398 | |
399 | /** |
400 | * Fetch and return all rows from the result |
401 | * |
402 | * @return array |
403 | */ |
404 | public function fetchAll(): array |
405 | { |
406 | $rows = []; |
407 | while (($row = $this->fetch())) { |
408 | $rows[] = $row; |
409 | } |
410 | return $rows; |
411 | } |
412 | |
413 | /** |
414 | * Disconnect from the database |
415 | * |
416 | * @return void |
417 | */ |
418 | public function disconnect(): void |
419 | { |
420 | if ($this->isConnected()) { |
421 | $this->connection->close(); |
422 | } |
423 | |
424 | parent::disconnect(); |
425 | } |
426 | |
427 | /** |
428 | * Escape the value |
429 | * |
430 | * @param ?string $value |
431 | * @return string |
432 | */ |
433 | public function escape(?string $value = null): string |
434 | { |
435 | return (!empty($value)) ? $this->connection->real_escape_string($value) : ''; |
436 | } |
437 | |
438 | /** |
439 | * Return the last ID of the last query |
440 | * |
441 | * @return int |
442 | */ |
443 | public function getLastId(): int |
444 | { |
445 | return $this->connection->insert_id; |
446 | } |
447 | |
448 | /** |
449 | * Return the number of rows from the last query |
450 | * |
451 | * @throws Exception |
452 | * @return int |
453 | */ |
454 | public function getNumberOfRows(): int |
455 | { |
456 | $count = 0; |
457 | |
458 | if ($this->statement !== null) { |
459 | $this->statement->store_result(); |
460 | $count = $this->statement->num_rows; |
461 | } else if ($this->result !== null) { |
462 | $count = $this->result->num_rows; |
463 | } else { |
464 | $this->throwError('Error: The database result resource is not currently set.'); |
465 | } |
466 | |
467 | return $count; |
468 | } |
469 | |
470 | /** |
471 | * Return the number of affected rows from the last query |
472 | * |
473 | * @return int |
474 | */ |
475 | public function getNumberOfAffectedRows(): int |
476 | { |
477 | return $this->connection->affected_rows; |
478 | } |
479 | |
480 | /** |
481 | * Return the database version. |
482 | * |
483 | * @return string |
484 | */ |
485 | public function getVersion(): string |
486 | { |
487 | return 'MySQL ' . $this->connection->server_info; |
488 | } |
489 | |
490 | /** |
491 | * Return the tables in the database |
492 | * |
493 | * @return array |
494 | */ |
495 | public function getTables(): array |
496 | { |
497 | $tables = []; |
498 | |
499 | $this->query('SHOW TABLES'); |
500 | while (($row = $this->fetch())) { |
501 | foreach($row as $value) { |
502 | $tables[] = $value; |
503 | } |
504 | } |
505 | |
506 | return $tables; |
507 | } |
508 | |
509 | } |