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