From 502103d7a43f346ccdaa08b5ea35ae958c00cad5 Mon Sep 17 00:00:00 2001 From: romanb Date: Sun, 27 May 2007 20:00:53 +0000 Subject: [PATCH] Performance improvements and a small fix. --- lib/Doctrine/Connection.php | 2 +- lib/Doctrine/Hydrate.php | 1865 +++++++++++++------------- lib/Doctrine/Table.php | 2453 ++++++++++++++++++----------------- 3 files changed, 2160 insertions(+), 2160 deletions(-) diff --git a/lib/Doctrine/Connection.php b/lib/Doctrine/Connection.php index 6ba78261e..98b38c995 100644 --- a/lib/Doctrine/Connection.php +++ b/lib/Doctrine/Connection.php @@ -668,7 +668,7 @@ abstract class Doctrine_Connection extends Doctrine_Configurable implements Coun } $class = $name . 'Table'; - if (class_exists($class) && in_array('Doctrine_Table', class_parents($class))) { + if (class_exists($class, false) && in_array('Doctrine_Table', class_parents($class))) { $table = new $class($name, $this); } else { $table = new Doctrine_Table($name, $this); diff --git a/lib/Doctrine/Hydrate.php b/lib/Doctrine/Hydrate.php index 91fe45a1c..5a213cdeb 100644 --- a/lib/Doctrine/Hydrate.php +++ b/lib/Doctrine/Hydrate.php @@ -1,933 +1,932 @@ -. - */ - -/** - * 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 implements Serializable -{ - /** - * 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 - * - * agg the aggregates of this component - */ - protected $_aliasMap = array(); - /** - * - */ - protected $pendingAggregates = array(); - /** - * @var array $aggregateMap an array containing all aggregate aliases, keys as dql aliases - * and values as sql aliases - */ - protected $aggregateMap = array(); - /** - * @var array $_options an array of options - */ - protected $_options = array( - 'fetchMode' => Doctrine::FETCH_RECORD, - 'parserCache' => false, - 'resultSetCache' => false, - ); - /** - * @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; - - protected $_cache; - - protected $_tableAliases = array(); - /** - * @var array $_tableAliasSeeds A simple array keys representing table aliases and values - * as table alias seeds. The seeds are used for generating short table - * aliases. - */ - protected $_tableAliasSeeds = array(); - /** - * constructor - * - * @param Doctrine_Connection|null $connection - */ - public function __construct($connection = null) - { - if ( ! ($connection instanceof Doctrine_Connection)) { - $connection = Doctrine_Manager::getInstance()->getCurrentConnection(); - } - $this->_conn = $connection; - } - - public function getSql() - { - return $this->getQuery(); - } - public function setCache(Doctrine_Cache_Interface $cache) - { - $this->_cache = $cache; - } - public function getCache() - { - return $this->_cache; - } - /** - * serialize - * this method is automatically called when this Doctrine_Hydrate is serialized - * - * @return array an array of serialized properties - */ - public function serialize() - { - $vars = get_object_vars($this); - - } - /** - * unseralize - * this method is automatically called everytime a Doctrine_Hydrate object is unserialized - * - * @param string $serialized Doctrine_Record as serialized string - * @return void - */ - public function unserialize($serialized) - { - - } - /** - * generateNewTableAlias - * generates a new alias from given table alias - * - * @param string $tableAlias table alias from which to generate the new alias from - * @return string the created table alias - */ - public function generateNewTableAlias($tableAlias) - { - if (isset($this->_tableAliases[$tableAlias])) { - // generate a new alias - $name = substr($tableAlias, 0, 1); - $i = ((int) substr($tableAlias, 1)); - - if ($i == 0) { - $i = 1; - } - - $newIndex = ($this->_tableAliasSeeds[$name] + $i); - - return $name . $newIndex; - } - - return $alias; - } - /** - * hasTableAlias - * whether or not this object has given tableAlias - * - * @param string $tableAlias the table alias to be checked - * @return boolean true if this object has given alias, otherwise false - */ - public function hasTableAlias($tableAlias) - { - return (isset($this->_tableAliases[$tableAlias])); - } - /** - * getComponentAlias - * get component alias associated with given table alias - * - * @param string $tableAlias the table alias that identifies the component alias - * @return string component alias - */ - public function getComponentAlias($tableAlias) - { - if ( ! isset($this->_tableAliases[$tableAlias])) { - throw new Doctrine_Hydrate_Exception('Unknown table alias ' . $tableAlias); - } - return $this->_tableAliases[$tableAlias]; - } - /** - * getTableAliasSeed - * returns the alias seed for given table alias - * - * @param string $tableAlias table alias that identifies the alias seed - * @return integer table alias seed - */ - public function getTableAliasSeed($tableAlias) - { - if ( ! isset($this->_tableAliasSeeds[$tableAlias])) { - return 0; - } - return $this->_tableAliasSeeds[$tableAlias]; - } - /** - * generateTableAlias - * generates a table alias from given table name and associates - * it with given component alias - * - * @param string $componentAlias the component alias to be associated with generated table alias - * @param string $tableName the table name from which to generate the table alias - * @return string the generated table alias - */ - public function generateTableAlias($componentAlias, $tableName) - { - $char = strtolower(substr($tableName, 0, 1)); - - $alias = $char; - - if ( ! isset($this->_tableAliasSeeds[$alias])) { - $this->_tableAliasSeeds[$alias] = 1; - } - while (isset($this->_tableAliases[$alias])) { - $alias = $char . ++$this->_tableAliasSeeds[$alias]; - } - - $this->_tableAliases[$alias] = $componentAlias; - - return $alias; - } - /** - * getTableAliases - * returns all table aliases - * - * @return array table aliases as an array - */ - public function getTableAliases() - { - return $this->_tableAliases; - } - /** - * addTableAlias - * adds an alias for table and associates it with given component alias - * - * @param string $componentAlias the alias for the query component associated with given tableAlias - * @param string $tableAlias the table alias to be added - * @return Doctrine_Hydrate - */ - public function addTableAlias($tableAlias, $componentAlias) - { - $this->_tableAliases[$tableAlias] = $componentAlias; - - return $this; - } - /** - * getTableAlias - * some database such as Oracle need the identifier lengths to be < ~30 chars - * hence Doctrine creates as short identifier aliases as possible - * - * this method is used for the creation of short table aliases, its also - * smart enough to check if an alias already exists for given component (componentAlias) - * - * @param string $componentAlias the alias for the query component to search table alias for - * @param string $tableName the table name from which the table alias is being created - * @return string the generated / fetched short alias - */ - public function getTableAlias($componentAlias, $tableName = null) - { - $alias = array_search($componentAlias, $this->_tableAliases); - - if ($alias !== false) { - return $alias; - } - - if ($tableName === null) { - throw new Doctrine_Hydrate_Exception("Couldn't get short alias for " . $componentAlias); - } - - return $this->generateTableAlias($componentAlias, $tableName); - } - /** - * addQueryPart - * adds a query part in the query part array - * - * @param string $name the name of the query part to be added - * @param string $part query part string - * @throws Doctrine_Hydrate_Exception if trying to add unknown query part - * @return Doctrine_Hydrate this object - */ - public function addQueryPart($name, $part) - { - if ( ! isset($this->parts[$name])) { - throw new Doctrine_Hydrate_Exception('Unknown query part ' . $name); - } - $this->parts[$name][] = $part; - - return $this; - } - /** - * setQueryPart - * sets a query part in the query part array - * - * @param string $name the name of the query part to be set - * @param string $part query part string - * @throws Doctrine_Hydrate_Exception if trying to set unknown query part - * @return Doctrine_Hydrate this object - */ - public function getQueryPart($part) - { - if ( ! isset($this->parts[$part])) { - throw new Doctrine_Hydrate_Exception('Unknown query part ' . $part); - } - - return $this->parts[$part]; - } - /** - * removeQueryPart - * removes a query part from the query part array - * - * @param string $name the name of the query part to be removed - * @throws Doctrine_Hydrate_Exception if trying to remove unknown query part - * @return Doctrine_Hydrate this object - */ - public function removeQueryPart($name) - { - if (isset($this->parts[$name])) { - if ($name == 'limit' || $name == 'offset') { - $this->parts[$name] = false; - } else { - $this->parts[$name] = array(); - } - } else { - throw new Doctrine_Hydrate_Exception('Unknown query part ' . $part); - } - return $this; - } - /** - * setQueryPart - * sets a query part in the query part array - * - * @param string $name the name of the query part to be set - * @param string $part query part string - * @throws Doctrine_Hydrate_Exception if trying to set unknown query part - * @return Doctrine_Hydrate this object - */ - 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; - } - - return $this; - } - /** - * getAliasDeclaration - * get the declaration for given component alias - * - * @param string $componentAlias the component alias the retrieve the declaration from - * @return array the alias declaration - */ - public function getAliasDeclaration($componentAlias) - { - if ( ! isset($this->_aliasMap[$componentAlias])) { - throw new Doctrine_Hydrate_Exception('Unknown component alias ' . $componentAlias); - } - - return $this->_aliasMap[$componentAlias]; - } - /** - * copyAliases - * copy aliases from another Hydrate object - * - * this method is needed by DQL subqueries which need the aliases - * of the parent query - * - * @param Doctrine_Hydrate $query the query object from which the - * aliases are copied from - * @return Doctrine_Hydrate this object - */ - public function copyAliases(Doctrine_Hydrate $query) - { - $this->_tableAliases = $query->_tableAliases; - - return $this; - } - /** - * createSubquery - * creates a subquery - * - * @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 - * whether or not limit subquery was used - * - * @return boolean - */ - public function isLimitSubqueryUsed() - { - return false; - } - /** - * 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; - } - /** - * 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; - } - public function convertEnums($params) - { - return $params; - } - /** - * setAliasMap - * sets the whole component alias map - * - * @param array $map alias map - * @return Doctrine_Hydrate this object - */ - public function setAliasMap(array $map) - { - $this->_aliasMap = $map; - - return $this; - } - /** - * getAliasMap - * returns the component alias map - * - * @return array component alias 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->_aliasMap[$alias]['agg'][$index])) { - $agg = $this->_aliasMap[$alias]['agg'][$index]; - } - $record->mapValue($agg, $value); - $found = true; - } - } - return $found; - } - public function getCachedForm(array $resultSet) - { - $map = ''; - - foreach ($this->getAliasMap() as $k => $v) { - if ( ! isset($v['parent'])) { - $map[$k][] = $v['table']->getComponentName(); - } else { - $map[$k][] = $v['parent'] . '.' . $v['relation']->getAlias(); - } - if (isset($v['agg'])) { - $map[$k][] = $v['agg']; - } - } - - return serialize(array($resultSet, $map, $this->getTableAliases())); - } - public function _execute($params, $return = 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); - } - - if ($this->type !== self::SELECT) { - return $this->_conn->exec($query, $params); - } - - $stmt = $this->_conn->execute($query, $params); - return (array) $this->parseData($stmt); - } - /** - * execute - * executes the query and populates the data set - * - * @param string $params - * @return Doctrine_Collection the root collection - */ - public function execute($params = array(), $return = Doctrine::FETCH_RECORD) - { - if ($this->_cache) { - $dql = $this->getDql(); - // calculate hash for dql query - $hash = strlen($dql) . md5($dql); - - $cached = $this->_cache->fetch($hash); - - if ($cached === null) { - // cache miss - $array = $this->_execute($params, $return); - - $cached = $this->getCachedForm($array); - - $this->_cache->save($hash, $cached); - } else { - $cached = unserialize($cached); - $this->_tableAliases = $cached[2]; - $array = $cached[0]; - - $map = array(); - foreach ($cached[1] as $k => $v) { - $e = explode('.', $v[0]); - if (count($e) === 1) { - $map[$k]['table'] = $this->_conn->getTable($e[0]); - } else { - $map[$k]['parent'] = $e[0]; - $map[$k]['relation'] = $map[$e[0]]['table']->getRelation($e[1]); - $map[$k]['table'] = $map[$k]['relation']->getTable(); - } - if (isset($v[1])) { - $map[$k]['agg'] = $v[1]; - } - } - $this->_aliasMap = $map; - } - } else { - $array = $this->_execute($params, $return); - } - - if ($return === Doctrine::FETCH_ARRAY) { - return $array; - } - - 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->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()); - } -} +. + */ + +/** + * 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 implements Serializable +{ + /** + * 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 + * + * agg the aggregates of this component + */ + protected $_aliasMap = array(); + /** + * + */ + protected $pendingAggregates = array(); + /** + * @var array $aggregateMap an array containing all aggregate aliases, keys as dql aliases + * and values as sql aliases + */ + protected $aggregateMap = array(); + /** + * @var array $_options an array of options + */ + protected $_options = array( + 'fetchMode' => Doctrine::FETCH_RECORD, + 'parserCache' => false, + 'resultSetCache' => false, + ); + /** + * @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; + + protected $_cache; + + protected $_tableAliases = array(); + /** + * @var array $_tableAliasSeeds A simple array keys representing table aliases and values + * as table alias seeds. The seeds are used for generating short table + * aliases. + */ + protected $_tableAliasSeeds = array(); + /** + * constructor + * + * @param Doctrine_Connection|null $connection + */ + public function __construct($connection = null) + { + if ( ! ($connection instanceof Doctrine_Connection)) { + $connection = Doctrine_Manager::getInstance()->getCurrentConnection(); + } + $this->_conn = $connection; + } + + public function getSql() + { + return $this->getQuery(); + } + public function setCache(Doctrine_Cache_Interface $cache) + { + $this->_cache = $cache; + } + public function getCache() + { + return $this->_cache; + } + /** + * serialize + * this method is automatically called when this Doctrine_Hydrate is serialized + * + * @return array an array of serialized properties + */ + public function serialize() + { + $vars = get_object_vars($this); + + } + /** + * unseralize + * this method is automatically called everytime a Doctrine_Hydrate object is unserialized + * + * @param string $serialized Doctrine_Record as serialized string + * @return void + */ + public function unserialize($serialized) + { + + } + /** + * generateNewTableAlias + * generates a new alias from given table alias + * + * @param string $tableAlias table alias from which to generate the new alias from + * @return string the created table alias + */ + public function generateNewTableAlias($tableAlias) + { + if (isset($this->_tableAliases[$tableAlias])) { + // generate a new alias + $name = substr($tableAlias, 0, 1); + $i = ((int) substr($tableAlias, 1)); + + if ($i == 0) { + $i = 1; + } + + $newIndex = ($this->_tableAliasSeeds[$name] + $i); + + return $name . $newIndex; + } + + return $alias; + } + /** + * hasTableAlias + * whether or not this object has given tableAlias + * + * @param string $tableAlias the table alias to be checked + * @return boolean true if this object has given alias, otherwise false + */ + public function hasTableAlias($tableAlias) + { + return (isset($this->_tableAliases[$tableAlias])); + } + /** + * getComponentAlias + * get component alias associated with given table alias + * + * @param string $tableAlias the table alias that identifies the component alias + * @return string component alias + */ + public function getComponentAlias($tableAlias) + { + if ( ! isset($this->_tableAliases[$tableAlias])) { + throw new Doctrine_Hydrate_Exception('Unknown table alias ' . $tableAlias); + } + return $this->_tableAliases[$tableAlias]; + } + /** + * getTableAliasSeed + * returns the alias seed for given table alias + * + * @param string $tableAlias table alias that identifies the alias seed + * @return integer table alias seed + */ + public function getTableAliasSeed($tableAlias) + { + if ( ! isset($this->_tableAliasSeeds[$tableAlias])) { + return 0; + } + return $this->_tableAliasSeeds[$tableAlias]; + } + /** + * generateTableAlias + * generates a table alias from given table name and associates + * it with given component alias + * + * @param string $componentAlias the component alias to be associated with generated table alias + * @param string $tableName the table name from which to generate the table alias + * @return string the generated table alias + */ + public function generateTableAlias($componentAlias, $tableName) + { + $char = strtolower(substr($tableName, 0, 1)); + + $alias = $char; + + if ( ! isset($this->_tableAliasSeeds[$alias])) { + $this->_tableAliasSeeds[$alias] = 1; + } + while (isset($this->_tableAliases[$alias])) { + $alias = $char . ++$this->_tableAliasSeeds[$alias]; + } + + $this->_tableAliases[$alias] = $componentAlias; + + return $alias; + } + /** + * getTableAliases + * returns all table aliases + * + * @return array table aliases as an array + */ + public function getTableAliases() + { + return $this->_tableAliases; + } + /** + * addTableAlias + * adds an alias for table and associates it with given component alias + * + * @param string $componentAlias the alias for the query component associated with given tableAlias + * @param string $tableAlias the table alias to be added + * @return Doctrine_Hydrate + */ + public function addTableAlias($tableAlias, $componentAlias) + { + $this->_tableAliases[$tableAlias] = $componentAlias; + + return $this; + } + /** + * getTableAlias + * some database such as Oracle need the identifier lengths to be < ~30 chars + * hence Doctrine creates as short identifier aliases as possible + * + * this method is used for the creation of short table aliases, its also + * smart enough to check if an alias already exists for given component (componentAlias) + * + * @param string $componentAlias the alias for the query component to search table alias for + * @param string $tableName the table name from which the table alias is being created + * @return string the generated / fetched short alias + */ + public function getTableAlias($componentAlias, $tableName = null) + { + $alias = array_search($componentAlias, $this->_tableAliases); + + if ($alias !== false) { + return $alias; + } + + if ($tableName === null) { + throw new Doctrine_Hydrate_Exception("Couldn't get short alias for " . $componentAlias); + } + + return $this->generateTableAlias($componentAlias, $tableName); + } + /** + * addQueryPart + * adds a query part in the query part array + * + * @param string $name the name of the query part to be added + * @param string $part query part string + * @throws Doctrine_Hydrate_Exception if trying to add unknown query part + * @return Doctrine_Hydrate this object + */ + public function addQueryPart($name, $part) + { + if ( ! isset($this->parts[$name])) { + throw new Doctrine_Hydrate_Exception('Unknown query part ' . $name); + } + $this->parts[$name][] = $part; + + return $this; + } + /** + * setQueryPart + * sets a query part in the query part array + * + * @param string $name the name of the query part to be set + * @param string $part query part string + * @throws Doctrine_Hydrate_Exception if trying to set unknown query part + * @return Doctrine_Hydrate this object + */ + public function getQueryPart($part) + { + if ( ! isset($this->parts[$part])) { + throw new Doctrine_Hydrate_Exception('Unknown query part ' . $part); + } + + return $this->parts[$part]; + } + /** + * removeQueryPart + * removes a query part from the query part array + * + * @param string $name the name of the query part to be removed + * @throws Doctrine_Hydrate_Exception if trying to remove unknown query part + * @return Doctrine_Hydrate this object + */ + public function removeQueryPart($name) + { + if (isset($this->parts[$name])) { + if ($name == 'limit' || $name == 'offset') { + $this->parts[$name] = false; + } else { + $this->parts[$name] = array(); + } + } else { + throw new Doctrine_Hydrate_Exception('Unknown query part ' . $part); + } + return $this; + } + /** + * setQueryPart + * sets a query part in the query part array + * + * @param string $name the name of the query part to be set + * @param string $part query part string + * @throws Doctrine_Hydrate_Exception if trying to set unknown query part + * @return Doctrine_Hydrate this object + */ + 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; + } + + return $this; + } + /** + * getAliasDeclaration + * get the declaration for given component alias + * + * @param string $componentAlias the component alias the retrieve the declaration from + * @return array the alias declaration + */ + public function getAliasDeclaration($componentAlias) + { + if ( ! isset($this->_aliasMap[$componentAlias])) { + throw new Doctrine_Hydrate_Exception('Unknown component alias ' . $componentAlias); + } + + return $this->_aliasMap[$componentAlias]; + } + /** + * copyAliases + * copy aliases from another Hydrate object + * + * this method is needed by DQL subqueries which need the aliases + * of the parent query + * + * @param Doctrine_Hydrate $query the query object from which the + * aliases are copied from + * @return Doctrine_Hydrate this object + */ + public function copyAliases(Doctrine_Hydrate $query) + { + $this->_tableAliases = $query->_tableAliases; + + return $this; + } + /** + * createSubquery + * creates a subquery + * + * @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 + * whether or not limit subquery was used + * + * @return boolean + */ + public function isLimitSubqueryUsed() + { + return false; + } + /** + * 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; + } + /** + * 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; + } + public function convertEnums($params) + { + return $params; + } + /** + * setAliasMap + * sets the whole component alias map + * + * @param array $map alias map + * @return Doctrine_Hydrate this object + */ + public function setAliasMap(array $map) + { + $this->_aliasMap = $map; + + return $this; + } + /** + * getAliasMap + * returns the component alias map + * + * @return array component alias 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->_aliasMap[$alias]['agg'][$index])) { + $agg = $this->_aliasMap[$alias]['agg'][$index]; + } + $record->mapValue($agg, $value); + $found = true; + } + } + return $found; + } + public function getCachedForm(array $resultSet) + { + $map = ''; + + foreach ($this->getAliasMap() as $k => $v) { + if ( ! isset($v['parent'])) { + $map[$k][] = $v['table']->getComponentName(); + } else { + $map[$k][] = $v['parent'] . '.' . $v['relation']->getAlias(); + } + if (isset($v['agg'])) { + $map[$k][] = $v['agg']; + } + } + + return serialize(array($resultSet, $map, $this->getTableAliases())); + } + public function _execute($params, $return = 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); + } + + if ($this->type !== self::SELECT) { + return $this->_conn->exec($query, $params); + } + + $stmt = $this->_conn->execute($query, $params); + return (array) $this->parseData($stmt); + } + /** + * execute + * executes the query and populates the data set + * + * @param string $params + * @return Doctrine_Collection the root collection + */ + public function execute($params = array(), $return = Doctrine::FETCH_RECORD) + { + if ($this->_cache) { + $dql = $this->getDql(); + // calculate hash for dql query + $hash = strlen($dql) . md5($dql); + + $cached = $this->_cache->fetch($hash); + + if ($cached === null) { + // cache miss + $array = $this->_execute($params, $return); + + $cached = $this->getCachedForm($array); + + $this->_cache->save($hash, $cached); + } else { + $cached = unserialize($cached); + $this->_tableAliases = $cached[2]; + $array = $cached[0]; + + $map = array(); + foreach ($cached[1] as $k => $v) { + $e = explode('.', $v[0]); + if (count($e) === 1) { + $map[$k]['table'] = $this->_conn->getTable($e[0]); + } else { + $map[$k]['parent'] = $e[0]; + $map[$k]['relation'] = $map[$e[0]]['table']->getRelation($e[1]); + $map[$k]['table'] = $map[$k]['relation']->getTable(); + } + if (isset($v[1])) { + $map[$k]['agg'] = $v[1]; + } + } + $this->_aliasMap = $map; + } + } else { + $array = $this->_execute($params, $return); + } + + if ($return === Doctrine::FETCH_ARRAY) { + return $array; + } + + 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->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(); + $cache = array(); + + while ($data = $stmt->fetch(PDO::FETCH_ASSOC)) { + foreach ($data as $key => $value) { + if (!isset($cache[$key])) { + $e = explode('__', $key); + $cache[$key]['field'] = strtolower(array_pop($e)); + $cache[$key]['component'] = strtolower(implode('__', $e)); + } + + $data[$cache[$key]['component']][$cache[$key]['field']] = $value; + + unset($data[$key]); + }; + $array[] = $data; + }; + $stmt->closeCursor(); + unset($cache); + return $array; + } + /** + * @return string returns a string representation of this object + */ + public function __toString() + { + return Doctrine_Lib::formatSql($this->getQuery()); + } +} diff --git a/lib/Doctrine/Table.php b/lib/Doctrine/Table.php index 115ae4df8..0ec4ed3d1 100644 --- a/lib/Doctrine/Table.php +++ b/lib/Doctrine/Table.php @@ -1,1226 +1,1227 @@ -. - */ -/** - * Doctrine_Table represents a database table - * each Doctrine_Table holds the information of foreignKeys and associations - * - * - * @author Konsta Vesterinen - * @package Doctrine - * @license http://www.opensource.org/licenses/lgpl-license.php LGPL - * @version $Revision$ - * @category Object Relational Mapping - * @link www.phpdoctrine.com - * @since 1.0 - */ -class Doctrine_Table extends Doctrine_Configurable implements Countable -{ - /** - * @var array $data temporary data which is then loaded into Doctrine_Record::$data - */ - private $data = array(); - /** - * @var array $primaryKeys an array containing all primary key column names - */ - private $primaryKeys = array(); - /** - * @var mixed $identifier - */ - private $identifier; - /** - * @see Doctrine_Identifier constants - * @var integer $identifierType the type of identifier this table uses - */ - private $identifierType; - /** - * @var string $query cached simple query - */ - private $query; - /** - * @var Doctrine_Connection $conn Doctrine_Connection object that created this table - */ - private $conn; - /** - * @var string $name - */ - private $name; - /** - * @var array $identityMap first level cache - */ - private $identityMap = array(); - /** - * @var Doctrine_Table_Repository $repository record repository - */ - private $repository; - /** - * @var array $columns an array of column definitions, - * keys as column names and values as column definitions - * - * the value array has three values: - * - * the column type, eg. 'integer' - * the column length, eg. 11 - * the column options/constraints/validators. eg array('notnull' => true) - * - * so the full columns array might look something like the following: - * array( - * 'name' => array('string', 20, array('notnull' => true, 'default' => 'someone')), - * 'age' => array('integer', 11, array('notnull' => true)) - * ) - */ - protected $columns = array(); - /** - * @var array $columnAliases an array of column aliases - * keys as column aliases and values as column names - */ - protected $columnAliases = array(); - /** - * @var integer $columnCount cached column count, Doctrine_Record uses this column count in when - * determining its state - */ - private $columnCount; - /** - * @var boolean $hasDefaultValues whether or not this table has default values - */ - private $hasDefaultValues; - /** - * @var array $options an array containing all options - * - * -- name name of the component, for example component name of the GroupTable is 'Group' - * - * -- parents the parent classes of this component - * - * -- declaringClass name of the table definition declaring class (when using inheritance the class - * that defines the table structure can be any class in the inheritance hierarchy, - * hence we need reflection to check out which class actually calls setTableDefinition) - * - * -- tableName database table name, in most cases this is the same as component name but in some cases - * where one-table-multi-class inheritance is used this will be the name of the inherited table - * - * -- sequenceName Some databases need sequences instead of auto incrementation primary keys, - * you can set specific sequence for your table by calling setOption('sequenceName', $seqName) - * where $seqName is the name of the desired sequence - * - * -- enumMap enum value arrays - * - * -- inheritanceMap inheritanceMap is used for inheritance mapping, keys representing columns and values - * the column values that should correspond to child classes - * - * -- type table type (mysql example: INNODB) - * - * -- charset character set - * - * -- foreignKeys the foreign keys of this table - * - * -- collation - * - * -- indexes the index definitions of this table - * - * -- treeImpl the tree implementation of this table (if any) - * - * -- treeOptions the tree options - */ - protected $options = array('name' => null, - 'tableName' => null, - 'sequenceName' => null, - 'inheritanceMap' => array(), - 'enumMap' => array(), - 'engine' => null, - 'charset' => null, - 'collation' => null, - 'treeImpl' => null, - 'treeOptions' => null, - 'indexes' => array(), - 'parents' => array(), - ); - /** - * @var Doctrine_Tree $tree tree object associated with this table - */ - protected $tree; - /** - * @var Doctrine_Relation_Parser $_parser relation parser object - */ - protected $_parser; - /** - * the constructor - * @throws Doctrine_Connection_Exception if there are no opened connections - * @throws Doctrine_Table_Exception if there is already an instance of this table - * @return void - */ - public function __construct($name, Doctrine_Connection $conn) - { - $this->conn = $conn; - - $this->setParent($this->conn); - - $this->options['name'] = $name; - $this->_parser = new Doctrine_Relation_Parser($this); - - if ( ! class_exists($name) || empty($name)) { - throw new Doctrine_Exception("Couldn't find class " . $name); - } - $record = new $name($this); - - $names = array(); - - $class = $name; - - // get parent classes - - do { - if ($class == "Doctrine_Record") { - break; - } - - $name = $class; - $names[] = $name; - } while ($class = get_parent_class($class)); - - // reverse names - $names = array_reverse($names); - // save parents - array_pop($names); - $this->options['parents'] = $names; - - // create database table - if (method_exists($record, 'setTableDefinition')) { - $record->setTableDefinition(); - - // set the table definition for the given tree implementation - if ($this->isTree()) { - $this->getTree()->setTableDefinition(); - } - - $this->columnCount = count($this->columns); - - if (isset($this->columns)) { - // get the declaring class of setTableDefinition method - $method = new ReflectionMethod($this->options['name'], 'setTableDefinition'); - $class = $method->getDeclaringClass(); - - $this->options['declaringClass'] = $class; - - if ( ! isset($this->options['tableName'])) { - $this->options['tableName'] = Doctrine::tableize($class->getName()); - } - switch (count($this->primaryKeys)) { - case 0: - $this->columns = array_merge(array('id' => - array('integer', - 20, - array('autoincrement' => true, - 'primary' => true, - ) - ) - ), $this->columns); - - $this->primaryKeys[] = 'id'; - $this->identifier = 'id'; - $this->identifierType = Doctrine_Identifier::AUTO_INCREMENT; - $this->columnCount++; - break; - default: - if (count($this->primaryKeys) > 1) { - $this->identifier = $this->primaryKeys; - $this->identifierType = Doctrine_Identifier::COMPOSITE; - - } else { - foreach ($this->primaryKeys as $pk) { - $e = $this->columns[$pk][2]; - - $found = false; - - foreach ($e as $option => $value) { - if ($found) - break; - - $e2 = explode(':', $option); - - switch (strtolower($e2[0])) { - case 'autoincrement': - case 'autoinc': - $this->identifierType = Doctrine_Identifier::AUTO_INCREMENT; - $found = true; - break; - case 'seq': - case 'sequence': - $this->identifierType = Doctrine_Identifier::SEQUENCE; - $found = true; - - if ($value) { - $this->options['sequenceName'] = $value; - } else { - if (($sequence = $this->getAttribute(Doctrine::ATTR_DEFAULT_SEQUENCE)) !== null) { - $this->options['sequenceName'] = $sequence; - } else { - $this->options['sequenceName'] = $this->conn->getSequenceName($this->options['tableName']); - } - } - break; - } - } - if ( ! isset($this->identifierType)) { - $this->identifierType = Doctrine_Identifier::NORMAL; - } - $this->identifier = $pk; - } - } - } - } - } else { - throw new Doctrine_Table_Exception("Class '$name' has no table definition."); - } - - $record->setUp(); - - // if tree, set up tree - if ($this->isTree()) { - $this->getTree()->setUp(); - } - $this->repository = new Doctrine_Table_Repository($this); - } - /** - * export - * exports this table to database based on column and option definitions - * - * @throws Doctrine_Connection_Exception if some error other than Doctrine::ERR_ALREADY_EXISTS - * occurred during the create table operation - * @return boolean whether or not the export operation was successful - * false if table already existed in the database - */ - public function export() - { - if ( ! Doctrine::isValidClassname($this->options['declaringClass']->getName())) { - throw new Doctrine_Table_Exception('Class name not valid.'); - } - - try { - $columns = array(); - $primary = array(); - - foreach ($this->columns as $name => $column) { - $definition = $column[2]; - $definition['type'] = $column[0]; - $definition['length'] = $column[1]; - - switch ($definition['type']) { - case 'enum': - if (isset($definition['default'])) { - $definition['default'] = $this->enumIndex($name, $definition['default']); - } - break; - case 'boolean': - if (isset($definition['default'])) { - $definition['default'] = $this->conn->convertBooleans($definition['default']); - } - break; - } - $columns[$name] = $definition; - - if(isset($definition['primary']) && $definition['primary']) { - $primary[] = $name; - } - } - - if ($this->getAttribute(Doctrine::ATTR_EXPORT) & Doctrine::EXPORT_CONSTRAINTS) { - - foreach ($this->getRelations() as $name => $relation) { - $fk = $relation->toArray(); - $fk['foreignTable'] = $relation->getTable()->getTableName(); - - if ($relation->getTable() === $this && in_array($relation->getLocal(), $primary)) { - continue; - } - - if ($relation->hasConstraint()) { - - $options['foreignKeys'][] = $fk; - } elseif ($relation instanceof Doctrine_Relation_LocalKey) { - $options['foreignKeys'][] = $fk; - } - } - } - - $options['primary'] = $primary; - - $this->conn->export->createTable($this->options['tableName'], $columns, array_merge($this->options, $options)); - } catch(Doctrine_Connection_Exception $e) { - // we only want to silence table already exists errors - if($e->getPortableCode() !== Doctrine::ERR_ALREADY_EXISTS) { - throw $e; - } - } - } - /** - * exportConstraints - * exports the constraints of this table into database based on option definitions - * - * @throws Doctrine_Connection_Exception if something went wrong on db level - * @return void - */ - public function exportConstraints() - { - try { - $this->conn->beginTransaction(); - - foreach ($this->options['index'] as $index => $definition) { - $this->conn->export->createIndex($this->options['tableName'], $index, $definition); - } - $this->conn->commit(); - } catch(Doctrine_Connection_Exception $e) { - $this->conn->rollback(); - - throw $e; - } - } - /** - * getRelationParser - * return the relation parser associated with this table - * - * @return Doctrine_Relation_Parser relation parser object - */ - public function getRelationParser() - { - return $this->_parser; - } - /** - * __get - * an alias for getOption - * - * @param string $option - */ - public function __get($option) - { - if (isset($this->options[$option])) { - return $this->options[$option]; - } - return null; - } - /** - * __isset - * - * @param string $option - */ - public function __isset($option) - { - return isset($this->options[$option]); - } - /** - * addForeignKey - * - * adds a foreignKey to this table - * - * @return void - */ - public function addForeignKey(array $definition) - { - $this->options['foreignKeys'][] = $definition; - } - /** - * addIndex - * - * adds an index to this table - * - * @return void - */ - public function addIndex($index, array $definition) - { - $index = $this->conn->getIndexName($index); - $this->options['indexes'][$index] = $definition; - } - /** - * getIndex - * - * @return array|boolean array on success, FALSE on failure - */ - public function getIndex($index) - { - if (isset($this->options['indexes'][$index])) { - return $this->options['indexes'][$index]; - } - - return false; - } - public function bind($args, $type) - { - $options = array(); - $options['type'] = $type; - - // the following is needed for backwards compatibility - if (is_string($args[1])) { - if ( ! isset($args[2])) { - $args[2] = array(); - } elseif (is_string($args[2])) { - $args[2] = (array) $args[2]; - } - - $classes = array_merge($this->options['parents'], array($this->getComponentName())); - - - $e = explode('.', $args[1]); - if (in_array($e[0], $classes)) { - if ($options['type'] >= Doctrine_Relation::MANY) { - $options['foreign'] = $e[1]; - } else { - $options['local'] = $e[1]; - } - } else { - $e2 = explode(' as ', $args[0]); - if ($e[0] !== $e2[0] && ( ! isset($e2[1]) || $e[0] !== $e2[1])) { - $options['refClass'] = $e[0]; - } - - $options['foreign'] = $e[1]; - } - - $options = array_merge($args[2], $options); - - $this->_parser->bind($args[0], $options); - } else { - $options = array_merge($args[1], $options); - $this->_parser->bind($args[0], $options); - } - } - /** - * getRelation - * - * @param string $alias relation alias - */ - public function getRelation($alias, $recursive = true) - { - return $this->_parser->getRelation($alias, $recursive); - } - /** - * getRelations - * returns an array containing all relation objects - * - * @return array an array of Doctrine_Relation objects - */ - public function getRelations() - { - return $this->_parser->getRelations(); - } - /** - * createQuery - * creates a new Doctrine_Query object and adds the component name - * of this table as the query 'from' part - * - * @return Doctrine_Query - */ - public function createQuery() - { - return Doctrine_Query::create()->from($this->getComponentName()); - } - /** - * getRepository - * - * @return Doctrine_Table_Repository - */ - public function getRepository() - { - return $this->repository; - } - /** - * setOption - * sets an option and returns this object in order to - * allow flexible method chaining - * - * @see Doctrine_Table::$_options for available options - * @param string $name the name of the option to set - * @param mixed $value the value of the option - * @return Doctrine_Table this object - */ - public function setOption($name, $value) - { - switch ($name) { - case 'name': - case 'tableName': - break; - case 'enumMap': - case 'inheritanceMap': - case 'index': - case 'treeOptions': - if ( ! is_array($value)) { - throw new Doctrine_Table_Exception($name . ' should be an array.'); - } - break; - } - $this->options[$name] = $value; - } - /** - * getOption - * returns the value of given option - * - * @param string $name the name of the option - * @return mixed the value of given option - */ - public function getOption($name) - { - if (isset($this->options[$name])) { - return $this->options[$name]; - } - return null; - } - /** - * getColumnName - * - * returns a column name for column alias - * if the actual name for the alias cannot be found - * this method returns the given alias - * - * @param string $alias column alias - * @return string column name - */ - public function getColumnName($alias) - { - if(isset($this->columnAliases[$alias])) { - return $this->columnAliases[$alias]; - } - - return $alias; - } - /** - * setColumn - * - * @param string $name - * @param string $type - * @param integer $length - * @param mixed $options - * @throws Doctrine_Table_Exception if trying use wrongly typed parameter - * @return void - */ - public function setColumn($name, $type, $length = null, $options = array()) - { - if (is_string($options)) { - $options = explode('|', $options); - } - - foreach ($options as $k => $option) { - if (is_numeric($k)) { - if ( ! empty($option)) { - $options[$option] = true; - } - unset($options[$k]); - } - } - - $name = strtolower($name); - $parts = explode(' as ', $name); - - if (count($parts) > 1) { - $this->columnAliases[$parts[1]] = $parts[0]; - $name = $parts[0]; - } - - - if ($length == null) { - $length = 2147483647; - } - - if ((string) (int) $length !== (string) $length) { - throw new Doctrine_Table_Exception('Invalid argument given for column length'); - } - - $this->columns[$name] = array($type, $length, $options); - - if (isset($options['primary'])) { - $this->primaryKeys[] = $name; - } - if (isset($options['default'])) { - $this->hasDefaultValues = true; - } - } - /** - * hasDefaultValues - * returns true if this table has default values, otherwise false - * - * @return boolean - */ - public function hasDefaultValues() - { - return $this->hasDefaultValues; - } - /** - * getDefaultValueOf - * returns the default value(if any) for given column - * - * @param string $column - * @return mixed - */ - public function getDefaultValueOf($column) - { - $column = strtolower($column); - if ( ! isset($this->columns[$column])) { - throw new Doctrine_Table_Exception("Couldn't get default value. Column ".$column." doesn't exist."); - } - if (isset($this->columns[$column][2]['default'])) { - return $this->columns[$column][2]['default']; - } else { - return null; - } - } - /** - * @return mixed - */ - public function getIdentifier() - { - return $this->identifier; - } - /** - * @return integer - */ - public function getIdentifierType() - { - return $this->identifierType; - } - /** - * hasColumn - * @return boolean - */ - public function hasColumn($name) - { - return isset($this->columns[$name]); - } - /** - * @param mixed $key - * @return void - */ - public function setPrimaryKey($key) - { - switch (gettype($key)) { - case "array": - $this->primaryKeys = array_values($key); - break; - case "string": - $this->primaryKeys[] = $key; - break; - }; - } - /** - * returns all primary keys - * @return array - */ - public function getPrimaryKeys() - { - return $this->primaryKeys; - } - /** - * @return boolean - */ - public function hasPrimaryKey($key) - { - return in_array($key,$this->primaryKeys); - } - /** - * @return Doctrine_Connection - */ - public function getConnection() - { - return $this->conn; - } - /** - * create - * creates a new record - * - * @param $array an array where keys are field names and values representing field values - * @return Doctrine_Record - */ - public function create(array $array = array()) { - $this->data = $array; - $record = new $this->options['name']($this, true); - $this->data = array(); - return $record; - } - /** - * finds a record by its identifier - * - * @param $id database row id - * @return Doctrine_Record|false a record for given database identifier - */ - public function find($id) - { - if ($id !== null) { - if ( ! is_array($id)) { - $id = array($id); - } else { - $id = array_values($id); - } - - $query = 'SELECT ' . implode(', ', array_keys($this->columns)) . ' FROM ' . $this->getTableName() - . ' WHERE ' . implode(' = ? AND ', $this->primaryKeys) . ' = ?'; - $query = $this->applyInheritance($query); - - $params = array_merge($id, array_values($this->options['inheritanceMap'])); - - $stmt = $this->conn->execute($query, $params); - - $this->data = $stmt->fetch(PDO::FETCH_ASSOC); - - if ($this->data === false) - return false; - - return $this->getRecord(); - } - return false; - } - /** - * applyInheritance - * @param $where query where part to be modified - * @return string query where part with column aggregation inheritance added - */ - final public function applyInheritance($where) - { - if ( ! empty($this->options['inheritanceMap'])) { - $a = array(); - foreach ($this->options['inheritanceMap'] as $field => $value) { - $a[] = $field . ' = ?'; - } - $i = implode(' AND ', $a); - $where .= ' AND ' . $i; - } - return $where; - } - /** - * findAll - * returns a collection of records - * - * @return Doctrine_Collection - */ - public function findAll() - { - $graph = new Doctrine_Query($this->conn); - $users = $graph->query("FROM ".$this->options['name']); - return $users; - } - /** - * findByDql - * finds records with given DQL where clause - * returns a collection of records - * - * @param string $dql DQL after WHERE clause - * @param array $params query parameters - * @return Doctrine_Collection - */ - public function findBySql($dql, array $params = array()) { - $q = new Doctrine_Query($this->conn); - $users = $q->query("FROM ".$this->options['name']." WHERE ".$dql, $params); - return $users; - } - - public function findByDql($dql, array $params = array()) { - return $this->findBySql($dql, $params); - } - /** - * clear - * clears the first level cache (identityMap) - * - * @return void - */ - public function clear() - { - $this->identityMap = array(); - } - /** - * getRecord - * first checks if record exists in identityMap, if not - * returns a new record - * - * @return Doctrine_Record - */ - public function getRecord() - { - if ( ! empty($this->data)) { - $this->data = array_change_key_case($this->data, CASE_LOWER); - - $key = $this->getIdentifier(); - - if ( ! is_array($key)) { - $key = array($key); - } - - foreach ($key as $k) { - if ( ! isset($this->data[$k])) { - throw new Doctrine_Table_Exception("Primary key value for $k wasn't found"); - } - $id[] = $this->data[$k]; - } - - $id = implode(' ', $id); - - if (isset($this->identityMap[$id])) { - $record = $this->identityMap[$id]; - $record->hydrate($this->data); - } else { - $recordName = $this->getClassnameToReturn(); - $record = new $recordName($this); - $this->identityMap[$id] = $record; - } - $this->data = array(); - } else { - $recordName = $this->getClassnameToReturn(); - $record = new $recordName($this, true); - } - - - return $record; - } - - /** - * Get the classname to return. Most often this is just the options['name'] - * - * Check the subclasses option and the inheritanceMap for each subclass to see - * if all the maps in a subclass is met. If this is the case return that - * subclass name. If no subclasses match or if there are no subclasses defined - * return the name of the class for this tables record. - * - * @todo this function could use reflection to check the first time it runs - * if the subclassing option is not set. - * - * @return string The name of the class to create - * - */ - public function getClassnameToReturn() - { - if (!isset($this->options['subclasses'])) { - return $this->options['name']; - } - foreach ($this->options['subclasses'] as $subclass) { - $table = $this->conn->getTable($subclass); - $inheritanceMap = $table->getOption('inheritanceMap'); - $nomatch = false; - foreach ($inheritanceMap as $key => $value) { - if (!isset($this->data[$key]) || $this->data[$key] != $value) { - $nomatch = true; - break; - } - } - if ( ! $nomatch) { - return $table->getComponentName(); - } - } - return $this->options['name']; - } - - /** - * @param $id database row id - * @throws Doctrine_Find_Exception - */ - final public function getProxy($id = null) - { - if ($id !== null) { - $query = 'SELECT ' . implode(', ',$this->primaryKeys) - . ' FROM ' . $this->getTableName() - . ' WHERE ' . implode(' = ? && ',$this->primaryKeys).' = ?'; - $query = $this->applyInheritance($query); - - $params = array_merge(array($id), array_values($this->options['inheritanceMap'])); - - $this->data = $this->conn->execute($query,$params)->fetch(PDO::FETCH_ASSOC); - - if ($this->data === false) - return false; - } - return $this->getRecord(); - } - /** - * count - * - * @return integer - */ - public function count() - { - $a = $this->conn->getDBH()->query("SELECT COUNT(1) FROM ".$this->options['tableName'])->fetch(PDO::FETCH_NUM); - return current($a); - } - /** - * @return Doctrine_Query a Doctrine_Query object - */ - public function getQueryObject() - { - $graph = new Doctrine_Query($this->getConnection()); - $graph->load($this->getComponentName()); - return $graph; - } - /** - * execute - * @param string $query - * @param array $array - * @param integer $limit - * @param integer $offset - */ - public function execute($query, array $array = array(), $limit = null, $offset = null) { - $coll = new Doctrine_Collection($this); - $query = $this->conn->modifyLimitQuery($query,$limit,$offset); - if ( ! empty($array)) { - $stmt = $this->conn->getDBH()->prepare($query); - $stmt->execute($array); - } else { - $stmt = $this->conn->getDBH()->query($query); - } - $data = $stmt->fetchAll(PDO::FETCH_ASSOC); - $stmt->closeCursor(); - - foreach ($data as $row) { - $this->data = $row; - $record = $this->getRecord(); - $coll->add($record); - } - return $coll; - } - /** - * @param string $field - * @return array - */ - final public function getEnumValues($field) - { - if (isset($this->columns[$field][2]['values'])) { - return $this->columns[$field][2]['values']; - } else { - return array(); - } - } - /** - * enumValue - * - * @param string $field - * @param integer $index - * @return mixed - */ - public function enumValue($field, $index) - { - if ($index instanceof Doctrine_Null) - return $index; - - return isset($this->columns[$field][2]['values'][$index]) ? $this->columns[$field][2]['values'][$index] : $index; - } - /** - * enumIndex - * - * @param string $field - * @param mixed $value - * @return mixed - */ - public function enumIndex($field, $value) - { - $values = $this->getEnumValues($field); - - return array_search($value, $values); - } - /** - * invokeSet - * - * @param mixed $value - */ - public function invokeSet(Doctrine_Record $record, $name, $value) - { - if ( ! ($this->getAttribute(Doctrine::ATTR_ACCESSORS) & Doctrine::ACCESSOR_SET)) { - return $value; - } - $prefix = $this->getAttribute(Doctrine::ATTR_ACCESSOR_PREFIX_SET); - if (!$prefix) - $prefix = 'set'; - - $method = $prefix . $name; - - if (method_exists($record, $method)) { - return $record->$method($value); - } - - return $value; - } - /** - * invokeGet - * - * @param mixed $value - */ - public function invokeGet(Doctrine_Record $record, $name, $value) - { - if ( ! ($this->getAttribute(Doctrine::ATTR_ACCESSORS) & Doctrine::ACCESSOR_GET)) { - return $value; - } - $prefix = $this->getAttribute(Doctrine::ATTR_ACCESSOR_PREFIX_GET); - if (!$prefix) - $prefix = 'get'; - - $method = $prefix . $name; - - if (method_exists($record, $method)) { - return $record->$method($value); - } - - return $value; - } - - /** - * getDefinitionOf - * - * @return string ValueWrapper class name on success, false on failure - */ - public function getValueWrapperOf($column) - { - if (isset($this->columns[$column][2]['wrapper'])) { - return $this->columns[$column][2]['wrapper']; - } - return false; - } - /** - * getColumnCount - * - * @return integer the number of columns in this table - */ - final public function getColumnCount() - { - return $this->columnCount; - } - - /** - * returns all columns and their definitions - * - * @return array - */ - final public function getColumns() - { - return $this->columns; - } - /** - * returns an array containing all the column names - * - * @return array - */ - public function getColumnNames() - { - return array_keys($this->columns); - } - /** - * getDefinitionOf - * - * @return mixed array on success, false on failure - */ - public function getDefinitionOf($column) - { - if (isset($this->columns[$column])) { - return $this->columns[$column]; - } - return false; - } - /** - * getTypeOf - * - * @return mixed string on success, false on failure - */ - public function getTypeOf($column) - { - if (isset($this->columns[$column])) { - return $this->columns[$column][0]; - } - return false; - } - /** - * setData - * doctrine uses this function internally - * users are strongly discouraged to use this function - * - * @param array $data internal data - * @return void - */ - public function setData(array $data) - { - $this->data = $data; - } - /** - * returns the maximum primary key value - * - * @return integer - */ - final public function getMaxIdentifier() - { - $sql = "SELECT MAX(".$this->getIdentifier().") FROM ".$this->getTableName(); - $stmt = $this->conn->getDBH()->query($sql); - $data = $stmt->fetch(PDO::FETCH_NUM); - return isset($data[0])?$data[0]:1; - } - /** - * returns simple cached query - * - * @return string - */ - final public function getQuery() - { - return $this->query; - } - /** - * returns internal data, used by Doctrine_Record instances - * when retrieving data from database - * - * @return array - */ - final public function getData() - { - return $this->data; - } - /** - * getter for associated tree - * - * @return mixed if tree return instance of Doctrine_Tree, otherwise returns false - */ - public function getTree() { - if (isset($this->options['treeImpl'])) { - if ( ! $this->tree) { - $options = isset($this->options['treeOptions']) ? $this->options['treeOptions'] : array(); - $this->tree = Doctrine_Tree::factory($this, - $this->options['treeImpl'], - $options - ); - } - return $this->tree; - } - return false; - } - public function getComponentName() - { - return $this->options['name']; - } - public function getTableName() - { - return $this->options['tableName']; - } - public function setTableName($tableName) - { - $this->options['tableName'] = $tableName; - } - /** - * determine if table acts as tree - * - * @return mixed if tree return true, otherwise returns false - */ - public function isTree() { - return ( ! is_null($this->options['treeImpl'])) ? true : false; - } - /** - * returns a string representation of this object - * - * @return string - */ - public function __toString() - { - return Doctrine_Lib::getTableAsString($this); - } -} +. + */ +/** + * Doctrine_Table represents a database table + * each Doctrine_Table holds the information of foreignKeys and associations + * + * + * @author Konsta Vesterinen + * @package Doctrine + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @version $Revision$ + * @category Object Relational Mapping + * @link www.phpdoctrine.com + * @since 1.0 + */ +class Doctrine_Table extends Doctrine_Configurable implements Countable +{ + /** + * @var array $data temporary data which is then loaded into Doctrine_Record::$data + */ + private $data = array(); + /** + * @var array $primaryKeys an array containing all primary key column names + */ + private $primaryKeys = array(); + /** + * @var mixed $identifier + */ + private $identifier; + /** + * @see Doctrine_Identifier constants + * @var integer $identifierType the type of identifier this table uses + */ + private $identifierType; + /** + * @var string $query cached simple query + */ + private $query; + /** + * @var Doctrine_Connection $conn Doctrine_Connection object that created this table + */ + private $conn; + /** + * @var string $name + */ + private $name; + /** + * @var array $identityMap first level cache + */ + private $identityMap = array(); + /** + * @var Doctrine_Table_Repository $repository record repository + */ + private $repository; + /** + * @var array $columns an array of column definitions, + * keys as column names and values as column definitions + * + * the value array has three values: + * + * the column type, eg. 'integer' + * the column length, eg. 11 + * the column options/constraints/validators. eg array('notnull' => true) + * + * so the full columns array might look something like the following: + * array( + * 'name' => array('string', 20, array('notnull' => true, 'default' => 'someone')), + * 'age' => array('integer', 11, array('notnull' => true)) + * ) + */ + protected $columns = array(); + /** + * @var array $columnAliases an array of column aliases + * keys as column aliases and values as column names + */ + protected $columnAliases = array(); + /** + * @var integer $columnCount cached column count, Doctrine_Record uses this column count in when + * determining its state + */ + private $columnCount; + /** + * @var boolean $hasDefaultValues whether or not this table has default values + */ + private $hasDefaultValues; + /** + * @var array $options an array containing all options + * + * -- name name of the component, for example component name of the GroupTable is 'Group' + * + * -- parents the parent classes of this component + * + * -- declaringClass name of the table definition declaring class (when using inheritance the class + * that defines the table structure can be any class in the inheritance hierarchy, + * hence we need reflection to check out which class actually calls setTableDefinition) + * + * -- tableName database table name, in most cases this is the same as component name but in some cases + * where one-table-multi-class inheritance is used this will be the name of the inherited table + * + * -- sequenceName Some databases need sequences instead of auto incrementation primary keys, + * you can set specific sequence for your table by calling setOption('sequenceName', $seqName) + * where $seqName is the name of the desired sequence + * + * -- enumMap enum value arrays + * + * -- inheritanceMap inheritanceMap is used for inheritance mapping, keys representing columns and values + * the column values that should correspond to child classes + * + * -- type table type (mysql example: INNODB) + * + * -- charset character set + * + * -- foreignKeys the foreign keys of this table + * + * -- collation + * + * -- indexes the index definitions of this table + * + * -- treeImpl the tree implementation of this table (if any) + * + * -- treeOptions the tree options + */ + protected $options = array('name' => null, + 'tableName' => null, + 'sequenceName' => null, + 'inheritanceMap' => array(), + 'enumMap' => array(), + 'engine' => null, + 'charset' => null, + 'collation' => null, + 'treeImpl' => null, + 'treeOptions' => null, + 'indexes' => array(), + 'parents' => array(), + ); + /** + * @var Doctrine_Tree $tree tree object associated with this table + */ + protected $tree; + /** + * @var Doctrine_Relation_Parser $_parser relation parser object + */ + protected $_parser; + /** + * the constructor + * @throws Doctrine_Connection_Exception if there are no opened connections + * @throws Doctrine_Table_Exception if there is already an instance of this table + * @return void + */ + public function __construct($name, Doctrine_Connection $conn) + { + $this->conn = $conn; + + $this->setParent($this->conn); + + $this->options['name'] = $name; + $this->_parser = new Doctrine_Relation_Parser($this); + + if ( ! class_exists($name) || empty($name)) { + throw new Doctrine_Exception("Couldn't find class " . $name); + } + $record = new $name($this); + + $names = array(); + + $class = $name; + + // get parent classes + + do { + if ($class == "Doctrine_Record") { + break; + } + + $name = $class; + $names[] = $name; + } while ($class = get_parent_class($class)); + + // reverse names + $names = array_reverse($names); + // save parents + array_pop($names); + $this->options['parents'] = $names; + + // create database table + if (method_exists($record, 'setTableDefinition')) { + $record->setTableDefinition(); + + // set the table definition for the given tree implementation + if ($this->isTree()) { + $this->getTree()->setTableDefinition(); + } + + $this->columnCount = count($this->columns); + + if (isset($this->columns)) { + // get the declaring class of setTableDefinition method + $method = new ReflectionMethod($this->options['name'], 'setTableDefinition'); + $class = $method->getDeclaringClass(); + + $this->options['declaringClass'] = $class; + + if ( ! isset($this->options['tableName'])) { + $this->options['tableName'] = Doctrine::tableize($class->getName()); + } + switch (count($this->primaryKeys)) { + case 0: + $this->columns = array_merge(array('id' => + array('integer', + 20, + array('autoincrement' => true, + 'primary' => true, + ) + ) + ), $this->columns); + + $this->primaryKeys[] = 'id'; + $this->identifier = 'id'; + $this->identifierType = Doctrine_Identifier::AUTO_INCREMENT; + $this->columnCount++; + break; + default: + if (count($this->primaryKeys) > 1) { + $this->identifier = $this->primaryKeys; + $this->identifierType = Doctrine_Identifier::COMPOSITE; + + } else { + foreach ($this->primaryKeys as $pk) { + $e = $this->columns[$pk][2]; + + $found = false; + + foreach ($e as $option => $value) { + if ($found) + break; + + $e2 = explode(':', $option); + + switch (strtolower($e2[0])) { + case 'autoincrement': + case 'autoinc': + $this->identifierType = Doctrine_Identifier::AUTO_INCREMENT; + $found = true; + break; + case 'seq': + case 'sequence': + $this->identifierType = Doctrine_Identifier::SEQUENCE; + $found = true; + + if ($value) { + $this->options['sequenceName'] = $value; + } else { + if (($sequence = $this->getAttribute(Doctrine::ATTR_DEFAULT_SEQUENCE)) !== null) { + $this->options['sequenceName'] = $sequence; + } else { + $this->options['sequenceName'] = $this->conn->getSequenceName($this->options['tableName']); + } + } + break; + } + } + if ( ! isset($this->identifierType)) { + $this->identifierType = Doctrine_Identifier::NORMAL; + } + $this->identifier = $pk; + } + } + } + } + } else { + throw new Doctrine_Table_Exception("Class '$name' has no table definition."); + } + + $record->setUp(); + + // if tree, set up tree + if ($this->isTree()) { + $this->getTree()->setUp(); + } + $this->repository = new Doctrine_Table_Repository($this); + } + /** + * export + * exports this table to database based on column and option definitions + * + * @throws Doctrine_Connection_Exception if some error other than Doctrine::ERR_ALREADY_EXISTS + * occurred during the create table operation + * @return boolean whether or not the export operation was successful + * false if table already existed in the database + */ + public function export() + { + if ( ! Doctrine::isValidClassname($this->options['declaringClass']->getName())) { + throw new Doctrine_Table_Exception('Class name not valid.'); + } + + try { + $columns = array(); + $primary = array(); + + foreach ($this->columns as $name => $column) { + $definition = $column[2]; + $definition['type'] = $column[0]; + $definition['length'] = $column[1]; + + switch ($definition['type']) { + case 'enum': + if (isset($definition['default'])) { + $definition['default'] = $this->enumIndex($name, $definition['default']); + } + break; + case 'boolean': + if (isset($definition['default'])) { + $definition['default'] = $this->conn->convertBooleans($definition['default']); + } + break; + } + $columns[$name] = $definition; + + if(isset($definition['primary']) && $definition['primary']) { + $primary[] = $name; + } + } + + if ($this->getAttribute(Doctrine::ATTR_EXPORT) & Doctrine::EXPORT_CONSTRAINTS) { + + foreach ($this->getRelations() as $name => $relation) { + $fk = $relation->toArray(); + $fk['foreignTable'] = $relation->getTable()->getTableName(); + + if ($relation->getTable() === $this && in_array($relation->getLocal(), $primary)) { + continue; + } + + if ($relation->hasConstraint()) { + + $options['foreignKeys'][] = $fk; + } elseif ($relation instanceof Doctrine_Relation_LocalKey) { + $options['foreignKeys'][] = $fk; + } + } + } + + $options['primary'] = $primary; + + $this->conn->export->createTable($this->options['tableName'], $columns, array_merge($this->options, $options)); + } catch(Doctrine_Connection_Exception $e) { + // we only want to silence table already exists errors + if($e->getPortableCode() !== Doctrine::ERR_ALREADY_EXISTS) { + throw $e; + } + } + } + /** + * exportConstraints + * exports the constraints of this table into database based on option definitions + * + * @throws Doctrine_Connection_Exception if something went wrong on db level + * @return void + */ + public function exportConstraints() + { + try { + $this->conn->beginTransaction(); + + foreach ($this->options['index'] as $index => $definition) { + $this->conn->export->createIndex($this->options['tableName'], $index, $definition); + } + $this->conn->commit(); + } catch(Doctrine_Connection_Exception $e) { + $this->conn->rollback(); + + throw $e; + } + } + /** + * getRelationParser + * return the relation parser associated with this table + * + * @return Doctrine_Relation_Parser relation parser object + */ + public function getRelationParser() + { + return $this->_parser; + } + /** + * __get + * an alias for getOption + * + * @param string $option + */ + public function __get($option) + { + if (isset($this->options[$option])) { + return $this->options[$option]; + } + return null; + } + /** + * __isset + * + * @param string $option + */ + public function __isset($option) + { + return isset($this->options[$option]); + } + /** + * addForeignKey + * + * adds a foreignKey to this table + * + * @return void + */ + public function addForeignKey(array $definition) + { + $this->options['foreignKeys'][] = $definition; + } + /** + * addIndex + * + * adds an index to this table + * + * @return void + */ + public function addIndex($index, array $definition) + { + $index = $this->conn->getIndexName($index); + $this->options['indexes'][$index] = $definition; + } + /** + * getIndex + * + * @return array|boolean array on success, FALSE on failure + */ + public function getIndex($index) + { + if (isset($this->options['indexes'][$index])) { + return $this->options['indexes'][$index]; + } + + return false; + } + public function bind($args, $type) + { + $options = array(); + $options['type'] = $type; + + // the following is needed for backwards compatibility + if (is_string($args[1])) { + if ( ! isset($args[2])) { + $args[2] = array(); + } elseif (is_string($args[2])) { + $args[2] = (array) $args[2]; + } + + $classes = array_merge($this->options['parents'], array($this->getComponentName())); + + + $e = explode('.', $args[1]); + if (in_array($e[0], $classes)) { + if ($options['type'] >= Doctrine_Relation::MANY) { + $options['foreign'] = $e[1]; + } else { + $options['local'] = $e[1]; + } + } else { + $e2 = explode(' as ', $args[0]); + if ($e[0] !== $e2[0] && ( ! isset($e2[1]) || $e[0] !== $e2[1])) { + $options['refClass'] = $e[0]; + } + + $options['foreign'] = $e[1]; + } + + $options = array_merge($args[2], $options); + + $this->_parser->bind($args[0], $options); + } else { + $options = array_merge($args[1], $options); + $this->_parser->bind($args[0], $options); + } + } + /** + * getRelation + * + * @param string $alias relation alias + */ + public function getRelation($alias, $recursive = true) + { + return $this->_parser->getRelation($alias, $recursive); + } + /** + * getRelations + * returns an array containing all relation objects + * + * @return array an array of Doctrine_Relation objects + */ + public function getRelations() + { + return $this->_parser->getRelations(); + } + /** + * createQuery + * creates a new Doctrine_Query object and adds the component name + * of this table as the query 'from' part + * + * @return Doctrine_Query + */ + public function createQuery() + { + return Doctrine_Query::create()->from($this->getComponentName()); + } + /** + * getRepository + * + * @return Doctrine_Table_Repository + */ + public function getRepository() + { + return $this->repository; + } + /** + * setOption + * sets an option and returns this object in order to + * allow flexible method chaining + * + * @see Doctrine_Table::$_options for available options + * @param string $name the name of the option to set + * @param mixed $value the value of the option + * @return Doctrine_Table this object + */ + public function setOption($name, $value) + { + switch ($name) { + case 'name': + case 'tableName': + break; + case 'enumMap': + case 'inheritanceMap': + case 'index': + case 'treeOptions': + if ( ! is_array($value)) { + throw new Doctrine_Table_Exception($name . ' should be an array.'); + } + break; + } + $this->options[$name] = $value; + } + /** + * getOption + * returns the value of given option + * + * @param string $name the name of the option + * @return mixed the value of given option + */ + public function getOption($name) + { + if (isset($this->options[$name])) { + return $this->options[$name]; + } + return null; + } + /** + * getColumnName + * + * returns a column name for column alias + * if the actual name for the alias cannot be found + * this method returns the given alias + * + * @param string $alias column alias + * @return string column name + */ + public function getColumnName($alias) + { + $alias = strtolower($alias); + if(isset($this->columnAliases[$alias])) { + return $this->columnAliases[$alias]; + } + + return $alias; + } + /** + * setColumn + * + * @param string $name + * @param string $type + * @param integer $length + * @param mixed $options + * @throws Doctrine_Table_Exception if trying use wrongly typed parameter + * @return void + */ + public function setColumn($name, $type, $length = null, $options = array()) + { + if (is_string($options)) { + $options = explode('|', $options); + } + + foreach ($options as $k => $option) { + if (is_numeric($k)) { + if ( ! empty($option)) { + $options[$option] = true; + } + unset($options[$k]); + } + } + + $name = strtolower($name); + $parts = explode(' as ', $name); + + if (count($parts) > 1) { + $this->columnAliases[$parts[1]] = $parts[0]; + $name = $parts[0]; + } + + + if ($length == null) { + $length = 2147483647; + } + + if ((string) (int) $length !== (string) $length) { + throw new Doctrine_Table_Exception('Invalid argument given for column length'); + } + + $this->columns[$name] = array($type, $length, $options); + + if (isset($options['primary'])) { + $this->primaryKeys[] = $name; + } + if (isset($options['default'])) { + $this->hasDefaultValues = true; + } + } + /** + * hasDefaultValues + * returns true if this table has default values, otherwise false + * + * @return boolean + */ + public function hasDefaultValues() + { + return $this->hasDefaultValues; + } + /** + * getDefaultValueOf + * returns the default value(if any) for given column + * + * @param string $column + * @return mixed + */ + public function getDefaultValueOf($column) + { + $column = strtolower($column); + if ( ! isset($this->columns[$column])) { + throw new Doctrine_Table_Exception("Couldn't get default value. Column ".$column." doesn't exist."); + } + if (isset($this->columns[$column][2]['default'])) { + return $this->columns[$column][2]['default']; + } else { + return null; + } + } + /** + * @return mixed + */ + public function getIdentifier() + { + return $this->identifier; + } + /** + * @return integer + */ + public function getIdentifierType() + { + return $this->identifierType; + } + /** + * hasColumn + * @return boolean + */ + public function hasColumn($name) + { + return isset($this->columns[$name]); + } + /** + * @param mixed $key + * @return void + */ + public function setPrimaryKey($key) + { + switch (gettype($key)) { + case "array": + $this->primaryKeys = array_values($key); + break; + case "string": + $this->primaryKeys[] = $key; + break; + }; + } + /** + * returns all primary keys + * @return array + */ + public function getPrimaryKeys() + { + return $this->primaryKeys; + } + /** + * @return boolean + */ + public function hasPrimaryKey($key) + { + return in_array($key,$this->primaryKeys); + } + /** + * @return Doctrine_Connection + */ + public function getConnection() + { + return $this->conn; + } + /** + * create + * creates a new record + * + * @param $array an array where keys are field names and values representing field values + * @return Doctrine_Record + */ + public function create(array $array = array()) { + $this->data = $array; + $record = new $this->options['name']($this, true); + $this->data = array(); + return $record; + } + /** + * finds a record by its identifier + * + * @param $id database row id + * @return Doctrine_Record|false a record for given database identifier + */ + public function find($id) + { + if ($id !== null) { + if ( ! is_array($id)) { + $id = array($id); + } else { + $id = array_values($id); + } + + $query = 'SELECT ' . implode(', ', array_keys($this->columns)) . ' FROM ' . $this->getTableName() + . ' WHERE ' . implode(' = ? AND ', $this->primaryKeys) . ' = ?'; + $query = $this->applyInheritance($query); + + $params = array_merge($id, array_values($this->options['inheritanceMap'])); + + $stmt = $this->conn->execute($query, $params); + + $this->data = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($this->data === false) + return false; + + return $this->getRecord(); + } + return false; + } + /** + * applyInheritance + * @param $where query where part to be modified + * @return string query where part with column aggregation inheritance added + */ + final public function applyInheritance($where) + { + if ( ! empty($this->options['inheritanceMap'])) { + $a = array(); + foreach ($this->options['inheritanceMap'] as $field => $value) { + $a[] = $field . ' = ?'; + } + $i = implode(' AND ', $a); + $where .= ' AND ' . $i; + } + return $where; + } + /** + * findAll + * returns a collection of records + * + * @return Doctrine_Collection + */ + public function findAll() + { + $graph = new Doctrine_Query($this->conn); + $users = $graph->query("FROM ".$this->options['name']); + return $users; + } + /** + * findByDql + * finds records with given DQL where clause + * returns a collection of records + * + * @param string $dql DQL after WHERE clause + * @param array $params query parameters + * @return Doctrine_Collection + */ + public function findBySql($dql, array $params = array()) { + $q = new Doctrine_Query($this->conn); + $users = $q->query("FROM ".$this->options['name']." WHERE ".$dql, $params); + return $users; + } + + public function findByDql($dql, array $params = array()) { + return $this->findBySql($dql, $params); + } + /** + * clear + * clears the first level cache (identityMap) + * + * @return void + */ + public function clear() + { + $this->identityMap = array(); + } + /** + * getRecord + * first checks if record exists in identityMap, if not + * returns a new record + * + * @return Doctrine_Record + */ + public function getRecord() + { + if ( ! empty($this->data)) { + $this->data = array_change_key_case($this->data, CASE_LOWER); + + $key = $this->getIdentifier(); + + if ( ! is_array($key)) { + $key = array($key); + } + + foreach ($key as $k) { + if ( ! isset($this->data[$k])) { + throw new Doctrine_Table_Exception("Primary key value for $k wasn't found"); + } + $id[] = $this->data[$k]; + } + + $id = implode(' ', $id); + + if (isset($this->identityMap[$id])) { + $record = $this->identityMap[$id]; + $record->hydrate($this->data); + } else { + $recordName = $this->getClassnameToReturn(); + $record = new $recordName($this); + $this->identityMap[$id] = $record; + } + $this->data = array(); + } else { + $recordName = $this->getClassnameToReturn(); + $record = new $recordName($this, true); + } + + + return $record; + } + + /** + * Get the classname to return. Most often this is just the options['name'] + * + * Check the subclasses option and the inheritanceMap for each subclass to see + * if all the maps in a subclass is met. If this is the case return that + * subclass name. If no subclasses match or if there are no subclasses defined + * return the name of the class for this tables record. + * + * @todo this function could use reflection to check the first time it runs + * if the subclassing option is not set. + * + * @return string The name of the class to create + * + */ + public function getClassnameToReturn() + { + if (!isset($this->options['subclasses'])) { + return $this->options['name']; + } + foreach ($this->options['subclasses'] as $subclass) { + $table = $this->conn->getTable($subclass); + $inheritanceMap = $table->getOption('inheritanceMap'); + $nomatch = false; + foreach ($inheritanceMap as $key => $value) { + if (!isset($this->data[$key]) || $this->data[$key] != $value) { + $nomatch = true; + break; + } + } + if ( ! $nomatch) { + return $table->getComponentName(); + } + } + return $this->options['name']; + } + + /** + * @param $id database row id + * @throws Doctrine_Find_Exception + */ + final public function getProxy($id = null) + { + if ($id !== null) { + $query = 'SELECT ' . implode(', ',$this->primaryKeys) + . ' FROM ' . $this->getTableName() + . ' WHERE ' . implode(' = ? && ',$this->primaryKeys).' = ?'; + $query = $this->applyInheritance($query); + + $params = array_merge(array($id), array_values($this->options['inheritanceMap'])); + + $this->data = $this->conn->execute($query,$params)->fetch(PDO::FETCH_ASSOC); + + if ($this->data === false) + return false; + } + return $this->getRecord(); + } + /** + * count + * + * @return integer + */ + public function count() + { + $a = $this->conn->getDBH()->query("SELECT COUNT(1) FROM ".$this->options['tableName'])->fetch(PDO::FETCH_NUM); + return current($a); + } + /** + * @return Doctrine_Query a Doctrine_Query object + */ + public function getQueryObject() + { + $graph = new Doctrine_Query($this->getConnection()); + $graph->load($this->getComponentName()); + return $graph; + } + /** + * execute + * @param string $query + * @param array $array + * @param integer $limit + * @param integer $offset + */ + public function execute($query, array $array = array(), $limit = null, $offset = null) { + $coll = new Doctrine_Collection($this); + $query = $this->conn->modifyLimitQuery($query,$limit,$offset); + if ( ! empty($array)) { + $stmt = $this->conn->getDBH()->prepare($query); + $stmt->execute($array); + } else { + $stmt = $this->conn->getDBH()->query($query); + } + $data = $stmt->fetchAll(PDO::FETCH_ASSOC); + $stmt->closeCursor(); + + foreach ($data as $row) { + $this->data = $row; + $record = $this->getRecord(); + $coll->add($record); + } + return $coll; + } + /** + * @param string $field + * @return array + */ + final public function getEnumValues($field) + { + if (isset($this->columns[$field][2]['values'])) { + return $this->columns[$field][2]['values']; + } else { + return array(); + } + } + /** + * enumValue + * + * @param string $field + * @param integer $index + * @return mixed + */ + public function enumValue($field, $index) + { + if ($index instanceof Doctrine_Null) + return $index; + + return isset($this->columns[$field][2]['values'][$index]) ? $this->columns[$field][2]['values'][$index] : $index; + } + /** + * enumIndex + * + * @param string $field + * @param mixed $value + * @return mixed + */ + public function enumIndex($field, $value) + { + $values = $this->getEnumValues($field); + + return array_search($value, $values); + } + /** + * invokeSet + * + * @param mixed $value + */ + public function invokeSet(Doctrine_Record $record, $name, $value) + { + if ( ! ($this->getAttribute(Doctrine::ATTR_ACCESSORS) & Doctrine::ACCESSOR_SET)) { + return $value; + } + $prefix = $this->getAttribute(Doctrine::ATTR_ACCESSOR_PREFIX_SET); + if (!$prefix) + $prefix = 'set'; + + $method = $prefix . $name; + + if (method_exists($record, $method)) { + return $record->$method($value); + } + + return $value; + } + /** + * invokeGet + * + * @param mixed $value + */ + public function invokeGet(Doctrine_Record $record, $name, $value) + { + if ( ! ($this->getAttribute(Doctrine::ATTR_ACCESSORS) & Doctrine::ACCESSOR_GET)) { + return $value; + } + $prefix = $this->getAttribute(Doctrine::ATTR_ACCESSOR_PREFIX_GET); + if (!$prefix) + $prefix = 'get'; + + $method = $prefix . $name; + + if (method_exists($record, $method)) { + return $record->$method($value); + } + + return $value; + } + + /** + * getDefinitionOf + * + * @return string ValueWrapper class name on success, false on failure + */ + public function getValueWrapperOf($column) + { + if (isset($this->columns[$column][2]['wrapper'])) { + return $this->columns[$column][2]['wrapper']; + } + return false; + } + /** + * getColumnCount + * + * @return integer the number of columns in this table + */ + final public function getColumnCount() + { + return $this->columnCount; + } + + /** + * returns all columns and their definitions + * + * @return array + */ + final public function getColumns() + { + return $this->columns; + } + /** + * returns an array containing all the column names + * + * @return array + */ + public function getColumnNames() + { + return array_keys($this->columns); + } + /** + * getDefinitionOf + * + * @return mixed array on success, false on failure + */ + public function getDefinitionOf($column) + { + if (isset($this->columns[$column])) { + return $this->columns[$column]; + } + return false; + } + /** + * getTypeOf + * + * @return mixed string on success, false on failure + */ + public function getTypeOf($column) + { + if (isset($this->columns[$column])) { + return $this->columns[$column][0]; + } + return false; + } + /** + * setData + * doctrine uses this function internally + * users are strongly discouraged to use this function + * + * @param array $data internal data + * @return void + */ + public function setData(array $data) + { + $this->data = $data; + } + /** + * returns the maximum primary key value + * + * @return integer + */ + final public function getMaxIdentifier() + { + $sql = "SELECT MAX(".$this->getIdentifier().") FROM ".$this->getTableName(); + $stmt = $this->conn->getDBH()->query($sql); + $data = $stmt->fetch(PDO::FETCH_NUM); + return isset($data[0])?$data[0]:1; + } + /** + * returns simple cached query + * + * @return string + */ + final public function getQuery() + { + return $this->query; + } + /** + * returns internal data, used by Doctrine_Record instances + * when retrieving data from database + * + * @return array + */ + final public function getData() + { + return $this->data; + } + /** + * getter for associated tree + * + * @return mixed if tree return instance of Doctrine_Tree, otherwise returns false + */ + public function getTree() { + if (isset($this->options['treeImpl'])) { + if ( ! $this->tree) { + $options = isset($this->options['treeOptions']) ? $this->options['treeOptions'] : array(); + $this->tree = Doctrine_Tree::factory($this, + $this->options['treeImpl'], + $options + ); + } + return $this->tree; + } + return false; + } + public function getComponentName() + { + return $this->options['name']; + } + public function getTableName() + { + return $this->options['tableName']; + } + public function setTableName($tableName) + { + $this->options['tableName'] = $tableName; + } + /** + * determine if table acts as tree + * + * @return mixed if tree return true, otherwise returns false + */ + public function isTree() { + return ( ! is_null($this->options['treeImpl'])) ? true : false; + } + /** + * returns a string representation of this object + * + * @return string + */ + public function __toString() + { + return Doctrine_Lib::getTableAsString($this); + } +}