union) { $union = implode(PHP_EOL, $this->union) . PHP_EOL; } return $union . $this->build(); } /** * * Sets the number of rows per page. * * @param int $paging The number of rows to page at. * * @return $this * */ public function setPaging($paging) { $this->paging = (int) $paging; if ($this->page) { $this->setPagingLimitOffset(); } return $this; } /** * * Gets the number of rows per page. * * @return int The number of rows per page. * */ public function getPaging() { return $this->paging; } /** * * Makes the select FOR UPDATE (or not). * * @param bool $enable Whether or not the SELECT is FOR UPDATE (default * true). * * @return $this * */ public function forUpdate($enable = true) { $this->for_update = (bool) $enable; return $this; } /** * * Makes the select DISTINCT (or not). * * @param bool $enable Whether or not the SELECT is DISTINCT (default * true). * * @return $this * */ public function distinct($enable = true) { $this->setFlag('DISTINCT', $enable); return $this; } /** * * Adds columns to the query. * * Multiple calls to cols() will append to the list of columns, not * overwrite the previous columns. * * @param array $cols The column(s) to add to the query. The elements can be * any mix of these: `array("col", "col AS alias", "col" => "alias")` * * @return $this * */ public function cols(array $cols) { foreach ($cols as $key => $val) { $this->addCol($key, $val); } return $this; } /** * * Adds a column and alias to the columns to be selected. * * @param mixed $key If an integer, ignored. Otherwise, the column to be * added. * * @param mixed $val If $key was an integer, the column to be added; * otherwise, the column alias. * * @return null * */ protected function addCol($key, $val) { if (is_string($key)) { // [col => alias] $this->cols[$val] = $key; } else { $this->addColWithAlias($val); } } /** * * Adds a column with an alias to the columns to be selected. * * @param string $spec The column specification: "col alias", * "col AS alias", or something else entirely. * * @return null * */ protected function addColWithAlias($spec) { $parts = explode(' ', $spec); $count = count($parts); if ($count == 2) { // "col alias" $this->cols[$parts[1]] = $parts[0]; } elseif ($count == 3 && strtoupper($parts[1]) == 'AS') { // "col AS alias" $this->cols[$parts[2]] = $parts[0]; } else { // no recognized alias $this->cols[] = $spec; } } /** * * Remove a column via its alias. * * @param string $alias The column to remove * * @return null * */ public function removeCol($alias) { if (isset($this->cols[$alias])) { unset($this->cols[$alias]); return true; } $index = array_search($alias, $this->cols); if ($index !== false) { unset($this->cols[$index]); return true; } return false; } /** * * Does the query have any columns in it? * * @return bool * */ public function hasCols() { return (bool) $this->cols; } /** * * Returns a list of columns. * * @return array * */ public function getCols() { return $this->cols; } /** * * Tracks table references. * * @param string $type FROM, JOIN, etc. * * @param string $spec The table and alias name. * * @return null * * @throws Exception when the reference has already been used. * */ protected function addTableRef($type, $spec) { $name = $spec; $pos = strripos($name, ' AS '); if ($pos !== false) { $name = trim(substr($name, $pos + 4)); } if (isset($this->table_refs[$name])) { $used = $this->table_refs[$name]; throw new Exception("Cannot reference '$type $spec' after '$used'"); } $this->table_refs[$name] = "$type $spec"; } /** * * Adds a FROM element to the query; quotes the table name automatically. * * @param string $spec The table specification; "foo" or "foo AS bar". * * @return $this * */ public function from($spec) { $this->addTableRef('FROM', $spec); return $this->addFrom($this->quoter->quoteName($spec)); } /** * * Adds a raw unquoted FROM element to the query; useful for adding FROM * elements that are functions. * * @param string $spec The table specification, e.g. "function_name()". * * @return $this * */ public function fromRaw($spec) { $this->addTableRef('FROM', $spec); return $this->addFrom($spec); } /** * * Adds to the $from property and increments the key count. * * @param string $spec The table specification. * * @return $this * */ protected function addFrom($spec) { $this->from[] = array($spec); $this->from_key ++; return $this; } /** * * Adds an aliased sub-select to the query. * * @param string|Select $spec If a Select object, use as the sub-select; * if a string, the sub-select string. * * @param string $name The alias name for the sub-select. * * @return $this * */ public function fromSubSelect($spec, $name) { $this->addTableRef('FROM (SELECT ...) AS', $name); $spec = $this->subSelect($spec, ' '); $name = $this->quoter->quoteName($name); return $this->addFrom("({$spec} ) AS $name"); } /** * * Formats a sub-SELECT statement, binding values from a Select object as * needed. * * @param string|SelectInterface $spec A sub-SELECT specification. * * @param string $indent Indent each line with this string. * * @return string The sub-SELECT string. * */ protected function subSelect($spec, $indent) { if ($spec instanceof SelectInterface) { $this->bindValues($spec->getBindValues()); } return PHP_EOL . $indent . ltrim(preg_replace('/^/m', $indent, (string) $spec)) . PHP_EOL; } /** * * Adds a JOIN table and columns to the query. * * @param string $join The join type: inner, left, natural, etc. * * @param string $spec The table specification; "foo" or "foo AS bar". * * @param string $cond Join on this condition. * * @param array $bind Values to bind to ?-placeholders in the condition. * * @return $this * * @throws Exception * */ public function join($join, $spec, $cond = null, array $bind = array()) { $join = strtoupper(ltrim("$join JOIN")); $this->addTableRef($join, $spec); $spec = $this->quoter->quoteName($spec); $cond = $this->fixJoinCondition($cond, $bind); return $this->addJoin(rtrim("$join $spec $cond")); } /** * * Fixes a JOIN condition to quote names in the condition and prefix it * with a condition type ('ON' is the default and 'USING' is recognized). * * @param string $cond Join on this condition. * * @param array $bind Values to bind to ?-placeholders in the condition. * * @return string * */ protected function fixJoinCondition($cond, array $bind) { if (! $cond) { return; } $cond = $this->quoter->quoteNamesIn($cond); $cond = $this->rebuildCondAndBindValues($cond, $bind); if (strtoupper(substr(ltrim($cond), 0, 3)) == 'ON ') { return $cond; } if (strtoupper(substr(ltrim($cond), 0, 6)) == 'USING ') { return $cond; } return 'ON ' . $cond; } /** * * Adds a INNER JOIN table and columns to the query. * * @param string $spec The table specification; "foo" or "foo AS bar". * * @param string $cond Join on this condition. * * @param array $bind Values to bind to ?-placeholders in the condition. * * @return $this * * @throws Exception * */ public function innerJoin($spec, $cond = null, array $bind = array()) { return $this->join('INNER', $spec, $cond, $bind); } /** * * Adds a LEFT JOIN table and columns to the query. * * @param string $spec The table specification; "foo" or "foo AS bar". * * @param string $cond Join on this condition. * * @param array $bind Values to bind to ?-placeholders in the condition. * * @return $this * * @throws Exception * */ public function leftJoin($spec, $cond = null, array $bind = array()) { return $this->join('LEFT', $spec, $cond, $bind); } /** * * Adds a JOIN to an aliased subselect and columns to the query. * * @param string $join The join type: inner, left, natural, etc. * * @param string|Select $spec If a Select * object, use as the sub-select; if a string, the sub-select * command string. * * @param string $name The alias name for the sub-select. * * @param string $cond Join on this condition. * * @param array $bind Values to bind to ?-placeholders in the condition. * * @return $this * * @throws Exception * */ public function joinSubSelect($join, $spec, $name, $cond = null, array $bind = array()) { $join = strtoupper(ltrim("$join JOIN")); $this->addTableRef("$join (SELECT ...) AS", $name); $spec = $this->subSelect($spec, ' '); $name = $this->quoter->quoteName($name); $cond = $this->fixJoinCondition($cond, $bind); $text = rtrim("$join ($spec ) AS $name $cond"); return $this->addJoin(' ' . $text); } /** * * Adds the JOIN to the right place, given whether or not a FROM has been * specified yet. * * @param string $spec The JOIN clause. * * @return $this * */ protected function addJoin($spec) { $from_key = ($this->from_key == -1) ? 0 : $this->from_key; $this->join[$from_key][] = $spec; return $this; } /** * * Adds grouping to the query. * * @param array $spec The column(s) to group by. * * @return $this * */ public function groupBy(array $spec) { foreach ($spec as $col) { $this->group_by[] = $this->quoter->quoteNamesIn($col); } return $this; } /** * * Adds a HAVING condition to the query by AND. If the condition has * ?-placeholders, additional arguments to the method will be bound to * those placeholders sequentially. * * @param string $cond The HAVING condition. * * @return $this * */ public function having($cond) { $this->addClauseCondWithBind('having', 'AND', func_get_args()); return $this; } /** * * Adds a HAVING condition to the query by AND. If the condition has * ?-placeholders, additional arguments to the method will be bound to * those placeholders sequentially. * * @param string $cond The HAVING condition. * * @return $this * * @see having() * */ public function orHaving($cond) { $this->addClauseCondWithBind('having', 'OR', func_get_args()); return $this; } /** * * Sets the limit and count by page number. * * @param int $page Limit results to this page number. * * @return $this * */ public function page($page) { $this->page = (int) $page; $this->setPagingLimitOffset(); return $this; } /** * * Updates the limit and offset values when changing pagination. * * @return null * */ protected function setPagingLimitOffset() { $this->limit = 0; $this->offset = 0; if ($this->page) { $this->limit = $this->paging; $this->offset = $this->paging * ($this->page - 1); } } /** * * Returns the page number being selected. * * @return int * */ public function getPage() { return $this->page; } /** * * Takes the current select properties and retains them, then sets * UNION for the next set of properties. * * @return $this * */ public function union() { $this->union[] = $this->build() . PHP_EOL . 'UNION'; $this->reset(); return $this; } /** * * Takes the current select properties and retains them, then sets * UNION ALL for the next set of properties. * * @return $this * */ public function unionAll() { $this->union[] = $this->build() . PHP_EOL . 'UNION ALL'; $this->reset(); return $this; } /** * * Returns the LIMIT value. * * @return int * */ public function getLimit() { return $this->limit; } /** * * Returns the OFFSET value. * * @return int * */ public function getOffset() { return $this->offset; } /** * * Clears the current select properties; generally used after adding a * union. * * @return null * */ protected function reset() { $this->resetFlags(); $this->resetCols(); $this->resetTables(); $this->resetWhere(); $this->resetGroupBy(); $this->resetHaving(); $this->resetOrderBy(); $this->limit(0); $this->offset(0); $this->page(0); $this->forUpdate(false); } /** * * Resets the columns on the SELECT. * * @return $this * */ public function resetCols() { $this->cols = array(); return $this; } /** * * Resets the FROM and JOIN clauses on the SELECT. * * @return $this * */ public function resetTables() { $this->from = array(); $this->from_key = -1; $this->join = array(); $this->table_refs = array(); return $this; } /** * * Resets the WHERE clause on the SELECT. * * @return $this * */ public function resetWhere() { $this->where = array(); return $this; } /** * * Resets the GROUP BY clause on the SELECT. * * @return $this * */ public function resetGroupBy() { $this->group_by = array(); return $this; } /** * * Resets the HAVING clause on the SELECT. * * @return $this * */ public function resetHaving() { $this->having = array(); return $this; } /** * * Resets the ORDER BY clause on the SELECT. * * @return $this * */ public function resetOrderBy() { $this->order_by = array(); return $this; } /** * * Resets the UNION and UNION ALL clauses on the SELECT. * * @return $this * */ public function resetUnions() { $this->union = array(); return $this; } /** * * Builds this query object into a string. * * @return string * */ protected function build() { return 'SELECT' . $this->buildFlags() . $this->buildCols() . $this->buildFrom() // includes JOIN . $this->buildWhere() . $this->buildGroupBy() . $this->buildHaving() . $this->buildOrderBy() . $this->buildLimit() . $this->buildForUpdate(); } /** * * Builds the columns clause. * * @return string * * @throws Exception when there are no columns in the SELECT. * */ protected function buildCols() { if (! $this->cols) { throw new Exception('No columns in the SELECT.'); } $cols = array(); foreach ($this->cols as $key => $val) { if (is_int($key)) { $cols[] = $this->quoter->quoteNamesIn($val); } else { $cols[] = $this->quoter->quoteNamesIn("$val AS $key"); } } return $this->indentCsv($cols); } /** * * Builds the FROM clause. * * @return string * */ protected function buildFrom() { if (! $this->from) { return ''; // not applicable } $refs = array(); foreach ($this->from as $from_key => $from) { if (isset($this->join[$from_key])) { $from = array_merge($from, $this->join[$from_key]); } $refs[] = implode(PHP_EOL, $from); } return PHP_EOL . 'FROM' . $this->indentCsv($refs); } /** * * Builds the GROUP BY clause. * * @return string * */ protected function buildGroupBy() { if (! $this->group_by) { return ''; // not applicable } return PHP_EOL . 'GROUP BY' . $this->indentCsv($this->group_by); } /** * * Builds the HAVING clause. * * @return string * */ protected function buildHaving() { if (! $this->having) { return ''; // not applicable } return PHP_EOL . 'HAVING' . $this->indent($this->having); } /** * * Builds the FOR UPDATE clause. * * @return string * */ protected function buildForUpdate() { if (! $this->for_update) { return ''; // not applicable } return PHP_EOL . 'FOR UPDATE'; } /** * * Adds a WHERE condition to the query by AND. If the condition has * ?-placeholders, additional arguments to the method will be bound to * those placeholders sequentially. * * @param string $cond The WHERE condition. * @param mixed ...$bind arguments to bind to placeholders * * @return $this * */ public function where($cond) { $this->addWhere('AND', func_get_args()); return $this; } /** * * Adds a WHERE condition to the query by OR. If the condition has * ?-placeholders, additional arguments to the method will be bound to * those placeholders sequentially. * * @param string $cond The WHERE condition. * @param mixed ...$bind arguments to bind to placeholders * * @return $this * * @see where() * */ public function orWhere($cond) { $this->addWhere('OR', func_get_args()); return $this; } /** * * Sets a limit count on the query. * * @param int $limit The number of rows to select. * * @return $this * */ public function limit($limit) { $this->limit = (int) $limit; if ($this->page) { $this->page = 0; $this->offset = 0; } return $this; } /** * * Sets a limit offset on the query. * * @param int $offset Start returning after this many rows. * * @return $this * */ public function offset($offset) { $this->offset = (int) $offset; if ($this->page) { $this->page = 0; $this->limit = 0; } return $this; } /** * * Adds a column order to the query. * * @param array $spec The columns and direction to order by. * * @return $this * */ public function orderBy(array $spec) { return $this->addOrderBy($spec); } }