<?php /** * TDbCommandBuilder class file. * * @author Wei Zhuo <weizhuo[at]gmail[dot]com> * @link http://www.pradosoft.com/ * @copyright Copyright © 2005-2014 PradoSoft * @license http://www.pradosoft.com/license/ * @package System.Data.Common */ /** * TDbCommandBuilder provides basic methods to create query commands for tables * giving by {@link setTableInfo TableInfo} the property. * * @author Wei Zhuo <weizho[at]gmail[dot]com> * @package System.Data.Common * @since 3.1 */ class TDbCommandBuilder extends TComponent { private $_connection; private $_tableInfo; /** * @param TDbConnection database connection. * @param TDbTableInfo table information. */ public function __construct($connection=null, $tableInfo=null) { $this->setDbConnection($connection); $this->setTableInfo($tableInfo); } /** * @return TDbConnection database connection. */ public function getDbConnection() { return $this->_connection; } /** * @param TDbConnection database connection. */ public function setDbConnection($value) { $this->_connection=$value; } /** * @param TDbTableInfo table information. */ public function setTableInfo($value) { $this->_tableInfo=$value; } /** * @param TDbTableInfo table information. */ public function getTableInfo() { return $this->_tableInfo; } /** * Iterate through all the columns and returns the last insert id of the * first column that has a sequence or serial. * @return mixed last insert id, null if none is found. */ public function getLastInsertID() { foreach($this->getTableInfo()->getColumns() as $column) { if($column->hasSequence()) return $this->getDbConnection()->getLastInsertID($column->getSequenceName()); } } /** * Alters the sql to apply $limit and $offset. Default implementation is applicable * for PostgreSQL, MySQL and SQLite. * @param string SQL query string. * @param integer maximum number of rows, -1 to ignore limit. * @param integer row offset, -1 to ignore offset. * @return string SQL with limit and offset. */ public function applyLimitOffset($sql, $limit=-1, $offset=-1) { $limit = $limit!==null ? (int)$limit : -1; $offset = $offset!==null ? (int)$offset : -1; $limitStr = $limit >= 0 ? ' LIMIT '.$limit : ''; $offsetStr = $offset >= 0 ? ' OFFSET '.$offset : ''; return $sql.$limitStr.$offsetStr; } /** * @param string SQL string without existing ordering. * @param array pairs of column names as key and direction as value. * @return string modified SQL applied with ORDER BY. */ public function applyOrdering($sql, $ordering) { $orders=array(); foreach($ordering as $name => $direction) { $direction = strtolower($direction) == 'desc' ? 'DESC' : 'ASC'; if(false !== strpos($name, '(') && false !== strpos($name, ')')) { // key is a function (bad practice, but we need to handle it) $key = $name; } else { // key is a column $key = $this->getTableInfo()->getColumn($name)->getColumnName(); } $orders[] = $key.' '.$direction; } if(count($orders) > 0) $sql .= ' ORDER BY '.implode(', ', $orders); return $sql; } /** * Computes the SQL condition for search a set of column using regular expression * (or LIKE, depending on database implementation) to match a string of * keywords (default matches all keywords). * @param array list of column id for potential search condition. * @param string string of keywords * @return string SQL search condition matching on a set of columns. */ public function getSearchExpression($fields, $keywords) { if(strlen(trim($keywords)) == 0) return ''; $words = preg_split('/\s/u', $keywords); $conditions = array(); foreach($fields as $field) { $column = $this->getTableInfo()->getColumn($field)->getColumnName(); $conditions[] = $this->getSearchCondition($column, $words); } return '('.implode(' OR ', $conditions).')'; } /** * @param string column name. * @param array keywords * @return string search condition for all words in one column. */ protected function getSearchCondition($column, $words) { $conditions=array(); foreach($words as $word) $conditions[] = $column.' LIKE '.$this->getDbConnection()->quoteString('%'.$word.'%'); return '('.implode(' AND ', $conditions).')'; } /** * * Different behavior depends on type of passed data * string * usage without modification * * null * will be expanded to full list of quoted table column names (quoting depends on database) * * array * - Column names will be quoted if used as key or value of array * <code> * array('col1', 'col2', 'col2') * // SELECT `col1`, `col2`, `col3` FROM... * </code> * * - Column aliasing * <code> * array('mycol1' => 'col1', 'mycol2' => 'COUNT(*)') * // SELECT `col1` AS mycol1, COUNT(*) AS mycol2 FROM... * </code> * * - NULL and scalar values (strings will be quoted depending on database) * <code> * array('col1' => 'my custom string', 'col2' => 1.0, 'col3' => 'NULL') * // SELECT "my custom string" AS `col1`, 1.0 AS `col2`, NULL AS `col3` FROM... * </code> * * - If the *-wildcard char is used as key or value, add the full list of quoted table column names * <code> * array('col1' => 'NULL', '*') * // SELECT `col1`, `col2`, `col3`, NULL AS `col1` FROM... * </code> * @param mixed $value * @return array of generated fields - use implode(', ', $selectfieldlist) to collapse field list for usage * @since 3.1.7 * @todo add support for table aliasing * @todo add support for quoting of column aliasing */ public function getSelectFieldList($data='*') { if(is_scalar($data)) { $tmp = explode(',', $data); $result = array(); foreach($tmp as $v) $result[] = trim($v); return $result; } $bHasWildcard = false; $result = array(); if(is_array($data) || $data instanceof Traversable) { $columns = $this->getTableInfo()->getColumns(); foreach($data as $key=>$value) { if($key==='*' || $value==='*') { $bHasWildcard = true; continue; } if(strToUpper($key)==='NULL') { $result[] = 'NULL'; continue; } if(strpos($key, '(')!==false && strpos($key, ')')!==false) { $result[] = $key; continue; } if(stripos($key, 'AS')!==false) { $result[] = $key; continue; } if(stripos($value, 'AS')!==false) { $result[] = $value; continue; } $v = isset($columns[$value]); $k = isset($columns[$key]); if(is_integer($key) && $v) { $key = $value; $k = $v; } if(strToUpper($value)==='NULL') { if($k) $result[] = 'NULL AS ' . $columns[$key]->getColumnName(); else $result[] = 'NULL' . (is_string($key) ? (' AS ' . (string)$key) : ''); continue; } if(strpos($value, '(')!==false && strpos($value, ')')!==false) { if($k) $result[] = $value . ' AS ' . $columns[$key]->getColumnName(); else $result[] = $value . (is_string($key) ? (' AS ' . (string)$key) : ''); continue; } if($v && $key==$value) { $result[] = $columns[$value]->getColumnName(); continue; } if($k && $value==null) { $result[] = $columns[$key]->getColumnName(); continue; } if(is_string($key) && $v) { $result[] = $columns[$value]->getColumnName() . ' AS ' . $key; continue; } if(is_numeric($value) && $k) { $result[] = $value . ' AS ' . $columns[$key]->getColumnName(); continue; } if(is_string($value) && $k) { $result[] = $this->getDbConnection()->quoteString($value) . ' AS ' . $columns[$key]->getColumnName(); continue; } if(!$v && !$k && is_integer($key)) { $result[] = is_numeric($value) ? $value : $this->getDbConnection()->quoteString((string)$value); continue; } $result[] = (is_numeric($value) ? $value : $this->getDbConnection()->quoteString((string)$value)) . ' AS ' . $key; } } if($data===null || count($result) == 0 || $bHasWildcard) $result = $result = array_merge($this->getTableInfo()->getColumnNames(), $result); return $result; } /** * Appends the $where condition to the string "SELECT * FROM tableName WHERE ". * The tableName is obtained from the {@link setTableInfo TableInfo} property. * @param string query condition * @param array condition parameters. * @return TDbCommand query command. */ public function createFindCommand($where='1=1', $parameters=array(), $ordering=array(), $limit=-1, $offset=-1, $select='*') { $table = $this->getTableInfo()->getTableFullName(); $fields = implode(', ', $this -> getSelectFieldList($select)); $sql = "SELECT {$fields} FROM {$table}"; if(!empty($where)) $sql .= " WHERE {$where}"; return $this->applyCriterias($sql, $parameters, $ordering, $limit, $offset); } public function applyCriterias($sql, $parameters=array(),$ordering=array(), $limit=-1, $offset=-1) { if(count($ordering) > 0) $sql = $this->applyOrdering($sql, $ordering); if($limit>=0 || $offset>=0) $sql = $this->applyLimitOffset($sql, $limit, $offset); $command = $this->createCommand($sql); $this->bindArrayValues($command, $parameters); return $command; } /** * Creates a count(*) command for the table described in {@link setTableInfo TableInfo}. * @param string count condition. * @param array binding parameters. * @return TDbCommand count command. */ public function createCountCommand($where='1=1', $parameters=array(),$ordering=array(), $limit=-1, $offset=-1) { return $this->createFindCommand($where, $parameters, $ordering, $limit, $offset, 'COUNT(*)'); } /** * Creates a delete command for the table described in {@link setTableInfo TableInfo}. * The conditions for delete is given by the $where argument and the parameters * for the condition is given by $parameters. * @param string delete condition. * @param array delete parameters. * @return TDbCommand delete command. */ public function createDeleteCommand($where,$parameters=array()) { $table = $this->getTableInfo()->getTableFullName(); if (!empty($where)) $where = ' WHERE '.$where; $command = $this->createCommand("DELETE FROM {$table}".$where); $this->bindArrayValues($command, $parameters); return $command; } /** * Creates an insert command for the table described in {@link setTableInfo TableInfo} for the given data. * Each array key in the $data array must correspond to the column name of the table * (if a column allows to be null, it may be omitted) to be inserted with * the corresponding array value. * @param array name-value pairs of new data to be inserted. * @return TDbCommand insert command */ public function createInsertCommand($data) { $table = $this->getTableInfo()->getTableFullName(); list($fields, $bindings) = $this->getInsertFieldBindings($data); $command = $this->createCommand("INSERT INTO {$table}({$fields}) VALUES ($bindings)"); $this->bindColumnValues($command, $data); return $command; } /** * Creates an update command for the table described in {@link setTableInfo TableInfo} for the given data. * Each array key in the $data array must correspond to the column name to be updated with the corresponding array value. * @param array name-value pairs of data to be updated. * @param string update condition. * @param array update parameters. * @return TDbCommand update command. */ public function createUpdateCommand($data, $where, $parameters=array()) { $table = $this->getTableInfo()->getTableFullName(); if($this->hasIntegerKey($parameters)) $fields = implode(', ', $this->getColumnBindings($data, true)); else $fields = implode(', ', $this->getColumnBindings($data)); if (!empty($where)) $where = ' WHERE '.$where; $command = $this->createCommand("UPDATE {$table} SET {$fields}".$where); $this->bindArrayValues($command, array_merge($data, $parameters)); return $command; } /** * Returns a list of insert field name and a list of binding names. * @param object array or object to be inserted. * @return array tuple ($fields, $bindings) */ protected function getInsertFieldBindings($values) { $fields = array(); $bindings=array(); foreach(array_keys($values) as $name) { $fields[] = $this->getTableInfo()->getColumn($name)->getColumnName(); $bindings[] = ':'.$name; } return array(implode(', ',$fields), implode(', ', $bindings)); } /** * Create a name-value or position-value if $position=true binding strings. * @param array data for binding. * @param boolean true to bind as position values. * @return string update column names with corresponding binding substrings. */ protected function getColumnBindings($values, $position=false) { $bindings=array(); foreach(array_keys($values) as $name) { $column = $this->getTableInfo()->getColumn($name)->getColumnName(); $bindings[] = $position ? $column.' = ?' : $column.' = :'.$name; } return $bindings; } /** * @param string SQL query string. * @return TDbCommand corresponding database command. */ public function createCommand($sql) { $this->getDbConnection()->setActive(true); return $this->getDbConnection()->createCommand($sql); } /** * Bind the name-value pairs of $values where the array keys correspond to column names. * @param TDbCommand database command. * @param array name-value pairs. */ public function bindColumnValues($command, $values) { foreach($values as $name=>$value) { $column = $this->getTableInfo()->getColumn($name); if($value === null && $column->getAllowNull()) $command->bindValue(':'.$name, null, PDO::PARAM_NULL); else $command->bindValue(':'.$name, $value, $column->getPdoType()); } } /** * @param TDbCommand database command * @param array values for binding. */ public function bindArrayValues($command, $values) { if($this->hasIntegerKey($values)) { $values = array_values($values); for($i = 0, $max=count($values); $i<$max; $i++) $command->bindValue($i+1, $values[$i], $this->getPdoType($values[$i])); } else { foreach($values as $name=>$value) { $prop = $name[0]===':' ? $name : ':'.$name; $command->bindValue($prop, $value, $this->getPdoType($value)); } } } /** * @param mixed PHP value * @return integer PDO parameter types. */ public static function getPdoType($value) { switch(gettype($value)) { case 'boolean': return PDO::PARAM_BOOL; case 'integer': return PDO::PARAM_INT; case 'string' : return PDO::PARAM_STR; case 'NULL' : return PDO::PARAM_NULL; } } /** * @param array * @return boolean true if any array key is an integer. */ protected function hasIntegerKey($array) { foreach($array as $k=>$v) { if(gettype($k)==='integer') return true; } return false; } }