This is a PHP script I wrote it back in 2015 and have been updating it since.
It is a MySQLI wrapper called SqlObject
(class) that was originally created to be used by multiple different calling functions so the same SQL connection would be used, unlike some other wrappers (or code) that opens a connection for each query.
The idea is simply, any SQL query that you are expecting a row as a result (or a variable) you call the getData($query, $mode)
function where $query
is the SQL string and $mode
is a const
: SqlObject::SINGLE_ROW
or SqlObject::MULTI_ROW
where SINGLE_ROW
returns an associative array as a row from the query and MULTI_ROW
returns an array of associative arrays (rows).
Any SQL query that you are not expecting a row from like INSERT
, UPDATE
, etc. you use the setData($query)
method. setData
returns TRUE
or FALSE
if the query was successful.
Error Handling
SqlObject
has two kinds of error handling: hard exceptions where code has failed such as a broken connection and switched error handling. Upon a MySQLi error such as a bad query or duplicate entry, the error number is passed into a switch where the error can be handled appropriately. For example, if there is a DEADLOCK error when trying to setData
, SqlObject
will retry 5 more times before returning FALSE
.
Wrapper Helpers
There are many helper functions that make general MySQL development less hellish.
insertQuery($table, $values)
updateQuery($table, $values)
isSqlSafe($str)
sanitizeForQuery($str, $blacklist = NULL, $whitelist = NULL)
escapeSpecialChars($str)
insertQuery
- Returns an SQL query string for a table name from an associative array of$value
->$key
pairs.updateQuery
- same asInsertQuery
but is for theUPDATE
command. It is up to the programmer to append theWHERE
clause (if any) to the result of this before using it.isSqlSafe
- Lightly scans a string for bogus SQL injection attempts. ReturnsTRUE
orFALSE
.sanitizeForQuery
- Scans a string for potentially dangerous text that can lead to code injection. It takes a string as the first argument to scan, an optional array of strings to be appended to the blacklisted strings within the function, and an optional array of strings that are to be whitelisted from the default blacklist within the function.escapeSpecialChars
- Escapes and cleans up user input (or any) text that is to be included in an SQL query. This is NOT a safeguard from injections, it only ensures that typical characters that should be escaped are done so properly.
I would appreciate any feedback from the community.
Here is a list of all of the functions:
public function setData($query)
public function getData($query, $returnType = self::SINGLE_ROW)
public function get_table_names($filter = NULL)
function get_last_arraysize()
public function get_affected_rows()
public function get_last_insert_id()
public function get_last_query()
public function get_error_number()
public function get_error()
public function get_database_name()
public function get_username()
public function change_database($new_database, $username = NULL, $password = NULL)
public function terminate()
public static function insertQuery($table, $values)
public static function updateQuery($table, $values)
public static function isSqlSafe($str)
public static function sanitizeForQuery($str, $blacklist = NULL, $whitelist = NULL)
public static function escapeSpecialChars($str)
private function retry_deadlock($query)
Here is an example of the usage (this is a hypothetical use case):
$sql = new SqlObject();
//Reuse the $sql object for both functions
getNames($sql);
getOccupation('John Doe', $sql);
function getNames($sql = NULL){
if($sql == NULL) { $sql = new SqlObject(); }
$query = "SELECT `ID`,`name` FROM `table` WHERE 1";
$result = $sql->getData($query, SqlObject::MULTI_ROW);
$numRows = $sql->get_last_arraysize();
for($i = 0; $i < $numRows; $i++){
printf("Name (ID=%d): %s\n", $result[$i]['ID'], $result[$i]['name']);
}
}
function getOccupation($name, $sql = NULL){
if($sql == NULL) { $sql = new SqlObject(); }
if(!SqlObject::is_sql_safe($name)){
throw new Exception("Unsafe user input");
return FALSE;
}
$query = "SELECT `occupation` FROM `jobs` WHERE `name` LIKE '$name'";
$result = $sql->getData($query, SqlObject::SINGLE_ROW); //Single row (assoc)
if($result !== NULL){
printf("Name: %s Occupation: %s\n", $result['occupation']);
}
}
SqlObject.php
<?php
/*
SqlObject.php - A SQL php helper module.
Copyright (c) Jacob Psimos
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* The original author's name (Jacob A Psimos) remain in this file or any
refactored versions.
* The original author's name (Jacob A Psimos) be included in the acknowledgements
of the application using this library.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
class SqlObject{
/* CONFIGURABLE VARIABLES - hard code these or use object contructor every time */
private $SERVER_ADDRESS = 'localhost';
private $SERVER_PORT = 3306;
private $DATABASE_NAME = '';
private $USERNAME = '';
private $PASSWORD = '';
private $MYSQLI_CHARSET = 'utf8'; //change for a custom charset
/* PRIVATE GLOBAL VARIABLES */
private $connection = NULL;
private $last_count = 0;
private $last_query = '';
const SINGLE_ROW = 0;
const MULTI_ROW = 1;
const ERR_DUPLICATE = 1062;
const ERR_DEADLOCK = 1213;
const RETRY_DEADLOCK_ATTEMPTS = 3;
const ACCESS_DENIED_CHANGE_USER = 1873;
const ACCESS_DENIED_DATABASE = 1044;
const ACCESS_DENIED_USER = 1045;
/*
If no username and no password and no database is provided,
it will attempt to open a connection using the variables in the CONFIGURABLE section
above. If your scripts will be using the same username and password and database most of the time,
just hard code the variables above so you can just call:
$sql = new SqlObject() and it will open without filling out the constructor every time
*/
public function __construct($username = NULL, $password = NULL, $database = NULL){
if($username != NULL && $password != NULL && $database != NULL){
$this->USERNAME = $username;
$this->PASSWORD = $password;
$this->DATABASE = $database;
}
$this->connection = new mysqli($this->SERVER_ADDRESS, $this->USERNAME, $this->PASSWORD, $this->DATABASE_NAME, $this->SERVER_PORT);
if($this->connection->connect_error){
throw new Exception("SqlObject::construct() Connection error: " . $this->connection->connect_error);
}else{
mysqli_set_charset($this->connection, $this->MYSQLI_CHARSET);
if($this->connection->error){
throw new Exception("SqlObject::construct() Charset Error: " . $this->connection->error);
}
}
}
/*
Use this function when a query is INSERT, UPDATE, etc
where you know the SQL query is not going to return a row
*/
public function setData($query){
if($this->connection){
$result = $this->connection->query($query);
$this->last_query = $query;
if($result == FALSE){
/* How to handle certain SQL errors */
switch($this->connection->errno){
case self::ERR_DEADLOCK:
return $this->retry_deadlock($query);
break;
case self::ERR_DUPLICATE:
return FALSE;
break;
case self::ACCESS_DENIED_CHANGE_USER:
throw new Exception($this->connection->error);
break;
case self::ACCESS_DENIED_USER:
throw new Exception($this->connection->error);
break;
case self::ACCESS_DENIED_DATABASE:
throw new Exception($this->connection->error);
break;
default:
//file_put_contents('debug.txt', $this->connection->errno);
throw new Exception($this->connection->error);
break;
}
} //end error catching switch
return TRUE; //Successful
}
return FALSE; //normally not reached
}
/*
This this to retrieve rows using an SQL query.
When returnType is SINGLE_ROW, only one row is returned even if there are many.
When returnType is MULTI_ROW, an array of rows is returned.
NULL is returned if the query returns nothing.
*/
public function getData($query, $returnType = self::SINGLE_ROW){
if(!$this->connection){
throw new Exception('The Database connection is broken' . "\n" . $query);
}
/* Run the provided query, if the query is invalid the return will be FALSE */
$datum = $this->connection->query($query);
$this->last_query = $query;
if(!$datum){
throw new Exception("SqlObject::getData() Error: " . $this->connection->error);
}
/* Single row (0) returns an associative array of the selected row */
/* Multi row (1) returns an array of associative array(s) from the query */
switch($returnType){
case self::SINGLE_ROW:
$result = $datum->fetch_assoc();
$datum->free();
return $result;
break;
case self::MULTI_ROW:
$this->last_count = 0;
$returnData = array();
while($nextRow = $datum->fetch_assoc()){
array_push($returnData, $nextRow);
$this->last_count++;
}
$datum->free();
if($this->last_count > 0){
return $returnData;
}
break;
default:
throw new Exception("Invalid argument for setData(); expected returnType is invalid");
break;
}
return NULL;
}
/*
Returns an array of strings containing all of the table names in the current database
*/
public function get_table_names($filter = NULL){
$query = $filter != NULL ? "SHOW TABLES LIKE '$filter'" : 'SHOW TABLES';
$datum = $this->connection->query($query);
//$this->last_query = $query;
if(!$datum){
throw new Exception($this->connection->error);
}
$names = array();
while($nextRow = $datum->fetch_array(MYSQLI_NUM)){
if(count($datum) > 0){
array_push($names, $nextRow[0]);
}
}
$datum->free();
return $names;
}
/* gets the size of the last returned array from getData */
function get_last_arraysize(){
return $this->last_count;
}
/* gets the number of rows affected by the last query */
public function get_affected_rows(){
return $this->connection->affected_rows;
}
/* returns the ID of the last INSERT query
NOTE this does not return the ID from an UPDATE query */
public function get_last_insert_id(){
return $this->connection->insert_id;
}
/* returns the last query that was executed */
public function get_last_query(){
return $this->last_query;
}
/* pass back the sql error number */
public function get_error_number(){
return $this->connection->errno;
}
/* return the sql error message */
public function get_error(){
return $this->connection->error;
}
/* get the current database name */
public function get_database_name(){
return $this->DATABASE_NAME;
}
/* get the current username */
public function get_username(){
return $this->USERNAME;
}
/* attempt to connect to a new database with optional different credentials */
public function change_database($new_database, $username = NULL, $password = NULL){
if(!$this->connection){
throw new Exception("change_database($new_database) failed because of a broken connection");
}
if($this->connection->change_user($username != NULL ? $username : $this->USERNAME,
$password != NULL ? $password : $this->PASSWORD, $new_database)){
$this->DATABASE_NAME = $new_database;
$this->USERNAME = $username != NULL ? $username : $this->USERNAME;
$this->PASSWORD = $password != NULL ? $password : $this->PASSWORD;
}else{
throw new Exception($this->get_error());
}
}
/* end the sql connection */
public function terminate(){
if($this->connection){
$this->connection->close();
unset($this->connection);
}
}
/*
Returns an SQL query string given a table name and an associative array
of key -> value items to be included in the query
*/
public static function insertQuery($table, $values){
$query = "INSERT INTO `$table`(";
$firstpart = '';
$lastpart = '';
$len = count($values);
$i = 0;
foreach($values as $key => $value){
$firstpart .= ($i < $len - 1) ? "`$key`," : "`$key`) VALUES(";
if(is_numeric($value) && $value[0] != '+'){
$lastpart .= ($i < $len - 1) ? "$value," : "$value)";
}else{
$lastpart .= ($i < $len - 1) ? "'$value'," : "'$value')";
}
$i++;
}
return ($query . $firstpart . $lastpart);
}
/*
Returns an SQL UPDATE query string given a table name
and an associative array of key -> value pairs for the update parameters.
It is up to the programmer to append the WHERE clause (if needed) to the returned string
*/
public static function updateQuery($table, $values){
$query = "UPDATE `$table` SET ";
$num = count($values);
$i = 0;
foreach($values as $key => $value){
if(is_numeric($value) && $value[0] != '+'){
$query .= "`$key`=$value";
}else{
$query .= "`$key`='$value'";
}
if($i < $num - 1){
$query .= ', ';
}
$i += 1;
}
return $query;
}
/*
A light scan of a string for common SQL injection commands.
returns true or false if the string contains a commands
*** This function does not detect all types of SQL injection ***
*** Combine this with sanitizeForQuery() for stronger protection ***
*/
public static function isSqlSafe($str){
$select_safe = stripos($str, "SELECT `") === FALSE && stripos($str, "SELECT *") === FALSE;
$insert_safe = stripos($str, "INSERT INTO `") === FALSE && stripos($str, "INSERT INTO *") === FALSE;
$update_safe = stripos($str, "UPDATE `") === FALSE && stripos($str, "UPDATE *") === FALSE;
$join_safe = stripos($str, "JOIN `") === FALSE && stripos($str, "INNER JOIN `") === FALSE;
return $select_safe && $insert_safe && $update_safe && $join_safe && $bogus_quote;
}
/*
Takes a string containing possibly harmful SQL text and characters and
returns a sanitized version with those items removed.
$blacklist - an array of strings that should be included in the sanitization default blacklisted
$whitelist - an array of strings that should be whitelisted
*/
public static function sanitizeForQuery($str, $blacklist = NULL, $whitelist = NULL){
//These phraises are to be removed from the resulting value
$blacklist1 = array(
'<?php',
'?>',
'\x',
);
$str = SqlObject::escapeSpecialChars($str);
/* Loop through all of the blacklisted phraises and remove them */
foreach($blacklist1 as $blockedchar){
if($whitelist !== NULL){
if(!in_array($blockedchar, $whitelist)){
$temp = str_replace($blockedchar, '', $str);
unset($str);
$str = $temp;
}
}else{
$temp = str_replace($blockedchar, '', $str);
unset($str);
$str = $temp;
}
}
if($blacklist !== NULL){
foreach($blacklist as $filter){
$temp = str_replace($filter, '', $str);
unset($str);
$str = $temp;
}
}
return $str;
}
/*
Escape single quote, double quote, percent, underline (_)
also replaces multiple consecutive \\ slashes with a \ single one
so escapes dont get messed up
*/
public static function escapeSpecialChars($str){
$temp = preg_replace("/\\'+/", "\'", $str);
$temp = preg_replace('/\"+/', '\"', $temp);
$temp = preg_replace('/\%+/', '\%', $temp);
$temp = preg_replace('/\_+/', '\_', $temp);
return preg_replace('/\\+/', '\\', $temp);
}
/* automatically close the connection when the object is disposed */
public function __destruct(){
if(isset($this->connection)){
$this->connection->close();
unset($this->connection);
}
}
/* reattempt to run the query that caused a deadlock */
private function retry_deadlock($query){
for($i = 0; $i < self::RETRY_DEADLOCK_ATTEMPTS; $i++){
$result = $this->connection->query($query);
if($result){
return TRUE;
}
sleep(1);
}
return FALSE;
}
} /* end class */
?>