From 67012faa63685624dbb8ffc0d10d902c4b620e4d Mon Sep 17 00:00:00 2001 From: impeck Date: Wed, 23 Aug 2023 15:10:28 +0500 Subject: [PATCH 01/28] format code --- safemysql.class.php | 153 +++++++++++++++++--------------------------- 1 file changed, 59 insertions(+), 94 deletions(-) diff --git a/safemysql.class.php b/safemysql.class.php index 919599b..b863bf3 100644 --- a/safemysql.class.php +++ b/safemysql.class.php @@ -1,4 +1,5 @@ defaults,$opt); + $opt = array_merge($this->defaults, $opt); $this->emode = $opt['errmode']; $this->exname = $opt['exception']; - if (isset($opt['mysqli'])) - { - if ($opt['mysqli'] instanceof mysqli) - { + if (isset($opt['mysqli'])) { + if ($opt['mysqli'] instanceof mysqli) { $this->conn = $opt['mysqli']; return; - } else { $this->error("mysqli option must be valid instance of mysqli class"); } } - if ($opt['pconnect']) - { - $opt['host'] = "p:".$opt['host']; + if ($opt['pconnect']) { + $opt['host'] = "p:" . $opt['host']; } @$this->conn = mysqli_connect($opt['host'], $opt['user'], $opt['pass'], $opt['db'], $opt['port'], $opt['socket']); - if ( !$this->conn ) - { - $this->error(mysqli_connect_errno()." ".mysqli_connect_error()); + if (!$this->conn) { + $this->error(mysqli_connect_errno() . " " . mysqli_connect_error()); } mysqli_set_charset($this->conn, $opt['charset']) or $this->error(mysqli_error($this->conn)); @@ -136,7 +132,7 @@ function __construct($opt = array()) * @return resource|FALSE whatever mysqli_query returns */ public function query() - { + { return $this->rawQuery($this->prepareQuery(func_get_args())); } @@ -147,7 +143,7 @@ public function query() * @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) + public function fetch($result, $mode = self::RESULT_ASSOC) { return mysqli_fetch_array($result, $mode); } @@ -159,7 +155,7 @@ public function fetch($result,$mode=self::RESULT_ASSOC) */ public function affectedRows() { - return mysqli_affected_rows ($this->conn); + return mysqli_affected_rows($this->conn); } /** @@ -205,8 +201,7 @@ public function free($result) public function getOne() { $query = $this->prepareQuery(func_get_args()); - if ($res = $this->rawQuery($query)) - { + if ($res = $this->rawQuery($query)) { $row = $this->fetch($res); if (is_array($row)) { return reset($row); @@ -253,10 +248,8 @@ public function getCol() { $ret = array(); $query = $this->prepareQuery(func_get_args()); - if ( $res = $this->rawQuery($query) ) - { - while($row = $this->fetch($res)) - { + if ($res = $this->rawQuery($query)) { + while ($row = $this->fetch($res)) { $ret[] = reset($row); } $this->free($res); @@ -279,10 +272,8 @@ public function getAll() { $ret = array(); $query = $this->prepareQuery(func_get_args()); - if ( $res = $this->rawQuery($query) ) - { - while($row = $this->fetch($res)) - { + if ($res = $this->rawQuery($query)) { + while ($row = $this->fetch($res)) { $ret[] = $row; } $this->free($res); @@ -309,10 +300,8 @@ public function getInd() $query = $this->prepareQuery($args); $ret = array(); - if ( $res = $this->rawQuery($query) ) - { - while($row = $this->fetch($res)) - { + if ($res = $this->rawQuery($query)) { + while ($row = $this->fetch($res)) { $ret[$row[$index]] = $row; } $this->free($res); @@ -338,10 +327,8 @@ public function getIndCol() $query = $this->prepareQuery($args); $ret = array(); - if ( $res = $this->rawQuery($query) ) - { - while($row = $this->fetch($res)) - { + if ($res = $this->rawQuery($query)) { + while ($row = $this->fetch($res)) { $key = $row[$index]; unset($row[$index]); $ret[$key] = reset($row); @@ -398,9 +385,9 @@ public function parse() * @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) + public function whiteList($input, $allowed, $default = FALSE) { - $found = array_search($input,$allowed); + $found = array_search($input, $allowed); return ($found === FALSE) ? $default : $allowed[$found]; } @@ -420,12 +407,10 @@ public function whiteList($input,$allowed,$default=FALSE) * @param array $allowed - an array with allowed field names * @return array filtered out source array */ - public function filterArray($input,$allowed) + public function filterArray($input, $allowed) { - foreach(array_keys($input) as $key ) - { - if ( !in_array($key,$allowed) ) - { + foreach (array_keys($input) as $key) { + if (!in_array($key, $allowed)) { unset($input[$key]); } } @@ -471,15 +456,14 @@ protected function rawQuery($query) 'start' => $start, 'timer' => $timer, ); - if (!$res) - { + if (!$res) { $error = mysqli_error($this->conn); - + end($this->stats); $key = key($this->stats); $this->stats[$key]['error'] = $error; $this->cutStats(); - + $this->error("$error. Full query: [$query]"); } $this->cutStats(); @@ -490,25 +474,21 @@ protected function prepareQuery($args) { $query = ''; $raw = array_shift($args); - $array = preg_split('~(\?[nsiuap])~u',$raw,-1,PREG_SPLIT_DELIM_CAPTURE); + $array = preg_split('~(\?[nsiuap])~u', $raw, -1, PREG_SPLIT_DELIM_CAPTURE); $anum = count($args); $pnum = floor(count($array) / 2); - if ( $pnum != $anum ) - { + if ($pnum != $anum) { $this->error("Number of args ($anum) doesn't match number of placeholders ($pnum) in [$raw]"); } - foreach ($array as $i => $part) - { - if ( ($i % 2) == 0 ) - { + foreach ($array as $i => $part) { + if (($i % 2) == 0) { $query .= $part; continue; } $value = array_shift($args); - switch ($part) - { + switch ($part) { case '?n': $part = $this->escapeIdent($value); break; @@ -535,36 +515,31 @@ protected function prepareQuery($args) protected function escapeInt($value) { - if ($value === NULL) - { + if ($value === NULL) { return 'NULL'; } - if(!is_numeric($value)) - { - $this->error("Integer (?i) placeholder expects numeric value, ".gettype($value)." given"); + if (!is_numeric($value)) { + $this->error("Integer (?i) placeholder expects numeric value, " . gettype($value) . " given"); return FALSE; } - if (is_float($value)) - { + if (is_float($value)) { $value = number_format($value, 0, '.', ''); // may lose precision on big numbers - } + } return $value; } protected function escapeString($value) { - if ($value === NULL) - { + if ($value === NULL) { return 'NULL'; } - return "'".mysqli_real_escape_string($this->conn,$value)."'"; + return "'" . mysqli_real_escape_string($this->conn, $value) . "'"; } protected function escapeIdent($value) { - if ($value) - { - return "`".str_replace("`","``",$value)."`"; + if ($value) { + return "`" . str_replace("`", "``", $value) . "`"; } else { $this->error("Empty value for identifier (?n) placeholder"); } @@ -572,19 +547,16 @@ protected function escapeIdent($value) protected function createIN($data) { - if (!is_array($data)) - { + if (!is_array($data)) { $this->error("Value for IN (?a) placeholder should be array"); return; } - if (!$data) - { + if (!$data) { return 'NULL'; } $query = $comma = ''; - foreach ($data as $value) - { - $query .= $comma.$this->escapeString($value); + foreach ($data as $value) { + $query .= $comma . $this->escapeString($value); $comma = ","; } return $query; @@ -592,20 +564,17 @@ protected function createIN($data) protected function createSET($data) { - if (!is_array($data)) - { - $this->error("SET (?u) placeholder expects array, ".gettype($data)." given"); + if (!is_array($data)) { + $this->error("SET (?u) placeholder expects array, " . gettype($data) . " given"); return; } - if (!$data) - { + if (!$data) { $this->error("Empty array for SET (?u) placeholder"); return; } $query = $comma = ''; - foreach ($data as $key => $value) - { - $query .= $comma.$this->escapeIdent($key).'='.$this->escapeString($value); + foreach ($data as $key => $value) { + $query .= $comma . $this->escapeIdent($key) . '=' . $this->escapeString($value); $comma = ","; } return $query; @@ -613,12 +582,11 @@ protected function createSET($data) protected function error($err) { - $err = __CLASS__.": ".$err; + $err = __CLASS__ . ": " . $err; - if ( $this->emode == 'error' ) - { - $err .= ". Error initiated in ".$this->caller().", thrown"; - trigger_error($err,E_USER_ERROR); + if ($this->emode == 'error') { + $err .= ". Error initiated in " . $this->caller() . ", thrown"; + trigger_error($err, E_USER_ERROR); } else { throw new $this->exname($err); } @@ -628,11 +596,9 @@ protected function caller() { $trace = debug_backtrace(); $caller = ''; - foreach ($trace as $t) - { - if ( isset($t['class']) && $t['class'] == __CLASS__ ) - { - $caller = $t['file']." on line ".$t['line']; + foreach ($trace as $t) { + if (isset($t['class']) && $t['class'] == __CLASS__) { + $caller = $t['file'] . " on line " . $t['line']; } else { break; } @@ -646,8 +612,7 @@ protected function caller() */ protected function cutStats() { - if ( count($this->stats) > 100 ) - { + if (count($this->stats) > 100) { reset($this->stats); $first = key($this->stats); unset($this->stats[$first]); From 54699ca637b92cd7ea8eae7d3f44218225844ca3 Mon Sep 17 00:00:00 2001 From: impeck Date: Thu, 24 Aug 2023 12:21:44 +0500 Subject: [PATCH 02/28] refactoring --- composer.json | 11 +- safemysql.class.php | 644 +++++++++++++++++++++++++--------------- tests/SafeMySQLTest.php | 231 ++++++++++++++ 3 files changed, 638 insertions(+), 248 deletions(-) create mode 100644 tests/SafeMySQLTest.php diff --git a/composer.json b/composer.json index a09de20..65d8de1 100644 --- a/composer.json +++ b/composer.json @@ -19,9 +19,14 @@ "issues": "https://github.com/colshrapnel/safemysql/issues" }, "require": { - "php": ">=5.0.0" + "php": "^7.4 || ^8.0" }, "autoload": { - "files": ["safemysql.class.php"] + "files": [ + "safemysql.class.php" + ] + }, + "require-dev": { + "phpunit/phpunit": "^10.3" } -} +} \ No newline at end of file diff --git a/safemysql.class.php b/safemysql.class.php index b863bf3..db1c18f 100644 --- a/safemysql.class.php +++ b/safemysql.class.php @@ -1,62 +1,60 @@ 'user', - * 'pass' => 'pass', - * 'db' => 'db', - * 'charset' => 'latin1' - * ); - * $db = new SafeMySQL($opts); // with some of the default settings overwritten + * $db = new SafeMySQL(); // with default settings. + * + * $opts = [ + * 'user' => 'user', + * 'pass' => 'pass', + * 'db' => 'db', + * 'charset' => 'latin1' + * ]; + * $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! + * instead of creating a new connection. An excellent choice for migration! * * $db = new SafeMySQL(['mysqli' => $mysqli]); * * Some examples: * - * $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); + * $name = $db->getOne('SELECT name FROM table WHERE id = ?i', $_GET['id']); + * $data = $db->getInd('id', 'SELECT * FROM ?n WHERE id IN ?a', 'table', [1, 2]); + * $data = $db->getAll("SELECT * FROM ?n WHERE mod=?s LIMIT ?i", $table, $mod, $limit); * - * $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); + * $ids = $db->getCol("SELECT id FROM tags WHERE tagname = ?s", $tag); + * $data = $db->getAll("SELECT * FROM table WHERE category IN (?a)", $ids); + * + * $data = ['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); * * if ($var === NULL) { * $sqlpart = "field is NULL"; @@ -70,32 +68,72 @@ class SafeMySQL { + /** + * @var mysqli|null The mysqli connection. + */ protected $conn; + + /** + * @var array Query statistics. + */ protected $stats; + + /** + * @var string The error mode ('exception' or 'error'). + */ protected $emode; + + /** + * @var string The exception class name. + */ protected $exname; - protected $defaults = array( + /** + * Default settings. + * + * @var array + */ + protected $defaults = [ 'host' => 'localhost', 'user' => 'root', 'pass' => '', 'db' => 'test', - 'port' => NULL, - 'socket' => NULL, - 'pconnect' => FALSE, + 'port' => null, + 'socket' => null, + 'pconnect' => false, 'charset' => 'utf8', - 'errmode' => 'exception', //or 'error' - 'exception' => 'Exception', //Exception class name - ); + 'errmode' => 'exception', // or 'error' + 'exception' => 'Exception', // Exception class name + ]; + /** + * Result format constants. + */ const RESULT_ASSOC = MYSQLI_ASSOC; const RESULT_NUM = MYSQLI_NUM; - function __construct($opt = array()) + /** + * Constructs a new instance of the class. + * + * @param array $opt An array of options to configure the instance. + * - $opt['errmode']: The error mode to use. + * - $opt['exception']: The exception class to throw. + * - $opt['mysqli']: An optional mysqli object to use for the connection. + * - $opt['pconnect']: Whether to use a persistent connection. + * - $opt['host']: The hostname of the database server. + * - $opt['user']: The username to use for the connection. + * - $opt['pass']: The password to use for the connection. + * - $opt['db']: The name of the database to use. + * - $opt['port']: The port number to use for the connection. + * - $opt['socket']: The socket to use for the connection. + * - $opt['charset']: The character set to use for the connection. + * @return void + */ + function __construct(array $opt = []) { $opt = array_merge($this->defaults, $opt); - $this->emode = $opt['errmode']; + $this->emode = $opt['errmode']; $this->exname = $opt['exception']; if (isset($opt['mysqli'])) { @@ -103,7 +141,6 @@ function __construct($opt = array()) $this->conn = $opt['mysqli']; return; } else { - $this->error("mysqli option must be valid instance of mysqli class"); } } @@ -112,7 +149,15 @@ function __construct($opt = array()) $opt['host'] = "p:" . $opt['host']; } - @$this->conn = mysqli_connect($opt['host'], $opt['user'], $opt['pass'], $opt['db'], $opt['port'], $opt['socket']); + @$this->conn = mysqli_connect( + $opt['host'], + $opt['user'], + $opt['pass'], + $opt['db'], + $opt['port'], + $opt['socket'] + ); + if (!$this->conn) { $this->error(mysqli_connect_errno() . " " . mysqli_connect_error()); } @@ -122,85 +167,91 @@ function __construct($opt = array()) } /** - * Conventional function to run a query with placeholders. A mysqli_query wrapper with placeholders support - * - * Examples: - * $db->query("DELETE FROM table WHERE id=?i", $id); + * Conventional function to run a query with placeholders. A mysqli_query wrapper with placeholders support. * - * @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 + * @param string $query - An SQL query with placeholders. + * @param mixed ...$args - Unlimited number of arguments to match placeholders in the query. + * + * @return mysqli|bool The result of the query or false if an error occurred. + * + * @example $result = $db->query("DELETE FROM table WHERE id=?i", $id); */ - public function query() + public function query(string $query, ...$args): mysqli|bool { - return $this->rawQuery($this->prepareQuery(func_get_args())); + return $this->rawQuery($this->prepareQuery($query, ...$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 + * Fetch a row from a result set as an associative array or a numeric array. + * + * @param mysqli_result|null $result The result set from which to fetch the row. + * @param int $mode The type of array to return (RESULT_ASSOC for associative array, or RESULT_NUM for numeric array). Defaults to RESULT_ASSOC. + * @return array|null The fetched row as an associative array or a numeric array, or null if no more rows are available. */ - public function fetch($result, $mode = self::RESULT_ASSOC) + public function fetch(mysqli_result|null $result, int $mode = self::RESULT_ASSOC): array|null { - return mysqli_fetch_array($result, $mode); + return $result === null ? null : mysqli_fetch_array($result, $mode); } /** - * Conventional function to get number of affected rows. - * - * @return int whatever mysqli_affected_rows returns + * Retrieve the number of rows affected by the last database operation. + * + * @return int The number of affected rows. */ - public function affectedRows() + public function affectedRows(): int { return mysqli_affected_rows($this->conn); } /** - * Conventional function to get last insert id. - * - * @return int whatever mysqli_insert_id returns + * Retrieves the last inserted ID from the database connection. + * + * @return int The ID of the last inserted row. */ - public function insertId() + public function insertId(): int { return mysqli_insert_id($this->conn); } /** - * Conventional function to get number of rows in the resultset. + * Returns the number of rows in the resultset. * - * @param resource $result - myqli result - * @return int whatever mysqli_num_rows returns + * @param mysqli_result $result - The mysqli result object. + * @return int - The number of rows in the resultset. */ - public function numRows($result) + public function numRows(mysqli_result $result): int { return mysqli_num_rows($result); } /** - * Conventional function to free the resultset. + * Frees the memory associated with a result. + * + * @param mixed $result The result to free. + * @return void */ - public function free($result) + public function free($result): void { mysqli_free_result($result); } /** - * Helper function to get scalar value right out of query and optional arguments - * - * Examples: - * $name = $db->getOne("SELECT name FROM table WHERE id=1"); - * $name = $db->getOne("SELECT name FROM table WHERE id=?i", $id); + * Helper function to get a scalar value directly from a query and optional arguments. + * + * This function retrieves a scalar value from a query result and returns it as a string. + * You can use placeholders in the SQL query and provide optional arguments to match those placeholders. + * + * @param string $query - An SQL query with placeholders. + * @param mixed ...$args - An unlimited number of arguments to match placeholders in the query. * - * @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 + * @return string|false - The retrieved result as a string, or false if no result is found. + * + * @example $name = $db->getOne("SELECT name FROM table WHERE id=1"); + * @example $name = $db->getOne("SELECT name FROM table WHERE id=?i", $id); */ - public function getOne() + public function getOne(string $query, ...$args): string|false { - $query = $this->prepareQuery(func_get_args()); + $query = $this->prepareQuery($query, ...$args); if ($res = $this->rawQuery($query)) { $row = $this->fetch($res); if (is_array($row)) { @@ -208,46 +259,54 @@ public function getOne() } $this->free($res); } - return FALSE; + return false; } /** - * Helper function to get single row right out of query and optional arguments - * - * Examples: - * $data = $db->getRow("SELECT * FROM table WHERE id=1"); - * $data = $db->getRow("SELECT * FROM table WHERE id=?i", $id); + * Retrieve a single row from a query result along with optional arguments. * - * @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 + * This function retrieves a single row from a query result and returns it as an associative array. + * You can use placeholders in the SQL query and provide optional arguments to match those placeholders. + * + * @param string $query - An SQL query with placeholders. + * @param mixed ...$args - Unlimited number of arguments to match placeholders in the query. + * + * @return array|null - An associative array containing the fetched row from the database, + * or null if the query execution fails or no rows are found. + * + * @example $data = $db->getRow("SELECT * FROM table WHERE id=1"); + * @example $data = $db->getRow("SELECT * FROM table WHERE id=?i", $id); */ - public function getRow() + public function getRow(string $query, ...$args): array|null { - $query = $this->prepareQuery(func_get_args()); + $query = $this->prepareQuery($query, ...$args); if ($res = $this->rawQuery($query)) { $ret = $this->fetch($res); $this->free($res); return $ret; } - return FALSE; + return null; } /** - * Helper function to get single column right out of query and optional arguments - * - * Examples: - * $ids = $db->getCol("SELECT id FROM table WHERE cat=1"); - * $ids = $db->getCol("SELECT id FROM tags WHERE tagname = ?s", $tag); + * Retrieve a single column from a query result along with optional arguments. * - * @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 + * This function retrieves a single column from a query result and returns it as an enumerated array. + * You can use placeholders in the SQL query and provide optional arguments to match those placeholders. + * + * @param string $query - An SQL query with placeholders. + * @param mixed ...$args - Unlimited number of arguments to match placeholders in the query. + * + * @return array - An enumerated array containing the values of the first field of all rows in the resultset. + * Returns an empty array if no rows are found. + * + * @example $ids = $db->getCol("SELECT id FROM table WHERE cat=1"); + * @example $ids = $db->getCol("SELECT id FROM tags WHERE tagname = ?s", $tag); */ - public function getCol() + public function getCol(string $query, ...$args): array { $ret = array(); - $query = $this->prepareQuery(func_get_args()); + $query = $this->prepareQuery($query, ...$args); if ($res = $this->rawQuery($query)) { while ($row = $this->fetch($res)) { $ret[] = reset($row); @@ -258,20 +317,23 @@ public function getCol() } /** - * Helper function to get all the rows of resultset right out of query and optional arguments - * - * Examples: - * $data = $db->getAll("SELECT * FROM table"); - * $data = $db->getAll("SELECT * FROM table LIMIT ?i,?i", $start, $rows); + * Retrieve all rows from a query result along with optional arguments. + * + * This function retrieves all rows from a query result and structures them into an enumerated 2D array. + * You can use placeholders in the SQL query and provide optional arguments to match those placeholders. * - * @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. + * @param string $query - An SQL query with placeholders. + * @param mixed ...$args - Unlimited number of arguments to match placeholders in the query. + * + * @return array - An enumerated 2D array containing the resultset. Returns an empty array if no rows are found. + * + * @example $data = $db->getAll("SELECT * FROM table"); + * @example $data = $db->getAll("SELECT * FROM table LIMIT ?i,?i", $start, $rows); */ - public function getAll() + public function getAll(string $query, ...$args): array { $ret = array(); - $query = $this->prepareQuery(func_get_args()); + $query = $this->prepareQuery($query, ...$args); if ($res = $this->rawQuery($query)) { while ($row = $this->fetch($res)) { $ret[] = $row; @@ -282,51 +344,58 @@ public function getAll() } /** - * Helper function to get all the rows of resultset into indexed array right out of query and optional arguments - * - * Examples: - * $data = $db->getInd("id", "SELECT * FROM table"); - * $data = $db->getInd("id", "SELECT * FROM table LIMIT ?i,?i", $start, $rows); + * Retrieve rows from a query and organize them into an indexed array. + * + * Retrieves rows from a query result and structures them into an indexed array. + * This function is especially useful when you want to index the resulting array by a specific field. * - * @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. + * @param string $index - The name of the field used to index the resulting array. + * @param string $query - An SQL query with placeholders. + * @param mixed ...$args - Unlimited number of arguments to match placeholders in the query (optional). + * + * @return array - An associative 2D array containing the resultset. Returns an empty array if no rows are found. + * + * @example $data = $db->getInd("id", "SELECT * FROM table"); + * @example $data = $db->getInd("id", "SELECT * FROM table LIMIT ?i,?i", $start, $rows); */ - public function getInd() + public function getInd(string $index, string $query, ...$args): array { - $args = func_get_args(); - $index = array_shift($args); - $query = $this->prepareQuery($args); + if ($query !== null) { + $query = $this->prepareQuery($query, ...$args); + } - $ret = array(); - if ($res = $this->rawQuery($query)) { + $result = []; + if ($query === null || $res = $this->rawQuery($query)) { while ($row = $this->fetch($res)) { - $ret[$row[$index]] = $row; + $result[$row[$index]] = $row; + } + if ($query !== null) { + $this->free($res); } - $this->free($res); } - return $ret; + + return $result; } /** - * Helper function to get a dictionary-style array right out of query and optional arguments - * - * Examples: - * $data = $db->getIndCol("name", "SELECT name, id FROM cities"); + * Helper function to get a dictionary-style array right out of a query and optional arguments. * - * @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. + * This function retrieves rows from a query result and structures them into a dictionary-style array. + * It's particularly useful when you want to index the resulting array by a specific field. + * + * @param string $index - The name of the field to use as the index in the resulting array. + * @param string $query - An SQL query with placeholders. + * @param mixed ...$args - An unlimited number of arguments to match placeholders in the query. + * + * @return array - An associative array containing key=value pairs from the resultset. Returns an empty array if no rows are found. + * + * @example $data = $db->getIndCol("name", "SELECT name, id FROM cities"); */ - public function getIndCol() + public function getIndCol(string $index, string $query, ...$args): array { - $args = func_get_args(); - $index = array_shift($args); - $query = $this->prepareQuery($args); + $query = $this->prepareQuery($query, ...$args); - $ret = array(); + $ret = []; if ($res = $this->rawQuery($query)) { while ($row = $this->fetch($res)) { $key = $row[$index]; @@ -335,79 +404,84 @@ public function getIndCol() } $this->free($res); } + return $ret; } /** - * Function to parse placeholders either in the full query or a query part - * unlike native prepared statements, allows ANY query part to be parsed - * - * useful for debug - * 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 - * - * Examples: + * Function to parse placeholders either in the full query or a query part. + * + * Unlike native prepared statements, this function allows ANY query part to be parsed. + * It is useful for debugging and EXTREMELY useful for conditional query building, + * such as adding various query parts using loops, conditions, etc. + * Already parsed parts have to be added via ?p placeholder. + * + * @param string $query - Whatever expression contains placeholders. + * @param mixed ...$args - An unlimited number of arguments to match placeholders in the expression. + * + * @return string - The initial expression with placeholders substituted with data. + * + * {@example * $query = $db->parse("SELECT * FROM table WHERE foo=?s AND bar=?s", $foo, $bar); * echo $query; - * + * * if ($foo) { * $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. + * } */ - public function parse() + public function parse(string $query, ...$args): string { - return $this->prepareQuery(func_get_args()); + return $this->prepareQuery($query, ...$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 - * - * Example: + * Implements whitelisting feature. + * + * This function whitelists user-supplied data to ensure it matches one of the allowed values. + * It's especially useful when dealing with SQL queries, particularly for SQL operators and sorting/ordering fields. + * + * @param string $input - Field name to test. + * @param array $allowed - An array with allowed variants. + * @param string $default - Optional variable to set if no match is found. Defaults to false. + * @return string|false - Either sanitized value or false. * - * $order = $db->whiteList($_GET['order'], array('name','price')); - * $dir = $db->whiteList($_GET['dir'], array('ASC','DESC')); - * if (!$order || !dir) { - * throw new http404(); //non-expected values should cause 404 or similar response + * {@example + * $order = $db->whiteList($_GET['order'], ['name','price']); + * $dir = $db->whiteList($_GET['dir'], ['ASC','DESC']); + * if (!$order || !$dir) { + * throw new Http404Exception(); // Non-expected values should trigger a 404 or similar response. + * } + * $sql = "SELECT * FROM table ORDER BY ?p ?p LIMIT ?i,?i"; + * $data = $db->getArr($sql, $order, $dir, $start, $per_page); * } - * $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) + public function whiteList($input, $allowed, $default = false) { $found = array_search($input, $allowed); - return ($found === FALSE) ? $default : $allowed[$found]; + 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. - * - * Example: - * $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 + * Filter an array to only include allowed field names for whitelisting purposes. + * + * This function is useful when you want to filter an array, such as a superglobal, to ensure that only + * the specified fields are retained. It should be used, especially when preparing data for INSERT or UPDATE queries, + * to restrict access to fields that a user should not have access to. + * + * @param array $input The source array to filter. + * @param array $allowed An array containing allowed field names. + * @return array The filtered source array containing only allowed fields. + * + * {@example + * $allowed = ['title', 'url', 'body', 'rating', 'term', 'type']; + * $data = $db->filterArray($_POST, $allowed); + * $sql = "INSERT INTO ?n SET ?u"; + * $db->query($sql, $table, $data); + * } */ - public function filterArray($input, $allowed) + public function filterArray(array $input, array $allowed): array { foreach (array_keys($input) as $key) { if (!in_array($key, $allowed)) { @@ -418,44 +492,45 @@ public function filterArray($input, $allowed) } /** - * Function to get last executed query. - * - * @return string|NULL either last executed query or NULL if were none + * Get the last executed query. + * + * @return string|null The last executed query, or null if there were none. */ - public function lastQuery() + public function lastQuery(): ?string { $last = end($this->stats); return $last['query']; } /** - * Function to get all query statistics. - * - * @return array contains all executed queries with timings and errors + * Get all query statistics. + * + * @return array An array containing all executed queries with timings and errors. */ - public function getStats() + public function getStats(): array { return $this->stats; } /** - * 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 which actually runs a query against the MySQL server. + * It also logs some statistics like profiling information and error messages. + * + * @param string $query - A regular SQL query. + * @return mysqli_result|bool A mysqli_result object on success, or bool on error. */ - protected function rawQuery($query) + protected function rawQuery(string $query): mysqli_result|bool { $start = microtime(TRUE); $res = mysqli_query($this->conn, $query); $timer = microtime(TRUE) - $start; - $this->stats[] = array( + $this->stats[] = [ 'query' => $query, 'start' => $start, 'timer' => $timer, - ); + ]; + if (!$res) { $error = mysqli_error($this->conn); @@ -466,19 +541,37 @@ protected function rawQuery($query) $this->error("$error. Full query: [$query]"); } + $this->cutStats(); return $res; } - protected function prepareQuery($args) + /** + * Protected function to prepare an SQL query with placeholders and values. + * + * This function replaces placeholders in the SQL query with the provided values and returns the prepared query. + * It supports several placeholder types: + * - ?n for identifiers (table/column names). + * - ?s for string values. + * - ?i for integer values. + * - ?a for IN clause values (array of values). + * - ?u for SET clause values (associative array of column-value pairs). + * - ?p for raw, unescaped values. + * + * @param string $raw The raw SQL query with placeholders. + * @param mixed ...$args An unlimited number of arguments to match placeholders in the query. + * + * @return string The prepared SQL query. + */ + protected function prepareQuery(string $raw, ...$args): string { $query = ''; - $raw = array_shift($args); $array = preg_split('~(\?[nsiuap])~u', $raw, -1, PREG_SPLIT_DELIM_CAPTURE); $anum = count($args); $pnum = floor(count($array) / 2); + if ($pnum != $anum) { - $this->error("Number of args ($anum) doesn't match number of placeholders ($pnum) in [$raw]"); + $this->error("Number of arguments ($anum) doesn't match the number of placeholders ($pnum) in [$raw]"); } foreach ($array as $i => $part) { @@ -510,79 +603,133 @@ protected function prepareQuery($args) } $query .= $part; } + return $query; } - protected function escapeInt($value) + /** + * Protected function to escape an integer value. + * + * @param int|float|null $value The value to escape as an integer. May also be NULL. + * + * @return string|false The escaped integer value as a string or FALSE if an error occurs. + */ + protected function escapeInt(int|float|null $value): string|false { - if ($value === NULL) { + if ($value === null) { return 'NULL'; } + if (!is_numeric($value)) { - $this->error("Integer (?i) placeholder expects numeric value, " . gettype($value) . " given"); - return FALSE; + $this->error("Integer (?i) placeholder expects a numeric value, " . gettype($value) . " given"); + return false; } + if (is_float($value)) { - $value = number_format($value, 0, '.', ''); // may lose precision on big numbers + $value = number_format($value, 0, '.', ''); // May lose precision on big numbers. } - return $value; + + return (string) $value; } - protected function escapeString($value) + /** + * Protected function to escape a string value. + * + * @param string|null $value The string value to escape. May also be NULL. + * + * @return string The escaped string value enclosed in single quotes, or 'NULL' if the value is NULL. + */ + protected function escapeString(string|null $value): string { - if ($value === NULL) { + if ($value === null) { return 'NULL'; } - return "'" . mysqli_real_escape_string($this->conn, $value) . "'"; + + return "'" . mysqli_real_escape_string($this->conn, $value) . "'"; } - protected function escapeIdent($value) + /** + * Protected function to escape an identifier value. + * + * @param string|null $value The identifier value to escape. May also be NULL. + * + * @return string The escaped identifier value enclosed in backticks or an empty string if the value is NULL. + */ + protected function escapeIdent(string|null $value): string { if ($value) { return "`" . str_replace("`", "``", $value) . "`"; } else { $this->error("Empty value for identifier (?n) placeholder"); + return ''; } } - protected function createIN($data) + /** + * Protected function to create an IN clause for SQL queries. + * + * @param array|null $data The array of values for the IN clause. May also be NULL. + * + * @return string|null The formatted IN clause or NULL if the array is empty or NULL. + */ + protected function createIN(array|null $data): string|null { if (!is_array($data)) { - $this->error("Value for IN (?a) placeholder should be array"); - return; + $this->error("Value for IN (?a) placeholder should be an array"); + return null; } - if (!$data) { + + if (empty($data)) { return 'NULL'; } + $query = $comma = ''; foreach ($data as $value) { $query .= $comma . $this->escapeString($value); $comma = ","; } + return $query; } - protected function createSET($data) + /** + * Protected function to create a SET clause for SQL queries. + * + * @param array|null $data The associative array of key-value pairs for the SET clause. May also be NULL. + * + * @return string|null The formatted SET clause or NULL if the array is empty or NULL. + */ + protected function createSET(array|null $data): string|null { if (!is_array($data)) { - $this->error("SET (?u) placeholder expects array, " . gettype($data) . " given"); - return; + $this->error("SET (?u) placeholder expects an array, " . gettype($data) . " given"); + return null; } - if (!$data) { + + if (empty($data)) { $this->error("Empty array for SET (?u) placeholder"); - return; + return null; } + $query = $comma = ''; foreach ($data as $key => $value) { $query .= $comma . $this->escapeIdent($key) . '=' . $this->escapeString($value); $comma = ","; } + return $query; } - protected function error($err) + /** + * Protected function to handle errors in the query builder. + * + * @param string $err The error message to be handled. + * + * @throws QueryBuilderException If the error handling mode is 'exception'. + */ + protected function error(string $err): void { - $err = __CLASS__ . ": " . $err; + $err = __CLASS__ . ": " . $err; if ($this->emode == 'error') { $err .= ". Error initiated in " . $this->caller() . ", thrown"; @@ -592,9 +739,14 @@ protected function error($err) } } - protected function caller() + /** + * Protected function to determine the caller location. + * + * @return string The file and line number where the error was initiated. + */ + protected function caller(): string { - $trace = debug_backtrace(); + $trace = debug_backtrace(); $caller = ''; foreach ($trace as $t) { if (isset($t['class']) && $t['class'] == __CLASS__) { @@ -607,10 +759,12 @@ protected function caller() } /** - * On a long run we can eat up too much memory with mere statsistics - * Let's keep it at reasonable size, leaving only last 100 entries. + * Trim the query statistics to limit memory usage. + * + * On a long run, statistics can consume excessive memory. This method keeps the statistics array + * at a reasonable size by retaining only the last 100 entries. */ - protected function cutStats() + protected function cutStats(): void { if (count($this->stats) > 100) { reset($this->stats); diff --git a/tests/SafeMySQLTest.php b/tests/SafeMySQLTest.php new file mode 100644 index 0000000..71242ee --- /dev/null +++ b/tests/SafeMySQLTest.php @@ -0,0 +1,231 @@ + 'root', + 'pass' => '', + 'db' => 'test_users' + ]; + + $this->db = new SafeMySQL($opts); + + $createTableQuery = ' + CREATE TABLE IF NOT EXISTS users ( + id INT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL, + created_at DATETIME NOT NULL + ) + '; + $this->db->query($createTableQuery); + } + + public function testUsersTableExists() + { + $query = "SHOW TABLES LIKE 'users'"; + $result = $this->db->getRow($query); + $this->assertIsArray($result); + $this->assertNotEmpty($result); + } + + public function testInsertQuery() + { + $query = 'INSERT INTO users (username, email, created_at) VALUES (?s, ?s, ?s)'; + $result = $this->db->query($query, 'Username1', 'email@example.com', '2023-08-24 10:42:12'); + $this->assertTrue($result); + + $data = ['username' => 'Username1', 'email' => 'email@example.com']; + $query = "INSERT INTO users SET id=?i, created_at=NOW(), ?u ON DUPLICATE KEY UPDATE ?u"; + $result = $this->db->query($query, 2, $data, $data); + $this->assertTrue($result); + } + + public function testSelectQuery() + { + $query = 'SELECT username FROM users WHERE id = ?i'; + $result = $this->db->getOne($query, 1); + + $this->assertEquals('Username1', $result); + } + + public function testGetRow() + { + // Test case 1: Test retrieving a single row from a query result + $result = $this->db->getRow("SELECT * FROM users WHERE id=1"); + $this->assertIsArray($result); + $this->assertEquals(4, count($result)); + + // Test case 2: Test retrieving a single row from a query result with placeholders + $result = $this->db->getRow("SELECT * FROM users WHERE id=?i", 1); + $this->assertIsArray($result); + $this->assertEquals(4, count($result)); + + // Test case 3: Test retrieving a single row from a query result with multiple placeholders + $result = $this->db->getRow("SELECT * FROM users WHERE id=?i AND username=?s", 1, "Username1"); + $this->assertIsArray($result); + $this->assertEquals(4, count($result)); + + // Test case 4: Test retrieval of no rows + $result = $this->db->getRow("SELECT * FROM users WHERE id=999"); + $this->assertNull($result); + } + + public function testGetColWithNoRows() + { + $result = $this->db->getCol("SELECT id FROM users WHERE id=1"); + + $this->assertIsArray($result); + $this->assertEquals(1, count($result)); + } + + public function testGetColWithRows() + { + $result = $this->db->getCol("SELECT id FROM users WHERE username = ?s LIMIT 1", 'Username1'); + + $this->assertIsArray($result); + $this->assertEquals(1, count($result)); + } + + public function testGetAll(): void + { + // Test case 1: Retrieving all rows from a query result with no placeholders + $query = "SELECT * FROM users"; + + $result = $this->db->getAll($query); + $this->assertIsArray($result); + + // Test case 2: Retrieving all rows from a query result with placeholders + $start = 0; + $rows = 2; + $query = "SELECT * FROM users LIMIT ?i,?i"; + + $result = $this->db->getAll($query, $start, $rows); + $this->assertIsArray($result); + $this->assertEquals(2, count($result)); + + // Test case 3: Retrieving no rows from a query result + $query = "SELECT * FROM users WHERE id = ?i"; + $result = $this->db->getAll($query, 1); + $this->assertIsArray($result); + $this->assertEquals(1, count($result)); + } + + public function testGetInd() + { + $expectedResult = [ + '1' => ['id' => '1', 'username' => 'Username1', 'email' => 'email@example.com', 'created_at' => '2023-08-24 10:42:12'] + ]; + + $result = $this->db->getInd('id', 'SELECT * FROM users WHERE id = ?i', 1); + $this->assertEquals($expectedResult, $result); + } + + public function testGetIndWithNoRows() + { + $expectedResult = []; + + $result = $this->db->getInd('id', 'SELECT * FROM users WHERE id > 10'); + + $this->assertEquals($expectedResult, $result); + } + + public function testGetIndCol() + { + // Testing when resultset is empty + $result = $this->db->getIndCol('username', 'SELECT username, id FROM users WHERE id > 10'); + $this->assertEquals([], $result); + + // Testing when resultset has one row + $result = $this->db->getIndCol('username', 'SELECT username, id FROM users WHERE id = 1'); + $this->assertEquals(['Username1' => 1], $result); + + // Testing when resultset has multiple rows + $result = $this->db->getIndCol('username', 'SELECT username, id FROM users'); + $this->assertIsArray($result); + } + + public function testParseFunction() + { + // Test case 1: Test parsing a simple query with placeholders + $query1 = "SELECT * FROM table WHERE foo=?s AND bar=?s"; + $foo1 = 'value1'; + $bar1 = 'value2'; + $expected1 = "SELECT * FROM table WHERE foo='value1' AND bar='value2'"; + $result1 = $this->db->parse($query1, $foo1, $bar1); + $this->assertEquals($expected1, $result1); + + // Test case 2: Test parsing a query with a query part + $query2 = "SELECT * FROM table WHERE bar=?s ?p"; + $bar2 = 'value3'; + $qpart2 = " AND foo='value1'"; + $expected2 = "SELECT * FROM table WHERE bar='value3' AND foo='value1'"; + $result2 = $this->db->parse($query2, $bar2, $qpart2); + $this->assertEquals($expected2, $result2); + } + + public function testWhiteList() + { + // Testing when $input matches one of the allowed values + $this->assertEquals('name', $this->db->whiteList('name', ['name', 'price'])); + $this->assertEquals('ASC', $this->db->whiteList('ASC', ['ASC', 'DESC'])); + + // Testing when $input does not match any of the allowed values + $this->assertEquals(false, $this->db->whiteList('color', ['name', 'price'])); + $this->assertEquals(false, $this->db->whiteList('NONE', ['ASC', 'DESC'])); + + // Testing when $default is provided and $input does not match any of the allowed values + $this->assertEquals('default', $this->db->whiteList('color', ['name', 'price'], 'default')); + $this->assertEquals('default', $this->db->whiteList('NONE', ['ASC', 'DESC'], 'default')); + } + + public function testFilterArray() + { + $input = [ + 'title' => 'Title', + 'url' => 'http://example.com', + 'body' => 'Lorem ipsum dolor sit amet', + 'rating' => 5, + 'term' => 'Term', + 'type' => 'Type', + 'extra_field' => 'Extra Field', + ]; + $allowed = ['title', 'url', 'body', 'rating', 'term', 'type']; + + $expected = [ + 'title' => 'Title', + 'url' => 'http://example.com', + 'body' => 'Lorem ipsum dolor sit amet', + 'rating' => 5, + 'term' => 'Term', + 'type' => 'Type', + ]; + + $filtered = $this->db->filterArray($input, $allowed); + + $this->assertEquals($expected, $filtered); + } + + public function testLastQuery(): void + { + $this->db->getCol("SELECT id FROM users WHERE id=1"); + + $this->assertEquals('SELECT id FROM users WHERE id=1', $this->db->lastQuery()); + } + + + public function testdropUsersTable() + { + $query = "DROP TABLE IF EXISTS users"; + + $result = $this->db->query($query); + $this->assertTrue($result); + } +} From 28656f17fc0943c44fa3dad6016a08b9897ce5f4 Mon Sep 17 00:00:00 2001 From: impeck Date: Thu, 24 Aug 2023 12:36:55 +0500 Subject: [PATCH 03/28] fix readme --- README.md | 118 ++++++++++++++++++++++++--------------------------- README.ru.md | 68 +++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+), 62 deletions(-) create mode 100644 README.ru.md diff --git a/README.md b/README.md index 2ed6a98..6cdfc02 100644 --- a/README.md +++ b/README.md @@ -1,74 +1,68 @@ -SafeMySQL -========= - -SafeMySQL is a PHP class for safe and convenient handling of MySQL queries. -- Safe because every dynamic query part goes into the query via placeholder -- Convenient because it makes application code short and meaningful, without useless repetitions, making it ''extra'' DRY - -This class is distinguished by three main features -- Unlike standard libraries, it is using **type-hinted placeholders**, for the **everything** that may be put into the query -- Unlike standard libraries, it requires no repetitive binding, fetching and such, -thanks to set of helper methods to get the desired result right out of the query -- Unlike standard libraries, it can parse placeholders not in the whole query only, but in the arbitary query part, -thanks to the indispensabe **parse()** method, making complex queries as easy and safe as regular ones. - -Yet, it is very easy to use. You need to learn only a few things: - -1. You have to **always** pass whatever dynamical data into the query via *placeholder* -2. Each placeholder have to be marked with data type. At the moment there are six types: - * ?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. -3. To get data right out of the query there are helper methods for the most used: - * query($query,$param1,$param2, ...) - returns mysqli resource. - * getOne($query,$param1,$param2, ...) - returns scalar value - * getRow($query,$param1,$param2, ...) - returns 1-dimensional array, a row - * getCol($query,$param1,$param2, ...) - returns 1-dimensional array, a column - * getAll($query,$param1,$param2, ...) - returns 2-dimensional array, an array of rows - * getInd($key,$query,$par1,$par2, ...) - returns an indexed 2-dimensional array, an array of rows - * getIndCol($key,$query,$par1,$par2, ...) - returns 1-dimensional array, an indexed column, consists of key => value pairs -4. For the whatever complex case always use the **parse()** method. And insert - -The rest is as usual - just create a regular SQL (with placeholders) and get a result: - -* ```$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);``` - -The main feature of this class is a type-hinted placeholders. -And it's a really great step further from just ordinal placeholders used in prepared statements. -Simply because dynamical parts of the query aren't limited to just scalar data! -In the real life we have to add identifiers, arrays for ```IN``` operator, and arrays for ```INSERT``` and ```UPDATE``` queries. -So - we need many different types of data formatting. Thus, we need the way to tell the driver how to format this particular data. -Conventional prepared statements use toilsome and repeating bind_* functions. -But there is a way more sleek and useful way - to set the type along with placeholder itself. It is not something new - well-known ```printf()``` function uses exactly the same mechanism. So, I hesitated not to borrow such a brilliant idea. - -To implement such a feature, no doubt one have to have their own query parser. No problem, it's not a big deal. But the benefits are innumerable. -Look at all the questions on Stack Overflow where developers are trying in vain to bind a field name. -Voila - with the identifier placeholder it is as easy as adding a field value: +# SafeMySQL + +SafeMySQL is a PHP class designed for secure and efficient MySQL query handling. It stands out for several key features: + +- **Safety:** All dynamic query parts are incorporated into the query using placeholders, enhancing security. +- **Convenience:** It streamlines application code, reducing redundancy, and following the DRY (Don't Repeat Yourself) principle. + +## Features + +SafeMySQL offers three primary features that distinguish it from standard libraries: + +1. **Type-Hinted Placeholders:** Unlike traditional libraries, SafeMySQL employs type-hinted placeholders for all query elements. +2. **Streamlined Usage:** It eliminates the need for repetitive binding and fetching, thanks to a range of helper methods. +3. **Partial Placeholder Parsing:** SafeMySQL allows placeholder parsing in any part of the query, making complex queries as easy as standard ones through the **parse()** method. + +## Getting Started + +Using SafeMySQL is straightforward. Here are the key steps: + +1. Always use placeholders for dynamic data in your queries. +2. Mark each placeholder with a data type, including: + - ?s ("string"): For strings (including `DATE`, `FLOAT`, and `DECIMAL`). + - ?i ("integer"): For integers. + - ?n ("name"): For identifiers (table and field names). + - ?a ("array"): For complex placeholders used with the `IN()` operator (substituted with a string in 'a,'b,'c' format, without parentheses). + - ?u ("update"): For complex placeholders used with the `SET` operator (substituted with a string in `field`='value',`field`='value' format). + - ?p ("parsed"): A special placeholder type for inserting pre-parsed statements without further processing to avoid double parsing. +3. Utilize helper methods to retrieve data from queries, including: + - `query($query, $param1, $param2, ...)`: Returns a mysqli resource. + - `getOne($query, $param1, $param2, ...)`: Returns a scalar value. + - `getRow($query, $param1, $param2, ...)`: Returns a 1-dimensional array (a row). + - `getCol($query, $param1, $param2, ...)`: Returns a 1-dimensional array (a column). + - `getAll($query, $param1, $param2, ...)`: Returns a 2-dimensional array (an array of rows). + - `getInd($key, $query, $par1, $par2, ...)`: Returns an indexed 2-dimensional array (an array of rows). + - `getIndCol($key, $query, $par1, $par2, ...)`: Returns a 1-dimensional array (an indexed column) consisting of key => value pairs. +4. For complex cases, rely on the **parse()** method. + +### Example Usage + +Here are some examples of how to use SafeMySQL: + +```php +$name = $db->getOne('SELECT name FROM table WHERE id = ?i', $_GET['id']); +$data = $db->getInd('id', 'SELECT * FROM ?n WHERE id IN (?a)', 'table', [1, 2]); +$data = $db->getAll("SELECT * FROM ?n WHERE mod=?s LIMIT ?i", $table, $mod, $limit); +``` + +The standout feature of SafeMySQL is its type-hinted placeholders. This approach extends beyond simple scalar data, allowing you to include identifiers, arrays for the `IN` operator, and arrays for `INSERT` and `UPDATE` queries. No more struggling with binding field names or constructing complex queries manually. + +For instance, consider binding a field name effortlessly: ```php $field = $_POST['field']; $value = $_POST['value']; $sql = "SELECT * FROM table WHERE ?n LIKE ?s"; -$data = $db->query($sql,$field,"%$value%"); +$data = $db->query($sql, $field, "%$value%"); ``` -Nothing could be easier! - -Of course we will have placeholders for the common types - strings and numbers. -But as we started inventing new placeholders - let's make some more! - -Another trouble in creating prepared queries - arrays going to the IN operator. Everyone is trying to do it their own way, but the type-hinted placeholder makes it as simple as adding a string: +Simplifying queries involving arrays for the `IN` operator: ```php -$array = array(1,2,3); -$data = $db->query("SELECT * FROM table WHERE id IN (?a)",$array); +$array = [1, 2, 3]; +$data = $db->query("SELECT * FROM table WHERE id IN (?a)", $array); ``` -The same goes for such toilsome queries like ```INSERT``` and ```UPDATE```. +The same convenience extends to complex queries like `INSERT` and `UPDATE`. -And, of course, we have a set of helper functions to turn type-hinted placeholders into real brilliant, making almost every call to the database as simple as one or two lines of code for all the regular real life tasks. +SafeMySQL also provides a set of helper functions, making database calls for everyday tasks quick and straightforward. \ No newline at end of file diff --git a/README.ru.md b/README.ru.md new file mode 100644 index 0000000..f5f1c30 --- /dev/null +++ b/README.ru.md @@ -0,0 +1,68 @@ +# SafeMySQL + +SafeMySQL - это класс PHP, разработанный для безопасной и удобной обработки запросов к MySQL. Он выделяется несколькими ключевыми особенностями: + +- **Безопасность:** Все динамические части запросов включаются в запрос с использованием заполнителей, повышая безопасность. +- **Удобство:** Он упрощает код приложения, уменьшая избыточность и следуя принципу DRY (Don't Repeat Yourself). + +## Особенности + +SafeMySQL предлагает три основных особенности, которые отличают его от стандартных библиотек: + +1. **Заполнители с подсказками типов:** В отличие от традиционных библиотек, SafeMySQL использует заполнители с подсказками типов для всех элементов запроса. +2. **Упрощенное использование:** Это устраняет необходимость в повторном привязывании и извлечении данных благодаря набору вспомогательных методов. +3. **Частичный анализ заполнителей:** SafeMySQL позволяет анализировать заполнители не только во всем запросе, но и в любой его части, с помощью метода **parse()**, что делает выполнение сложных запросов так же простым и безопасным, как стандартные. + +## Начало работы + +Использование SafeMySQL просто. Вот ключевые шаги: + +1. Всегда используйте заполнители для динамических данных в ваших запросах. +2. Помечайте каждый заполнитель типом данных, включая: + - ?s ("строка"): для строк (включая `DATE`, `FLOAT` и `DECIMAL`). + - ?i ("целое число"): для целых чисел. + - ?n ("имя"): для идентификаторов (имен таблиц и полей). + - ?a ("массив"): для сложных заполнителей, используемых с оператором `IN()` (заменяется строкой в формате 'a,'b,'c', без скобок). + - ?u ("обновление"): для сложных заполнителей, используемых с оператором `SET` (заменяется строкой в формате `поле`='значение',`поле`='значение'). + - ?p ("разобранный"): специальный тип заполнителя для вставки предварительно разобранных выражений без дополнительной обработки для избегания двойного анализа. +3. Используйте вспомогательные методы для извлечения данных из запросов, включая: + - `query($query, $param1, $param2, ...)`: Возвращает ресурс mysqli. + - `getOne($query, $param1, $param2, ...)`: Возвращает скалярное значение. + - `getRow($query, $param1, $param2, ...)`: Возвращает одномерный массив (строку). + - `getCol($query, $param1, $param2, ...)`: Возвращает одномерный массив (столбец). + - `getAll($query, $param1, $param2, ...)`: Возвращает двумерный массив (массив строк). + - `getInd($key, $query, $par1, $par2, ...)`: Возвращает индексированный двумерный массив (массив строк). + - `getIndCol($key, $query, $par1, $par2, ...)`: Возвращает одномерный массив (индексированный столбец), состоящий из пар ключ => значение. +4. Для сложных случаев полагайтесь на метод **parse()**. + +### Пример использования + +Вот несколько примеров использования SafeMySQL: + +```php +$name = $db->getOne('SELECT name FROM table WHERE id = ?i', $_GET['id']); +$data = $db->getInd('id', 'SELECT * FROM ?n WHERE id IN (?a)', 'table', [1, 2]); +$data = $db->getAll("SELECT * FROM ?n WHERE mod=?s LIMIT ?i", $table, $mod, $limit); +``` + +Основная особенность SafeMySQL - это заполнители с подсказками типов. Этот подход расширяется за пределы простых скалярных данных, позволяя включать идентификаторы, массивы для оператора `IN`, а также массивы для запросов `INSERT` и `UPDATE`. Забудьте о сложностях при привязке имен полей или создании сложных запросов вручную. + +Например, рассмотрим привязку имени поля без усилий: + +```php +$field = $_POST['field']; +$value = $_POST['value']; +$sql = "SELECT * FROM table WHERE ?n LIKE ?s"; +$data = $db->query($sql, $field, "%$value%"); +``` + +Упрощение запросов, связанных с массивами для оператора `IN`: + +```php +$array = [1, 2, 3]; +$data = $db->query("SELECT * FROM table WHERE id IN (?a)", $array); +``` + +Та же удобство распространяется и на сложные запросы, такие как `INSERT` и `UPDATE`. + +SafeMySQL также предоставляет набор вспомогательных функций, которые делают вызовы к базе данных для повседневных задач быстрыми и простыми, сокращая их к одной или двум строкам кода. \ No newline at end of file From e24f9fedeef8e1d895b4c8d7a5044d9ec2131576 Mon Sep 17 00:00:00 2001 From: impeck Date: Wed, 13 Sep 2023 16:54:26 +0500 Subject: [PATCH 04/28] fix to psr-4 --- composer.json | 36 +++++++++++++----------- safemysql.class.php => src/SafeMySQL.php | 20 +++++++------ tests/SafeMySQLTest.php | 1 + 3 files changed, 32 insertions(+), 25 deletions(-) rename safemysql.class.php => src/SafeMySQL.php (97%) diff --git a/composer.json b/composer.json index 65d8de1..f015925 100644 --- a/composer.json +++ b/composer.json @@ -1,30 +1,34 @@ { - "name": "colshrapnel/safemysql", - "description": "A real safe and convenient way to handle MySQL queries.", + "name": "impeck/safemysql", + "description": "PHP class designed for secure and efficient MySQL query handling", "type": "library", + "license": "Apache-2.0", "keywords": [ "db", - "mysql" + "mysql", + "safemysql", + "safe", + "secure", + "query", + "handling" ], - "homepage": "https://github.com/colshrapnel/safemysql", - "license": "Apache-2.0", + "autoload": { + "psr-4": { + "Impeck\\": "src/" + } + }, "authors": [ { "name": "Colonel Shrapnel", - "email": "col.shrapnel@gmail.com", - "role": "lead" + "email": "col.shrapnel@gmail.com" + }, + { + "name": "Impeck", + "email": "impeck@ya.ru" } ], - "support": { - "issues": "https://github.com/colshrapnel/safemysql/issues" - }, "require": { - "php": "^7.4 || ^8.0" - }, - "autoload": { - "files": [ - "safemysql.class.php" - ] + "php": "^7.4||^8.0" }, "require-dev": { "phpunit/phpunit": "^10.3" diff --git a/safemysql.class.php b/src/SafeMySQL.php similarity index 97% rename from safemysql.class.php rename to src/SafeMySQL.php index db1c18f..0e2d2c7 100644 --- a/safemysql.class.php +++ b/src/SafeMySQL.php @@ -65,11 +65,13 @@ * */ +namespace Impeck; + class SafeMySQL { /** - * @var mysqli|null The mysqli connection. + * @var \mysqli|null The mysqli connection. */ protected $conn; @@ -137,7 +139,7 @@ function __construct(array $opt = []) $this->exname = $opt['exception']; if (isset($opt['mysqli'])) { - if ($opt['mysqli'] instanceof mysqli) { + if ($opt['mysqli'] instanceof \mysqli) { $this->conn = $opt['mysqli']; return; } else { @@ -176,7 +178,7 @@ function __construct(array $opt = []) * * @example $result = $db->query("DELETE FROM table WHERE id=?i", $id); */ - public function query(string $query, ...$args): mysqli|bool + public function query(string $query, ...$args): \mysqli|bool { return $this->rawQuery($this->prepareQuery($query, ...$args)); } @@ -184,11 +186,11 @@ public function query(string $query, ...$args): mysqli|bool /** * Fetch a row from a result set as an associative array or a numeric array. * - * @param mysqli_result|null $result The result set from which to fetch the row. + * @param \mysqli_result|null $result The result set from which to fetch the row. * @param int $mode The type of array to return (RESULT_ASSOC for associative array, or RESULT_NUM for numeric array). Defaults to RESULT_ASSOC. * @return array|null The fetched row as an associative array or a numeric array, or null if no more rows are available. */ - public function fetch(mysqli_result|null $result, int $mode = self::RESULT_ASSOC): array|null + public function fetch(\mysqli_result|null $result, int $mode = self::RESULT_ASSOC): array|null { return $result === null ? null : mysqli_fetch_array($result, $mode); } @@ -216,10 +218,10 @@ public function insertId(): int /** * Returns the number of rows in the resultset. * - * @param mysqli_result $result - The mysqli result object. + * @param \mysqli_result $result - The mysqli result object. * @return int - The number of rows in the resultset. */ - public function numRows(mysqli_result $result): int + public function numRows(\mysqli_result $result): int { return mysqli_num_rows($result); } @@ -517,9 +519,9 @@ public function getStats(): array * It also logs some statistics like profiling information and error messages. * * @param string $query - A regular SQL query. - * @return mysqli_result|bool A mysqli_result object on success, or bool on error. + * @return \mysqli_result|bool A mysqli_result object on success, or bool on error. */ - protected function rawQuery(string $query): mysqli_result|bool + protected function rawQuery(string $query): \mysqli_result|bool { $start = microtime(TRUE); $res = mysqli_query($this->conn, $query); diff --git a/tests/SafeMySQLTest.php b/tests/SafeMySQLTest.php index 71242ee..f62ec17 100644 --- a/tests/SafeMySQLTest.php +++ b/tests/SafeMySQLTest.php @@ -2,6 +2,7 @@ include 'vendor/autoload.php'; use PHPUnit\Framework\TestCase; +use Impeck\SafeMySQL; class SafeMySQLTest extends TestCase { From 8cdde3974bef3614d2537d078b16c43427a3424e Mon Sep 17 00:00:00 2001 From: impeck Date: Wed, 13 Sep 2023 16:54:49 +0500 Subject: [PATCH 05/28] add link --- README.md | 8 +++++++- README.ru.md | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6cdfc02..fa4d5bc 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,12 @@ # SafeMySQL -SafeMySQL is a PHP class designed for secure and efficient MySQL query handling. It stands out for several key features: +English | [Русский](https://github.com/Impeck/safemysql/blob/master/README.ru.md) + +SafeMySQL is a PHP class designed for secure and efficient MySQL query handling. + +Forked from [colshrapnel/safemysql](https://github.com/colshrapnel/safemysql). + +It stands out for several key features: - **Safety:** All dynamic query parts are incorporated into the query using placeholders, enhancing security. - **Convenience:** It streamlines application code, reducing redundancy, and following the DRY (Don't Repeat Yourself) principle. diff --git a/README.ru.md b/README.ru.md index f5f1c30..3875c10 100644 --- a/README.ru.md +++ b/README.ru.md @@ -1,6 +1,12 @@ # SafeMySQL -SafeMySQL - это класс PHP, разработанный для безопасной и удобной обработки запросов к MySQL. Он выделяется несколькими ключевыми особенностями: +[English](https://github.com/Impeck/safemysql/blob/master/README.md) | Русский + +SafeMySQL - это класс PHP, разработанный для безопасной и удобной обработки запросов к MySQL. + +Форк [colshrapnel/safemysql](https://github.com/colshrapnel/safemysql). + +Он выделяется несколькими ключевыми особенностями: - **Безопасность:** Все динамические части запросов включаются в запрос с использованием заполнителей, повышая безопасность. - **Удобство:** Он упрощает код приложения, уменьшая избыточность и следуя принципу DRY (Don't Repeat Yourself). From dc86ef272335cc539539841a29ddce1fbdf59aa7 Mon Sep 17 00:00:00 2001 From: impeck Date: Wed, 13 Sep 2023 16:55:28 +0500 Subject: [PATCH 06/28] add link --- composer.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/composer.json b/composer.json index f015925..fa6fd81 100644 --- a/composer.json +++ b/composer.json @@ -27,6 +27,10 @@ "email": "impeck@ya.ru" } ], + "homepage": "https://github.com/Impeck/safemysql", + "support": { + "issues": "https://github.com/Impeck/safemysql/issues" + }, "require": { "php": "^7.4||^8.0" }, From 01748cd5fd0b8217e425ff8dbe8323f6e95d1f7d Mon Sep 17 00:00:00 2001 From: impeck Date: Wed, 13 Sep 2023 16:55:50 +0500 Subject: [PATCH 07/28] ignore DS_Store --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 57872d0..58f394d 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /vendor/ +.DS_Store From 5531ddbbbbe566fa3c92fce9365838f701fb85ff Mon Sep 17 00:00:00 2001 From: impeck Date: Wed, 13 Sep 2023 16:55:59 +0500 Subject: [PATCH 08/28] add tests --- .gitattributes | 8 ++++++++ .github/workflows/tests.yml | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 .gitattributes create mode 100644 .github/workflows/tests.yml diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..80c1f55 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,8 @@ +# Path-based git attributes +# https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html + +# This way, the files would be available in the repository but it would not be downloaded when the package is required by another project. +/.gitattributes export-ignore +/.github export-ignore +/tests export-ignore +/phpunit.xml export-ignore \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..cb02a0b --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,37 @@ +# .github/workflows/tests.yaml +name: Tests + +on: ["push", "pull_request"] + +jobs: + tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['8.0', '8.1', '8.2', '8.3'] + stability: [ prefer-lowest, prefer-stable ] + + name: PHP ${{ matrix.php }} - ${{ matrix.stability }} tests + steps: + # basically git clone + - uses: actions/checkout@v3 + + - name: Cache dependencies + uses: actions/cache@v3 + with: + path: ~/.composer/cache/files + key: dependencies-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} + + # use PHP of specific version + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: pcov + coverage: pcov + + - name: Install dependencies + run: composer update --${{ matrix.stability }} --prefer-dist --no-interaction --no-progress --no-suggest + + - name: Execute tests + run: vendor/bin/phpunit --verbose \ No newline at end of file From 73e9daf4ddeac32e21c56114900051860f3faadf Mon Sep 17 00:00:00 2001 From: impeck Date: Wed, 13 Sep 2023 17:00:24 +0500 Subject: [PATCH 09/28] phpunit ^9.5 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index fa6fd81..22f0330 100644 --- a/composer.json +++ b/composer.json @@ -35,6 +35,6 @@ "php": "^7.4||^8.0" }, "require-dev": { - "phpunit/phpunit": "^10.3" + "phpunit/phpunit" : "^9.5" } } \ No newline at end of file From 0f17049de3e7727dce5186e98bd49acb27e5d600 Mon Sep 17 00:00:00 2001 From: impeck Date: Wed, 13 Sep 2023 17:14:34 +0500 Subject: [PATCH 10/28] add mysql test --- .github/workflows/tests.yml | 21 +++++++++++++++++++-- tests/SafeMySQLTest.php | 4 ++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cb02a0b..653f929 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,7 +11,24 @@ jobs: matrix: php: ['8.0', '8.1', '8.2', '8.3'] stability: [ prefer-lowest, prefer-stable ] - + services: + # mysql-service Label used to access the service container + mysql-service: + # Docker Hub image (also with version) + image: mysql:5.7 + env: + ## Accessing to Github secrets, where you can store your configuration + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: db_test + ## map the "external" 33306 port with the "internal" 3306 + ports: + - 33306:3306 + # Set health checks to wait until mysql database has started (it takes some seconds to start) + options: >- + --health-cmd="mysqladmin ping" + --health-interval=10s + --health-timeout=5s + --health-retries=3 name: PHP ${{ matrix.php }} - ${{ matrix.stability }} tests steps: # basically git clone @@ -34,4 +51,4 @@ jobs: run: composer update --${{ matrix.stability }} --prefer-dist --no-interaction --no-progress --no-suggest - name: Execute tests - run: vendor/bin/phpunit --verbose \ No newline at end of file + run: vendor/bin/phpunit tests \ No newline at end of file diff --git a/tests/SafeMySQLTest.php b/tests/SafeMySQLTest.php index f62ec17..a976a5a 100644 --- a/tests/SafeMySQLTest.php +++ b/tests/SafeMySQLTest.php @@ -12,8 +12,8 @@ public function setUp(): void { $opts = [ 'user' => 'root', - 'pass' => '', - 'db' => 'test_users' + 'pass' => 'root', + 'db' => 'db_test' ]; $this->db = new SafeMySQL($opts); From 742a4c17d708ed9281067a6485dc75ff785edb99 Mon Sep 17 00:00:00 2001 From: impeck Date: Thu, 14 Sep 2023 11:16:27 +0500 Subject: [PATCH 11/28] test --- .github/workflows/tests.yml | 14 +++++++------- .gitignore | 3 +++ composer.json | 4 ++-- phpunit.xml | 14 ++++++++++++++ src/SafeMySQL.php | 4 +--- tests/SafeMySQLTest.php | 34 +++++++++++++++++++++++++--------- 6 files changed, 52 insertions(+), 21 deletions(-) create mode 100644 phpunit.xml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 653f929..5282c6a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,15 +11,15 @@ jobs: matrix: php: ['8.0', '8.1', '8.2', '8.3'] stability: [ prefer-lowest, prefer-stable ] + # Service container Mysql mysql services: - # mysql-service Label used to access the service container - mysql-service: + # Label used to access the service container + mysql: # Docker Hub image (also with version) - image: mysql:5.7 + image: mysql:latest env: - ## Accessing to Github secrets, where you can store your configuration - MYSQL_ROOT_PASSWORD: root - MYSQL_DATABASE: db_test + MYSQL_ALLOW_EMPTY_PASSWORD: yes + MYSQL_DATABASE: db_test ## map the "external" 33306 port with the "internal" 3306 ports: - 33306:3306 @@ -51,4 +51,4 @@ jobs: run: composer update --${{ matrix.stability }} --prefer-dist --no-interaction --no-progress --no-suggest - name: Execute tests - run: vendor/bin/phpunit tests \ No newline at end of file + run: vendor/bin/phpunit --testdox \ No newline at end of file diff --git a/.gitignore b/.gitignore index 58f394d..0370088 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ /vendor/ .DS_Store +.vscode +.phpunit.result.cache +.phpunit.cache/test-results diff --git a/composer.json b/composer.json index 22f0330..a1b031d 100644 --- a/composer.json +++ b/composer.json @@ -32,9 +32,9 @@ "issues": "https://github.com/Impeck/safemysql/issues" }, "require": { - "php": "^7.4||^8.0" + "php": "^8.0" }, "require-dev": { - "phpunit/phpunit" : "^9.5" + "phpunit/phpunit": "^9.5" } } \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..5f436a9 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,14 @@ + + + + + + ./tests + + + + + ./src + + + diff --git a/src/SafeMySQL.php b/src/SafeMySQL.php index 0e2d2c7..cdeeb09 100644 --- a/src/SafeMySQL.php +++ b/src/SafeMySQL.php @@ -174,7 +174,7 @@ function __construct(array $opt = []) * @param string $query - An SQL query with placeholders. * @param mixed ...$args - Unlimited number of arguments to match placeholders in the query. * - * @return mysqli|bool The result of the query or false if an error occurred. + * @return \mysqli|bool The result of the query or false if an error occurred. * * @example $result = $db->query("DELETE FROM table WHERE id=?i", $id); */ @@ -726,8 +726,6 @@ protected function createSET(array|null $data): string|null * Protected function to handle errors in the query builder. * * @param string $err The error message to be handled. - * - * @throws QueryBuilderException If the error handling mode is 'exception'. */ protected function error(string $err): void { diff --git a/tests/SafeMySQLTest.php b/tests/SafeMySQLTest.php index a976a5a..5fcf385 100644 --- a/tests/SafeMySQLTest.php +++ b/tests/SafeMySQLTest.php @@ -1,22 +1,39 @@ 'localhost', + 'user' => 'root', + 'pass' => '', + 'db' => 'test' + ]; protected $db; - public function setUp(): void + public static function setUpBeforeClass(): void { - $opts = [ - 'user' => 'root', - 'pass' => 'root', - 'db' => 'db_test' - ]; + $conn = @new mysqli(self::$opts['host'], self::$opts['user'], self::$opts['password']); + + if ($conn->connect_error) { + die("Connection failed: " . $conn->connect_error); + } - $this->db = new SafeMySQL($opts); + $sql = "CREATE SCHEMA IF NOT EXISTS " . self::$opts['db']; + if ($conn->query($sql) === TRUE) { + echo "Database created successfully"; + } else { + echo "Error creating database: " . $conn->error; + } + + $conn->close(); + } + + public function setUp(): void + { + $this->db = new SafeMySQL(self::$opts); $createTableQuery = ' CREATE TABLE IF NOT EXISTS users ( @@ -221,7 +238,6 @@ public function testLastQuery(): void $this->assertEquals('SELECT id FROM users WHERE id=1', $this->db->lastQuery()); } - public function testdropUsersTable() { $query = "DROP TABLE IF EXISTS users"; From d984a1b764141522ecee21fa9090cb99c1099e84 Mon Sep 17 00:00:00 2001 From: impeck Date: Thu, 14 Sep 2023 11:28:01 +0500 Subject: [PATCH 12/28] test --- .gitignore | 1 + phpunit.xml | 25 +++++++++++++++++-------- tests/SafeMySQLTest.php | 7 ++----- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index 0370088..206cc06 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ .vscode .phpunit.result.cache .phpunit.cache/test-results +composer.lock diff --git a/phpunit.xml b/phpunit.xml index 5f436a9..63d51a8 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,14 +1,23 @@ - - + + + + ./src + + ./tests - - - ./src - - - + \ No newline at end of file diff --git a/tests/SafeMySQLTest.php b/tests/SafeMySQLTest.php index 5fcf385..bef2fa9 100644 --- a/tests/SafeMySQLTest.php +++ b/tests/SafeMySQLTest.php @@ -22,11 +22,8 @@ public static function setUpBeforeClass(): void } $sql = "CREATE SCHEMA IF NOT EXISTS " . self::$opts['db']; - if ($conn->query($sql) === TRUE) { - echo "Database created successfully"; - } else { - echo "Error creating database: " . $conn->error; - } + + $conn->query($sql); $conn->close(); } From f538378e6936e989c5c48ceb3883a8b737a06f46 Mon Sep 17 00:00:00 2001 From: impeck Date: Thu, 14 Sep 2023 11:43:14 +0500 Subject: [PATCH 13/28] test --- .github/workflows/tests.yml | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5282c6a..1ff45fb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,24 +11,6 @@ jobs: matrix: php: ['8.0', '8.1', '8.2', '8.3'] stability: [ prefer-lowest, prefer-stable ] - # Service container Mysql mysql - services: - # Label used to access the service container - mysql: - # Docker Hub image (also with version) - image: mysql:latest - env: - MYSQL_ALLOW_EMPTY_PASSWORD: yes - MYSQL_DATABASE: db_test - ## map the "external" 33306 port with the "internal" 3306 - ports: - - 33306:3306 - # Set health checks to wait until mysql database has started (it takes some seconds to start) - options: >- - --health-cmd="mysqladmin ping" - --health-interval=10s - --health-timeout=5s - --health-retries=3 name: PHP ${{ matrix.php }} - ${{ matrix.stability }} tests steps: # basically git clone @@ -46,6 +28,8 @@ jobs: php-version: ${{ matrix.php }} extensions: pcov coverage: pcov + # Install MySQL + - uses: mirromutth/mysql-action@v1.1 - name: Install dependencies run: composer update --${{ matrix.stability }} --prefer-dist --no-interaction --no-progress --no-suggest From 2479c069fdf819dd96e113d01595146cd9f115a4 Mon Sep 17 00:00:00 2001 From: impeck Date: Thu, 14 Sep 2023 11:49:08 +0500 Subject: [PATCH 14/28] test --- .github/workflows/tests.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1ff45fb..1de802e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -30,7 +30,10 @@ jobs: coverage: pcov # Install MySQL - uses: mirromutth/mysql-action@v1.1 - + with: + mysql root password: '' # Required if "mysql user" is empty, default is empty. The root superuser password + mysql user: '' # Required if "mysql root password" is empty, default is empty. The superuser for the specified database. Can use secrets, too + mysql password: '' # Required if "mysql user" exists. The password for the "mysql user" - name: Install dependencies run: composer update --${{ matrix.stability }} --prefer-dist --no-interaction --no-progress --no-suggest From 8ba990a2bf2f02cdce7a05afddb114afa3cbadd4 Mon Sep 17 00:00:00 2001 From: impeck Date: Thu, 14 Sep 2023 11:53:09 +0500 Subject: [PATCH 15/28] test --- .github/workflows/tests.yml | 6 +++--- tests/SafeMySQLTest.php | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1de802e..7218148 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -31,9 +31,9 @@ jobs: # Install MySQL - uses: mirromutth/mysql-action@v1.1 with: - mysql root password: '' # Required if "mysql user" is empty, default is empty. The root superuser password - mysql user: '' # Required if "mysql root password" is empty, default is empty. The superuser for the specified database. Can use secrets, too - mysql password: '' # Required if "mysql user" exists. The password for the "mysql user" + mysql root password: 'root' # Required if "mysql user" is empty, default is empty. The root superuser password + mysql user: 'user' # Required if "mysql root password" is empty, default is empty. The superuser for the specified database. Can use secrets, too + mysql password: 'user' # Required if "mysql user" exists. The password for the "mysql user" - name: Install dependencies run: composer update --${{ matrix.stability }} --prefer-dist --no-interaction --no-progress --no-suggest diff --git a/tests/SafeMySQLTest.php b/tests/SafeMySQLTest.php index bef2fa9..b76df76 100644 --- a/tests/SafeMySQLTest.php +++ b/tests/SafeMySQLTest.php @@ -8,7 +8,7 @@ class SafeMySQLTest extends TestCase protected static $opts = [ 'host' => 'localhost', 'user' => 'root', - 'pass' => '', + 'pass' => 'root', 'db' => 'test' ]; protected $db; From 0a06e154d69f90c5c19c8bdcd9f8462790e93521 Mon Sep 17 00:00:00 2001 From: impeck Date: Thu, 14 Sep 2023 12:12:33 +0500 Subject: [PATCH 16/28] test --- .github/workflows/tests.yml | 4 ++-- composer.json | 3 ++- tests/SafeMySQLTest.php | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7218148..69801e4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -26,12 +26,12 @@ jobs: - uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: pcov + extensions: pcov, pdo_mysql coverage: pcov # Install MySQL - uses: mirromutth/mysql-action@v1.1 with: - mysql root password: 'root' # Required if "mysql user" is empty, default is empty. The root superuser password + mysql root password: '' # Required if "mysql user" is empty, default is empty. The root superuser password mysql user: 'user' # Required if "mysql root password" is empty, default is empty. The superuser for the specified database. Can use secrets, too mysql password: 'user' # Required if "mysql user" exists. The password for the "mysql user" - name: Install dependencies diff --git a/composer.json b/composer.json index a1b031d..1493c49 100644 --- a/composer.json +++ b/composer.json @@ -32,7 +32,8 @@ "issues": "https://github.com/Impeck/safemysql/issues" }, "require": { - "php": "^8.0" + "php": "^8.0", + "ext-mysqli": "*" }, "require-dev": { "phpunit/phpunit": "^9.5" diff --git a/tests/SafeMySQLTest.php b/tests/SafeMySQLTest.php index b76df76..d2b39db 100644 --- a/tests/SafeMySQLTest.php +++ b/tests/SafeMySQLTest.php @@ -8,8 +8,8 @@ class SafeMySQLTest extends TestCase protected static $opts = [ 'host' => 'localhost', 'user' => 'root', - 'pass' => 'root', - 'db' => 'test' + 'pass' => '', + 'db' => 'savemysql' ]; protected $db; From 69260958e1605d0e616b85418c22d82418dc9f75 Mon Sep 17 00:00:00 2001 From: impeck Date: Thu, 14 Sep 2023 12:17:45 +0500 Subject: [PATCH 17/28] test --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 69801e4..408d6b3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -26,7 +26,7 @@ jobs: - uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: pcov, pdo_mysql + extensions: mysql coverage: pcov # Install MySQL - uses: mirromutth/mysql-action@v1.1 From 5c643e43b9642d3bda6bdbf1cbc37c0670433f68 Mon Sep 17 00:00:00 2001 From: impeck Date: Thu, 14 Sep 2023 12:22:18 +0500 Subject: [PATCH 18/28] test --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 408d6b3..2960e3d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -26,7 +26,7 @@ jobs: - uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: mysql + extensions: mysql pdo pdo_mysql coverage: pcov # Install MySQL - uses: mirromutth/mysql-action@v1.1 From c8c2338c9d8ceb0dcdb18121377a46dc7646460e Mon Sep 17 00:00:00 2001 From: impeck Date: Thu, 14 Sep 2023 12:29:07 +0500 Subject: [PATCH 19/28] test --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2960e3d..31a93f0 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -26,7 +26,7 @@ jobs: - uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: mysql pdo pdo_mysql + extensions: mysql, pdo, pdo_mysql coverage: pcov # Install MySQL - uses: mirromutth/mysql-action@v1.1 From e312155b4680d2dc85ee6088d39b175fa977b415 Mon Sep 17 00:00:00 2001 From: impeck Date: Wed, 20 Sep 2023 14:03:05 +0500 Subject: [PATCH 20/28] test --- .github/workflows/tests.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 31a93f0..8097ef7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,13 +9,17 @@ jobs: strategy: fail-fast: false matrix: - php: ['8.0', '8.1', '8.2', '8.3'] + php: ['8.0'] stability: [ prefer-lowest, prefer-stable ] name: PHP ${{ matrix.php }} - ${{ matrix.stability }} tests + steps: # basically git clone - uses: actions/checkout@v3 + - name: Shutdown Ubuntu MySQL (SUDO) + run: sudo service mysql stop # Shutdown the Default MySQL, "sudo" is necessary, please not remove it + - name: Cache dependencies uses: actions/cache@v3 with: From 9b0558c44f30f49e54b87a781c9845bf96d2f5b8 Mon Sep 17 00:00:00 2001 From: impeck Date: Wed, 20 Sep 2023 14:10:09 +0500 Subject: [PATCH 21/28] test --- .github/workflows/tests.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8097ef7..f870e2e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,10 +15,10 @@ jobs: steps: # basically git clone - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Shutdown Ubuntu MySQL (SUDO) - run: sudo service mysql stop # Shutdown the Default MySQL, "sudo" is necessary, please not remove it + run: sudo service mysql stop && sudo service mysql status - name: Cache dependencies uses: actions/cache@v3 @@ -33,11 +33,11 @@ jobs: extensions: mysql, pdo, pdo_mysql coverage: pcov # Install MySQL - - uses: mirromutth/mysql-action@v1.1 + - uses: shogo82148/actions-setup-mysql@v1 with: - mysql root password: '' # Required if "mysql user" is empty, default is empty. The root superuser password - mysql user: 'user' # Required if "mysql root password" is empty, default is empty. The superuser for the specified database. Can use secrets, too - mysql password: 'user' # Required if "mysql user" exists. The password for the "mysql user" + mysql-version: "8.0" + - run: mysql -uroot -e 'SELECT version()' + - name: Install dependencies run: composer update --${{ matrix.stability }} --prefer-dist --no-interaction --no-progress --no-suggest From ec1db862b048c39dc2a3ad6b2cf90c1e6a08002d Mon Sep 17 00:00:00 2001 From: impeck Date: Wed, 20 Sep 2023 14:11:32 +0500 Subject: [PATCH 22/28] test --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f870e2e..d2e1012 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -18,7 +18,7 @@ jobs: - uses: actions/checkout@v4 - name: Shutdown Ubuntu MySQL (SUDO) - run: sudo service mysql stop && sudo service mysql status + run: sudo service mysql stop - name: Cache dependencies uses: actions/cache@v3 From 81b3e4b597a9e989070f662f167a459fbc6adc41 Mon Sep 17 00:00:00 2001 From: impeck Date: Wed, 20 Sep 2023 14:14:58 +0500 Subject: [PATCH 23/28] test --- .github/workflows/tests.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d2e1012..4f7d5f1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -26,17 +26,18 @@ jobs: path: ~/.composer/cache/files key: dependencies-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} + # Install MySQL + - uses: shogo82148/actions-setup-mysql@v1 + with: + mysql-version: "8.0" + - run: mysql -uroot -e 'SELECT version()' + # use PHP of specific version - uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} extensions: mysql, pdo, pdo_mysql coverage: pcov - # Install MySQL - - uses: shogo82148/actions-setup-mysql@v1 - with: - mysql-version: "8.0" - - run: mysql -uroot -e 'SELECT version()' - name: Install dependencies run: composer update --${{ matrix.stability }} --prefer-dist --no-interaction --no-progress --no-suggest From 9c1f23ca0a3177b75d4fe11bab8db468d0913d48 Mon Sep 17 00:00:00 2001 From: impeck Date: Wed, 20 Sep 2023 14:16:16 +0500 Subject: [PATCH 24/28] test --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4f7d5f1..4daf43b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,7 +9,7 @@ jobs: strategy: fail-fast: false matrix: - php: ['8.0'] + php: ['8.3'] stability: [ prefer-lowest, prefer-stable ] name: PHP ${{ matrix.php }} - ${{ matrix.stability }} tests From 77ab005d397430a323ffcc38d95949be531a271d Mon Sep 17 00:00:00 2001 From: impeck Date: Wed, 20 Sep 2023 14:21:47 +0500 Subject: [PATCH 25/28] test --- .github/workflows/tests.yml | 1 + tests/SafeMySQLTest.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4daf43b..86eaf21 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -31,6 +31,7 @@ jobs: with: mysql-version: "8.0" - run: mysql -uroot -e 'SELECT version()' + - run: mysql -uroot -e 'CREATE SCHEMA IF NOT EXISTS savemysql' # use PHP of specific version - uses: shivammathur/setup-php@v2 diff --git a/tests/SafeMySQLTest.php b/tests/SafeMySQLTest.php index d2b39db..def36f7 100644 --- a/tests/SafeMySQLTest.php +++ b/tests/SafeMySQLTest.php @@ -6,7 +6,7 @@ class SafeMySQLTest extends TestCase { protected static $opts = [ - 'host' => 'localhost', + 'host' => '127.0.0.1', 'user' => 'root', 'pass' => '', 'db' => 'savemysql' From 547f9c77b1674078b509e29a8fd4308a7c2a70e6 Mon Sep 17 00:00:00 2001 From: impeck Date: Wed, 20 Sep 2023 14:25:19 +0500 Subject: [PATCH 26/28] test --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 86eaf21..74437a5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,7 +9,7 @@ jobs: strategy: fail-fast: false matrix: - php: ['8.3'] + php: ['8.0', '8.1', '8.2', '8.3'] stability: [ prefer-lowest, prefer-stable ] name: PHP ${{ matrix.php }} - ${{ matrix.stability }} tests From 8fa6d9ac8ad2ab3ac6bed34b4998c6b1853589b6 Mon Sep 17 00:00:00 2001 From: impeck Date: Wed, 20 Sep 2023 14:36:02 +0500 Subject: [PATCH 27/28] test --- .github/workflows/tests.yml | 2 +- composer.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 74437a5..5f4175e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,7 +9,7 @@ jobs: strategy: fail-fast: false matrix: - php: ['8.0', '8.1', '8.2', '8.3'] + php: ['7.4', '8.0', '8.1', '8.2', '8.3'] stability: [ prefer-lowest, prefer-stable ] name: PHP ${{ matrix.php }} - ${{ matrix.stability }} tests diff --git a/composer.json b/composer.json index 1493c49..34920f6 100644 --- a/composer.json +++ b/composer.json @@ -32,7 +32,7 @@ "issues": "https://github.com/Impeck/safemysql/issues" }, "require": { - "php": "^8.0", + "php": "^7.4 || ^8.0", "ext-mysqli": "*" }, "require-dev": { From ae78449f538f1df6df6b932c8f97857497122ff3 Mon Sep 17 00:00:00 2001 From: impeck Date: Wed, 20 Sep 2023 14:37:43 +0500 Subject: [PATCH 28/28] test --- .github/workflows/tests.yml | 2 +- composer.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5f4175e..74437a5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,7 +9,7 @@ jobs: strategy: fail-fast: false matrix: - php: ['7.4', '8.0', '8.1', '8.2', '8.3'] + php: ['8.0', '8.1', '8.2', '8.3'] stability: [ prefer-lowest, prefer-stable ] name: PHP ${{ matrix.php }} - ${{ matrix.stability }} tests diff --git a/composer.json b/composer.json index 34920f6..1493c49 100644 --- a/composer.json +++ b/composer.json @@ -32,7 +32,7 @@ "issues": "https://github.com/Impeck/safemysql/issues" }, "require": { - "php": "^7.4 || ^8.0", + "php": "^8.0", "ext-mysqli": "*" }, "require-dev": {