. * * @author TechFuze * @copyright Copyright (c) 2013 - 2016, Techfuze. (http://techfuze.net) * @copyright Copyright (c) 1996 - 2015, Free Software Foundation, Inc. (http://www.fsf.org/) * @license http://opensource.org/licenses/GPL-3.0 GPLv3 License * * @link http://fuzeworks.techfuze.net * @since Version 0.0.1 * * @version Version 0.0.1 */ namespace Module\DatabaseUtils; use FuzeWorks\Modules; use FuzeWorks\DatabaseException; use FuzeWorks\Config; use FuzeWorks\Logger; /** * Class Query. * * @author GOScripting * @copyright Copyright (c) 2014 - 2015, GOScripting B.V. (http://goscripting.com) * @license http://opensource.org/licenses/GPL-3.0 GPLv3 License * * @link http://goframework.net * * @method $this or() or(string $field, string $arg2) OR $arg2 is the value of the field, or an operator in which case the value is pushed to the third argument * @method $this and() and(string $field, string $arg2) AND $arg2 is the value of the field, or an operator in which case the value is pushed to the third argument */ class Query { /** * @var array An array containing all the counted functions */ private $functions = array(); /** * @var null|string The table used */ private $table = ''; /** * @var null|\PDOStatement */ private $sth = null; /** * @param string $table The string to set as a table, optional */ public function __construct($table = '') { if ($table != '') { $this->setTable($table); } } /** * Each argument is another field to select. * * @param string $field,... The field to select, if no field is given, all fields(*) will be selected * * @return $this */ public function select($field = '*') { return $this->__call(__FUNCTION__, func_get_args()); } private function internalSelect($field = '*') { return array( 'sql' => 'SELECT '.($field == '*' ? '*' : implode(', ', func_get_args()).''), 'binds' => null, ); } /** * From. * * @param $table,... null|string The table, if it is null, the table set with setTable() will be used * * @return $this * * @throws Exception */ public function from($table = null) { return $this->__call(__FUNCTION__, func_get_args()); } private function internalFrom($table = null) { $tables = func_get_args(); $query = 'FROM '; if (count($tables) == 0) { $tables[] = "$this->table"; } else { foreach ($tables as $i => $t) { if ($t == '' && $this->table == '') { throw new Exception('No table given'); } $tables[$i] = strpos($t, ' ') === false ? "$t" : ''.implode(' ', explode(' ', $t)); } } $query .= implode(', ', $tables); return array( 'sql' => $query, 'binds' => null, ); } /** * (Inner) join. * * @param string $table The table to join * @param string $type The type of join to perform (JOIN, LEFT JOIN, RIGHT JOIN, FULL JOIN) * * @return $this * * @throws Exception */ public function join($table, $type = '') { return $this->__call(__FUNCTION__, func_get_args()); } private function internalJoin($table, $type = '') { if ($table === '') { if ($this->table == '') { throw new Exception('No table given'); } $table = $this->table; } $query = ''; if ($type != '') { $query .= $type.' '; } $query .= 'JOIN '.(strpos($table, ' ') === false ? "$table" : ''.implode(' ', explode(' ', $table))); return array( 'sql' => $query, 'binds' => null, ); } /** * Left join. * * @param string $table The table to join * * @return $this * * @throws Exception */ public function left_join($table) { return $this->join($table, 'LEFT'); } /** * Right join. * * @param string $table The table to join * * @return $this * * @throws Exception */ public function right_join($table) { return $this->join($table, 'RIGHT'); } /** * Full join. * * @param string $table The table to join * * @return $this * * @throws Exception */ public function full_join($table) { return $this->join($table, 'FULL'); } /** * On. * * @param string $field Field name, or raw SQL * @param $arg2 string, The value of the field, or an operator in which case the value is pushed to $arg3 * @param null|string $arg3 The value of the field when $arg2 is used for an operator * * @return $this */ public function on($field, $arg2 = null, $arg3 = null) { return $this->where($field, $arg2, $arg3, 'ON'); } /** * Where. * * @param string|null $field Field name, or raw SQL, If this is left empty, only the $type will be added to the query * @param string $arg2, The value of the field, or an operator in which case the value is pushed to $arg3 * @param null|string $arg3 The value of the field when $arg2 is used for an operator * @param string $type Whether this is an WHERE or ON operation * * @return $this */ public function where($field = null, $arg2 = null, $arg3 = null, $type = 'WHERE') { return $this->__call(__FUNCTION__, func_get_args()); } private function internalWhere($field = null, $arg2 = null, $arg3 = null, $type = 'WHERE') { if ($field == null) { return array('sql' => $type, 'binds' => array()); } if ($arg2 === null) { return $this->internalSql($type.' '.$field); } //The value is the second parameter, unless the second parameter is an operator, in which case it's the third $value = ($arg3 == null ? $arg2 : $arg3); //If the third parameter is not given, the default operator should be used $operator = strtoupper($arg3 == null ? '=' : $arg2); $query = ''; $binds = array(); switch ($operator) { case 'IN': $query .= $type.($type == '' ? '' : ' ').$field.' IN ('; if (is_array($value)) { foreach ($value as $k => $v) { $query .= '?,'; $binds[] = $v; } //Remove the trailing comma and close it $query = rtrim($query, ',').')'; } elseif (get_class($value) == 'System\Core\Query') { /* @var Query $value */ $data = $this->internalSql($value->getSql(), $value->getBinds()); $query .= $data['sql'].')'; $binds = array_merge($binds, $data['binds']); } break; case 'BETWEEN': $query .= $type.($type == '' ? '' : ' ').$field.' BETWEEN ? AND ?'; $binds[] = $value[0]; $binds[] = $value[1]; break; default: $query .= $type.($type == '' ? '' : ' ').$field.' '.$operator.' ?'; $binds[] = $value; break; } return array( 'sql' => $query, 'binds' => $binds, ); } /** *And, keep in mind this is only the OR statement. For A = B call ->where. * * @return array */ private function internalOr() { return array( 'sql' => 'OR', 'binds' => array(), ); } /** * And, keep in mind this is only the AND statement. For A = B call ->where. * * @return array */ private function internalAnd() { return array( 'sql' => 'AND', 'binds' => array(), ); } /** * An opening bracket. * * @return $this */ public function open() { return $this->__call(__FUNCTION__, func_get_args()); } private function internalOpen() { return array( 'sql' => '(', 'binds' => '', ); } /** * A closing bracket. * * @return $this */ public function close() { return $this->__call(__FUNCTION__, func_get_args()); } private function internalClose() { return array( 'sql' => ')', 'binds' => null, ); } /** * Order By. * * Each argument is another order. If you put a minus in front of the name, the order will be DESC instead of ASC * * @param string $field,... The field to order by, add a minus in front of the name and the order will be DESC * * @return $this */ public function order($field) { return $this->__call(__FUNCTION__, func_get_args()); } private function internalOrder($field) { $query = 'ORDER BY'; foreach (func_get_args() as $field) { if (substr($query, -2) != 'BY') { $query .= ','; } $mode = 'ASC'; if (substr($field, 0, 1) == '-') { $field = substr($field, 1); $mode = 'DESC'; } $query .= ' '.$field.' '.$mode; } return array( 'sql' => $query, 'binds' => null, ); } /** * Group by. * * Each argument is another field to group by. * * @param string $field,... The field to group by * * @return $this */ public function groupBy($field) { return $this->__call(__FUNCTION__, func_get_args()); } private function internalGroupBy($field) { $query = 'GROUP BY '; $first = true; foreach (func_get_args() as $field) { if (!$first) { $query .= ', '; } $first = false; $query .= $field; } return array( 'sql' => $query, 'binds' => null, ); } /** * limit. * * @param $limit int Limit * @param int $offset int Offset * * @return $this */ public function limit($limit, $offset = 0) { return $this->__call(__FUNCTION__, func_get_args()); } private function internalLimit($limit, $offset = 0) { return array( 'sql' => 'LIMIT '.$offset.', '.$limit, 'binds' => null, ); } /** * Having. * * @param $field * @param $arg2 string, The value of the field, or an operator in which case the value is pushed to $arg3 * @param null|string $arg3 The value of the field when $arg2 is used for an operator * * @return $this */ public function having($field = null, $arg2 = null, $arg3 = null) { return $this->where($field, $arg2, $arg3, 'HAVING'); } /** * Update. * * @param $table null|string Name of the table, if it is null, the table set with setTable() will be used * * @return $this * * @throws Exception */ public function update($table = '') { return $this->__call(__FUNCTION__, func_get_args()); } private function internalUpdate($table = '') { if ($table === '') { if ($this->table == '') { throw new Exception('No table given'); } $table = $this->table; } return array( 'sql' => 'UPDATE '.$table, 'binds' => array(), ); } /** * Set. * * @param $data array|string Key value, $field => $value or raw SQL * * @return $this */ public function set($data) { return $this->__call(__FUNCTION__, func_get_args()); } private function internalSet($data) { if (is_string($data)) { return $this->internalSql('SET '.$data); } $query = 'SET'; $binds = array(); $first = true; foreach ($data as $field => $value) { if (!$first) { $query .= ','; } $first = false; $query .= ' '.$field.'=?'; $binds[] = $value; } return array( 'sql' => $query, 'binds' => $binds, ); } /** * Delete. * * @return $this */ public function delete() { return $this->__call(__FUNCTION__, func_get_args()); } private function internalDelete() { return array( 'sql' => 'DELETE', 'binds' => array(), ); } /** * Insert. * * @param array array Key value, $field => $value * @param $table string|null Table name, if it is null, the table set with setTable() will be used * * @return $this * * @throws Exception */ public function insert($array, $table = '') { return $this->__call(__FUNCTION__, func_get_args()); } private function internalInsert($array, $table = '') { if ($table === '') { if ($this->table == '') { throw new DatabaseException('No table given'); } $table = $this->table; } $query = ''; $binds = array(); //Implode the array to get a list with the fields $query .= 'INSERT INTO '.$table.' ('.implode(',', array_keys($array)).') VALUES ('; //Add all the values as ? and add them to the binds foreach ($array as $field => $value) { $query .= '?,'; $binds[] = $value; } $query = rtrim($query, ',').')'; return array( 'sql' => $query, 'binds' => $binds, ); } /** * Replace. * * @param array array Key value, $field => $value * @param $table string|null Table name, if it is null, the table set with setTable() will be used * * @return $this * * @throws Exception */ public function replace($array, $table = '') { return $this->__call(__FUNCTION__, func_get_args()); } private function internalReplace($array, $table = '') { if ($table === '') { if ($this->table == '') { throw new DatabaseException('No table given'); } $table = $this->table; } $query = ''; $binds = array(); //Implode the array to get a list with the fields $query .= 'REPLACE INTO '.$table.' ('.implode(',', array_keys($array)).') VALUES ('; //Add all the values as ? and add them to the binds foreach ($array as $field => $value) { $query .= '?,'; $binds[] = $value; } $query = rtrim($query, ',').')'; return array( 'sql' => $query, 'binds' => $binds, ); } /** * Add raw SQL to the query string. * * @param string $sql The SQL to add * @param array $binds The optional binds to add * * @return $this */ public function sql($sql, $binds = array()) { return $this->__call(__FUNCTION__, func_get_args()); } private function internalSql($sql, $binds = array()) { return array( 'sql' => $sql, 'binds' => $binds, ); } /** * Builds The SQL query and its binds. * * @return array */ public function build() { $sql = array(); $binds = array(); foreach ($this->functions as $i => $function) { //The current function is a WHERE or HAVING, //and it after a or, and or open, this means we do not have to //add the WHERE/HAVING statement again if (($function['name'] == 'internalWhere' || $function['name'] == 'internalHaving') && ($this->functions[$i - 1]['name'] == 'internalOpen' || $this->functions[$i - 1]['name'] == 'internalAnd' || $this->functions[$i - 1]['name'] == 'internalOr') ) { if (!isset($function['arguments'][2])) { $function['arguments'][2] = null; } $function['arguments'][3] = ''; } $data = call_user_func_array(array($this, $function['name']), $function['arguments']); $sql[] = $data['sql']; if ($data['binds'] != null) { $binds = array_merge($binds, $data['binds']); } } return array( 'sql' => implode(' ', $sql), 'binds' => $binds, ); } /** * Returns the query as an array. * * @return array */ public function getSql() { return $this->build()['sql']; } /** * Return the built SQL query. * * @return string */ public function getBinds() { return $this->build()['binds']; } /** * @param $table * * @return $this */ public function setTable($table) { $this->table = $table; return $this; } /** * @return null|string */ public function getTable() { return $this->table; } /** * @return $this */ public function commit() { Modules::get('core/database')->commit(); return $this; } /** * @return $this */ public function beginTransaction() { Modules::get('core/database')->beginTransaction(); return $this; } /** * @return $this */ public function rollback() { Modules::get('core/database')->rollback(); return $this; } /** * Executes the query generated. * * @return $this * * @throws DatabaseException */ public function execute() { if (Config::get('database')->debug) { Logger::log(get_class($this).': '.$this->getSql()); } try { $this->sth = Modules::get('core/database')->prepare($this->getSql()); if (count($this->getBinds()) === 0) { $this->sth->execute(); } else { $this->sth->execute($this->getBinds()); } } catch (\PDOException $e) { throw new DatabaseException('Could not execute SQL-query due PDO-exception '.$e->getMessage()); } return $this; } /** * Returns the results of the query in the given type. All the arguments of this function will be passed onto * fetchAll. * * @param int $type The default type is \PDO::FETCH_ASSOC, all the types that are possible for fetchAll() are valid * * @return array * * @throws DatabaseException */ public function getResults($type = \PDO::FETCH_ASSOC) { if (!isset($this->sth)) { $this->execute(); } return call_user_func_array( array( $this->sth, 'fetchAll', ), func_get_args() ); } /** * Returns the amount of rows that are affected. ->execute must be called first. * * @return int Amount of rows that are affected * * @throws DatabaseException */ public function getRowCount() { if (!isset($this->sth)) { $this->execute(); } return $this->sth->rowCount(); } /** * @return string */ public function getLastInsertId() { return Modules::get('core/database')->lastInsertId(); } /** * All the called functions are saved in an array, so when build is called, the query is built from all the functions. * * @param $name string * @param $arguments array * * @return $this * * @throws DatabaseException */ public function __call($name, $arguments) { $name = 'internal'.ucfirst($name); if (!method_exists($this, $name)) { throw new DatabaseException('Unknown function "'.$name.'"'); } $this->functions[] = array( 'name' => $name, 'arguments' => $arguments, ); return $this; } }