Source for file Query.php
Documentation is available at Query.php
* $Id: Query.php 2287 2007-08-29 21:36:36Z zYne $
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
* This software consists of voluntary contributions made by many individuals
* and is licensed under the LGPL. For more information, see
* <http://www.phpdoctrine.com>.
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL
* @category Object Relational Mapping
* @link www.phpdoctrine.com
* @version $Revision: 2287 $
* @author Konsta Vesterinen <kvesteri@cc.hut.fi>
* @param boolean $needsSubquery
* @param boolean $isSubquery whether or not this query object is a subquery of another
* @var array $_neededTableAliases an array containing the needed table aliases
* @var array $pendingFields
* @var array $pendingSubqueries SELECT part subqueries, these are called pending subqueries since
* they cannot be parsed directly (some queries might be correlated)
* @var array $_parsers an array of parser objects, each DQL query part has its own parser
* @var array $_enumParams an array containing the keys of the parameters that should be enumerated
* @var array $_dqlParts an array containing all DQL query parts
* @var array $_pendingJoinConditions an array containing pending joins
protected $_state =
Doctrine_Query::STATE_CLEAN;
* returns a new Doctrine_Query object
* @param Doctrine_Connection $conn optional connection parameter
public static function create($conn =
null)
* @param string $name option name
* @param string $value option value
* @return Doctrine_Query this object
* addPendingJoinCondition
* @param string $componentAlias component alias
* @param string $joinCondition dql join condition
* @return Doctrine_Query this object
$this->_pendingJoins[$componentAlias] =
$joinCondition;
* sets input parameter as an enumerated parameter
* @param string $key the key of the input parameter
public function addEnumParam($key, $table =
null, $column =
null)
$array =
(isset
($table) || isset
($column)) ?
array($table, $column) :
array();
* get all enumerated parameters
* @return array all enumerated parameters
* convert enum parameters to their integer equivalents
* @return array converted parameter array
if (isset
($params[$key])) {
$params[$key] =
$values[0]->enumIndex($values[1], $params[$key]);
* if $bool parameter is set this method sets the value of
* Doctrine_Query::$isSubquery. If this value is set to true
* the query object will not load the primary key fields of the selected
* If null is given as the first parameter this method retrieves the current
* value of Doctrine_Query::$isSubquery.
* @param boolean $bool whether or not this query acts as a subquery
* @return Doctrine_Query|bool
* @param string $dqlAlias the dql alias of an aggregate value
// mark the expression as used
* @throws Doctrine_Query_Exception if unknown parser name given
* @return Doctrine_Query_Part
$this->_parsers[$name] =
new $class($this);
* parses given DQL query part
* @param string $queryPartName the name of the query part
* @param string $queryPart query part to be parsed
* @param boolean $append whether or not to append the query part to its stack
* if false is given, this method will overwrite
* the given query part stack with $queryPart
* @return Doctrine_Query this object
public function parseQueryPart($queryPartName, $queryPart, $append =
false)
if ($this->_state ===
self::STATE_LOCKED) {
if ($queryPart ===
'' ||
$queryPart ===
null) {
// add query part to the dql part array
$this->_dqlParts[$queryPartName][] =
$queryPart;
$this->_dqlParts[$queryPartName] =
array($queryPart);
if ($this->_state ===
self::STATE_DIRECT) {
$sql =
$parser->parse($queryPart);
* returns the given DQL query part
* @param string $queryPart the name of the query part
* @return string the DQL query part
if ( ! isset
($this->_dqlParts[$queryPart])) {
* returns the DQL query associated with this object
* the query is built from $_dqlParts
* @return string the DQL query
* the fields in SELECT clause cannot be parsed until the components
* in FROM clause are parsed, hence this method is called everytime a
* specific component is being parsed.
* @throws Doctrine_Query_Exception if unknown component alias has been given
* @param string $componentAlias the alias of the component
$table =
$this->_aliasMap[$componentAlias]['table'];
$fields =
$table->getColumnNames();
// only auto-add the primary key fields if this query object is not
// a subquery of another query object
foreach ($fields as $name) {
$name =
$table->getColumnName($name);
$this->parts['select'][] =
$this->_conn->quoteIdentifier($tableAlias .
'.' .
$name)
.
$this->_conn->quoteIdentifier($tableAlias .
'__' .
$name);
$this->neededTables[] =
$tableAlias;
* parses the query select part and
* adds selected fields to pendingFields array
$first =
substr($refs[0], 0, $pos);
if ($first ===
'DISTINCT') {
$this->parts['distinct'] =
true;
$refs[0] =
substr($refs[0], ++
$pos);
foreach ($refs as $reference) {
$reference =
trim($reference);
if (strpos($reference, '(') !==
false) {
if (substr($reference, 0, 1) ===
'(') {
// subselect found in SELECT part
* parses the subquery found in DQL SELECT part and adds the
* parsed form into $pendingSubqueries stack
* @param string $reference
$subquery =
substr($e[0], 1, -
1);
* parses given DQL clause
* this method handles five tasks:
* 1. Converts all DQL functions to their native SQL equivalents
* 2. Converts all component references to their table alias equivalents
* 3. Converts all column aliases to actual column names
* 4. Quotes all identifiers
* 5. Parses nested clauses and subqueries recursively
* @return string SQL string
foreach ($terms as $term) {
$name =
substr($term[0], 0, $pos);
$argStr =
substr($term[0], ($pos +
1), -
1);
// convert DQL function to its RDBMS specific equivalent
// check for possible subqueries
if (substr($trimmed, 0, 4) ==
'FROM' ||
substr($trimmed, 0, 6) ==
'SELECT') {
$term[0] =
'(' .
$trimmed .
')';
if (substr($term[0], 0, 1) !==
"'" &&
substr($term[0], -
1) !==
"'") {
if (strpos($term[0], '.') !==
false) {
$componentAlias =
implode('.', $e);
// check the existence of the component alias
if ( ! isset
($this->_aliasMap[$componentAlias])) {
$table =
$this->_aliasMap[$componentAlias]['table'];
// get the actual field name from alias
$field =
$table->getColumnName($field);
// check column existence
if ( ! $table->hasColumn($field)) {
$term[0] =
$this->_conn->quoteIdentifier($tableAlias)
.
$this->_conn->quoteIdentifier($field);
$str .=
$term[0] .
$term[1];
* parses an aggregate function and returns the parsed form
* @see Doctrine_Expression
* @param string $expr DQL aggregate function
* @throws Doctrine_Query_Exception if unknown aggregate function given
* @return array parsed form of given function
// get the name of the function
$name =
substr($func, 0, $pos);
$argStr =
substr($func, ($pos +
1), -
1);
// convert DQL function to its RDBMS specific equivalent
// try to find all component references
preg_match_all("/[a-z0-9_]+\.[a-z0-9_]+[\.[a-z0-9]+]*/i", $argStr, $m);
* processPendingSubqueries
* processes pending subqueries
* subqueries can only be processed when the query is fully constructed
* since some subqueries may be correlated
list
($dql, $alias) =
$value;
$sql =
$subquery->parseQuery($dql, false)->getQuery();
$this->parts['select'][] =
'(' .
$sql .
') AS ' .
$this->_conn->quoteIdentifier($sqlAlias);
$this->_aliasMap[$componentAlias]['agg'][] =
$alias;
* processPendingAggregates
* processes pending aggregate values for given component alias
// iterate trhough all aggregates
list
($expression, $components, $alias) =
$aggregate;
// iterate through the component references within the aggregate function
if ( ! empty ($components)) {
foreach ($components as $component) {
$componentAlias =
implode('.', $e);
// check the existence of the component alias
if ( ! isset
($this->_aliasMap[$componentAlias])) {
$table =
$this->_aliasMap[$componentAlias]['table'];
$field =
$table->getColumnName($field);
// check column existence
if ( ! $table->hasColumn($field)) {
$tableAliases[$tableAlias] =
true;
$identifier =
$this->_conn->quoteIdentifier($tableAlias .
'.' .
$field);
$expression =
str_replace($component, $identifier, $expression);
if (count($tableAliases) !==
1) {
$componentAlias =
reset($this->tableAliases);
$tableAlias =
key($this->tableAliases);
$sqlAlias =
$this->_conn->quoteIdentifier($tableAlias .
'__' .
$index);
$this->parts['select'][] =
$expression .
' AS ' .
$sqlAlias;
$this->_aliasMap[$componentAlias]['agg'][$index] =
$alias;
$this->neededTables[] =
$tableAlias;
* returns the base of the generated sql query
* On mysql driver special strategy has to be used for DELETE statements
* @return string the base of the generated sql query
$distinct =
($this->parts['distinct']) ?
'DISTINCT ' :
'';
$q =
'SELECT ' .
$distinct .
implode(', ', $this->parts['select']) .
' FROM ';
* builds the from part of the query and returns it
* @return string the query sql from part
foreach ($this->parts['from'] as $k =>
$part) {
// preserve LEFT JOINs only if needed
if (substr($part, 0, 9) ===
'LEFT JOIN') {
if (strpos($part, ' ON ') !==
false) {
$this->parts['from'][$k] =
$part;
* Empty template method to provide Query subclasses with the possibility
* to hook into the query building procedure, doing any custom / specialized
* query building procedures that are neccessary.
* Empty template method to provide Query subclasses with the possibility
* to hook into the query building procedure, doing any custom / specialized
* post query procedures (for example logging) that are neccessary.
* builds the sql query from the given parameters and applies things such as
* column aggregation inheritance and limit subqueries if needed
* @param array $params an array of prepared statement params (needed only in mysql driver
* when limit subquery algorithm is used)
* @return string the built sql query
public function getQuery($params =
array())
if ($this->_state !==
self::STATE_DIRTY) {
foreach ($this->_dqlParts as $queryPartName =>
$queryParts) {
if (is_array($queryParts) &&
! empty($queryParts)) {
foreach ($queryParts as $queryPart) {
$sql =
$parser->parse($queryPart);
if ($queryPartName ==
'limit' ||
$queryPartName ==
'offset') {
$this->_state =
self::STATE_DIRECT;
// invoke the preQuery hook
$this->_state =
self::STATE_CLEAN;
if (empty($this->parts['from'])) {
// process all pending SELECT part subqueries
if ( ! empty($this->parts['set'])) {
// apply inheritance to WHERE part
$this->parts['where'][] =
'(' .
$string .
')';
if ( ! empty($this->parts['limit']) ||
! empty($this->parts['offset'])) {
// mysql doesn't support LIMIT in subqueries
$list =
$this->_conn->execute($subquery, $params)->fetchAll(Doctrine::FETCH_COLUMN);
// pgsql needs special nested LIMIT subquery
$subquery =
'SELECT doctrine_subquery_alias.' .
$table->getIdentifier().
' FROM (' .
$subquery .
') AS doctrine_subquery_alias';
$field =
$this->getTableAlias($rootAlias) .
'.' .
$table->getIdentifier();
// only append the subquery if it actually contains something
$q .=
( ! empty($this->parts['where']))?
' WHERE ' .
implode(' AND ', $this->parts['where']) :
'';
$q .=
( ! empty($this->parts['groupby']))?
' GROUP BY ' .
implode(', ', $this->parts['groupby']) :
'';
$q .=
( ! empty($this->parts['having']))?
' HAVING ' .
implode(' AND ', $this->parts['having']):
'';
$q .=
( ! empty($this->parts['orderby']))?
' ORDER BY ' .
implode(', ', $this->parts['orderby']) :
'';
$q =
$this->_conn->modifyLimitQuery($q, $this->parts['limit'], $this->parts['offset']);
// return to the previous state
* this is method is used by the record limit algorithm
* when fetching one-to-many, many-to-many associated data with LIMIT clause
* an additional subquery is needed for limiting the number of returned records instead
* of limiting the number of sql result set rows
* @return string the limit subquery
$primaryKey =
$alias .
'.' .
$table->getIdentifier();
// initialize the base of the subquery
$subquery =
'SELECT DISTINCT ' .
$this->_conn->quoteIdentifier($primaryKey);
$driverName =
$this->_conn->getAttribute(Doctrine::ATTR_DRIVER_NAME);
// pgsql needs the order by fields to be preserved in select clause
if ($driverName ==
'pgsql') {
foreach ($this->parts['orderby'] as $part) {
if (strpos($part, '.') ===
false) {
if (strpos($part, '(') !==
false) {
// don't add primarykey column (its already in the select clause)
if ($part !==
$primaryKey) {
$subquery .=
', ' .
$part;
if ($driverName ==
'mysql' ||
$driverName ==
'pgsql') {
$subquery .=
', ' .
$expr[0] .
' AS ' .
$this->aggregateMap[$dqlAlias];
foreach ($this->parts['from'] as $part) {
// preserve LEFT JOINs only if needed
if (substr($part, 0, 9) ===
'LEFT JOIN') {
if (empty($this->parts['orderby']) &&
empty($this->parts['where'])) {
$subquery .=
' ' .
$part;
// all conditions must be preserved in subquery
$subquery .=
( ! empty($this->parts['where']))?
' WHERE ' .
implode(' AND ', $this->parts['where']) :
'';
$subquery .=
( ! empty($this->parts['groupby']))?
' GROUP BY ' .
implode(', ', $this->parts['groupby']) :
'';
$subquery .=
( ! empty($this->parts['having']))?
' HAVING ' .
implode(' AND ', $this->parts['having']) :
'';
$subquery .=
( ! empty($this->parts['orderby']))?
' ORDER BY ' .
implode(', ', $this->parts['orderby']) :
'';
// add driver specific limit clause
$subquery =
$this->_conn->modifyLimitQuery($subquery, $this->parts['limit'], $this->parts['offset']);
foreach ($parts as $k =>
$part) {
if (strpos($part, ' ') !==
false) {
$part =
trim($part, "\"'`");
if (strpos($part, '.') ===
false) {
foreach ($m[0] as $match) {
if ($driverName ==
'mysql' ||
$driverName ==
'pgsql') {
foreach ($parts as $k =>
$part) {
if (strpos($part, "'") !==
false) {
if (strpos($part, '__') ==
false) {
foreach ($m[0] as $match) {
* splits the given dql query into an array where keys
* represent different query part names and values are
* arrays splitted using sqlExplode method
* $query = "SELECT u.* FROM User u WHERE u.name LIKE ?"
* array('select' => array('u.*'),
* 'from' => array('User', 'u'),
* 'where' => array('u.name', 'LIKE', '?'))
* @param string $query DQL query
* @throws Doctrine_Query_Exception if some generic parsing error occurs
* @return array an array containing the query string parts
foreach ($e as $k=>
$part) {
if (isset
($e[$i]) &&
strtolower($e[$i]) ===
'by') {
* first splits the query in parts and then uses individual
* @param string $query DQL query
* @param boolean $clear whether or not to clear the aliases
* @throws Doctrine_Query_Exception if some generic parsing error occurs
foreach($parts as $k =>
$part) {
$this->type =
self::CREATE;
$this->type =
self::INSERT;
$this->type =
self::DELETE;
$this->type =
self::SELECT;
$this->type =
self::UPDATE;
public function load($path, $loadFields =
true)
// parse custom join conditions
$componentAlias =
$originalAlias =
(count($tmp) >
1) ?
end($tmp) :
null;
$fullLength =
strlen($fullPath);
foreach ($e as $key =>
$name) {
// get length of the previous path
// build the current component path
$prevPath =
($prevPath) ?
$prevPath .
'.' .
$name :
$name;
$delimeter =
substr($fullPath, $length, 1);
// if an alias is not given use the current path as an alias identifier
if (strlen($prevPath) ===
$fullLength && isset
($originalAlias)) {
$componentAlias =
$originalAlias;
$componentAlias =
$prevPath;
// if the current alias already exists, skip it
if (isset
($this->_aliasMap[$componentAlias])) {
// process the root of the path
$table =
$this->loadRoot($name, $componentAlias);
$join =
($delimeter ==
':') ?
'INNER JOIN ' :
'LEFT JOIN ';
$relation =
$table->getRelation($name);
$table =
$relation->getTable();
$this->_aliasMap[$componentAlias] =
array('table' =>
$table,
'relation' =>
$relation);
if ( ! $relation->isOneToOne()) {
$localAlias =
$this->getTableAlias($parent, $table->getTableName());
$foreignAlias =
$this->getTableAlias($componentAlias, $relation->getTable()->getTableName());
$localSql =
$this->_conn->quoteIdentifier($table->getTableName())
.
$this->_conn->quoteIdentifier($localAlias);
$foreignSql =
$this->_conn->quoteIdentifier($relation->getTable()->getTableName())
.
$this->_conn->quoteIdentifier($foreignAlias);
$map =
$relation->getTable()->inheritanceMap;
if ( ! $loadFields ||
! empty($map) ||
$joinCondition) {
$asf =
$relation->getAssociationTable();
$assocTableName =
$asf->getTableName();
if( ! $loadFields ||
! empty($map) ||
$joinCondition) {
$assocPath =
$prevPath .
'.' .
$asf->getComponentName();
$assocAlias =
$this->getTableAlias($assocPath, $asf->getTableName());
$queryPart =
$join .
$assocTableName .
' ' .
$assocAlias;
$queryPart .=
' ON ' .
$localAlias
.
$localTable->getIdentifier()
.
$assocAlias .
'.' .
$relation->getLocal();
if ($relation->isEqual()) {
// equal nest relation needs additional condition
$queryPart .=
' OR ' .
$localAlias
.
$table->getColumnName($table->getIdentifier())
.
$assocAlias .
'.' .
$relation->getForeign();
$this->parts['from'][] =
$queryPart;
$queryPart =
$join .
$foreignSql;
if ($relation->isEqual()) {
$queryPart .=
$this->_conn->quoteIdentifier($foreignAlias .
'.' .
$relation->getTable()->getIdentifier())
.
$this->_conn->quoteIdentifier($assocAlias .
'.' .
$relation->getForeign());
if ($relation->isEqual()) {
.
$this->_conn->quoteIdentifier($foreignAlias .
'.' .
$table->getColumnName($table->getIdentifier()))
.
$this->_conn->quoteIdentifier($assocAlias .
'.' .
$relation->getLocal())
.
$this->_conn->quoteIdentifier($foreignAlias .
'.' .
$table->getIdentifier())
.
$this->_conn->quoteIdentifier($localAlias .
'.' .
$table->getIdentifier());
$queryPart =
$join .
$foreignSql;
.
$this->_conn->quoteIdentifier($localAlias .
'.' .
$relation->getLocal())
.
$this->_conn->quoteIdentifier($foreignAlias .
'.' .
$relation->getForeign());
$this->parts['from'][$componentAlias] =
$queryPart;
if ( ! empty($joinCondition)) {
// load fields if necessary
* @param string $componentAlias
public function loadRoot($name, $componentAlias)
// get the connection for the component
->getConnectionForComponent($name);
$table =
$this->_conn->getTable($name);
$tableName =
$table->getTableName();
// get the short alias for this table
$queryPart =
$this->_conn->quoteIdentifier($tableName);
if ($this->type ===
self::SELECT) {
$queryPart .=
' ' .
$this->_conn->quoteIdentifier($tableAlias);
$this->parts['from'][] =
$queryPart;
$this->tableAliases[$tableAlias] =
$componentAlias;
$this->_aliasMap[$componentAlias] =
array('table' =>
$table);
* fetches the count of the query
* This method executes the main query without all the
* selected fields, ORDER BY part, LIMIT part and OFFSET part.
* SELECT u.*, p.phonenumber FROM User u
* LEFT JOIN u.Phonenumber p
* WHERE p.phonenumber = '123 123' LIMIT 10
* The modified DQL query:
* SELECT COUNT(DISTINCT u.id) FROM User u
* LEFT JOIN u.Phonenumber p
* WHERE p.phonenumber = '123 123'
* @param array $params an array of prepared statement parameters
* @return integer the count of this query
public function count($params =
array())
// initialize temporary variables
$where =
$this->parts['where'];
$having =
$this->parts['having'];
$groupby =
$this->parts['groupby'];
$q =
'SELECT COUNT(DISTINCT ' .
$this->getTableAlias($componentAlias)
.
'.' .
implode(',', (array)
$table->getIdentifier())
foreach ($this->parts['select'] as $field) {
if (strpos($field, '(') !==
false) {
// append column aggregation inheritance (if needed)
$q .=
( ! empty($where)) ?
' WHERE ' .
implode(' AND ', $where) :
'';
$q .=
( ! empty($groupby)) ?
' GROUP BY ' .
implode(', ', $groupby) :
'';
$q .=
( ! empty($having)) ?
' HAVING ' .
implode(' AND ', $having):
'';
$params =
array($params);
if (count($results) >
1) {
foreach ($results as $result) {
$count +=
$result['num_results'];
$count = isset
($results[0]) ?
$results[0]['num_results']:
0;
* query the database with DQL (Doctrine Query Language)
* @param string $query DQL query
* @param array $params prepared statement parameters
* @see Doctrine::FETCH_* constants
public function query($query, $params =
array())
public function copy(Doctrine_Query $query =
null)
$new->_dqlParts =
$query->_dqlParts;
$new->_hydrationMode =
$query->_hydrationMode;
* Frees the resources used by the query object. It especially breaks a
* cyclic reference between the query object and it's parsers. This enables
* PHP's current GC to reclaim the memory.
* This method can therefore be used to reduce memory usage when creating a lot
* of query objects during a request.
* @return Doctrine_Query this object