false, self::FOR_UPDATE => false, self::COLUMNS => array(), self::FROM => array(), self::WHERE => array(), self::GROUP => array(), self::HAVING => array(), self::ORDER => array(), self::LIMIT_COUNT => null, self::LIMIT_OFFSET => null, ); /** * The component parts of a SELECT statement. * Initialized to the $_partsInit array in the constructor. * * @var array */ protected $_parts = array(); /** * Tracks which columns are being select from each table and join. * * @var array */ protected $_tableCols = array(); /** * Class constructor * * @param Zend_Db_Adapter_Abstract $adapter */ public function __construct(Zend_Db_Adapter_Abstract $adapter) { $this->_adapter = $adapter; $this->_parts = self::$_partsInit; } /** * Converts this object to an SQL SELECT string. * * @return string This object as a SELECT string. */ public function __toString() { // initial SELECT [DISTINCT] [FOR UPDATE] $sql = 'SELECT'; if ($this->_parts[self::DISTINCT]) { $sql .= ' DISTINCT'; } if ($this->_parts[self::FOR_UPDATE]) { $sql .= ' FOR UPDATE'; } $sql .= "\n\t"; // add columns $columns = array(); foreach ($this->_parts[self::COLUMNS] as $columnEntry) { list($correlationName, $column, $alias) = $columnEntry; if ($column instanceof Zend_Db_Expr) { $columns[] = $this->_adapter->quoteColumnAs($column, $alias, true); } else { if ($column == '*') { $column = new Zend_Db_Expr('*'); $alias = null; } if (empty($correlationName)) { $columns[] = $this->_adapter->quoteColumnAs($column, $alias, true); } else { $columns[] = $this->_adapter->quoteColumnAs(array($correlationName, $column), $alias, true); } } } $sql .= implode(",\n\t", $columns); // from these joined tables if ($this->_parts[self::FROM]) { $from = array(); foreach ($this->_parts[self::FROM] as $correlationName => $table) { $tmp = ''; if (empty($from)) { // Add schema if available if (null !== $table['schema']) { $tmp .= $this->_adapter->quoteIdentifier($table['schema'], true) . '.'; } // First table is named alone ignoring join information $tmp .= $this->_adapter->quoteTableAs($table['tableName'], $correlationName, true); } else { // Subsequent tables may have joins if (! empty($table['joinType'])) { $tmp .= ' ' . strtoupper($table['joinType']) . ' '; } // Add schema if available if (null !== $table['schema']) { $tmp .= $this->_adapter->quoteIdentifier($table['schema'], true) . '.'; } $tmp .= $this->_adapter->quoteTableAs($table['tableName'], $correlationName, true); if (! empty($table['joinCondition'])) { $tmp .= ' ON ' . $table['joinCondition']; } } // add the table name and condition // add to the list $from[] = $tmp; } // add the list of all joins if (!empty($from)) { $sql .= "\nFROM " . implode("\n", $from); } // with these where conditions if ($this->_parts[self::WHERE]) { $sql .= "\nWHERE\n\t"; $sql .= implode("\n\t", $this->_parts[self::WHERE]); } // grouped by these columns if ($this->_parts[self::GROUP]) { $sql .= "\nGROUP BY\n\t"; $l = array(); foreach ($this->_parts[self::GROUP] as $term) { $l[] = $this->_adapter->quoteIdentifier($term, true); } $sql .= implode(",\n\t", $l); } // having these conditions if ($this->_parts[self::HAVING]) { $sql .= "\nHAVING\n\t"; $sql .= implode("\n\t", $this->_parts[self::HAVING]); } } // ordered by these columns if ($this->_parts[self::ORDER]) { $sql .= "\nORDER BY\n\t"; $l = array(); foreach ($this->_parts[self::ORDER] as $term) { if (is_array($term)) { $l[] = $this->_adapter->quoteIdentifier($term[0], true) . ' ' . $term[1]; } else { $l[] = $this->_adapter->quoteIdentifier($term, true); } } $sql .= implode(",\n\t", $l); } // determine offset $count = 0; $offset = 0; if (!empty($this->_parts[self::LIMIT_OFFSET])) { $offset = (int) $this->_parts[self::LIMIT_OFFSET]; // this should be reduced to the max integer PHP can support $count = intval(9223372036854775807); } // determine count if (!empty($this->_parts[self::LIMIT_COUNT])) { $count = (int) $this->_parts[self::LIMIT_COUNT]; } // add limits clause if ($count > 0) { $sql .= "\n"; $sql = trim($this->_adapter->limit($sql, $count, $offset)); } return $sql; } /** * Makes the query SELECT DISTINCT. * * @param bool $flag Whether or not the SELECT is DISTINCT (default true). * @return Zend_Db_Select This Zend_Db_Select object. */ public function distinct($flag = true) { $this->_parts[self::DISTINCT] = (bool) $flag; return $this; } /** * Makes the query SELECT FOR UPDATE. * * @param bool $flag Whether or not the SELECT is FOR UPDATE (default true). * @return Zend_Db_Select This Zend_Db_Select object. */ public function forUpdate($flag = true) { $this->_parts[self::FOR_UPDATE] = (bool) $flag; return $this; } /** * Adds a FROM table and optional columns to the query. * * The first parameter $name can be a simple string, in which case the * correlation name is generated automatically. If you want to specify * the correlation name, the first parameter must be an associative * array in which the key is the physical table name, and the value is * the correlation name. For example, array('table' => 'alias'). * The correlation name is prepended to all columns fetched for this * table. * * The second parameter can be a single string or Zend_Db_Expr object, * or else an array of strings or Zend_Db_Expr objects. * * The first parameter can be null or an empty string, in which case * no correlation name is generated or prepended to the columns named * in the second parameter. * * @param array|string|Zend_Db_Expr $name The table name or an associative array relating table name to * correlation name. * @param array|string|Zend_Db_Expr $cols The columns to select from this table. * @param string $schema The schema name to specify, if any. * @return Zend_Db_Select This Zend_Db_Select object. */ public function from($name, $cols = '*', $schema = null) { return $this->joinInner($name, null, $cols, $schema); } /** * Populate the {@link $_parts} 'join' key * * Does the dirty work of populating the join key. * * The $name and $cols parameters follow the same logic * as described in the from() method. * * @param null|string $type Type of join; inner, left, and null are currently supported * @param array|string|Zend_Db_Expr $name Table name * @param string $cond Join on this condition * @param array|string $cols The columns to select from the joined table * @param string $schema The database name to specify, if any. * @return Zend_Db_Select This Zend_Db_Select object * @throws Zend_Db_Select_Exception */ protected function _join($type, $name, $cond, $cols, $schema = null) { $joinTypes = array(self::INNER_JOIN, self::LEFT_JOIN, self::RIGHT_JOIN, self::FULL_JOIN, self::CROSS_JOIN, self::NATURAL_JOIN); if (!in_array($type, $joinTypes)) { /** * @see Zend_Db_Select_Exception */ require_once 'Zend/Db/Select/Exception.php'; throw new Zend_Db_Select_Exception("Invalid join type '$type'"); } if (empty($name)) { $correlationName = $tableName = ''; } else if (is_array($name)) { // Must be array($correlationName => $tableName) or array($ident, ...) foreach ($name as $_correlationName => $_tableName) { if (is_string($_correlationName)) { // We assume the key is the correlation name and value is the table name $tableName = $_tableName; $correlationName = $_correlationName; } else { // We assume just an array of identifiers, with no correlation name $tableName = $name; $correlationName = $this->_uniqueCorrelation($tableName); } break; } } else if ($name instanceof Zend_Db_Expr) { $tableName = $name; $correlationName = $this->_uniqueCorrelation('t'); } else if (preg_match('/^(.+)\s+AS\s+(.+)$/i', $name, $m)) { $tableName = $m[1]; $correlationName = $m[2]; } else { $tableName = $name; $correlationName = $this->_uniqueCorrelation($tableName); } // Schema from table name overrides schema argument if (false !== strpos($tableName, '.')) { list($schema, $tableName) = explode('.', $tableName); } if (!empty($correlationName)) { if (array_key_exists($correlationName, $this->_parts[self::FROM])) { /** * @see Zend_Db_Select_Exception */ require_once 'Zend/Db/Select/Exception.php'; throw new Zend_Db_Select_Exception("You cannot define a correlation name '$correlationName' more than once"); } $this->_parts[self::FROM][$correlationName] = array( 'joinType' => $type, 'schema' => $schema, 'tableName' => $tableName, 'joinCondition' => $cond ); } // add to the columns from this joined table $this->_tableCols($correlationName, $cols); return $this; } /** * Generate a unique correlation name * * @param string|array $name A qualified identifier. * @return string A unique correlation name. */ private function _uniqueCorrelation($name) { if (is_array($name)) { $c = end($name); } else { // Extract just the last name of a qualified table name $dot = strrpos($name,'.'); $c = ($dot === false) ? $name : substr($name, $dot+1); } for ($i = 2; array_key_exists($c, $this->_parts[self::FROM]); ++$i) { $c = $name . '_' . (string) $i; } return $c; } /** * Adds a JOIN table and columns to the query. * * The $name and $cols parameters follow the same logic * as described in the from() method. * * @param array|string|Zend_Db_Expr $name The table name. * @param string $cond Join on this condition. * @param array|string $cols The columns to select from the joined table. * @param string $schema The database name to specify, if any. * @return Zend_Db_Select This Zend_Db_Select object. */ public function join($name, $cond, $cols = '*', $schema = null) { return $this->joinInner($name, $cond, $cols, $schema); } /** * Add an INNER JOIN table and colums to the query * Rows in both tables are matched according to the expression * in the $cond argument. The result set is comprised * of all cases where rows from the left table match * rows from the right table. * * The $name and $cols parameters follow the same logic * as described in the from() method. * * @param array|string|Zend_Db_Expr $name The table name. * @param string $cond Join on this condition. * @param array|string $cols The columns to select from the joined table. * @param string $schema The database name to specify, if any. * @return Zend_Db_Select This Zend_Db_Select object. */ public function joinInner($name, $cond, $cols = '*', $schema = null) { return $this->_join(self::INNER_JOIN, $name, $cond, $cols, $schema); } /** * Add a LEFT OUTER JOIN table and colums to the query * All rows from the left operand table are included, * matching rows from the right operand table included, * and the columns from the right operand table are filled * with NULLs if no row exists matching the left table. * * The $name and $cols parameters follow the same logic * as described in the from() method. * * @param array|string|Zend_Db_Expr $name The table name. * @param string $cond Join on this condition. * @param array|string $cols The columns to select from the joined table. * @param string $schema The database name to specify, if any. * @return Zend_Db_Select This Zend_Db_Select object. */ public function joinLeft($name, $cond, $cols = '*', $schema = null) { return $this->_join(self::LEFT_JOIN, $name, $cond, $cols, $schema); } /** * Add a RIGHT OUTER JOIN table and colums to the query. * Right outer join is the complement of left outer join. * All rows from the right operand table are included, * matching rows from the left operand table included, * and the columns from the left operand table are filled * with NULLs if no row exists matching the right table. * * The $name and $cols parameters follow the same logic * as described in the from() method. * * @param array|string|Zend_Db_Expr $name The table name. * @param string $cond Join on this condition. * @param array|string $cols The columns to select from the joined table. * @param string $schema The database name to specify, if any. * @return Zend_Db_Select This Zend_Db_Select object. */ public function joinRight($name, $cond, $cols = '*', $schema = null) { return $this->_join(self::RIGHT_JOIN, $name, $cond, $cols, $schema); } /** * Add a FULL OUTER JOIN table and colums to the query. * A full outer join is like combining a left outer join * and a right outer join. All rows from both tables are * included, paired with each other on the same row of the * result set if they satisfy the join condition, and otherwise * paired with NULLs in place of columns from the other table. * * The $name and $cols parameters follow the same logic * as described in the from() method. * * @param array|string|Zend_Db_Expr $name The table name. * @param string $cond Join on this condition. * @param array|string $cols The columns to select from the joined table. * @param string $schema The database name to specify, if any. * @return Zend_Db_Select This Zend_Db_Select object. */ public function joinFull($name, $cond, $cols = '*', $schema = null) { return $this->_join(self::FULL_JOIN, $name, $cond, $cols, $schema); } /** * Add a CROSS JOIN table and colums to the query. * A cross join is a cartesian product; there is no join condition. * * The $name and $cols parameters follow the same logic * as described in the from() method. * * @param array|string|Zend_Db_Expr $name The table name. * @param array|string $cols The columns to select from the joined table. * @param string $schema The database name to specify, if any. * @return Zend_Db_Select This Zend_Db_Select object. */ public function joinCross($name, $cols = '*', $schema = null) { return $this->_join(self::CROSS_JOIN, $name, null, $cols, $schema); } /** * Add a NATURAL JOIN table and colums to the query. * A natural join assumes an equi-join across any column(s) * that appear with the same name in both tables. * Only natural inner joins are supported by this API, * even though SQL permits natural outer joins as well. * * The $name and $cols parameters follow the same logic * as described in the from() method. * * @param array|string|Zend_Db_Expr $name The table name. * @param array|string $cols The columns to select from the joined table. * @param string $schema The database name to specify, if any. * @return Zend_Db_Select This Zend_Db_Select object. */ public function joinNatural($name, $cols = '*', $schema = null) { return $this->_join(self::NATURAL_JOIN, $name, null, $cols, $schema); } /** * Adds a WHERE condition to the query by AND. * * If a value is passed as the second param, it will be quoted * and replaced into the condition wherever a question-mark * appears. Array values are quoted and comma-separated. * * * // simplest but non-secure * $select->where("id = $id"); * * // secure (ID is quoted but matched anyway) * $select->where('id = ?', $id); * * // alternatively, with named binding * $select->where('id = :id'); * * * Note that it is more correct to use named bindings in your * queries for values other than strings. When you use named * bindings, don't forget to pass the values when actually * making a query: * * * $db->fetchAll($select, array('id' => 5)); * * * @param string $cond The WHERE condition. * @param string $val A single value to quote into the condition. * @return Zend_Db_Select This Zend_Db_Select object. */ public function where($cond) { if (func_num_args() > 1) { $val = func_get_arg(1); $cond = $this->_adapter->quoteInto($cond, $val); } if ($this->_parts[self::WHERE]) { $this->_parts[self::WHERE][] = "AND ($cond)"; } else { $this->_parts[self::WHERE][] = "($cond)"; } return $this; } /** * Adds a WHERE condition to the query by OR. * * Otherwise identical to where(). * * @param string $cond The WHERE condition. * @param string $val A value to quote into the condition. * @return Zend_Db_Select This Zend_Db_Select object. * * @see where() */ public function orWhere($cond) { if (func_num_args() > 1) { $val = func_get_arg(1); $cond = $this->_adapter->quoteInto($cond, $val); } if ($this->_parts[self::WHERE]) { $this->_parts[self::WHERE][] = "OR ($cond)"; } else { $this->_parts[self::WHERE][] = "($cond)"; } return $this; } /** * Adds grouping to the query. * * @param array|string $spec The column(s) to group by. * @return Zend_Db_Select This Zend_Db_Select object. */ public function group($spec) { if (!is_array($spec)) { $spec = array($spec); } foreach ($spec as $val) { if (preg_match('/\(.*\)/', (string) $val)) { $val = new Zend_Db_Expr($val); } $this->_parts[self::GROUP][] = $val; } return $this; } /** * Adds a HAVING condition to the query by AND. * * If a value is passed as the second param, it will be quoted * and replaced into the condition wherever a question-mark * appears. See {@link where()} for an example * * @param string $cond The HAVING condition. * @param string|Zend_Db_Expr $val A single value to quote into the condition. * @return Zend_Db_Select This Zend_Db_Select object. */ public function having($cond) { if (func_num_args() > 1) { $val = func_get_arg(1); $cond = $this->_adapter->quoteInto($cond, $val); } if ($this->_parts[self::HAVING]) { $this->_parts[self::HAVING][] = "AND ($cond)"; } else { $this->_parts[self::HAVING][] = "($cond)"; } return $this; } /** * Adds a HAVING condition to the query by OR. * * Otherwise identical to orHaving(). * * @param string $cond The HAVING condition. * @param string $val A single value to quote into the condition. * @return Zend_Db_Select This Zend_Db_Select object. * * @see having() */ public function orHaving($cond) { if (func_num_args() > 1) { $val = func_get_arg(1); $cond = $this->_adapter->quoteInto($cond, $val); } if ($this->_parts[self::HAVING]) { $this->_parts[self::HAVING][] = "OR ($cond)"; } else { $this->_parts[self::HAVING][] = "($cond)"; } return $this; } /** * Adds a row order to the query. * * @param mixed $spec The column(s) and direction to order by. * @return Zend_Db_Select This Zend_Db_Select object. */ public function order($spec) { if (!is_array($spec)) { $spec = array($spec); } // force 'ASC' or 'DESC' on each order spec, default is ASC. foreach ($spec as $val) { if ($val instanceof Zend_Db_Expr) { $expr = $val->__toString(); if (empty($expr)) { continue; } $this->_parts[self::ORDER][] = $val; } else { if (empty($val)) { continue; } $direction = 'ASC'; if (preg_match('/(.*\W)(ASC|DESC)\b/si', $val, $matches)) { $val = trim($matches[1]); $direction = $matches[2]; } if (preg_match('/\(.*\)/', $val)) { $val = new Zend_Db_Expr($val); } $this->_parts[self::ORDER][] = array($val, $direction); } } return $this; } /** * Sets a limit count and offset to the query. * * @param int $count OPTIONAL The number of rows to return. * @param int $offset OPTIONAL Start returning after this many rows. * @return Zend_Db_Select This Zend_Db_Select object. */ public function limit($count = null, $offset = null) { $this->_parts[self::LIMIT_COUNT] = (int) $count; $this->_parts[self::LIMIT_OFFSET] = (int) $offset; return $this; } /** * Sets the limit and count by page number. * * @param int $page Limit results to this page number. * @param int $rowCount Use this many rows per page. * @return Zend_Db_Select This Zend_Db_Select object. */ public function limitPage($page, $rowCount) { $page = ($page > 0) ? $page : 1; $rowCount = ($rowCount > 0) ? $rowCount : 1; $this->_parts[self::LIMIT_COUNT] = (int) $rowCount; $this->_parts[self::LIMIT_OFFSET] = (int) $rowCount * ($page - 1); return $this; } /** * Adds to the internal table-to-column mapping array. * * @param string $tbl The table/join the columns come from. * @param array|string $cols The list of columns; preferably as * an array, but possibly as a string containing one column. * @return void */ protected function _tableCols($correlationName, $cols) { if (!is_array($cols)) { $cols = array($cols); } if ($correlationName == null) { $correlationName = ''; } foreach ($cols as $alias => $col) { $currentCorrelationName = $correlationName; if (is_string($col)) { // Check for a column matching " AS " and extract the alias name if (preg_match('/^(.+)\s+AS\s+(.+)$/i', $col, $m)) { $col = $m[1]; $alias = $m[2]; } // Check for columns that look like functions and convert to Zend_Db_Expr if (preg_match('/\(.*\)/', $col)) { $col = new Zend_Db_Expr($col); } elseif (preg_match('/(.+)\.(.+)/', $col, $m)) { $currentCorrelationName = $m[1]; $col = $m[2]; } } $this->_parts[self::COLUMNS][] = array($currentCorrelationName, $col, is_string($alias) ? $alias : null); } } /** * Get part of the structured information for the currect query. * * @param string $part * @return mixed * @throws Zend_Db_Select_Exception */ public function getPart($part) { $part = strtolower($part); if (!array_key_exists($part, $this->_parts)) { /** * @see Zend_Db_Select_Exception */ require_once 'Zend/Db/Select/Exception.php'; throw new Zend_Db_Select_Exception("Invalid Select part '$part'"); } return $this->_parts[ $part ]; } /** * @param integer $fetchMode OPTIONAL * @return PDO_Statement|Zend_Db_Statement */ public function query($fetchMode = null) { $stmt = $this->_adapter->query($this); if ($fetchMode == null) { $fetchMode = $this->_adapter->getFetchMode(); } $stmt->setFetchMode($fetchMode); return $stmt; } /** * Clear parts of the Select object, or an individual part. * * @param string $part OPTIONAL * @return Zend_Db_Select */ public function reset($part = null) { if ($part == null) { $this->_parts = self::$_partsInit; } else if (array_key_exists($part, self::$_partsInit)) { $this->_parts[$part] = self::$_partsInit[$part]; } return $this; } }