: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
* @author col.shrapnel@gmail.com
* @link http://phpfaq.ru/safemysql
* Safe and convenient way to handle SQL queries utilizing type-hinted placeholders.
* - set of helper functions to get the desired result right out of query, like in PEAR::DB
* - conditional query building using parse() method to build queries of whatever comlexity,
* while keeping extra safety of placeholders
* - type-hinted placeholders
* Type-hinted placeholders are great because
* - safe, as any other [properly implemented] placeholders
* - no need for manual escaping or binding, makes the code extra DRY
* - allows support for non-standard types such as identifier or array, which saves A LOT of pain in the back.
* Supported placeholders at the moment are:
* ?s ("string") - strings (also DATE, FLOAT and DECIMAL)
* ?i ("integer") - the name says it all
* ?n ("name") - identifiers (table and field names)
* ?a ("array") - complex placeholder for IN() operator (substituted with string of 'a','b','c' format, without parentesis)
* ?u ("update") - complex placeholder for SET operator (substituted with string of `field`='value',`field`='value' format)
* ?p ("parsed") - special type placeholder, for inserting already parsed statements without any processing, to avoid double parsing.
* $db = new SafeMySQL(); // with default settings
* $db = new SafeMySQL($opts); // with some of the default settings overwritten
* Alternatively, you can just pass an existing mysqli instance that will be used to run queries
* instead of creating a new connection.
* Excellent choice for migration!
* $db = new SafeMySQL(['mysqli' => $mysqli]);
* $name = $db->getOne('SELECT name FROM table WHERE id = ?i',$_GET['id']);
* $data = $db->getInd('id','SELECT * FROM ?n WHERE id IN ?a','table', array(1,2));
* $data = $db->getAll("SELECT * FROM ?n WHERE mod=?s LIMIT ?i",$table,$mod,$limit);
* $ids = $db->getCol("SELECT id FROM tags WHERE tagname = ?s",$tag);
* $data = $db->getAll("SELECT * FROM table WHERE category IN (?a)",$ids);
* $data = array('offers_in' => $in, 'offers_out' => $out);
* $sql = "INSERT INTO stats SET pid=?i,dt=CURDATE(),?u ON DUPLICATE KEY UPDATE ?u";
* $db->query($sql,$pid,$data,$data);
* $sqlpart = "field is NULL";
* $sqlpart = $db->parse("field = ?s", $var);
* $data = $db->getAll("SELECT * FROM table WHERE ?p", $bar, $sqlpart);
protected $defaults = array(
'errmode' => 'exception', //or 'error'
'exception' => 'Exception', //Exception class name
const RESULT_ASSOC = MYSQLI_ASSOC;
const RESULT_NUM = MYSQLI_NUM;
function __construct($opt = array())
$opt = array_merge($this->defaults,$opt);
$this->emode = $opt['errmode'];
$this->exname = $opt['exception'];
if (isset($opt['mysqli']))
if ($opt['mysqli'] instanceof mysqli)
$this->conn = $opt['mysqli'];
$this->error("mysqli option must be valid instance of mysqli class");
$opt['host'] = "p:".$opt['host'];
@$this->conn = mysqli_connect($opt['host'], $opt['user'], $opt['pass'], $opt['db'], $opt['port'], $opt['socket']);
$this->error(mysqli_connect_errno()." ".mysqli_connect_error());
mysqli_set_charset($this->conn, $opt['charset']) or $this->error(mysqli_error($this->conn));
unset($opt); // I am paranoid
* Conventional function to run a query with placeholders. A mysqli_query wrapper with placeholders support
* $db->query("DELETE FROM table WHERE id=?i", $id);
* @param string $query - an SQL query with placeholders
* @param mixed $arg,... unlimited number of arguments to match placeholders in the query
* @return resource|FALSE whatever mysqli_query returns
return $this->rawQuery($this->prepareQuery(func_get_args()));
* Conventional function to fetch single row.
* @param resource $result - myqli result
* @param int $mode - optional fetch mode, RESULT_ASSOC|RESULT_NUM, default RESULT_ASSOC
* @return array|FALSE whatever mysqli_fetch_array returns
public function fetch($result,$mode=self::RESULT_ASSOC)
return mysqli_fetch_array($result, $mode);
* Conventional function to get number of affected rows.
* @return int whatever mysqli_affected_rows returns
public function affectedRows()
return mysqli_affected_rows ($this->conn);
* Conventional function to get last insert id.
* @return int whatever mysqli_insert_id returns
public function insertId()
return mysqli_insert_id($this->conn);
* Conventional function to get number of rows in the resultset.
* @param resource $result - myqli result
* @return int whatever mysqli_num_rows returns
public function numRows($result)
return mysqli_num_rows($result);
* Conventional function to free the resultset.
public function free($result)
mysqli_free_result($result);
* Helper function to get scalar value right out of query and optional arguments
* $name = $db->getOne("SELECT name FROM table WHERE id=1");
* $name = $db->getOne("SELECT name FROM table WHERE id=?i", $id);
* @param string $query - an SQL query with placeholders
* @param mixed $arg,... unlimited number of arguments to match placeholders in the query
* @return string|FALSE either first column of the first row of resultset or FALSE if none found
$query = $this->prepareQuery(func_get_args());
if ($res = $this->rawQuery($query))
$row = $this->fetch($res);
* Helper function to get single row right out of query and optional arguments
* $data = $db->getRow("SELECT * FROM table WHERE id=1");
* $data = $db->getRow("SELECT * FROM table WHERE id=?i", $id);
* @param string $query - an SQL query with placeholders
* @param mixed $arg,... unlimited number of arguments to match placeholders in the query
* @return array|FALSE either associative array contains first row of resultset or FALSE if none found
$query = $this->prepareQuery(func_get_args());
if ($res = $this->rawQuery($query)) {
$ret = $this->fetch($res);
* Helper function to get single column right out of query and optional arguments
* $ids = $db->getCol("SELECT id FROM table WHERE cat=1");
* $ids = $db->getCol("SELECT id FROM tags WHERE tagname = ?s", $tag);
* @param string $query - an SQL query with placeholders
* @param mixed $arg,... unlimited number of arguments to match placeholders in the query
* @return array enumerated array of first fields of all rows of resultset or empty array if none found
$query = $this->prepareQuery(func_get_args());
if ( $res = $this->rawQuery($query) )
while($row = $this->fetch($res))
* Helper function to get all the rows of resultset right out of query and optional arguments
* $data = $db->getAll("SELECT * FROM table");
* $data = $db->getAll("SELECT * FROM table LIMIT ?i,?i", $start, $rows);
* @param string $query - an SQL query with placeholders
* @param mixed $arg,... unlimited number of arguments to match placeholders in the query
* @return array enumerated 2d array contains the resultset. Empty if no rows found.
$query = $this->prepareQuery(func_get_args());
if ( $res = $this->rawQuery($query) )
while($row = $this->fetch($res))
* Helper function to get all the rows of resultset into indexed array right out of query and optional arguments
* $data = $db->getInd("id", "SELECT * FROM table");
* $data = $db->getInd("id", "SELECT * FROM table LIMIT ?i,?i", $start, $rows);
* @param string $index - name of the field which value is used to index resulting array
* @param string $query - an SQL query with placeholders
* @param mixed $arg,... unlimited number of arguments to match placeholders in the query
* @return array - associative 2d array contains the resultset. Empty if no rows found.
$index = array_shift($args);
$query = $this->prepareQuery($args);
if ( $res = $this->rawQuery($query) )
while($row = $this->fetch($res))
$ret[$row[$index]] = $row;
* Helper function to get a dictionary-style array right out of query and optional arguments
* $data = $db->getIndCol("name", "SELECT name, id FROM cities");
* @param string $index - name of the field which value is used to index resulting array
* @param string $query - an SQL query with placeholders
* @param mixed $arg,... unlimited number of arguments to match placeholders in the query
* @return array - associative array contains key=value pairs out of resultset. Empty if no rows found.
public function getIndCol()
$index = array_shift($args);
$query = $this->prepareQuery($args);
if ( $res = $this->rawQuery($query) )
while($row = $this->fetch($res))
$ret[$key] = reset($row);
* Function to parse placeholders either in the full query or a query part
* unlike native prepared statements, allows ANY query part to be parsed
* and EXTREMELY useful for conditional query building
* like adding various query parts using loops, conditions, etc.
* already parsed parts have to be added via ?p placeholder
* $query = $db->parse("SELECT * FROM table WHERE foo=?s AND bar=?s", $foo, $bar);
* $qpart = $db->parse(" AND foo=?s", $foo);
* $data = $db->getAll("SELECT * FROM table WHERE bar=?s ?p", $bar, $qpart);
* @param string $query - whatever expression contains placeholders
* @param mixed $arg,... unlimited number of arguments to match placeholders in the expression
* @return string - initial expression with placeholders substituted with data.
return $this->prepareQuery(func_get_args());
* function to implement whitelisting feature
* sometimes we can't allow a non-validated user-supplied data to the query even through placeholder
* especially if it comes down to SQL OPERATORS
* $order = $db->whiteList($_GET['order'], array('name','price'));
* $dir = $db->whiteList($_GET['dir'], array('ASC','DESC'));
* throw new http404(); //non-expected values should cause 404 or similar response
* $sql = "SELECT * FROM table ORDER BY ?p ?p LIMIT ?i,?i"
* $data = $db->getArr($sql, $order, $dir, $start, $per_page);
* @param string $iinput - field name to test
* @param array $allowed - an array with allowed variants
* @param string $default - optional variable to set if no match found. Default to false.
* @return string|FALSE - either sanitized value or FALSE
public function whiteList($input,$allowed,$default=FALSE)
$found = array_search($input,$allowed);
return ($found === FALSE) ? $default : $allowed[$found];
* function to filter out arrays, for the whitelisting purposes
* useful to pass entire superglobal to the INSERT or UPDATE query
* OUGHT to be used for this purpose,
* as there could be fields to which user should have no access to.
* $allowed = array('title','url','body','rating','term','type');
* $data = $db->filterArray($_POST,$allowed);
* $sql = "INSERT INTO ?n SET ?u";
* $db->query($sql,$table,$data);
* @param array $input - source array
* @param array $allowed - an array with allowed field names
* @return array filtered out source array
public function filterArray($input,$allowed)
foreach(array_keys($input) as $key )
if ( !in_array($key,$allowed) )
* Function to get last executed query.
* @return string|NULL either last executed query or NULL if were none
public function lastQuery()
$last = end($this->stats);
* Function to get all query statistics.
* @return array contains all executed queries with timings and errors
public function getStats()
* protected function which actually runs a query against Mysql server.
* also logs some stats like profiling info and error message
* @param string $query - a regular SQL query
* @return mysqli result resource or FALSE on error
protected function rawQuery($query)
$start = microtime(TRUE);
$res = mysqli_query($this->conn, $query);
$timer = microtime(TRUE) - $start;
$error = mysqli_error($this->conn);
$key = key($this->stats);
$this->stats[$key]['error'] = $error;
$this->error("$error. Full query: [$query]");
protected function prepareQuery($args)
$raw = array_shift($args);
$array = preg_split('~(\?[nsiuap])~u',$raw,null,PREG_SPLIT_DELIM_CAPTURE);
$pnum = floor(count($array) / 2);
$this->error("Number of args ($anum) doesn't match number of placeholders ($pnum) in [$raw]");