diff --git a/MysqliDb.php b/MysqliDb.php index 32d794bd..fc188bd5 100644 --- a/MysqliDb.php +++ b/MysqliDb.php @@ -24,7 +24,7 @@ class MysqliDb * * @var string */ - protected $_prefix; + protected static $_prefix; /** * MySQLi instance * @@ -93,6 +93,12 @@ class MysqliDb protected $db; protected $port; + /** + * Is Subquery object + * + */ + protected $isSubQuery = false; + /** * @param string $host * @param string $username @@ -100,7 +106,7 @@ class MysqliDb * @param string $db * @param int $port */ - public function __construct($host, $username, $password, $db, $port = NULL) + public function __construct($host = NULL, $username = NULL, $password = NULL, $db = NULL, $port = NULL) { $this->host = $host; $this->username = $username; @@ -110,7 +116,13 @@ public function __construct($host, $username, $password, $db, $port = NULL) $this->port = ini_get ('mysqli.default_port'); else $this->port = $port; - + + if ($host == null && $username == null && $db == null) { + $this->isSubQuery = true; + return; + } + + // for subqueries we do not need database connection and redefine root instance $this->connect(); $this->setPrefix(); self::$_instance = $this; @@ -122,6 +134,9 @@ public function __construct($host, $username, $password, $db, $port = NULL) */ public function connect() { + if ($this->isSubQuery) + return; + $this->_mysqli = new mysqli ($this->host, $this->username, $this->password, $this->db, $this->port) or die('There was a problem connecting to the database'); @@ -164,7 +179,8 @@ protected function reset() */ public function setPrefix($prefix = '') { - $this->_prefix = $prefix; + self::$_prefix = $prefix; + return $this; } /** @@ -177,7 +193,7 @@ public function setPrefix($prefix = '') */ public function rawQuery($query, $bindParams = null) { - $this->_query = filter_var ($query, FILTER_SANITIZE_MAGIC_QUOTES, + $this->_query = filter_var ($query, FILTER_SANITIZE_STRING, FILTER_FLAG_NO_ENCODE_QUOTES); $stmt = $this->_prepareQuery(); @@ -231,8 +247,12 @@ public function get($tableName, $numRows = null, $columns = '*') $columns = '*'; $column = is_array($columns) ? implode(', ', $columns) : $columns; - $this->_query = "SELECT $column FROM $this->_prefix$tableName"; + $this->_query = "SELECT $column FROM " .self::$_prefix . $tableName; $stmt = $this->_buildQuery($numRows); + + if ($this->isSubQuery) + return $this; + $stmt->execute(); $this->_stmtError = $stmt->error; $this->reset(); @@ -250,6 +270,10 @@ public function get($tableName, $numRows = null, $columns = '*') public function getOne($tableName, $columns = '*') { $res = $this->get ($tableName, 1, $columns); + + if (is_object($res)) + return $res; + if (isset($res[0])) return $res[0]; @@ -265,7 +289,10 @@ public function getOne($tableName, $columns = '*') */ public function insert($tableName, $insertData) { - $this->_query = "INSERT into $this->_prefix$tableName"; + if ($this->isSubQuery) + return; + + $this->_query = "INSERT into " .self::$_prefix . $tableName; $stmt = $this->_buildQuery(null, $insertData); $stmt->execute(); $this->_stmtError = $stmt->error; @@ -284,7 +311,10 @@ public function insert($tableName, $insertData) */ public function update($tableName, $tableData) { - $this->_query = "UPDATE $this->_prefix$tableName SET "; + if ($this->isSubQuery) + return; + + $this->_query = "UPDATE " . self::$_prefix . $tableName ." SET "; $stmt = $this->_buildQuery(null, $tableData); $stmt->execute(); @@ -304,7 +334,10 @@ public function update($tableName, $tableData) */ public function delete($tableName, $numRows = null) { - $this->_query = "DELETE FROM $this->_prefix$tableName"; + if ($this->isSubQuery) + return; + + $this->_query = "DELETE FROM " . self::$_prefix . $tableName; $stmt = $this->_buildQuery($numRows); $stmt->execute(); @@ -324,9 +357,12 @@ public function delete($tableName, $numRows = null) * * @return MysqliDb */ - public function where($whereProp, $whereValue = null) + public function where($whereProp, $whereValue = null, $operator = null) { - $this->_where[$whereProp] = Array ("AND", $whereValue); + if ($operator) + $whereValue = Array ($operator => $whereValue); + + $this->_where[] = Array ("AND", $whereValue, $whereProp); return $this; } @@ -340,9 +376,12 @@ public function where($whereProp, $whereValue = null) * * @return MysqliDb */ - public function orWhere($whereProp, $whereValue = null) + public function orWhere($whereProp, $whereValue = null, $operator = null) { - $this->_where[$whereProp] = Array ("OR", $whereValue); + if ($operator) + $whereValue = Array ($operator => $whereValue); + + $this->_where[] = Array ("OR", $whereValue, $whereProp); return $this; } /** @@ -365,7 +404,7 @@ public function join($joinTable, $joinCondition, $joinType = '') if ($joinType && !in_array ($joinType, $allowedTypes)) die ('Wrong JOIN type: '.$joinType); - $this->_join[$joinType . " JOIN " . $this->_prefix.$joinTable] = $joinCondition; + $this->_join[$joinType . " JOIN " . self::$_prefix . $joinTable] = $joinCondition; return $this; } @@ -488,6 +527,21 @@ protected function _bindParam($value) array_push ($this->_bindParams, $value); } + protected function _buildPair ($operator, $value) { + if (!is_object($value)) { + $this->_bindParam ($value); + $comparison = ' ' . $operator. ' ? '; + return $comparison; + } + + $subQuery = $value->getSubQuery(); + $comparison = " " . $operator . " (" . $subQuery['query'] . ")"; + foreach ($subQuery['params'] as $v) + $this->_bindParam ($v); + + return $comparison; + } + /** * Abstraction method that will compile the WHERE statement, * any passed update data, and the desired rows. @@ -525,7 +579,9 @@ protected function _buildQuery($numRows = null, $tableData = null) if ($isUpdate !== false) $this->_query .= "`" . $column . "` = "; - if (!is_array ($value)) { + if (is_object ($value)) { + $this->_query .= $this->_buildPair ("", $value) . ", "; + } else if (!is_array ($value)) { $this->_bindParam ($value); $this->_query .= '?, '; } else { @@ -562,50 +618,50 @@ protected function _buildQuery($numRows = null, $tableData = null) if ($hasConditional) { //Prepair the where portion of the query $this->_query .= ' WHERE '; - foreach ($this->_where as $column => $value) { - //value[0] -- AND/OR, value[1] -- condition array + $i = 0; + foreach ($this->_where as list($concat, $wValue, $wKey)) { // if its not a first condition insert its concatenator (AND or OR) - if (array_search ($column, array_keys ($this->_where)) != 0) - $this->_query .= ' ' . $value[0]. ' '; + if ($i++ != 0) + $this->_query .= " $concat "; + $this->_query .= $wKey; - if (is_array ($value[1])) { - //value[0] -- AND/OR, value[1] -- condition array + if (is_array ($wValue)) { // if the value is an array, then this isn't a basic = comparison - $key = key($value[1]); - $val = $value[1][$key]; + $key = key($wValue); + $val = $wValue[$key]; switch( strtolower($key) ) { case '0': - $comparison = ''; - foreach ($value[1] as $v) + foreach ($wValue as $v) $this->_bindParam ($v); break; case 'not in': case 'in': $comparison = ' ' . $key . ' ('; - foreach($val as $v){ - $comparison .= ' ?,'; - $this->_bindParam ($v); + if (is_object ($val)) { + $comparison .= $this->_buildPair ("", $val); + } else { + foreach ($val as $v) { + $comparison .= ' ?,'; + $this->_bindParam ($v); + } } - $comparison = rtrim($comparison, ',').' ) '; + $this->_query .= rtrim($comparison, ',').' ) '; break; case 'not between': case 'between': - $comparison = ' ' . $key . ' ? AND ? '; + $this->_query .= " $key ? AND ? "; $this->_bindParam ($val[0]); $this->_bindParam ($val[1]); break; default: // We are using a comparison operator with only one parameter after it - $comparison = ' '.$key.' ? '; - $this->_bindParam ($val); + $this->_query .= $this->_buildPair ($key, $val); } - } else if ($value[1] === null) { - $comparison = ''; + } else if ($wValue === null) { + // } else { - $comparison = ' = ? '; - $this->_bindParam ($value[1]); + $this->_query .= $this->_buildPair ("=", $wValue); } - $this->_query .= $column.$comparison; } } @@ -637,6 +693,11 @@ protected function _buildQuery($numRows = null, $tableData = null) $this->_query .= ' LIMIT ' . (int)$numRows; } + $this->_lastQuery = $this->replacePlaceHolders($this->_query, $this->_bindParams); + + if ($this->isSubQuery) + return; + // Prepare query $stmt = $this->_prepareQuery(); @@ -644,7 +705,6 @@ protected function _buildQuery($numRows = null, $tableData = null) if (count ($this->_bindParams) > 1) call_user_func_array(array($stmt, 'bind_param'), $this->refValues($this->_bindParams)); - $this->_lastQuery = $this->replacePlaceHolders($this->_query, $this->_bindParams); return $stmt; } @@ -708,7 +768,10 @@ protected function _prepareQuery() */ public function __destruct() { - $this->_mysqli->close(); + if (!$this->isSubQuery) + return; + if ($this->_mysqli) + $this->_mysqli->close(); } /** @@ -741,7 +804,10 @@ protected function replacePlaceHolders ($str, $vals) { $newStr = ""; while ($pos = strpos ($str, "?")) { - $newStr .= substr ($str, 0, $pos) . $vals[$i++]; + $val = $vals[$i++]; + if (is_object ($val)) + $val = '[object]'; + $newStr .= substr ($str, 0, $pos) . $val; $str = substr ($str, $pos + 1); } return $newStr; @@ -765,6 +831,24 @@ public function getLastError () { return $this->_stmtError . " " . $this->_mysqli->error; } + /** + * Mostly internal method to get query and its params out of subquery object + * after get() and getAll() + * + * @return array + */ + public function getSubQuery () { + if (!$this->isSubQuery) + return null; + + array_shift ($this->_bindParams); + $val = Array ('query' => $this->_query, + 'params' => $this->_bindParams + ); + $this->reset(); + return $val; + } + /* Helper functions */ /** * Method returns generated interval function as a string @@ -841,4 +925,69 @@ public function func ($expr, $bindParams = null) { return Array ("[F]" => Array($expr, $bindParams)); } + /** + * Method creates new mysqlidb object for a subquery generation + */ + public static function subQuery() + { + return new MysqliDb(); + } + + /** + * Method returns a copy of a mysqlidb subquery object + * + * @param object new mysqlidb object + */ + public function copy () + { + return clone $this; + } + + /** + * Begin a transaction + * + * @uses mysqli->autocommit(false) + * @uses register_shutdown_function(array($this, "_transaction_shutdown_check")) + */ + public function startTransaction () { + $this->_mysqli->autocommit (false); + $this->_transaction_in_progress = true; + register_shutdown_function (array ($this, "_transaction_status_check")); + } + + /** + * Transaction commit + * + * @uses mysqli->commit(); + * @uses mysqli->autocommit(true); + */ + public function commit () { + $this->_mysqli->commit (); + $this->_transaction_in_progress = false; + $this->_mysqli->autocommit (true); + } + + /** + * Transaction rollback function + * + * @uses mysqli->rollback(); + * @uses mysqli->autocommit(true); + */ + public function rollback () { + $this->_mysqli->rollback (); + $this->_transaction_in_progress = false; + $this->_mysqli->autocommit (true); + } + + /** + * Shutdown handler to rollback uncommited operations in order to keep + * atomic operations sane. + * + * @uses mysqli->rollback(); + */ + public function _transaction_status_check () { + if (!$this->_transaction_in_progress) + return; + $this->rollback (); + } } // END class diff --git a/readme.md b/readme.md index 96558337..64749db8 100644 --- a/readme.md +++ b/readme.md @@ -10,7 +10,7 @@ After that, create a new instance of the class. $db = new Mysqlidb('host', 'username', 'password', 'databaseName'); ``` -It's also possible to set a table prefix: +Its also possible to set a table prefix: ```php $db->setPrefix('tablePrefix'); ``` @@ -133,38 +133,42 @@ $results = $db->get('users'); ``` ```php -$db->where('id', Array('>=' => 50)); +$db->where('id', 50, ">="); +// or $db->where('id', Array('>=' => 50)); + $results = $db->get('users'); // Gives: SELECT * FROM users WHERE id >= 50; ``` -BETWEEN: +BETWEEN / NOT BETWEEN: ```php -$db->where('id', Array('between' => Array(4, 20) ) ); -//$db->where('id', Array('not between' => Array(4, 20) ) ); +$db->where('id', Array(4, 20), 'between'); +// or $db->where('id', Array('between' => Array(4, 20) ) ); + $results = $db->get('users'); // Gives: SELECT * FROM users WHERE id BETWEEN 4 AND 20 ``` -IN: +IN / NOT IN: ```php -$db->where('id', Array( 'in' => Array(1, 5, 27, -1, 'd') ) ); -//$db->where('id', Array( 'not in' => Array(1, 5, 27, -1, 'd') ) ); +$db->where('id', Array(1, 5, 27, -1, 'd'), 'IN'); +// or $db->where('id', Array( 'in' => Array(1, 5, 27, -1, 'd') ) ); + $results = $db->get('users'); // Gives: SELECT * FROM users WHERE id IN (1, 5, 27, -1, 'd'); ``` OR CASE ```php -$db->where('firstName','John'); -$db->orWhere('firstName','Peter'); -$results = $db->get('users'); +$db->where ('firstName', 'John'); +$db->orWhere ('firstName', 'Peter'); +$results = $db->get ('users'); // Gives: SELECT * FROM users WHERE firstName='John' OR firstName='peter' ``` NULL comparison: ```php -$db->where ("lastName", Array("<=>" => NULL)); +$db->where ("lastName", NULL, '<=>'); $results = $db->get("users"); // Gives: SELECT * FROM users where lastName <=> NULL ``` @@ -202,8 +206,8 @@ $results = $db->get('users'); ### Grouping method ```php -$db->groupBy("name"); -$results = $db->get('users'); +$db->groupBy ("name"); +$results = $db->get ('users'); // Gives: SELECT * FROM users GROUP BY name; ``` @@ -216,6 +220,46 @@ $products = $db->get ("products p", null, "u.name, p.productName"); print_r ($products); ``` +### Properties sharing +Its is also possible to copy properties +```php +$db->where ("agentId", 10); + +$customers = $db->copy (); +$res = $customers->get ("customers"); +// SELECT * FROM customers where agentId = 10 + +$db->orWhere ("agentId", 20); +$res = $db->get ("users"); +// SELECT * FROM users where agentId = 10 or agentId = 20 +``` + +### Subqueries +Subquery in selects: +```php +$ids = $db->subQuery (); +$ids->where ("qty", 2, ">"); +$ids->get ("products", null, "userId"); + +$db->where ("id", $ids, 'in'); +$res = $db->get ("users"); +// Gives SELECT * FROM users WHERE id IN (SELECT userId FROM products WHERE qty > 2) +``` + +Subquery in inserts: +```php +$userIdQ = $db->subQuery (); +$userIdQ->where ("id", 6); +$userIdQ->getOne ("users", "name"), + +$data = Array ( + "productName" => "test product", + "userId" => $userIdQ, + "lastUpdated" => $db->now() +); +$id = $db->insert ("products", $data); +// Gives INSERT INTO PRODUCTS (productName, userId, lastUpdated) values ("test product", (SELECT name FROM users WHERE id = 6), NOW()); +``` ### Helper commands Reconnect in case mysql connection died ```php @@ -227,3 +271,25 @@ Obtain an initialized instance of the class from another class ```php $db = MysqliDb::getInstance(); ``` + +Get last executed SQL query. +Please note that function returns SQL query only for debugging purposes as its execution most likely will fail due missing quotes around char variables. +```php + $db->get('users'); + echo "Last executed query was ". $db->getLastQuery(); +``` + +### Transaction helpers +Please keep in mind that transactions are working on innoDB tables. +Rollback transaction if insert fails: +```php +$db->startTransaction(); +... +if (!$db->insert ('myTable', $insertData)) { + //Error while saving, cancel new record + $db->rollback(); +} else { + //OK + $db->commit(); +} +``` diff --git a/tests.php b/tests.php index 2eb65255..f6590186 100644 --- a/tests.php +++ b/tests.php @@ -94,7 +94,7 @@ function createTable ($name, $data) { } foreach ($tables as $name => $fields) { - $db->rawQuery("DROP TABLE $name"); + $db->rawQuery("DROP TABLE ".$prefix.$name); createTable ($prefix.$name, $fields); } @@ -138,7 +138,7 @@ function createTable ($name, $data) { //$users = $db->get("users"); //print_r ($users); -$db->where("firstname", Array("LIKE" => '%John%')); +$db->where("firstname", '%John%', 'LIKE'); $users = $db->get("users"); if ($db->count != 1) { echo "Invalid insert count in LIKE: ".$db->count; @@ -172,14 +172,14 @@ function createTable ($name, $data) { exit; } -$db->where ("id", Array('in' => Array('1','2','3'))); +$db->where ("id", Array('1','2','3'), 'IN'); $db->get("users"); if ($db->count != 3) { echo "Invalid users count on where() with in "; exit; } -$db->where ("id", Array('between' => Array('2','3'))); +$db->where ("id", Array('2','3'), 'between'); $db->get("users"); if ($db->count != 2) { echo "Invalid users count on where() with between"; @@ -194,7 +194,7 @@ function createTable ($name, $data) { exit; } -$db->where ("lastName", Array("<=>" => NULL)); +$db->where ("lastName", NULL, '<=>'); $r = $db->get("users"); if ($db->count != 1) { echo "Invalid users count on null where()"; @@ -217,6 +217,25 @@ function createTable ($name, $data) { exit; } +$db->where("id = 1 or id = 2"); +$res = $db->get ("users"); +if ($db->count != 2) { + echo "Invalid users count on select with multiple params"; + exit; +} + +$usersQ = $db->subQuery(); +$usersQ->where ("login", "user2"); +$usersQ->getOne ("users", "id"); + +$db2 = $db->copy(); +$db2->where ("userId", $usersQ); +$res = $db2->getOne ("products", "count(id) as cnt"); +if ($res['cnt'] != 2) { + echo "Invalid select result with subquery"; + exit; +} +//TODO: insert test $db->delete("users"); $db->get("users"); if ($db->count != 0) {