* @link http://www.yiiframework.com/ * @copyright Copyright © 2008-2012 Yii Software LLC * @license http://www.yiiframework.com/license/ */ namespace yii\db\dao; /** * QueryBuilder builds a SQL statement based on the specification given as a [[Query]] object. * * @author Qiang Xue * @since 2.0 */ class QueryBuilder extends \yii\base\Component { private $_connection; public function __construct(Connection $connection) { $this->_connection = $connection; } /** * @return CDbConnection the connection associated with this command */ public function getConnection() { return $this->_connection; } public function build($query) { $clauses = array( $this->buildSelect($query->select, $query->distinct), $this->buildFrom($query->from), $this->buildJoin($query->join), $this->buildWhere($query->where), $this->buildGroupBy($query->groupBy), $this->buildHaving($query->having), $this->buildOrderBy($query->orderBy), $this->buildLimit($query->offset, $query->limit), $this->buildUnion($query->union), ); return implode("\n", array_filter($clauses)); } protected function buildSelect($columns, $distinct) { $select = $distinct ? 'SELECT DISTINCT' : 'SELECT'; if (empty($columns)) { return $select . ' *'; } if (is_string($columns)) { if (strpos($columns, '(') !== false) { return $select . ' ' . $columns; } $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); } foreach ($columns as $i => $column) { if (is_object($column)) { $columns[$i] = (string)$column; } elseif (strpos($column, '(') === false) { if (preg_match('/^(.*?)(?i:\s+as\s+|\s+)(.*)$/', $column, $matches)) { $columns[$i] = $this->_connection->quoteColumnName($matches[1]) . ' AS ' . $this->_connection->quoteColumnName($matches[2]); } else { $columns[$i] = $this->_connection->quoteColumnName($column); } } } return $select . ' ' . implode(', ', $columns); } protected function buildFrom($tables) { if (is_string($tables) && strpos($tables, '(') !== false) { return $tables; } if (!is_array($tables)) { $tables = preg_split('/\s*,\s*/', trim($tables), -1, PREG_SPLIT_NO_EMPTY); } foreach ($tables as $i => $table) { if (strpos($table, '(') === false) { if (preg_match('/^(.*?)(?i:\s+as\s+|\s+)(.*)$/', $table, $matches)) { // with alias $tables[$i] = $this->_connection->quoteTableName($matches[1]) . ' ' . $this->_connection->quoteTableName($matches[2]); } else { $tables[$i] = $this->_connection->quoteTableName($table); } } } return implode(', ', $tables); } $this->buildJoin($query->join), $this->buildWhere($query->where), $this->buildGroupBy($query->groupBy), $this->buildHaving($query->having), $this->buildOrderBy($query->orderBy), $this->buildLimit($query->offset, $query->limit), if (isset($query['union'])) $sql .= "\nUNION (\n" . (is_array($query['union']) ? implode("\n) UNION (\n", $query['union']) : $query['union']) . ')'; return $sql; } /** * Sets the WHERE part of the query. * * The method requires a $conditions parameter, and optionally a $params parameter * specifying the values to be bound to the query. * * The $conditions parameter should be either a string (e.g. 'id=1') or an array. * If the latter, it must be of the format array(operator, operand1, operand2, ...), * where the operator can be one of the followings, and the possible operands depend on the corresponding * operator: * * @param mixed $conditions the conditions that should be put in the WHERE part. * @param array $params the parameters (name=>value) to be bound to the query * @return Command the command object itself * @since 1.1.6 */ public function where($conditions, $params = array()) { $this->_query['where'] = $this->processConditions($conditions); foreach ($params as $name => $value) $this->params[$name] = $value; return $this; } /** * Appends an INNER JOIN part to the query. * @param string $table the table to be joined. * Table name can contain schema prefix (e.g. 'public.tbl_user') and/or table alias (e.g. 'tbl_user u'). * The method will automatically quote the table name unless it contains some parenthesis * (which means the table is given as a sub-query or DB expression). * @param mixed $conditions the join condition that should appear in the ON part. * Please refer to {@link where} on how to specify conditions. * @param array $params the parameters (name=>value) to be bound to the query * @return Command the command object itself * @since 1.1.6 */ public function join($table, $conditions, $params = array()) { return $this->joinInternal('join', $table, $conditions, $params); } /** * Sets the GROUP BY part of the query. * @param mixed $columns the columns to be grouped by. * Columns can be specified in either a string (e.g. "id, name") or an array (e.g. array('id', 'name')). * The method will automatically quote the column names unless a column contains some parenthesis * (which means the column contains a DB expression). * @return Command the command object itself * @since 1.1.6 */ public function group($columns) { if (is_string($columns) && strpos($columns, '(') !== false) $this->_query['group'] = $columns; else { if (!is_array($columns)) $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); foreach ($columns as $i => $column) { if (is_object($column)) $columns[$i] = (string)$column; elseif (strpos($column, '(') === false) $columns[$i] = $this->_connection->quoteColumnName($column); } $this->_query['group'] = implode(', ', $columns); } return $this; } /** * Sets the HAVING part of the query. * @param mixed $conditions the conditions to be put after HAVING. * Please refer to {@link where} on how to specify conditions. * @param array $params the parameters (name=>value) to be bound to the query * @return Command the command object itself * @since 1.1.6 */ public function having($conditions, $params = array()) { $this->_query['having'] = $this->processConditions($conditions); foreach ($params as $name => $value) $this->params[$name] = $value; return $this; } /** * Sets the ORDER BY part of the query. * @param mixed $columns the columns (and the directions) to be ordered by. * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array (e.g. array('id ASC', 'name DESC')). * The method will automatically quote the column names unless a column contains some parenthesis * (which means the column contains a DB expression). * @return Command the command object itself * @since 1.1.6 */ public function order($columns) { if (is_string($columns) && strpos($columns, '(') !== false) $this->_query['order'] = $columns; else { if (!is_array($columns)) $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); foreach ($columns as $i => $column) { if (is_object($column)) $columns[$i] = (string)$column; elseif (strpos($column, '(') === false) { if (preg_match('/^(.*?)\s+(asc|desc)$/i', $column, $matches)) $columns[$i] = $this->_connection->quoteColumnName($matches[1]) . ' ' . strtoupper($matches[2]); else $columns[$i] = $this->_connection->quoteColumnName($column); } } $this->_query['order'] = implode(', ', $columns); } return $this; } /** * Sets the LIMIT part of the query. * @param integer $limit the limit * @param integer $offset the offset * @return Command the command object itself * @since 1.1.6 */ public function limit($limit, $offset = null) { $this->_query['limit'] = (int)$limit; if ($offset !== null) $this->offset($offset); return $this; } /** * Appends a SQL statement using UNION operator. * @param string $sql the SQL statement to be appended using UNION * @return Command the command object itself * @since 1.1.6 */ public function union($sql) { if (isset($this->_query['union']) && is_string($this->_query['union'])) $this->_query['union'] = array($this->_query['union']); $this->_query['union'][] = $sql; return $this; } /** * Generates the condition string that will be put in the WHERE part * @param mixed $conditions the conditions that will be put in the WHERE part. * @return string the condition string to put in the WHERE part */ private function buildConditions($conditions) { if (!is_array($conditions)) return $conditions; elseif ($conditions === array()) return ''; $n = count($conditions); $operator = strtoupper($conditions[0]); if ($operator === 'OR' || $operator === 'AND') { $parts = array(); for ($i = 1;$i < $n;++$i) { $condition = $this->processConditions($conditions[$i]); if ($condition !== '') $parts[] = '(' . $condition . ')'; } return $parts === array() ? '' : implode(' ' . $operator . ' ', $parts); } if (!isset($conditions[1], $conditions[2])) return ''; $column = $conditions[1]; if (strpos($column, '(') === false) $column = $this->_connection->quoteColumnName($column); $values = $conditions[2]; if (!is_array($values)) $values = array($values); if ($operator === 'IN' || $operator === 'NOT IN') { if ($values === array()) return $operator === 'IN' ? '0=1' : ''; foreach ($values as $i => $value) { if (is_string($value)) $values[$i] = $this->_connection->quoteValue($value); else $values[$i] = (string)$value; } return $column . ' ' . $operator . ' (' . implode(', ', $values) . ')'; } if ($operator === 'LIKE' || $operator === 'NOT LIKE' || $operator === 'OR LIKE' || $operator === 'OR NOT LIKE') { if ($values === array()) return $operator === 'LIKE' || $operator === 'OR LIKE' ? '0=1' : ''; if ($operator === 'LIKE' || $operator === 'NOT LIKE') $andor = ' AND '; else { $andor = ' OR '; $operator = $operator === 'OR LIKE' ? 'LIKE' : 'NOT LIKE'; } $expressions = array(); foreach ($values as $value) $expressions[] = $column . ' ' . $operator . ' ' . $this->_connection->quoteValue($value); return implode($andor, $expressions); } throw new CDbException(Yii::t('yii', 'Unknown operator "{operator}".', array('{operator}' => $operator))); } /** * Appends an JOIN part to the query. * @param string $type the join type ('join', 'left join', 'right join', 'cross join', 'natural join') * @param string $table the table to be joined. * Table name can contain schema prefix (e.g. 'public.tbl_user') and/or table alias (e.g. 'tbl_user u'). * The method will automatically quote the table name unless it contains some parenthesis * (which means the table is given as a sub-query or DB expression). * @param mixed $conditions the join condition that should appear in the ON part. * Please refer to {@link where} on how to specify conditions. * @param array $params the parameters (name=>value) to be bound to the query * @return Command the command object itself * @since 1.1.6 */ private function joinInternal($type, $table, $conditions = '', $params = array()) { if (strpos($table, '(') === false) { if (preg_match('/^(.*?)(?i:\s+as\s+|\s+)(.*)$/', $table, $matches)) // with alias $table = $this->_connection->quoteTableName($matches[1]) . ' ' . $this->_connection->quoteTableName($matches[2]); else $table = $this->_connection->quoteTableName($table); } $conditions = $this->processConditions($conditions); if ($conditions != '') $conditions = ' ON ' . $conditions; if (isset($this->_query['join']) && is_string($this->_query['join'])) $this->_query['join'] = array($this->_query['join']); $this->_query['join'][] = strtoupper($type) . ' ' . $table . $conditions; foreach ($params as $name => $value) $this->params[$name] = $value; return $this; } }