Wrapper class for MySQLi prepared statements
Using MySQLi prepared statements is a great way to prevent against SQL injection, but it can start feeling tedious after a while. I thought this could be improved a little, which is why wanted to create an easy to use MySQLi database wrapper, while also ensuring that the SQL queries aren't broken up into proprietary syntactic sugar chaining. This way, so you can have extremely concise code, while still keeping your SQL syntax intact. The purpose of this class is to make using plain SQL queries as enjoyable as possible, without being an ORM. In a lot of ways, I modeled this class after what I believe the general syntax for vanilla MySQLi/PDO should be., (*1)
I specifically chose MySQLi over PDO to have the versatiliy to use MySQL-specific features. Currently, the only ones I'm using are mysqli::info and proper closing/freeing methods. Unfortunately, asynchronous queries don't have support for prepared statements yet, so I'll wait until they do to implement them., (*2)
On a side note, if you'd like to know how to use MySQLi the "vanilla way", check out this tutorial on MySQLi Prepared Statements., (*3)
The purpose of this class is to keep things as simple as possible, while accounting for the most common uses. If there's something you'd like me to add, feel free to suggest it or send a pull request., (*4)
PHP 7.1+, (*5)
Click here to view changes to each version., (*6)
Composer, (*7)
composer require websitebeaver/simple-mysqli
Then include or require the file in your php page., (*8)
require 'vendor/autoload.php';
Git, (*9)
Clone either the latest version or by tag., (*10)
//Get by version number git clone https://github.com/WebsiteBeaver/Simple-MySQLi/tree/{your version number} //Get the latest git clone https://github.com/WebsiteBeaver/Simple-MySQLi.git
Then include or require the file in your php page., (*11)
require 'simple-mysqli.php';
Let's get straight to the point! The best way to learn is by examples., (*12)
One of the aspects of MySQLi I actually like a lot is the fact that error reporting is automatically turned off. Unfortunately I wasn't able to replicate this, as I throw an excpetion on the the constructor, therefore potentially exposing the parameter values. This is why I turned on mysqli reporting by doing mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT)
— since you'll be wrapping it in a try/catch
block anyway. So you must either wrap it around in a try/catch
block or create your own custom handler. Make sure you use $e->getMessage()
and not $e
so your password isn't exposed. Either way, you must solely report your errors in your error log. To do this, make sure your php.ini
file has the following settings in production: display_errors = Off
and log_errors = On
. Keep in mind that using echo
, die
or exit
to print the error message is extremely dangerous as well., (*13)
Try/Catch, (*14)
try { $mysqli = new SimpleMySQLi("localhost", "username", "password", "dbName", "utf8mb4", "assoc"); } catch(Exception $e) { error_log($e->getMessage()); exit('Someting weird happened'); //Should be a message a typical user could understand }
Custom Exception Handler, (*15)
This is pretty neat, since you can avoid nesting. It is commonly used to redirect to a single error page, but can be used like the following as well. You can reassign this to give specific messages on your AJAX pages as well. This will catch all of your exceptions on each page this is used on. So you'll either have to call set_exception_handler()
on each page or use restore_exception_handler()
to revert to the previous one., (*16)
set_exception_handler(function($e) { error_log($e->getMessage()); exit('Someting weird happened'); //Should be a message a typical user could understand }); $mysqli = new SimpleMySQLi("localhost", "username", "password", "dbName", "utf8mb4", "assoc");
$insert = $mysqli->query("INSERT INTO myTable (name, age) VALUES (?, ?)", [$_POST['name'], $_POST['age']]); echo $insert->affectedRows(); echo $insert->insertId();
$update = $mysqli->query("UPDATE myTable SET name = ? WHERE id = ?", [$_POST['name'], $_SESSION['id']]); echo $update->affectedRows();
$delete = $mysqli->query("DELETE FROM myTable WHERE id = ?", [$_SESSION['id']]); echo $delete->affectedRows();
The problem with affectedRows()
is that it will literally just tell you if any rows are affected. So if it returned 0, you wouldn't know if that means that the WHERE clause didn't match or that you updated the row with the same values. One solution Simple MySQLi offers is to use info()
, which utilizes mysqli::info and converts the result string to an array. You can use this in other queries it supports as well., (*17)
$update = $mysqli->query("UPDATE myTable SET name = ? WHERE id = ?", [$_POST['name'], $_SESSION['id']]); echo $update->affectedRows(); var_export($update->info()); //For more specific version
Here's what info()
would print. This could be useful for checking if you updated your values with the exact same as the old ones., (*18)
['Rows matched' => 1, 'Changed' => 0, 'Warnings' => 0]
This is nice and all, but it might be more convenient in some cases to just change the behavior of affectedRows()
to use rows matched, rather than rows changed., (*19)
$update = $mysqli->query("UPDATE myTable SET name = ? WHERE id = ?", [$_POST['name'], $_SESSION['id']]); echo $update->rowsMatched(); //Rows Matched: 1 echo $update->affectedRows(); //Rows Changed: 0
You can either fetch your entire result in an array with fetchAll()
or loop through each row individually with fetch()
, if you're planning on modifying the array. You could obviously use fetchAll()
for any scenario, but using fetch()
is more efficient memory-wise if you're making changes to the array, as it will save you from having to loop through it a second time. However, from my experience, most queries don't need any modifications, so fetchAll()
should primarily be used. If you just need one row, then obviously fetch()
should be used., (*20)
$arr = $mysqli->query("SELECT id, name, age FROM events WHERE id <= ?", [4])->fetchAll("assoc"); if(!$arr) exit('No rows'); var_export($arr);
Output:, (*21)
[ ['id' => 24 'name' => 'Jerry', 'age' => 14], ['id' => 201 'name' => 'Alexa', 'age' => 22] ]
$arr = $mysqli->query("SELECT id, name, age FROM events WHERE id <= ?", [4])->fetchAll("obj"); if(!$arr) exit('No rows'); var_export($arr);
Output:, (*22)
[ stdClass Object ['id' => 24 'name' => 'Jerry', 'age' => 14], stdClass Object ['id' => 201 'name' => 'Alexa', 'age' => 22] ]
You can even fetch into a new instance of a class., (*23)
class myClass {} $arr = $mysqli->query("SELECT id, name, age FROM events WHERE id <= ?", [4])->fetchAll("obj", "myClass"); if(!$arr) exit('No rows'); var_export($arr);
Output:, (*24)
[ myClass Object ['id' => 24 'name' => 'Jerry', 'age' => 14], myClass Object ['id' => 201 'name' => 'Alexa', 'age' => 22] ]
$arr = $mysqli->query("SELECT id, name, age FROM events WHERE id <= ?", [12])->fetch("assoc"); if(!$arr) exit('No rows'); var_export($arr);
Output:, (*25)
['id' => 24 'name' => 'Jerry', 'age' => 14]
$arr = $mysqli->query("SELECT id, name, age FROM myTable WHERE name = ?", [$_POST['name']])->fetchAll("num"); //must use number array to use in list if(!$arr) exit('No rows'); list($id, $name, $age) = $arr; echo $age; //Output 34
This is an ideal way of fetching a scalar. Using the MySQL COUNT
function gives you a number, so you can easily check for truthiness, as no rows would give you a value of 0., (*26)
$count = $mysqli->query("SELECT COUNT(*) FROM myTable WHERE name = ?", [$_POST['name']])->fetch("col"); if(!$count) exit('No rows'); echo $count; //Output: 284
But what if you were to fetch a single value from your database in scenario where the column could have a boolean value, like NULL or 0? This would make it impossible to distinguish between no rows or a falsy values. This is why I'd suggest using numRows()
in every other case instead., (*27)
$favoriteSport = $mysqli->query("SELECT favorite_sport FROM myTable WHERE id = ?", [23])->fetch("col"); if($favoriteSport->numRows() < 1) exit('No rows'); echo $favoriteSport; //Output: 'basketball'
$heights = $mysqli->query("SELECT height FROM myTable WHERE id < ?", [500])->fetchAll("col"); if(!$heights) exit('No rows'); var_export($heights);
Output:, (*28)
[78, 64, 68, 54, 58, 63]
$result = $mysqli->query("SELECT name, email, number FROM events WHERE id <= ?", [450]); while($row = $result->fetch("assoc")) { $names[] = $row['name']; $emails[] = $row['email']; $numbers[] = $row['number']; } if(!isset($names) || !isset($emails) || !isset($numbers)) exit('No rows'); var_export($names);
Output:, (*29)
['Bobby', 'Jessica', 'Victor', 'Andrew', 'Mallory']
//First column must be unique, like a primary key; can only select 2 columns $arr = $mysqli->query("SELECT id, name FROM myTable WHERE age <= ?", [25])->fetchAll("keyPair"); if(!$arr) exit('No rows'); var_export($arr);
Output:, (*30)
[7 => 'Jerry', 10 => 'Bill', 29 => 'Bobby']
//First column must be unique, like a primary key $arr = $mysqli->query("SELECT id, max_bench, max_squat FROM myTable WHERE weight < ?", [205])->fetchAll("keyPairArr"); if(!$arr) exit('No rows'); var_export($arr);
Output:, (*31)
[ 17 => ['max_bench' => 230, 'max_squat' => 175], 84 => ['max_bench' => 195, 'max_squat' => 235], 136 => ['max_bench' => 135, 'max_squat' => 285] ]
//First column must be common value to group by $arr = $mysqli->query("SELECT eye_color, name, weight FROM myTable WHERE age < ?", [29])->fetchAll("group"); if(!$arr) exit('No rows'); var_export($arr);
Output:, (*32)
[ 'green' => [ ['name' => 'Patrick', 'weight' => 178], ['name' => 'Olivia', 'weight' => 132] ], 'blue' => [ ['name' => 'Kyle', 'weight' => 128], ['name' => 'Ricky', 'weight' => 143] ], 'brown' => [ ['name' => 'Jordan', 'weight' => 173], ['name' => 'Eric', 'weight' => 198] ] ]
//First column must be common value to group by $arr = $mysqli->query("SELECT eye_color, name FROM myTable WHERE age < ?", [29])->fetchAll("groupCol"); if(!$arr) exit('No rows'); var_export($arr);
Output:, (*33)
[ 'green' => ['Patrick', 'Olivia'], 'blue' => ['Kyle', 'Ricky'], 'brown' => ['Jordan', 'Eric'] ]
//First column must be common value to group by $arr = $mysqli->query("SELECT eye_color, name, weight FROM myTable WHERE age < ?", [29])->fetchAll("groupObj"); if(!$arr) exit('No rows'); var_export($arr);
You can even pass in a class name, like you would with 'obj'., (*34)
$mysqli->query("SELECT eye_color, name, weight FROM myTable WHERE age < ?", [29])->fetchAll("groupObj", "myClass");
Output:, (*35)
[ 'green' => [ stdClass Object ['name' => 'Patrick', 'weight' => 178], stdClass Object ['name' => 'Olivia', 'weight' => 132] ], 'blue' => [ stdClass Object ['name' => 'Kyle', 'weight' => 128], stdClass Object ['name' => 'Ricky', 'weight' => 143] ], 'brown' => [ stdClass Object ['name' => 'Jordan', 'weight' => 173], stdClass Object ['name' => 'Eric', 'weight' => 198] ] ]
$search = "%{$_POST['search']}%"; $arr = $mysqli->query("SELECT id, name, age FROM events WHERE name LIKE ?", [$search])->fetchAll(); if(!$arr) exit('No rows'); var_export($arr);
$inArr = [12, 23, 44]; $clause = $mysqli->whereIn($inArr); //Create question marks $arr = $mysqli->query("SELECT event_name, description, location FROM events WHERE id IN($clause)", $inArr)->fetchAll(); if(!$arr) exit('No rows'); var_export($arr);
$inArr = [12, 23, 44]; $clause = $mysqli->whereIn($inArr); //Create question marks $fullArr = array_merge($inArr, [5]); //Merge WHERE IN values with rest of query $arr = $mysqli->query("SELECT event_name, description, location FROM events WHERE id IN($clause) AND id < ?", $fullArr)->fetchAll(); if(!$arr) exit('No rows'); var_export($arr);
This is probably my favorite aspect of this class, since the difference in terms of lines of code is absurd. This will also automatically rollback if affectedRows()
is less than one, in case zero rows are affected, which wouldn't trigger an exception. If any error occurs, it will append the message to your error log. Additionally, it will throw any exception if affectedRows()
equals zero., (*36)
$sql[] = "INSERT INTO myTable (name, age) VALUES (?, ?)"; $sql[] = "UPDATE myTable SET name = ? WHERE id = ?"; $sql[] = "UPDATE myTable SET name = ? WHERE id = ?"; $arrOfValues = [[$_POST['name'], $_POST['age']], ['Pablo', 34], [$_POST['name'], $_SESSION['id']]]; $mysqli->atomicQuery($sql, $arrOfValues);
$sql = "INSERT INTO myTable (name, age) VALUES (?, ?)"; $arrOfValues = [[$_POST['name'], $_POST['age']], ['Pablo', 34], [$_POST['name'], 22]]; $mysqli->atomicQuery($sql, $arrOfValues);
The regular way of doing transactions in Simple MySQLi with atomicQuery()
is exceedingly concise and can be used in most cases. However, sometimes you might want a little more control. For instance, under the hood, it only checks if each query's affectedRows()
is greater than one. This isn't suitable for a query like INSERT multiple or DELETE/UPDATE query that affects multiple rows., (*37)
There's also no need to start the transaction, nor deal with rollbacks. If you want to rollback, simply throw an exception, and it'll rollback for you, while printing the exception solely in the error log. Execute allows you to efficiently reuse your prepared statement with different values., (*38)
$mysqli->transaction(function($mysqli) { $insert = $mysqli->query("INSERT INTO myTable (sender, receiver) VALUES (?, ?)", [28, 330]); if($insert->affectedRows() < 1) throw new Exception('Error inserting'); echo $insert->insertId(); $insert->execute([243, 49]); //reuse same insert query $delete = $mysqli->query("DELETE FROM myTable WHERE max_bench < ?", [125]); });
Either wrap all your queries with one try/catch
or use the set_exception_handler()
function to either redirect to a global error page or a separate one for each page. Don't forget to take out echo in production, as you obviously do not need the client to see this information., (*39)
For some reason, mysqli_sql_exception
doesn't correctly convert errors to exceptions when too many bound variables or types on bind_param()
. This is why you should probably set a global error handler to convert this error to an exception. I'm showing how to convert all errors to exceptions, but it should be noted that a lot of programmers view this as controversial. In this case you only really have to worry about E_WARNING
anyway., (*40)
set_error_handler(function($errno, $errstr, $errfile, $errline) { throw new Exception("$errstr on line $errline in file $errfile"); });
Try/Catch, (*41)
//include mysqli_connect.php try { $insert = $mysqli->query("INSERT INTO myTable (name, age) VALUES (?, ?)", [$_POST['name'], $_POST['age']]); } catch (Exception $e) { error_log($e); exit('Error inserting'); }
Custom Exception Handler, (*42)
//include mysqli_connect.php set_exception_handler(function($e) { error_log($e); exit('Error inserting'); }); $insert = $mysqli->query("INSERT INTO myTable (name, age) VALUES (?, ?)", [$_POST['name'], $_POST['age']]);
Freeing the result and closing the prepared statement is very simple., (*43)
$arr = $mysqli->query("SELECT id, name, age FROM events WHERE id <= ?", [4])->fetchAll("assoc"); if(!$arr) exit('No rows'); $mysqli->freeResult(); //Free result $mysqli->closeStmt(); //Close statement $mysqli->close(); //Close connection
You can even chain them., (*44)
$arr = $mysqli->query("SELECT id, name, age FROM events WHERE id <= ?", [4])->fetchAll("assoc"); if(!$arr) exit('No rows'); $mysqli->freeResult()->closeStmt()->close();
new SimpleMySQLi(string $host, string $username, string $password, string $dbName, string $charset = 'utf8mb4', string $defaultFetchType = 'assoc')
Parameters, (*45)
PDO::FETCH_COLUMN
Throws, (*46)
$defaultFetchType
specified isn't one of the allowed fetch modesmysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT)
function query(string $sql, array|string|int $values = [], string $types = ''): self
Description, (*47)
All queries go here. If select statement, needs to be used with either fetch()
for single row and loop fetching or fetchAll()
for fetching all results., (*48)
Parameters, (*49)
Returns, (*50)
$this
, (*51)
Throws, (*52)
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT)
function execute(array $values = [], string $types = ''): self
Description, (*53)
Used in order to be more efficient if same SQL is used with different values. Is really a re-execute function, (*54)
Parameters, (*55)
Returns, (*56)
$this
, (*57)
Throws, (*58)
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT)
function whereIn(array $inArr): string
Description, (*59)
Create correct number of questions marks for WHERE IN()
array., (*60)
Parameters, (*61)
Returns, (*62)
string Correct number of question marks, (*63)
Throws, (*64)
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT)
function numRows(): int
Description, (*65)
Get number of rows from SELECT., (*66)
Returns, (*67)
Throws, (*68)
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT)
function affectedRows(): int
Description, (*69)
Get affected rows. Can be used instead of numRows() in SELECT, (*70)
Returns, (*71)
Throws, (*72)
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT)
function info(): array
Description, (*73)
A more specific version of affectedRows() to give you more info what happened. Uses $mysqli::info under the hood. Can be used for the following cases, (*74)
Returns, (*75)
Throws, (*76)
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT)
function rowsMatched(): int
Description, (*77)
Get rows matched instead of rows changed. Can strictly be used on UPDATE. Otherwise returns false, (*78)
Returns, (*79)
Throws, (*80)
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT)
function insertId(): int
Description, (*81)
Get the latest primary key inserted, (*82)
Returns, (*83)
Throws, (*84)
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT)
function fetch(string $fetchType = '', string $className = 'stdClass', array $classParams = [])
Description, (*85)
Fetch one row at a time, (*86)
Parameters, (*87)
Returns, (*88)
$fetchType
specifiedThrows, (*89)
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT)
function fetchAll(string $fetchType = '', string $className = 'stdClass'): array
Description, (*90)
Fetch all results in array, (*91)
Parameters, (*92)
fetchAll()
also has additional fetch modes:
PDO::FETCH_KEY_PAIR
PDO::FETCH_UNIQUE
PDO::FETCH_GROUP
PDO::FETCH_GROUP | PDO::FETCH_COLUMN
PDO::FETCH_GROUP | PDO::FETCH_CLASS
Returns, (*93)
$fetchType
specified
Throws, (*94)
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT)
function atomicQuery(array|string $sql, array $values, array $types = []): void
Description, (*95)
Just a normal transaction that will automatically rollback and print your message to your error log. Will also rollback if affectedRows()
is less than 1., (*96)
Parameters, (*97)
Throws, (*98)
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT)
function transaction(callable $callback($this)): void
Description, (*99)
Do stuff inside of transaction, (*100)
Parameters, (*101)
$this
Throws, (*102)
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT)
function freeResult(): self
Description, (*103)
Frees MySQL result, (*104)
Returns, (*105)
$this
, (*106)
Throws, (*107)
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT)
function closeStmt(): self
Description, (*108)
Closes MySQL prepared statement, (*109)
Returns, (*110)
$this
, (*111)
Throws, (*112)
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT)
function close(): void
Description, (*113)
Closes the MySQL connections, (*114)
Throws, (*115)
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT)
Changelog can be found here, (*116)