. */ /** * Doctrine_Hydrate is a base class for Doctrine_RawSql and Doctrine_Query. * Its purpose is to populate object graphs. * * * @package Doctrine * @license http://www.opensource.org/licenses/lgpl-license.php LGPL * @category Object Relational Mapping * @link www.phpdoctrine.com * @since 1.0 * @version $Revision$ * @author Konsta Vesterinen */ class Doctrine_Hydrate { /** * QUERY TYPE CONSTANTS */ /** * constant for SELECT queries */ const SELECT = 0; /** * constant for DELETE queries */ const DELETE = 1; /** * constant for UPDATE queries */ const UPDATE = 2; /** * constant for INSERT queries */ const INSERT = 3; /** * constant for CREATE queries */ const CREATE = 4; /** * @var array $params query input parameters */ protected $params = array(); /** * @var Doctrine_Connection $conn Doctrine_Connection object */ protected $conn; /** * @var Doctrine_View $_view Doctrine_View object, when set this object will use the * the query given by the view object for object population */ protected $_view; /** * @var array $_aliasMap two dimensional array containing the map for query aliases * Main keys are component aliases * * table table object associated with given alias * * relation the relation object owned by the parent * * parent the alias of the parent */ protected $_aliasMap = array(); /** * */ protected $pendingAggregates = array(); /** * */ protected $subqueryAggregates = array(); /** * @var array $aggregateMap an array containing all aggregate aliases, keys as dql aliases * and values as sql aliases */ protected $aggregateMap = array(); /** * @var Doctrine_Hydrate_Alias $aliasHandler handles the creation and storage of table aliases and * binds the aliases to component aliases / paths */ protected $aliasHandler; /** * @var array $parts SQL query string parts */ protected $parts = array( 'select' => array(), 'distinct' => false, 'forUpdate' => false, 'from' => array(), 'set' => array(), 'join' => array(), 'where' => array(), 'groupby' => array(), 'having' => array(), 'orderby' => array(), 'limit' => false, 'offset' => false, ); /** * @var integer $type the query type * * @see Doctrine_Query::* constants */ protected $type = self::SELECT; /** * constructor * * @param Doctrine_Connection|null $connection */ public function __construct($connection = null) { if ( ! ($connection instanceof Doctrine_Connection)) { $connection = Doctrine_Manager::getInstance()->getCurrentConnection(); } $this->conn = $connection; $this->aliasHandler = new Doctrine_Hydrate_Alias(); } public function getTableAlias($componentAlias) { return $this->aliasHandler->getShortAlias($componentAlias); } public function addQueryPart($name, $part) { if ( ! isset($this->parts[$name])) { throw new Doctrine_Hydrate_Exception('Unknown query part ' . $name); } $this->parts[$name][] = $part; } public function getDeclaration($name) { if ( ! isset($this->_aliasMap[$name])) { throw new Doctrine_Hydrate_Exception('Unknown component alias ' . $name); } return $this->_aliasMap[$name]; } public function setQueryPart($name, $part) { if ( ! isset($this->parts[$name])) { throw new Doctrine_Hydrate_Exception('Unknown query part ' . $name); } if ($name !== 'limit' && $name !== 'offset') { $this->parts[$name] = array($part); } else { $this->parts[$name] = $part; } } /** * copyAliases * * @return void */ public function copyAliases($query) { $this->aliasHandler = $query->aliasHandler; return $this; } /** * createSubquery * * @return Doctrine_Hydrate */ public function createSubquery() { $class = get_class($this); $obj = new $class(); // copy the aliases to the subquery $obj->copyAliases($this); // this prevents the 'id' being selected, re ticket #307 $obj->isSubquery(true); return $obj; } /** * limitSubqueryUsed * * @return boolean */ public function isLimitSubqueryUsed() { return false; } public function getQueryPart($part) { if ( ! isset($this->parts[$part])) { throw new Doctrine_Hydrate_Exception('Unknown query part ' . $part); } return $this->parts[$part]; } /** * remove * * @param $name */ public function remove($name) { if (isset($this->parts[$name])) { if ($name == "limit" || $name == "offset") { $this->parts[$name] = false; } else { $this->parts[$name] = array(); } } return $this; } /** * clear * resets all the variables * * @return void */ protected function clear() { $this->parts = array( 'select' => array(), 'distinct' => false, 'forUpdate' => false, 'from' => array(), 'set' => array(), 'join' => array(), 'where' => array(), 'groupby' => array(), 'having' => array(), 'orderby' => array(), 'limit' => false, 'offset' => false, ); $this->inheritanceApplied = false; $this->aliasHandler->clear(); } /** * getConnection * * @return Doctrine_Connection */ public function getConnection() { return $this->conn; } /** * setView * sets a database view this query object uses * this method should only be called internally by doctrine * * @param Doctrine_View $view database view * @return void */ public function setView(Doctrine_View $view) { $this->_view = $view; } /** * getView * returns the view associated with this query object (if any) * * @return Doctrine_View the view associated with this query object */ public function getView() { return $this->_view; } /** * getParams * * @return array */ public function getParams() { return $this->params; } /** * setParams * * @param array $params */ public function setParams(array $params = array()) { $this->params = $params; } /** * _fetch * * @param array $params prepared statement parameters * @param integer $fetchMode the fetchmode * @see Doctrine::FETCH_* constants */ public function _fetch($params = array(), $fetchMode = Doctrine::FETCH_RECORD) { $params = $this->conn->convertBooleans(array_merge($this->params, $params)); $params = $this->convertEnums($params); if ( ! $this->_view) { $query = $this->getQuery($params); } else { $query = $this->_view->getSelectSql(); } if ($this->isLimitSubqueryUsed() && $this->conn->getDBH()->getAttribute(Doctrine::ATTR_DRIVER_NAME) !== 'mysql') { $params = array_merge($params, $params); } $stmt = $this->conn->execute($query, $params); return $this->parseData($stmt); } public function convertEnums($params) { return $params; } public function setAliasMap($map) { $this->_aliasMap = $map; } public function getAliasMap() { return $this->_aliasMap; } /** * mapAggregateValues * map the aggregate values of given dataset row to a given record * * @param Doctrine_Record $record * @param array $row * @return Doctrine_Record */ public function mapAggregateValues($record, array $row, $alias) { $found = false; // aggregate values have numeric keys if (isset($row[0])) { // map each aggregate value foreach ($row as $index => $value) { $agg = false; if (isset($this->pendingAggregates[$alias][$index])) { $agg = $this->pendingAggregates[$alias][$index][3]; } elseif (isset($this->subqueryAggregates[$alias][$index])) { $agg = $this->subqueryAggregates[$alias][$index]; } $record->mapValue($agg, $value); $found = true; } } return $found; } /** * execute * executes the dql query and populates all collections * * @param string $params * @return Doctrine_Collection the root collection */ public function execute($params = array(), $return = Doctrine::FETCH_RECORD) { $array = (array) $this->_fetch($params, $return = Doctrine::FETCH_RECORD); if (empty($this->_aliasMap)) { throw new Doctrine_Hydrate_Exception("Couldn't execute query. Component alias map was empty."); } // initialize some variables used within the main loop reset($this->_aliasMap); $rootMap = current($this->_aliasMap); $rootAlias = key($this->_aliasMap); $coll = new Doctrine_Collection($rootMap['table']); $prev[$rootAlias] = $coll; // we keep track of all the collections $colls = array(); $colls[] = $coll; $prevRow = array(); /** * iterate over the fetched data * here $data is a two dimensional array */ foreach ($array as $data) { /** * remove duplicated data rows and map data into objects */ foreach ($data as $tableAlias => $row) { // skip empty rows (not mappable) if (empty($row)) { continue; } $alias = $this->aliasHandler->getComponentAlias($tableAlias); $map = $this->_aliasMap[$alias]; // initialize previous row array if not set if ( ! isset($prevRow[$tableAlias])) { $prevRow[$tableAlias] = array(); } // don't map duplicate rows if ($prevRow[$tableAlias] !== $row) { $identifiable = $this->isIdentifiable($row, $map['table']->getIdentifier()); if ($identifiable) { // set internal data $map['table']->setData($row); } // initialize a new record $record = $map['table']->getRecord(); // map aggregate values (if any) if($this->mapAggregateValues($record, $row, $alias)) { $identifiable = true; } if ($alias == $rootAlias) { // add record into root collection if ($identifiable) { $coll->add($record); unset($prevRow); } } else { $relation = $map['relation']; $parentAlias = $map['parent']; $parentMap = $this->_aliasMap[$parentAlias]; $parent = $prev[$parentAlias]->getLast(); // check the type of the relation if ($relation->isOneToOne()) { if ( ! $identifiable) { continue; } $prev[$alias] = $record; } else { // one-to-many relation or many-to-many relation if ( ! $prev[$parentAlias]->getLast()->hasReference($relation->getAlias())) { // initialize a new collection $prev[$alias] = new Doctrine_Collection($map['table']); $prev[$alias]->setReference($parent, $relation); } else { // previous entry found from memory $prev[$alias] = $prev[$parentAlias]->getLast()->get($relation->getAlias()); } $colls[] = $prev[$alias]; // add record to the current collection if ($identifiable) { $prev[$alias]->add($record); } } // initialize the relation from parent to the current collection/record $parent->set($relation->getAlias(), $prev[$alias]); } // following statement is needed to ensure that mappings // are being done properly when the result set doesn't // contain the rows in 'right order' if ($prev[$alias] !== $record) { $prev[$alias] = $record; } } $prevRow[$tableAlias] = $row; } } // take snapshots from all initialized collections foreach(array_unique($colls) as $c) { $c->takeSnapshot(); } return $coll; } /** * isIdentifiable * returns whether or not a given data row is identifiable (it contains * all primary key fields specified in the second argument) * * @param array $row * @param mixed $primaryKeys * @return boolean */ public function isIdentifiable(array $row, $primaryKeys) { if (is_array($primaryKeys)) { foreach ($primaryKeys as $id) { if ($row[$id] == null) { return false; } } } else { if ( ! isset($row[$primaryKeys])) { return false; } } return true; } /** * getType * * returns the type of this query object * by default the type is Doctrine_Hydrate::SELECT but if update() or delete() * are being called the type is Doctrine_Hydrate::UPDATE and Doctrine_Hydrate::DELETE, * respectively * * @see Doctrine_Hydrate::SELECT * @see Doctrine_Hydrate::UPDATE * @see Doctrine_Hydrate::DELETE * * @return integer return the query type */ public function getType() { return $this->type; } /** * applyInheritance * applies column aggregation inheritance to DQL / SQL query * * @return string */ public function applyInheritance() { // get the inheritance maps $array = array(); foreach ($this->_aliasMap as $componentAlias => $data) { $tableAlias = $this->getTableAlias($componentAlias); $array[$tableAlias][] = $data['table']->inheritanceMap; } // apply inheritance maps $str = ''; $c = array(); $index = 0; foreach ($array as $tableAlias => $maps) { $a = array(); // don't use table aliases if the query isn't a select query if ($this->type !== Doctrine_Query::SELECT) { $tableAlias = ''; } else { $tableAlias .= '.'; } foreach ($maps as $map) { $b = array(); foreach ($map as $field => $value) { if ($index > 0) { $b[] = '(' . $tableAlias . $field . ' = ' . $value . ' OR ' . $tableAlias . $field . ' IS NULL)'; } else { $b[] = $tableAlias . $field . ' = ' . $value; } } if ( ! empty($b)) { $a[] = implode(' AND ', $b); } } if ( ! empty($a)) { $c[] = implode(' AND ', $a); } $index++; } $str .= implode(' AND ', $c); return $str; } /** * parseData * parses the data returned by statement object * * @param mixed $stmt * @return array */ public function parseData($stmt) { $array = array(); while ($data = $stmt->fetch(PDO::FETCH_ASSOC)) { /** * parse the data into two-dimensional array */ foreach ($data as $key => $value) { $e = explode('__', $key); $field = strtolower(array_pop($e)); $tableAlias = strtolower(implode('__', $e)); $data[$tableAlias][$field] = $value; unset($data[$key]); } $array[] = $data; } $stmt->closeCursor(); return $array; } /** * @return string returns a string representation of this object */ public function __toString() { return Doctrine_Lib::formatSql($this->getQuery()); } }