Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
79.12% covered (success)
79.12%
144 / 182
71.43% covered (success)
71.43%
15 / 21
CRAP
0.00% covered (danger)
0.00%
0 / 1
Mysql
79.12% covered (success)
79.12%
144 / 182
71.43% covered (success)
71.43%
15 / 21
148.22
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%
10 / 10
100.00% covered (success)
100.00%
1 / 1
5
 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
66.67% covered (warning)
66.67%
12 / 18
0.00% covered (danger)
0.00%
0 / 1
8.81
 prepare
61.54% covered (warning)
61.54%
8 / 13
0.00% covered (danger)
0.00%
0 / 1
6.42
 bindParams
47.06% covered (warning)
47.06%
16 / 34
0.00% covered (danger)
0.00%
0 / 1
43.08
 execute
66.67% covered (warning)
66.67%
8 / 12
0.00% covered (danger)
0.00%
0 / 1
7.33
 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
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 */
14namespace 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 */
26class 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}