diff --git a/draft/new-core/Collection.php b/draft/new-core/Collection.php new file mode 100644 index 000000000..40e0a31e0 --- /dev/null +++ b/draft/new-core/Collection.php @@ -0,0 +1,766 @@ +. + */ +Doctrine::autoload("Doctrine_Access"); +/** + * Doctrine_Collection + * Collection of Doctrine_Record objects. + * + * @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: 1207 $ + * @author Konsta Vesterinen + */ +class Doctrine_Collection extends Doctrine_Access implements Countable, IteratorAggregate, Serializable +{ + /** + * @var array $data an array containing the data access objects of this collection + */ + protected $data = array(); + /** + * @var Doctrine_Table $table each collection has only records of specified table + */ + protected $table; + /** + * @var Doctrine_Record $reference collection can belong to a record + */ + protected $reference; + /** + * @var string $reference_field the reference field of the collection + */ + protected $reference_field; + /** + * @var Doctrine_Relation the record this collection is related to, if any + */ + protected $relation; + /** + * @var boolean $expandable whether or not this collection has been expanded + */ + protected $expandable = true; + /** + * @var array $expanded + */ + protected $expanded = array(); + /** + * @var string $keyColumn the name of the column that is used for collection key mapping + */ + protected $keyColumn; + /** + * @var Doctrine_Null $null used for extremely fast null value testing + */ + protected static $null; + + protected $aggregateValues = array(); + + /** + * constructor + * + * @param Doctrine_Table|string $table + */ + public function __construct($table) + { + if ( ! ($table instanceof Doctrine_Table)) { + $table = Doctrine_Manager::getInstance() + ->getTable($table); + } + $this->table = $table; + + $name = $table->getAttribute(Doctrine::ATTR_COLL_KEY); + if ($name !== null) { + $this->keyColumn = $name; + } + } + /** + * initNullObject + * initializes the null object for this collection + * + * @return void + */ + public static function initNullObject(Doctrine_Null $null) + { + self::$null = $null; + } + /** + * getTable + * returns the table this collection belongs to + * + * @return Doctrine_Table + */ + public function getTable() + { + return $this->table; + } + /** + * setAggregateValue + * + * @param string $name + * @param string $value + * @return void + */ + public function setAggregateValue($name, $value) + { + $this->aggregateValues[$name] = $value; + } + /** + * getAggregateValue + * + * @param string $name + * @return mixed + */ + public function getAggregateValue($name) + { + return $this->aggregateValues[$name]; + } + /** + * this method is automatically called when this Doctrine_Collection is serialized + * + * @return array + */ + public function serialize() + { + $vars = get_object_vars($this); + + unset($vars['reference']); + unset($vars['reference_field']); + unset($vars['relation']); + unset($vars['expandable']); + unset($vars['expanded']); + unset($vars['generator']); + + $vars['table'] = $vars['table']->getComponentName(); + + return serialize($vars); + } + /** + * unseralize + * this method is automatically called everytime a Doctrine_Collection object is unserialized + * + * @return void + */ + public function unserialize($serialized) + { + $manager = Doctrine_Manager::getInstance(); + $connection = $manager->getCurrentConnection(); + + $array = unserialize($serialized); + + foreach ($array as $name => $values) { + $this->$name = $values; + } + + $this->table = $connection->getTable($this->table); + + $this->expanded = array(); + $this->expandable = true; + + $name = $this->table->getAttribute(Doctrine::ATTR_COLL_KEY); + if ($name !== null) { + $this->keyColumn = $name; + } + } + /** + * isExpanded + * + * whether or not an offset batch has been expanded + * @return boolean + */ + public function isExpanded($offset) + { + return isset($this->expanded[$offset]); + } + /** + * isExpandable + * + * whether or not this collection is expandable + * @return boolean + */ + public function isExpandable() + { + return $this->expandable; + } + /** + * setKeyColumn + * + * @param string $column + * @return void + */ + public function setKeyColumn($column) + { + $this->keyColumn = $column; + } + /** + * getKeyColumn + * returns the name of the key column + * + * @return string + */ + public function getKeyColumn() + { + return $this->column; + } + /** + * returns all the records as an array + * + * @return array + */ + public function getData() + { + return $this->data; + } + /** + * @param array $data + */ + public function addData(array $data) + { + $this->data[] = $data; + } + /** + * getFirst + * returns the first record in the collection + * + * @return mixed + */ + public function getFirst() + { + return reset($this->data); + } + /** + * getLast + * returns the last record in the collection + * + * @return mixed + */ + public function getLast() + { + return end($this->data); + } + /** + * setReference + * sets a reference pointer + * + * @return void + */ + public function setReference(Doctrine_Record $record, Doctrine_Relation $relation) + { + $this->reference = $record; + $this->relation = $relation; + + if ($relation instanceof Doctrine_Relation_ForeignKey + || $relation instanceof Doctrine_Relation_LocalKey + ) { + + $this->reference_field = $relation->getForeign(); + + $value = $record->get($relation->getLocal()); + + foreach ($this->getNormalIterator() as $record) { + if ($value !== null) { + $record->set($this->reference_field, $value, false); + } else { + $record->set($this->reference_field, $this->reference, false); + } + } + } elseif ($relation instanceof Doctrine_Relation_Association) { + + } + } + /** + * getReference + * + * @return mixed + */ + public function getReference() + { + return $this->reference; + } + /** + * expand + * expands the collection + * + * @return boolean + */ + public function expand($key) + { + $where = array(); + $params = array(); + $limit = null; + $offset = null; + + switch (get_class($this)) { + case "Doctrine_Collection_Offset": + $limit = $this->getLimit(); + $offset = (floor($key / $limit) * $limit); + + if ( ! $this->expandable && isset($this->expanded[$offset])) { + return false; + } + $fields = implode(", ",$this->table->getColumnNames()); + break; + default: + if ( ! $this->expandable) { + return false; + } + + if ( ! isset($this->reference)) { + return false; + } + + $id = $this->reference->obtainIdentifier(); + + if (empty($id)) { + return false; + } + + switch (get_class($this)) { + case "Doctrine_Collection_Immediate": + $fields = implode(", ",$this->table->getColumnNames()); + break; + default: + $fields = implode(", ",$this->table->getPrimaryKeys()); + }; + }; + + if (isset($this->relation)) { + if ($this->relation instanceof Doctrine_Relation_ForeignKey) { + $params[] = $this->reference->getIncremented(); + $where[] = $this->reference_field." = ?"; + + if ( ! isset($offset)) { + $ids = $this->getPrimaryKeys(); + + if ( ! empty($ids)) { + $where[] = $this->table->getIdentifier()." NOT IN (".substr(str_repeat("?, ",count($ids)),0,-2).")"; + $params = array_merge($params,$ids); + } + + $this->expandable = false; + } + + } elseif ($this->relation instanceof Doctrine_Relation_Association) { + + $asf = $this->relation->getAssociationFactory(); + $query = 'SELECT '.$foreign." FROM ".$asf->getTableName()." WHERE ".$local."=".$this->getIncremented(); + + $table = $fk->getTable(); + $graph = new Doctrine_Query($table->getConnection()); + + $q = 'FROM ' . $table->getComponentName() . ' WHERE ' . $table->getComponentName() . '.' . $table->getIdentifier()." IN ($query)"; + + } + } + + $query = "SELECT ".$fields." FROM ".$this->table->getTableName(); + + // apply column aggregation inheritance + $map = $this->table->inheritanceMap; + foreach ($map as $k => $v) { + $where[] = $k." = ?"; + $params[] = $v; + } + if ( ! empty($where)) { + $query .= " WHERE ".implode(" AND ",$where); + } + + $coll = $this->table->execute($query, $params, $limit, $offset); + + if ( ! isset($offset)) { + foreach ($coll as $record) { + if (isset($this->reference_field)) { + $record->set($this->reference_field,$this->reference, false); + } + $this->reference->addReference($record, $this->relation); + } + } else { + $i = $offset; + + foreach ($coll as $record) { + if (isset($this->reference)) { + $this->reference->addReference($record, $this->relation, $i); + } else { + $this->data[$i] = $record; + } + $i++; + } + + $this->expanded[$offset] = true; + + // check if the fetched collection's record count is smaller + // than the query limit, if so this collection has been expanded to its max size + + if (count($coll) < $limit) { + $this->expandable = false; + } + } + + return $coll; + } + /** + * remove + * removes a specified collection element + * + * @param mixed $key + * @return boolean + */ + public function remove($key) + { + if ( ! isset($this->data[$key])) { + $this->expand($key); + + throw new Doctrine_Collection_Exception('Unknown key ' . $key); + } + + $removed = $this->data[$key]; + + unset($this->data[$key]); + return $removed; + } + /** + * contains + * whether or not this collection contains a specified element + * + * @param mixed $key the key of the element + * @return boolean + */ + public function contains($key) + { + return isset($this->data[$key]); + } + /** + * get + * returns a record for given key + * + * There are two special cases: + * + * 1. if null is given as a key a new record is created and attached + * at the end of the collection + * + * 2. if given key does not exist, then a new record is create and attached + * to the given key + * + * Collection also maps referential information to newly created records + * + * @param mixed $key the key of the element + * @return Doctrine_Record return a specified record + */ + public function get($key) + { + if ($key === null) { + $record = $this->table->create(); + + if (isset($this->reference_field)) { + $record->set($this->reference_field, $this->reference, false); + } + + $this->data[] = $record; + + return $record; + } + + + if ( ! isset($this->data[$key])) { + $this->expand($key); + + if ( ! isset($this->data[$key])) { + $this->data[$key] = $this->table->create(); + } + if (isset($this->reference_field)) { + $value = $this->reference->get($this->relation->getLocal()); + + if ($value !== null) { + $this->data[$key]->set($this->reference_field, $value, false); + } else { + + $this->data[$key]->set($this->reference_field, $this->reference, false); + } + } + } + + return $this->data[$key]; + } + + /** + * @return array an array containing all primary keys + */ + public function getPrimaryKeys() + { + $list = array(); + $name = $this->table->getIdentifier(); + + foreach ($this->data as $record) { + if (is_array($record) && isset($record[$name])) { + $list[] = $record[$name]; + } else { + $list[] = $record->getIncremented(); + } + }; + return $list; + } + /** + * returns all keys + * @return array + */ + public function getKeys() + { + return array_keys($this->data); + } + /** + * count + * this class implements interface countable + * returns the number of records in this collection + * + * @return integer + */ + public function count() + { + return count($this->data); + } + /** + * set + * @param integer $key + * @param Doctrine_Record $record + * @return void + */ + public function set($key, Doctrine_Record $record) + { + if (isset($this->reference_field)) { + $record->set($this->reference_field, $this->reference, false); + } + $this->data[$key] = $record; + } + /** + * adds a record to collection + * @param Doctrine_Record $record record to be added + * @param string $key optional key for the record + * @return boolean + */ + public function add(Doctrine_Record $record,$key = null) + { + if (isset($this->reference_field)) { + $record->set($this->reference_field, $this->reference, false); + } + /** + * for some weird reason in_array cannot be used here (php bug ?) + * + * if used it results in fatal error : [ nesting level too deep ] + */ + foreach ($this->data as $val) { + if ($val === $record) { + return false; + } + } + + if (isset($key)) { + if (isset($this->data[$key])) { + return false; + } + $this->data[$key] = $record; + return true; + } + + if (isset($this->keyColumn)) { + $value = $record->get($this->keyColumn); + if ($value === null) { + throw new Doctrine_Collection_Exception("Couldn't create collection index. Record field '".$this->keyColumn."' was null."); + } + $this->data[$value] = $record; + } else { + $this->data[] = $record; + } + return true; + } + /** + * loadRelated + * + * @param mixed $name + * @return boolean + */ + public function loadRelated($name = null) + { + $list = array(); + $query = new Doctrine_Query($this->table->getConnection()); + + if ( ! isset($name)) { + foreach ($this->data as $record) { + $value = $record->getIncremented(); + if ($value !== null) { + $list[] = $value; + } + }; + $query->from($this->table->getComponentName() . '(' . implode(", ",$this->table->getPrimaryKeys()) . ')'); + $query->where($this->table->getComponentName() . '.id IN (' . substr(str_repeat("?, ", count($list)),0,-2) . ')'); + + return $query; + } + + $rel = $this->table->getRelation($name); + + if ($rel instanceof Doctrine_Relation_LocalKey || $rel instanceof Doctrine_Relation_ForeignKey) { + foreach ($this->data as $record) { + $list[] = $record[$rel->getLocal()]; + } + } else { + foreach ($this->data as $record) { + $value = $record->getIncremented(); + if ($value !== null) { + $list[] = $value; + } + } + } + + $dql = $rel->getRelationDql(count($list), 'collection'); + + $coll = $query->query($dql, $list); + + $this->populateRelated($name, $coll); + } + /** + * populateRelated + * + * @param string $name + * @param Doctrine_Collection $coll + * @return void + */ + public function populateRelated($name, Doctrine_Collection $coll) + { + $rel = $this->table->getRelation($name); + $table = $rel->getTable(); + $foreign = $rel->getForeign(); + $local = $rel->getLocal(); + + if ($rel instanceof Doctrine_Relation_LocalKey) { + foreach ($this->data as $key => $record) { + foreach ($coll as $k => $related) { + if ($related[$foreign] == $record[$local]) { + $this->data[$key]->setRelated($name, $related); + } + } + } + } elseif ($rel instanceof Doctrine_Relation_ForeignKey) { + foreach ($this->data as $key => $record) { + if ($record->state() == Doctrine_Record::STATE_TCLEAN + || $record->state() == Doctrine_Record::STATE_TDIRTY + ) { + continue; + } + $sub = new Doctrine_Collection($table); + + foreach ($coll as $k => $related) { + if ($related[$foreign] == $record[$local]) { + $sub->add($related); + $coll->remove($k); + } + } + + $this->data[$key]->setRelated($name, $sub); + } + } elseif ($rel instanceof Doctrine_Relation_Association) { + $identifier = $this->table->getIdentifier(); + $asf = $rel->getAssociationFactory(); + $name = $table->getComponentName(); + + foreach ($this->data as $key => $record) { + if ($record->state() == Doctrine_Record::STATE_TCLEAN + || $record->state() == Doctrine_Record::STATE_TDIRTY + ) { + continue; + } + $sub = new Doctrine_Collection($table); + foreach ($coll as $k => $related) { + if ($related->get($local) == $record[$identifier]) { + $sub->add($related->get($name)); + } + } + $this->data[$key]->setRelated($name, $sub); + + } + } + } + /** + * getNormalIterator + * returns normal iterator - an iterator that will not expand this collection + * + * @return Doctrine_Iterator_Normal + */ + public function getNormalIterator() + { + return new Doctrine_Collection_Iterator_Normal($this); + } + /** + * save + * saves all records of this collection + * + * @return void + */ + public function save(Doctrine_Connection $conn = null) + { + if ($conn == null) { + $conn = $this->table->getConnection(); + } + $conn->beginTransaction(); + + foreach ($this as $key => $record) { + $record->save($conn); + }; + + $conn->commit(); + } + /** + * single shot delete + * deletes all records from this collection + * and uses only one database query to perform this operation + * + * @return boolean + */ + public function delete(Doctrine_Connection $conn = null) + { + if ($conn == null) { + $conn = $this->table->getConnection(); + } + + $conn->beginTransaction(); + + foreach ($this as $key => $record) { + $record->delete($conn); + } + + $conn->commit(); + + $this->data = array(); + } + /** + * getIterator + * @return object ArrayIterator + */ + public function getIterator() + { + $data = $this->data; + return new ArrayIterator($data); + } + /** + * returns a string representation of this object + */ + public function __toString() + { + return Doctrine_Lib::getCollectionAsString($this); + } +} diff --git a/draft/new-core/Hydrate.php b/draft/new-core/Hydrate.php new file mode 100644 index 000000000..6a1b768b6 --- /dev/null +++ b/draft/new-core/Hydrate.php @@ -0,0 +1,778 @@ +. + */ +Doctrine::autoload('Doctrine_Access'); +/** + * 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: 1255 $ + * @author Konsta Vesterinen + */ +abstract class Doctrine_Hydrate extends Doctrine_Access +{ + /** + * 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 $fetchmodes an array containing all fetchmodes + */ + protected $fetchModes = array(); + /** + * @var array $tables an array containing all the tables used in the query + */ + protected $tables = array(); + /** + * @var array $collections an array containing all collections + * this hydrater has created/will create + */ + protected $collections = array(); + /** + * @var array $joins an array containing all table joins + */ + protected $joins = array(); + /** + * @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 + */ + protected $view; + /** + * @var boolean $inheritanceApplied + */ + protected $inheritanceApplied = false; + /** + * @var boolean $aggregate + */ + protected $aggregate = false; + /** + * @var array $compAliases + */ + protected $compAliases = array(); + /** + * @var array $tableAliases + */ + protected $tableAliases = array(); + /** + * @var array $tableIndexes + */ + protected $tableIndexes = array(); + + protected $pendingAggregates = array(); + + protected $subqueryAggregates = array(); + /** + * @var array $aggregateMap an array containing all aggregate aliases, keys as dql aliases + * and values as sql aliases + */ + protected $aggregateMap = array(); + /** + * @var Doctrine_Hydrate_Alias $aliasHandler + */ + protected $aliasHandler; + /** + * @var array $parts SQL query string parts + */ + protected $parts = array( + 'select' => array(), + 'from' => array(), + 'set' => array(), + 'join' => array(), + 'where' => array(), + 'groupby' => array(), + 'having' => array(), + 'orderby' => array(), + 'limit' => false, + 'offset' => false, + ); + /** + * @var integer $type the query type + * + * @see Doctrine_Query::* constants + */ + protected $type = self::SELECT; + /** + * constructor + * + * @param Doctrine_Connection|null $connection + */ + public function __construct($connection = null) + { + if ( ! ($connection instanceof Doctrine_Connection)) { + $connection = Doctrine_Manager::getInstance()->getCurrentConnection(); + } + $this->conn = $connection; + $this->aliasHandler = new Doctrine_Hydrate_Alias(); + } + /** + * getComponentAliases + * + * @return array + */ + public function getComponentAliases() + { + return $this->compAliases; + } + /** + * getTableAliases + * + * @return array + */ + public function getTableAliases() + { + return $this->tableAliases; + } + /** + * getTableIndexes + * + * @return array + */ + public function getTableIndexes() + { + return $this->tableIndexes; + } + /** + * getTables + * + * @return array + */ + public function getTables() + { + return $this->tables; + } + /** + * copyAliases + * + * @return void + */ + public function copyAliases(Doctrine_Hydrate $query) + { + $this->compAliases = $query->getComponentAliases(); + $this->tableAliases = $query->getTableAliases(); + $this->tableIndexes = $query->getTableIndexes(); + $this->aliasHandler = $query->aliasHandler; + + return $this; + } + + public function getPathAlias($path) + { + $s = array_search($path, $this->compAliases); + if ($s === false) + return $path; + + return $s; + } + /** + * createSubquery + * + * @return Doctrine_Hydrate + */ + public function createSubquery() + { + $class = get_class($this); + $obj = new $class(); + + // copy the aliases to the subquery + $obj->copyAliases($this); + + // this prevents the 'id' being selected, re ticket #307 + $obj->isSubquery(true); + + return $obj; + } + /** + * getQuery + * + * @return string + */ + abstract public function getQuery(); + /** + * limitSubqueryUsed + * + * @return boolean + */ + public function isLimitSubqueryUsed() + { + return false; + } + + /** + * remove + * + * @param $name + */ + public function remove($name) + { + if (isset($this->parts[$name])) { + if ($name == "limit" || $name == "offset") { + $this->parts[$name] = false; + } else { + $this->parts[$name] = array(); + } + } + return $this; + } + /** + * clear + * resets all the variables + * + * @return void + */ + protected function clear() + { + $this->fetchModes = array(); + $this->tables = array(); + $this->parts = array( + "select" => array(), + "from" => array(), + "join" => array(), + "where" => array(), + "groupby" => array(), + "having" => array(), + "orderby" => array(), + "limit" => false, + "offset" => false, + ); + $this->inheritanceApplied = false; + $this->aggregate = false; + + $this->collections = array(); + $this->joins = array(); + $this->tableIndexes = array(); + $this->tableAliases = array(); + $this->aliasHandler->clear(); + } + /** + * getConnection + * + * @return Doctrine_Connection + */ + public function getConnection() + { + return $this->conn; + } + /** + * setView + * sets a database view this query object uses + * this method should only be called internally by doctrine + * + * @param Doctrine_View $view database view + * @return void + */ + public function setView(Doctrine_View $view) + { + $this->view = $view; + } + /** + * getView + * returns the view associated with this query object (if any) + * + * @return Doctrine_View the view associated with this query object + */ + public function getView() + { + return $this->view; + } + /** + * getParams + * + * @return array + */ + public function getParams() + { + return $this->params; + } + /** + * getTableAlias + * + * @param string $path + * @return string + */ + final public function getTableAlias($path) + { + if (isset($this->compAliases[$path])) { + $path = $this->compAliases[$path]; + } + if ( ! isset($this->tableAliases[$path])) { + return false; + } + return $this->tableAliases[$path]; + } + /** + * getCollection + * + * @parma string $name component name + * @param integer $index + */ + private function getCollection($name) + { + $table = $this->tables[$name]; + if ( ! isset($this->fetchModes[$name])) { + return new Doctrine_Collection($table); + } + switch ($this->fetchModes[$name]) { + case Doctrine::FETCH_BATCH: + $coll = new Doctrine_Collection_Batch($table); + break; + case Doctrine::FETCH_LAZY: + $coll = new Doctrine_Collection_Lazy($table); + break; + case Doctrine::FETCH_OFFSET: + $coll = new Doctrine_Collection_Offset($table); + break; + case Doctrine::FETCH_IMMEDIATE: + $coll = new Doctrine_Collection_Immediate($table); + break; + case Doctrine::FETCH_LAZY_OFFSET: + $coll = new Doctrine_Collection_LazyOffset($table); + break; + default: + throw new Doctrine_Exception("Unknown fetchmode"); + }; + + return $coll; + } + /** + * setParams + * + * @param array $params + */ + public function setParams(array $params = array()) { + $this->params = $params; + } + /** + * execute + * executes the dql query and populates all collections + * + * @param string $params + * @return Doctrine_Collection the root collection + */ + public function execute($params = array(), $return = Doctrine::FETCH_RECORD) { + $this->collections = array(); + + $params = $this->conn->convertBooleans(array_merge($this->params, $params)); + + if ( ! $this->view) { + $query = $this->getQuery($params); + } else { + $query = $this->view->getSelectSql(); + } + + if ($this->isLimitSubqueryUsed() && + $this->conn->getDBH()->getAttribute(Doctrine::ATTR_DRIVER_NAME) !== 'mysql') { + + $params = array_merge($params, $params); + } + $stmt = $this->conn->execute($query, $params); + + if ($this->aggregate) { + return $stmt->fetchAll(Doctrine::FETCH_ASSOC); + } + + if (count($this->tables) == 0) { + throw new Doctrine_Query_Exception('No components selected'); + } + + $keys = array_keys($this->tables); + $root = $keys[0]; + + $previd = array(); + + $coll = $this->getCollection($root); + $prev[$root] = $coll; + + if ($this->aggregate) { + $return = Doctrine::FETCH_ARRAY; + } + + $array = $this->parseData($stmt); + + if ($return == Doctrine::FETCH_ARRAY) { + return $array; + } + + foreach ($array as $data) { + /** + * remove duplicated data rows and map data into objects + */ + foreach ($data as $key => $row) { + if (empty($row)) { + continue; + } + //$key = array_search($key, $this->shortAliases); + + foreach ($this->tables as $k => $t) { + if ( ! strcasecmp($key, $k)) { + $key = $k; + } + } + + if ( !isset($this->tables[$key]) ) { + throw new Doctrine_Exception('No table named ' . $key . ' found.'); + } + $ids = $this->tables[$key]->getIdentifier(); + $name = $key; + + if ($this->isIdentifiable($row, $ids)) { + if ($name !== $root) { + $prev = $this->initRelated($prev, $name); + } + // aggregate values have numeric keys + if (isset($row[0])) { + $component = $this->tables[$name]->getComponentName(); + + // if the collection already has objects, get the last object + // otherwise create a new one where the aggregate values are being mapped + + if ($prev[$name]->count() > 0) { + $record = $prev[$name]->getLast(); + } else { + $record = new $component(); + $prev[$name]->add($record); + } + + $path = array_search($name, $this->tableAliases); + $alias = $this->getPathAlias($path); + + // map each aggregate value + foreach ($row as $index => $value) { + $agg = false; + + if (isset($this->pendingAggregates[$alias][$index])) { + $agg = $this->pendingAggregates[$alias][$index][3]; + } elseif (isset($this->subqueryAggregates[$alias][$index])) { + $agg = $this->subqueryAggregates[$alias][$index]; + } + + $record->mapValue($agg, $value); + } + } + + continue; + + } + + if ( ! isset($previd[$name])) { + $previd[$name] = array(); + } + if ($previd[$name] !== $row) { + // set internal data + + $this->tables[$name]->setData($row); + + // initialize a new record + + $record = $this->tables[$name]->getRecord(); + + // aggregate values have numeric keys + if (isset($row[0])) { + $path = array_search($name, $this->tableAliases); + $alias = $this->getPathAlias($path); + + // map each aggregate value + foreach ($row as $index => $value) { + $agg = false; + + if (isset($this->pendingAggregates[$alias][$index])) { + $agg = $this->pendingAggregates[$alias][$index][3]; + } elseif (isset($this->subqueryAggregates[$alias][$index])) { + $agg = $this->subqueryAggregates[$alias][$index]; + } + $record->mapValue($agg, $value); + } + } + + if ($name == $root) { + // add record into root collection + + $coll->add($record); + unset($previd); + + } else { + $prev = $this->addRelated($prev, $name, $record); + } + + // 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[$name] !== $record) { + $prev[$name] = $record; + } + } + + $previd[$name] = $row; + } + } + + return $coll; + } + /** + * initRelation + * + * @param array $prev + * @param string $name + * @return array + */ + public function initRelated(array $prev, $name) + { + $pointer = $this->joins[$name]; + $path = array_search($name, $this->tableAliases); + $tmp = explode('.', $path); + $alias = end($tmp); + + if ( ! isset($prev[$pointer]) ) { + return $prev; + } + $fk = $this->tables[$pointer]->getRelation($alias); + + if ( ! $fk->isOneToOne()) { + if ($prev[$pointer]->getLast() instanceof Doctrine_Record) { + if ( ! $prev[$pointer]->getLast()->hasReference($alias)) { + $prev[$name] = $this->getCollection($name); + $prev[$pointer]->getLast()->initReference($prev[$name],$fk); + } else { + $prev[$name] = $prev[$pointer]->getLast()->get($alias); + } + } + } + + return $prev; + } + /** + * addRelated + * + * @param array $prev + * @param string $name + * @return array + */ + public function addRelated(array $prev, $name, Doctrine_Record $record) + { + $pointer = $this->joins[$name]; + + $path = array_search($name, $this->tableAliases); + $tmp = explode('.', $path); + $alias = end($tmp); + + $fk = $this->tables[$pointer]->getRelation($alias); + + if ($fk->isOneToOne()) { + $prev[$pointer]->getLast()->set($fk->getAlias(), $record); + + $prev[$name] = $record; + } else { + // one-to-many relation or many-to-many relation + + if ( ! $prev[$pointer]->getLast()->hasReference($alias)) { + $prev[$name] = $this->getCollection($name); + $prev[$pointer]->getLast()->initReference($prev[$name], $fk); + + } else { + // previous entry found from memory + $prev[$name] = $prev[$pointer]->getLast()->get($alias); + } + + $prev[$pointer]->getLast()->addReference($record, $fk); + } + return $prev; + } + /** + * isIdentifiable + * returns whether or not a given data row is identifiable (it contains + * all id fields specified in the second argument) + * + * @param array $row + * @param mixed $ids + * @return boolean + */ + public function isIdentifiable(array $row, $ids) + { + if (is_array($ids)) { + foreach ($ids as $id) { + if ($row[$id] == null) + return true; + } + } else { + if ( ! isset($row[$ids])) { + return true; + } + } + return false; + } + /** + * 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->tables as $alias => $table) { + $array[$alias][] = $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)); + $component = strtolower(implode('__', $e)); + + $data[$component][$field] = $value; + + unset($data[$key]); + }; + $array[] = $data; + }; + + $stmt->closeCursor(); + return $array; + } + /** + * returns a Doctrine_Table for given name + * + * @param string $name component name + * @return Doctrine_Table|boolean + */ + public function getTable($name) + { + if (isset($this->tables[$name])) { + return $this->tables[$name]; + } + return false; + } + /** + * @return string returns a string representation of this object + */ + public function __toString() + { + return Doctrine_Lib::formatSql($this->getQuery()); + } +} diff --git a/draft/new-core/Query.php b/draft/new-core/Query.php new file mode 100644 index 000000000..bfedad86f --- /dev/null +++ b/draft/new-core/Query.php @@ -0,0 +1,1680 @@ +. + */ +Doctrine::autoload('Doctrine_Hydrate'); +/** + * Doctrine_Query + * + * @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: 1296 $ + * @author Konsta Vesterinen + */ +class Doctrine_Query extends Doctrine_Hydrate implements Countable { + /** + * @param array $subqueryAliases the table aliases needed in some LIMIT subqueries + */ + private $subqueryAliases = array(); + /** + * @param boolean $needsSubquery + */ + private $needsSubquery = false; + /** + * @param boolean $limitSubqueryUsed + */ + private $limitSubqueryUsed = false; + /** + * @param boolean $isSubquery whether or not this query object is a subquery of another + * query object + */ + private $isSubquery; + + private $tableStack; + + private $relationStack = array(); + + private $isDistinct = false; + + protected $components = array(); + + private $neededTables = array(); + /** + * @var array $pendingFields + */ + private $pendingFields = array(); + /** + * @var array $pendingSubqueries SELECT part subqueries, these are called pending subqueries since + * they cannot be parsed directly (some queries might be correlated) + */ + private $pendingSubqueries = array(); + /** + * @var boolean $subqueriesProcessed Whether or not pending subqueries have already been processed. + * Consequent calls to getQuery would result badly constructed queries + * without this variable + */ + private $subqueriesProcessed = false; + + + + /** + * create + * returns a new Doctrine_Query object + * + * @return Doctrine_Query + */ + public static function create() + { + return new Doctrine_Query(); + } + /** + * isSubquery + * if $bool parameter is set this method sets the value of + * Doctrine_Query::$isSubquery. If this value is set to true + * the query object will not load the primary key fields of the selected + * components. + * + * If null is given as the first parameter this method retrieves the current + * value of Doctrine_Query::$isSubquery. + * + * @param boolean $bool whether or not this query acts as a subquery + * @return Doctrine_Query|bool + */ + public function isSubquery($bool = null) + { + if ($bool === null) { + return $this->isSubquery; + } + + $this->isSubquery = (bool) $bool; + return $this; + } + + /** + * getAggregateAlias + * + * @return string + */ + public function getAggregateAlias($dqlAlias) + { + if(isset($this->aggregateMap[$dqlAlias])) { + return $this->aggregateMap[$dqlAlias]; + } + + return null; + } + + public function getTableStack() + { + return $this->tableStack; + } + + public function getRelationStack() + { + return $this->relationStack; + } + + public function isDistinct($distinct = null) + { + if(isset($distinct)) + $this->isDistinct = (bool) $distinct; + + return $this->isDistinct; + } + + public function processPendingFields($componentAlias) + { + $tableAlias = $this->getTableAlias($componentAlias); + + if ( ! isset($this->tables[$tableAlias])) + throw new Doctrine_Query_Exception('Unknown component path '.$componentAlias); + + $table = $this->tables[$tableAlias]; + + if (isset($this->pendingFields[$componentAlias])) { + $fields = $this->pendingFields[$componentAlias]; + + if (in_array('*', $fields)) { + $fields = $table->getColumnNames(); + } else { + // only auto-add the primary key fields if this query object is not + // a subquery of another query object + if ( ! $this->isSubquery) { + $fields = array_unique(array_merge($table->getPrimaryKeys(), $fields)); + } + } + } + foreach ($fields as $name) { + $name = $table->getColumnName($name); + + $this->parts['select'][] = $tableAlias . '.' .$name . ' AS ' . $tableAlias . '__' . $name; + } + + $this->neededTables[] = $tableAlias; + + } + /** + * parseSelect + * parses the query select part and + * adds selected fields to pendingFields array + * + * @param string $dql + */ + public function parseSelect($dql) + { + $refs = Doctrine_Query::bracketExplode($dql, ','); + + foreach ($refs as $reference) { + if (strpos($reference, '(') !== false) { + if (substr($reference, 0, 1) === '(') { + // subselect found in SELECT part + $this->parseSubselect($reference); + } else { + $this->parseAggregateFunction2($reference); + } + } else { + + $e = explode('.', $reference); + if (count($e) > 2) { + $this->pendingFields[] = $reference; + } else { + $this->pendingFields[$e[0]][] = $e[1]; + } + } + } + } + /** + * parseSubselect + * + * parses the subquery found in DQL SELECT part and adds the + * parsed form into $pendingSubqueries stack + * + * @param string $reference + * @return void + */ + public function parseSubselect($reference) + { + $e = Doctrine_Query::bracketExplode($reference, ' '); + $alias = $e[1]; + + if (count($e) > 2) { + if (strtoupper($e[1]) !== 'AS') { + throw new Doctrine_Query_Exception('Syntax error near: ' . $reference); + } + $alias = $e[2]; + } + + $subquery = substr($e[0], 1, -1); + + $this->pendingSubqueries[] = array($subquery, $alias); + } + public function parseAggregateFunction2($func) + { + $e = Doctrine_Query::bracketExplode($func, ' '); + $func = $e[0]; + + $pos = strpos($func, '('); + $name = substr($func, 0, $pos); + + try { + $argStr = substr($func, ($pos + 1), -1); + $args = explode(',', $argStr); + + $func = call_user_func_array(array($this->conn->expression, $name), $args); + + if(substr($func, 0, 1) !== '(') { + $pos = strpos($func, '('); + $name = substr($func, 0, $pos); + } else { + $name = $func; + } + + $e2 = explode(' ', $args[0]); + + $distinct = ''; + if(count($e2) > 1) { + if(strtoupper($e2[0]) == 'DISTINCT') + $distinct = 'DISTINCT '; + + $args[0] = $e2[1]; + } + + + + $parts = explode('.', $args[0]); + $owner = $parts[0]; + $alias = (isset($e[1])) ? $e[1] : $name; + + $e3 = explode('.', $alias); + + if(count($e3) > 1) { + $alias = $e3[1]; + $owner = $e3[0]; + } + + // a function without parameters eg. RANDOM() + if ($owner === '') { + $owner = 0; + } + + $this->pendingAggregates[$owner][] = array($name, $args, $distinct, $alias); + } catch(Doctrine_Expression_Exception $e) { + throw new Doctrine_Query_Exception('Unknown function ' . $func . '.'); + } + } + public function processPendingSubqueries() + { + if ($this->subqueriesProcessed === true) { + return false; + } + + foreach ($this->pendingSubqueries as $value) { + list($dql, $alias) = $value; + + $sql = $this->createSubquery()->parseQuery($dql, false)->getQuery(); + + reset($this->tableAliases); + + $tableAlias = current($this->tableAliases); + + reset($this->compAliases); + + $componentAlias = key($this->compAliases); + + $sqlAlias = $tableAlias . '__' . count($this->aggregateMap); + + $this->parts['select'][] = '(' . $sql . ') AS ' . $sqlAlias; + + $this->aggregateMap[$alias] = $sqlAlias; + $this->subqueryAggregates[$componentAlias][] = $alias; + } + $this->subqueriesProcessed = true; + + return true; + } + public function processPendingAggregates($componentAlias) + { + $tableAlias = $this->getTableAlias($componentAlias); + + if ( ! isset($this->tables[$tableAlias])) { + throw new Doctrine_Query_Exception('Unknown component path ' . $componentAlias); + } + + $root = current($this->tables); + $table = $this->tables[$tableAlias]; + $aggregates = array(); + + if(isset($this->pendingAggregates[$componentAlias])) { + $aggregates = $this->pendingAggregates[$componentAlias]; + } + + if ($root === $table) { + if (isset($this->pendingAggregates[0])) { + $aggregates += $this->pendingAggregates[0]; + } + } + + foreach($aggregates as $parts) { + list($name, $args, $distinct, $alias) = $parts; + + $arglist = array(); + foreach($args as $arg) { + $e = explode('.', $arg); + + + if (is_numeric($arg)) { + $arglist[] = $arg; + } elseif (count($e) > 1) { + //$tableAlias = $this->getTableAlias($e[0]); + $table = $this->tables[$tableAlias]; + + $e[1] = $table->getColumnName($e[1]); + + if( ! $table->hasColumn($e[1])) { + throw new Doctrine_Query_Exception('Unknown column ' . $e[1]); + } + + $arglist[] = $tableAlias . '.' . $e[1]; + } else { + $arglist[] = $e[0]; + } + } + + $sqlAlias = $tableAlias . '__' . count($this->aggregateMap); + + if(substr($name, 0, 1) !== '(') { + $this->parts['select'][] = $name . '(' . $distinct . implode(', ', $arglist) . ') AS ' . $sqlAlias; + } else { + $this->parts['select'][] = $name . ' AS ' . $sqlAlias; + } + $this->aggregateMap[$alias] = $sqlAlias; + $this->neededTables[] = $tableAlias; + } + } + /** + * count + * + * @param array $params + * @return integer + */ + public function count($params = array()) + { + $parts_old = $this->parts; + + $this->remove('select'); + $join = $this->join; + $where = $this->where; + $having = $this->having; + $table = reset($this->tables); + + $q = 'SELECT COUNT(DISTINCT ' . $this->aliasHandler->getShortAlias($table->getTableName()) + . '.' . $table->getIdentifier() + . ') FROM ' . $table->getTableName() . ' ' . $this->aliasHandler->getShortAlias($table->getTableName()); + + foreach($join as $j) { + $q .= ' '.implode(' ',$j); + } + $string = $this->applyInheritance(); + + if( ! empty($where)) { + $q .= ' WHERE ' . implode(' AND ', $where); + if( ! empty($string)) + $q .= ' AND (' . $string . ')'; + } else { + if( ! empty($string)) + $q .= ' WHERE (' . $string . ')'; + } + + if( ! empty($having)) + $q .= ' HAVING ' . implode(' AND ',$having); + + if( ! is_array($params)) + $params = array($params); + + $params = array_merge($this->params, $params); + + $this->parts = $parts_old; + return (int) $this->getConnection()->fetchOne($q, $params); + } + /** + * loadFields + * loads fields for a given table and + * constructs a little bit of sql for every field + * + * fields of the tables become: [tablename].[fieldname] as [tablename]__[fieldname] + * + * @access private + * @param object Doctrine_Table $table a Doctrine_Table object + * @param integer $fetchmode fetchmode the table is using eg. Doctrine::FETCH_LAZY + * @param array $names fields to be loaded (only used in lazy property loading) + * @return void + */ + protected function loadFields(Doctrine_Table $table, $fetchmode, array $names, $cpath) + { + $name = $table->getComponentName(); + + switch($fetchmode): + case Doctrine::FETCH_OFFSET: + $this->limit = $table->getAttribute(Doctrine::ATTR_COLL_LIMIT); + case Doctrine::FETCH_IMMEDIATE: + if( ! empty($names)) { + // only auto-add the primary key fields if this query object is not + // a subquery of another query object + $names = array_unique(array_merge($table->getPrimaryKeys(), $names)); + } else { + $names = $table->getColumnNames(); + } + break; + case Doctrine::FETCH_LAZY_OFFSET: + $this->limit = $table->getAttribute(Doctrine::ATTR_COLL_LIMIT); + case Doctrine::FETCH_LAZY: + case Doctrine::FETCH_BATCH: + $names = array_unique(array_merge($table->getPrimaryKeys(), $names)); + break; + default: + throw new Doctrine_Exception("Unknown fetchmode."); + endswitch; + + $component = $table->getComponentName(); + $tablename = $this->tableAliases[$cpath]; + + $this->fetchModes[$tablename] = $fetchmode; + + $count = count($this->tables); + + foreach($names as $name) { + if($count == 0) { + $this->parts['select'][] = $tablename . '.' . $name; + } else { + $this->parts['select'][] = $tablename . '.' . $name . ' AS ' . $tablename . '__' . $name; + } + } + } + /** + * addFrom + * + * @param strint $from + * @return Doctrine_Query + */ + public function addFrom($from) + { + $class = 'Doctrine_Query_From'; + $parser = new $class($this); + $parser->parse($from); + + return $this; + } + /** + * leftJoin + * + * @param strint $join + * @return Doctrine_Query + */ + public function leftJoin($join) + { + $class = 'Doctrine_Query_From'; + $parser = new $class($this); + $parser->parse('LEFT JOIN ' . $join); + + return $this; + } + /** + * innerJoin + * + * @param strint $join + * @return Doctrine_Query + */ + public function innerJoin($join) + { + $class = 'Doctrine_Query_From'; + $parser = new $class($this); + $parser->parse('INNER JOIN ' . $join); + + return $this; + } + /** + * addOrderBy + * + * @param strint $orderby + * @return Doctrine_Query + */ + public function addOrderBy($orderby) + { + if (empty($orderby)) { + return $this; + } + $class = 'Doctrine_Query_Orderby'; + $parser = new $class($this); + $this->parts['orderby'][] = $parser->parse($orderby); + + return $this; + } + /** + * addWhere + * + * @param string $where + * @param mixed $params + */ + public function addWhere($where, $params = array()) + { + $class = 'Doctrine_Query_Where'; + $parser = new $class($this); + $this->parts['where'][] = $parser->parse($where); + + if(is_array($params)) { + $this->params = array_merge($this->params, $params); + } else { + $this->params[] = $params; + } + + return $this; + } + /** + * addSelect + * + * @param string $select + */ + public function addSelect($select) + { + $this->type = self::SELECT; + + $this->parseSelect($select); + + return $this; + } + /** + * addHaving + * + * @param string $having + */ + public function addHaving($having) + { + $class = 'Doctrine_Query_Having'; + $parser = new $class($this); + $this->parts['having'][] = $parser->parse($having); + + return $this; + } + /** + * sets a query part + * + * @param string $name + * @param array $args + * @return void + */ + public function __call($name, $args) + { + $name = strtolower($name); + + $method = 'parse' . ucwords($name); + + switch($name) { + case 'select': + $this->type = self::SELECT; + + if ( ! isset($args[0])) { + throw new Doctrine_Query_Exception('Empty select part'); + } + $this->parseSelect($args[0]); + break; + case 'delete': + $this->type = self::DELETE; + break; + case 'update': + $this->type = self::UPDATE; + $name = 'from'; + case 'from': + $this->parts['from'] = array(); + $this->parts['select'] = array(); + $this->parts['join'] = array(); + $this->joins = array(); + $this->tables = array(); + $this->fetchModes = array(); + $this->tableIndexes = array(); + $this->tableAliases = array(); + $this->aliasHandler->clear(); + + $class = "Doctrine_Query_".ucwords($name); + $parser = new $class($this); + + $parser->parse($args[0]); + break; + case 'where': + if(isset($args[1])) { + if(is_array($args[1])) { + $this->params = $args[1]; + } else { + $this->params = array($args[1]); + } + } + case 'having': + case 'orderby': + case 'groupby': + if (empty($args[0])) { + return $this; + } + + $class = 'Doctrine_Query_' . ucwords($name); + $parser = new $class($this); + + $this->parts[$name] = array($parser->parse($args[0])); + break; + case 'limit': + case 'offset': + if($args[0] == null) { + $args[0] = false; + } + + $this->parts[$name] = $args[0]; + break; + default: + $this->parts[$name] = array(); + if (method_exists($this, $method)) { + $this->$method($args[0]); + } + + throw new Doctrine_Query_Exception("Unknown overload method"); + } + + + return $this; + } + /** + * returns a query part + * + * @param $name query part name + * @return mixed + */ + public function get($name) + { + if( ! isset($this->parts[$name])) + return false; + + return $this->parts[$name]; + } + /** + * set + * sets a query SET part + * this method should only be used with UPDATE queries + * + * @param $name name of the field + * @param $value field value + * @return Doctrine_Query + */ + public function set($name, $value) + { + $class = new Doctrine_Query_Set($this); + $this->parts['set'][] = $class->parse($name . ' = ' . $value); + + return $this; + } + /** + * @return boolean + */ + public function isLimitSubqueryUsed() { + return $this->limitSubqueryUsed; + } + /** + * getQueryBase + * returns the base of the generated sql query + * On mysql driver special strategy has to be used for DELETE statements + * + * @return string the base of the generated sql query + */ + public function getQueryBase() + { + switch ($this->type) { + case self::DELETE: + /** + no longer needed? + + if ($this->conn->getName() == 'Mysql') { + $q = 'DELETE ' . end($this->tableAliases) . ' FROM '; + } else { + */ + $q = 'DELETE FROM '; + // } + break; + case self::UPDATE: + $q = 'UPDATE '; + break; + case self::SELECT: + $distinct = ($this->isDistinct()) ? 'DISTINCT ' : ''; + + $q = 'SELECT '.$distinct.implode(', ', $this->parts['select']).' FROM '; + break; + } + return $q; + } + /** + * builds the sql query from the given parameters and applies things such as + * column aggregation inheritance and limit subqueries if needed + * + * @param array $params an array of prepared statement params (needed only in mysql driver + * when limit subquery algorithm is used) + * @return string the built sql query + */ + public function getQuery($params = array()) + { + if(empty($this->parts["select"]) || empty($this->parts["from"])) + return false; + + $needsSubQuery = false; + $subquery = ''; + $k = array_keys($this->tables); + $table = $this->tables[$k[0]]; + + if( ! empty($this->parts['limit']) && $this->needsSubquery && $table->getAttribute(Doctrine::ATTR_QUERY_LIMIT) == Doctrine::LIMIT_RECORDS) { + $needsSubQuery = true; + $this->limitSubqueryUsed = true; + } + + // process all pending SELECT part subqueries + $this->processPendingSubqueries(); + + // build the basic query + + $str = ''; + if($this->isDistinct()) + $str = 'DISTINCT '; + + $q = $this->getQueryBase(); + + $q .= $this->parts['from']; + /** + var_dump($this->pendingFields); + var_dump($this->subqueryAliases); */ + //var_dump($this->parts['join']); + + foreach($this->parts['join'] as $parts) { + foreach($parts as $part) { + // preserve LEFT JOINs only if needed + + if(substr($part, 0,9) === 'LEFT JOIN') { + $e = explode(' ', $part); + + $aliases = array_merge($this->subqueryAliases, + array_keys($this->neededTables)); + + + if( ! in_array($e[3], $aliases) && + ! in_array($e[2], $aliases) && + + ! empty($this->pendingFields)) { + continue; + } + + } + + $e = explode(' ON ', $part); + + // we can always be sure that the first join condition exists + $e2 = explode(' AND ', $e[1]); + + $part = $e[0] . ' ON ' + . array_shift($e2); + + if( ! empty($e2)) { + $parser = new Doctrine_Query_JoinCondition($this); + $part .= ' AND ' . $parser->parse(implode(' AND ', $e2)); + } + + $q .= ' ' . $part; + } + } + /** + if( ! empty($this->parts['join'])) { + foreach($this->parts['join'] as $part) { + $q .= ' '.implode(' ', $part); + } + } + */ + + if( ! empty($this->parts['set'])) { + $q .= ' SET ' . implode(', ', $this->parts['set']); + } + + $string = $this->applyInheritance(); + + if( ! empty($string)) + $this->parts['where'][] = '('.$string.')'; + + + + $modifyLimit = true; + if( ! empty($this->parts["limit"]) || ! empty($this->parts["offset"])) { + + if($needsSubQuery) { + $subquery = $this->getLimitSubquery(); + + + switch(strtolower($this->conn->getName())) { + case 'mysql': + // mysql doesn't support LIMIT in subqueries + $list = $this->conn->execute($subquery, $params)->fetchAll(PDO::FETCH_COLUMN); + $subquery = implode(', ', $list); + break; + case 'pgsql': + // pgsql needs special nested LIMIT subquery + $subquery = 'SELECT doctrine_subquery_alias.' . $table->getIdentifier(). ' FROM (' . $subquery . ') AS doctrine_subquery_alias'; + break; + } + + $field = $this->aliasHandler->getShortAlias($table->getTableName()) . '.' . $table->getIdentifier(); + + // only append the subquery if it actually contains something + if($subquery !== '') + array_unshift($this->parts['where'], $field. ' IN (' . $subquery . ')'); + + $modifyLimit = false; + } + } + + $q .= ( ! empty($this->parts['where']))? ' WHERE ' . implode(' AND ', $this->parts['where']):''; + $q .= ( ! empty($this->parts['groupby']))? ' GROUP BY ' . implode(', ', $this->parts['groupby']):''; + $q .= ( ! empty($this->parts['having']))? ' HAVING ' . implode(' AND ', $this->parts['having']):''; + $q .= ( ! empty($this->parts['orderby']))? ' ORDER BY ' . implode(', ', $this->parts['orderby']):''; + + if($modifyLimit) + $q = $this->conn->modifyLimitQuery($q, $this->parts['limit'], $this->parts['offset']); + + // return to the previous state + if( ! empty($string)) + array_pop($this->parts['where']); + if($needsSubQuery) + array_shift($this->parts['where']); + + return $q; + } + /** + * this is method is used by the record limit algorithm + * + * when fetching one-to-many, many-to-many associated data with LIMIT clause + * an additional subquery is needed for limiting the number of returned records instead + * of limiting the number of sql result set rows + * + * @return string the limit subquery + */ + public function getLimitSubquery() + { + $k = array_keys($this->tables); + $table = $this->tables[$k[0]]; + + // get short alias + $alias = $this->aliasHandler->getShortAlias($table->getTableName()); + $primaryKey = $alias . '.' . $table->getIdentifier(); + + // initialize the base of the subquery + $subquery = 'SELECT DISTINCT ' . $primaryKey; + + if ($this->conn->getDBH()->getAttribute(PDO::ATTR_DRIVER_NAME) == 'pgsql') { + // pgsql needs the order by fields to be preserved in select clause + + foreach ($this->parts['orderby'] as $part) { + $e = explode(' ', $part); + + // don't add primarykey column (its already in the select clause) + if ($e[0] !== $primaryKey) { + $subquery .= ', ' . $e[0]; + } + } + } + + $subquery .= ' FROM ' . $this->conn->quoteIdentifier($table->getTableName()) . ' ' . $alias; + + foreach ($this->parts['join'] as $parts) { + foreach ($parts as $part) { + // preserve LEFT JOINs only if needed + if (substr($part,0,9) === 'LEFT JOIN') { + $e = explode(' ', $part); + + if ( ! in_array($e[3], $this->subqueryAliases) && + ! in_array($e[2], $this->subqueryAliases)) { + continue; + } + + } + + $subquery .= ' '.$part; + } + } + + // all conditions must be preserved in subquery + $subquery .= ( ! empty($this->parts['where']))? ' WHERE ' . implode(' AND ', $this->parts['where']) : ''; + $subquery .= ( ! empty($this->parts['groupby']))? ' GROUP BY ' . implode(', ', $this->parts['groupby']) : ''; + $subquery .= ( ! empty($this->parts['having']))? ' HAVING ' . implode(' AND ', $this->parts['having']) : ''; + $subquery .= ( ! empty($this->parts['orderby']))? ' ORDER BY ' . implode(', ', $this->parts['orderby']) : ''; + + // add driver specific limit clause + $subquery = $this->conn->modifyLimitQuery($subquery, $this->parts['limit'], $this->parts['offset']); + + $parts = self::quoteExplode($subquery, ' ', "'", "'"); + + foreach($parts as $k => $part) { + if(strpos($part, "'") !== false) { + continue; + } + + if($this->aliasHandler->hasAliasFor($part)) { + $parts[$k] = $this->aliasHandler->generateNewAlias($part); + } + + if(strpos($part, '.') !== false) { + $e = explode('.', $part); + + $trimmed = ltrim($e[0], '( '); + $pos = strpos($e[0], $trimmed); + + $e[0] = substr($e[0], 0, $pos) . $this->aliasHandler->generateNewAlias($trimmed); + $parts[$k] = implode('.', $e); + } + } + $subquery = implode(' ', $parts); + + return $subquery; + } + /** + * query the database with DQL (Doctrine Query Language) + * + * @param string $query DQL query + * @param array $params parameters + */ + public function query($query,$params = array()) + { + $this->parseQuery($query); + + if($this->aggregate) { + $keys = array_keys($this->tables); + $query = $this->getQuery(); + $stmt = $this->tables[$keys[0]]->getConnection()->select($query, $this->parts["limit"], $this->parts["offset"]); + $data = $stmt->fetch(PDO::FETCH_ASSOC); + if(count($data) == 1) { + return current($data); + } else { + return $data; + } + } else { + return $this->execute($params); + } + } + /** + * splitQuery + * splits the given dql query into an array where keys + * represent different query part names and values are + * arrays splitted using sqlExplode method + * + * example: + * + * parameter: + * $query = "SELECT u.* FROM User u WHERE u.name LIKE ?" + * returns: + * array('select' => array('u.*'), + * 'from' => array('User', 'u'), + * 'where' => array('u.name', 'LIKE', '?')) + * + * @param string $query DQL query + * @throws Doctrine_Query_Exception if some generic parsing error occurs + * @return array an array containing the query string parts + */ + public function splitQuery($query) + { + $e = self::sqlExplode($query, ' '); + + foreach($e as $k=>$part) { + $part = trim($part); + switch(strtolower($part)) { + case 'delete': + case 'update': + case 'select': + case 'set': + case 'from': + case 'where': + case 'limit': + case 'offset': + case 'having': + $p = $part; + $parts[$part] = array(); + break; + case 'order': + case 'group': + $i = ($k + 1); + if(isset($e[$i]) && strtolower($e[$i]) === "by") { + $p = $part; + $parts[$part] = array(); + } else + $parts[$p][] = $part; + break; + case "by": + continue; + default: + if( ! isset($p)) + throw new Doctrine_Query_Exception("Couldn't parse query."); + + $parts[$p][] = $part; + } + } + return $parts; + } + /** + * DQL PARSER + * parses a DQL query + * first splits the query in parts and then uses individual + * parsers for each part + * + * @param string $query DQL query + * @param boolean $clear whether or not to clear the aliases + * @throws Doctrine_Query_Exception if some generic parsing error occurs + * @return Doctrine_Query + */ + public function parseQuery($query, $clear = true) + { + if($clear) + $this->clear(); + + $query = trim($query); + $query = str_replace("\n", ' ', $query); + $query = str_replace("\r", ' ', $query); + + $parts = $this->splitQuery($query); + + foreach($parts as $k => $part) { + $part = implode(' ', $part); + switch(strtoupper($k)) { + case 'CREATE': + $this->type = self::CREATE; + break; + case 'INSERT': + $this->type = self::INSERT; + break; + case 'DELETE': + $this->type = self::DELETE; + break; + case 'SELECT': + $this->type = self::SELECT; + $this->parseSelect($part); + break; + case 'UPDATE': + $this->type = self::UPDATE; + $k = 'FROM'; + + case 'FROM': + $class = 'Doctrine_Query_' . ucwords(strtolower($k)); + $parser = new $class($this); + $parser->parse($part); + break; + case 'SET': + $class = 'Doctrine_Query_' . ucwords(strtolower($k)); + $parser = new $class($this); + $this->parts['set'][] = $parser->parse($part); + break; + case 'GROUP': + case 'ORDER': + $k .= 'by'; + case 'WHERE': + case 'HAVING': + $class = 'Doctrine_Query_' . ucwords(strtolower($k)); + $parser = new $class($this); + + $name = strtolower($k); + $this->parts[$name][] = $parser->parse($part); + break; + case 'LIMIT': + $this->parts['limit'] = trim($part); + break; + case 'OFFSET': + $this->parts['offset'] = trim($part); + break; + } + } + + return $this; + } + /** + * DQL ORDER BY PARSER + * parses the order by part of the query string + * + * @param string $str + * @return void + */ + final public function parseOrderBy($str) + { + $parser = new Doctrine_Query_Part_Orderby($this); + return $parser->parse($str); + } + /** + * returns Doctrine::FETCH_* constant + * + * @param string $mode + * @return integer + */ + final public function parseFetchMode($mode) + { + switch(strtolower($mode)): + case "i": + case "immediate": + $fetchmode = Doctrine::FETCH_IMMEDIATE; + break; + case "b": + case "batch": + $fetchmode = Doctrine::FETCH_BATCH; + break; + case "l": + case "lazy": + $fetchmode = Doctrine::FETCH_LAZY; + break; + case "o": + case "offset": + $fetchmode = Doctrine::FETCH_OFFSET; + break; + case "lo": + case "lazyoffset": + $fetchmode = Doctrine::FETCH_LAZYOFFSET; + default: + throw new Doctrine_Query_Exception("Unknown fetchmode '$mode'. The availible fetchmodes are 'i', 'b' and 'l'."); + endswitch; + return $fetchmode; + } + /** + * trims brackets + * + * @param string $str + * @param string $e1 the first bracket, usually '(' + * @param string $e2 the second bracket, usually ')' + */ + public static function bracketTrim($str,$e1 = '(',$e2 = ')') + { + if(substr($str,0,1) == $e1 && substr($str,-1) == $e2) + return substr($str,1,-1); + else + return $str; + } + /** + * bracketExplode + * + * example: + * + * parameters: + * $str = (age < 20 AND age > 18) AND email LIKE 'John@example.com' + * $d = ' AND ' + * $e1 = '(' + * $e2 = ')' + * + * would return an array: + * array("(age < 20 AND age > 18)", + * "email LIKE 'John@example.com'") + * + * @param string $str + * @param string $d the delimeter which explodes the string + * @param string $e1 the first bracket, usually '(' + * @param string $e2 the second bracket, usually ')' + * + */ + public static function bracketExplode($str, $d = ' ', $e1 = '(', $e2 = ')') + { + if(is_array($d)) { + $a = preg_split('/('.implode('|', $d).')/', $str); + $d = stripslashes($d[0]); + } else + $a = explode("$d",$str); + + $i = 0; + $term = array(); + foreach($a as $key=>$val) { + if (empty($term[$i])) { + $term[$i] = trim($val); + $s1 = substr_count($term[$i], "$e1"); + $s2 = substr_count($term[$i], "$e2"); + if($s1 == $s2) $i++; + } else { + $term[$i] .= "$d".trim($val); + $c1 = substr_count($term[$i], "$e1"); + $c2 = substr_count($term[$i], "$e2"); + if($c1 == $c2) $i++; + } + } + return $term; + } + /** + * quoteExplode + * + * example: + * + * parameters: + * $str = email LIKE 'John@example.com' + * $d = ' AND ' + * + * would return an array: + * array("email", "LIKE", "'John@example.com'") + * + * @param string $str + * @param string $d the delimeter which explodes the string + */ + public static function quoteExplode($str, $d = ' ') + { + if(is_array($d)) { + $a = preg_split('/('.implode('|', $d).')/', $str); + $d = stripslashes($d[0]); + } else + $a = explode("$d",$str); + + $i = 0; + $term = array(); + foreach($a as $key => $val) { + if (empty($term[$i])) { + $term[$i] = trim($val); + + if( ! (substr_count($term[$i], "'") & 1)) + $i++; + } else { + $term[$i] .= "$d".trim($val); + + if( ! (substr_count($term[$i], "'") & 1)) + $i++; + } + } + return $term; + } + /** + * sqlExplode + * + * explodes a string into array using custom brackets and + * quote delimeters + * + * + * example: + * + * parameters: + * $str = "(age < 20 AND age > 18) AND name LIKE 'John Doe'" + * $d = ' ' + * $e1 = '(' + * $e2 = ')' + * + * would return an array: + * array('(age < 20 AND age > 18)', + * 'name', + * 'LIKE', + * 'John Doe') + * + * @param string $str + * @param string $d the delimeter which explodes the string + * @param string $e1 the first bracket, usually '(' + * @param string $e2 the second bracket, usually ')' + * + * @return array + */ + public static function sqlExplode($str, $d = ' ', $e1 = '(', $e2 = ')') + { + if ($d == ' ') { + $d = array(' ', '\s'); + } + if(is_array($d)) { + if (in_array(' ', $d)) { + $d[] = '\s'; + } + $str = preg_split('/('.implode('|', $d).')/', $str); + $d = stripslashes($d[0]); + } else { + $str = explode("$d",$str); + } + + $i = 0; + $term = array(); + foreach($str as $key => $val) { + if (empty($term[$i])) { + $term[$i] = trim($val); + + $s1 = substr_count($term[$i],"$e1"); + $s2 = substr_count($term[$i],"$e2"); + + if (substr($term[$i],0,1) == "(") { + if($s1 == $s2) { + $i++; + } + } else { + if ( ! (substr_count($term[$i], "'") & 1) && + ! (substr_count($term[$i], "\"") & 1) && + ! (substr_count($term[$i], "�") & 1) + ) { $i++; } + } + } else { + $term[$i] .= "$d".trim($val); + $c1 = substr_count($term[$i],"$e1"); + $c2 = substr_count($term[$i],"$e2"); + + if(substr($term[$i],0,1) == "(") { + if($c1 == $c2) { + $i++; + } + } else { + if ( ! (substr_count($term[$i], "'") & 1) && + ! (substr_count($term[$i], "\"") & 1) && + ! (substr_count($term[$i], "�") & 1) + ) { $i++; } + } + } + } + return $term; + } + /** + * generateAlias + * + * @param string $tableName + * @return string + */ + public function generateAlias($tableName) + { + if(isset($this->tableIndexes[$tableName])) { + return $tableName.++$this->tableIndexes[$tableName]; + } else { + $this->tableIndexes[$tableName] = 1; + return $tableName; + } + } + + /** + * loads a component + * + * @param string $path the path of the loadable component + * @param integer $fetchmode optional fetchmode, if not set the components default fetchmode will be used + * @throws Doctrine_Query_Exception + * @return Doctrine_Table + */ + final public function load($path, $loadFields = true) + { + + // parse custom join conditions + $e = explode(' ON ', $path); + + $joinCondition = ''; + if(count($e) > 1) { + $joinCondition = ' AND ' . $e[1]; + $path = $e[0]; + } + + $tmp = explode(' ',$path); + $componentAlias = (count($tmp) > 1) ? end($tmp) : false; + + $e = preg_split("/[.:]/", $tmp[0], -1); + + + if(isset($this->compAliases[$e[0]])) { + $end = substr($tmp[0], strlen($e[0])); + $path = $this->compAliases[$e[0]] . $end; + $e = preg_split("/[.:]/", $path, -1); + } else { + $path = $tmp[0]; + } + + + + $index = 0; + $currPath = ''; + $this->tableStack = array(); + + foreach($e as $key => $fullname) { + try { + $e2 = preg_split("/[-(]/",$fullname); + $name = $e2[0]; + + $currPath .= '.' . $name; + + if($key == 0) { + $currPath = substr($currPath,1); + + $this->conn = Doctrine_Manager::getInstance() + ->getConnectionForComponent($name); + + $table = $this->conn->getTable($name); + + + $tname = $this->aliasHandler->getShortAlias($table->getTableName()); + + if( ! isset($this->tableAliases[$currPath])) { + $this->tableIndexes[$tname] = 1; + } + + $this->parts['from'] = $this->conn->quoteIdentifier($table->getTableName()); + + if ($this->type === self::SELECT) { + $this->parts['from'] .= ' ' . $tname; + } + + $this->tableAliases[$currPath] = $tname; + + $tableName = $tname; + } else { + + $index += strlen($e[($key - 1)]) + 1; + // the mark here is either '.' or ':' + $mark = substr($path, ($index - 1), 1); + + if(isset($this->tableAliases[$prevPath])) { + $tname = $this->tableAliases[$prevPath]; + } else { + $tname = $this->aliasHandler->getShortAlias($table->getTableName()); + } + + $fk = $table->getRelation($name); + $name = $fk->getTable()->getComponentName(); + $original = $fk->getTable()->getTableName(); + + + + if (isset($this->tableAliases[$currPath])) { + $tname2 = $this->tableAliases[$currPath]; + } else { + $tname2 = $this->aliasHandler->generateShortAlias($original); + } + + $aliasString = $this->conn->quoteIdentifier($original) . ' ' . $tname2; + + switch ($mark) { + case ':': + $join = 'INNER JOIN '; + break; + case '.': + $join = 'LEFT JOIN '; + break; + default: + throw new Doctrine_Query_Exception("Unknown operator '$mark'"); + } + + if( ! $fk->isOneToOne()) { + $this->needsSubquery = true; + } + + + $map = $fk->getTable()->inheritanceMap; + + if( ! $loadFields || ! empty($map) || $joinCondition) { + $this->subqueryAliases[] = $tname2; + } + + + if ($fk instanceof Doctrine_Relation_Association) { + $asf = $fk->getAssociationFactory(); + + $assocTableName = $asf->getTableName(); + + if( ! $loadFields || ! empty($map) || $joinCondition) { + $this->subqueryAliases[] = $assocTableName; + } + + $assocPath = $prevPath . '.' . $asf->getComponentName(); + + if (isset($this->tableAliases[$assocPath])) { + $assocAlias = $this->tableAliases[$assocPath]; + } else { + $assocAlias = $this->aliasHandler->generateShortAlias($assocTableName); + } + + $this->parts['join'][$tname][$assocTableName] = $join . $assocTableName . ' ' . $assocAlias . ' ON ' . $tname . '.' + . $table->getIdentifier() . ' = ' + . $assocAlias . '.' . $fk->getLocal(); + + if ($fk instanceof Doctrine_Relation_Association_Self) { + $this->parts['join'][$tname][$assocTableName] .= ' OR ' . $tname . '.' . $table->getIdentifier() . ' = ' + . $assocAlias . '.' . $fk->getForeign(); + } + + $this->parts['join'][$tname][$tname2] = $join . $aliasString . ' ON ' . $tname2 . '.' + . $fk->getTable()->getIdentifier() . ' = ' + . $assocAlias . '.' . $fk->getForeign() + . $joinCondition; + + if ($fk instanceof Doctrine_Relation_Association_Self) { + $this->parts['join'][$tname][$tname2] .= ' OR ' . $tname2 . '.' . $table->getIdentifier() . ' = ' + . $assocAlias . '.' . $fk->getLocal(); + } + + } else { + $this->parts['join'][$tname][$tname2] = $join . $aliasString + . ' ON ' . $tname . '.' + . $fk->getLocal() . ' = ' . $tname2 . '.' . $fk->getForeign() + . $joinCondition; + } + + + $this->joins[$tname2] = $prevTable; + + + $table = $fk->getTable(); + + $this->tableAliases[$currPath] = $tname2; + + $tableName = $tname2; + + $this->relationStack[] = $fk; + } + + $this->components[$currPath] = $table; + + $this->tableStack[] = $table; + + if( ! isset($this->tables[$tableName])) { + $this->tables[$tableName] = $table; + + if ($loadFields) { + + $skip = false; + + if ( ! empty($this->pendingFields) || + ! empty($this->pendingAggregates)) { + $skip = true; + } + + if ($componentAlias) { + $this->compAliases[$componentAlias] = $currPath; + + if(isset($this->pendingFields[$componentAlias])) { + $this->processPendingFields($componentAlias); + $skip = true; + } + if(isset($this->pendingAggregates[$componentAlias]) || + (current($this->tables) === $table && isset($this->pendingAggregates[0])) + ) { + $this->processPendingAggregates($componentAlias); + $skip = true; + } + } + + if ( ! $skip) { + $this->parseFields($fullname, $tableName, $e2, $currPath); + } + } + } + + + $prevPath = $currPath; + $prevTable = $tableName; + } catch(Exception $e) { + throw new Doctrine_Query_Exception($e->__toString()); + } + } + + if($componentAlias !== false) { + $this->compAliases[$componentAlias] = $currPath; + } + + return $table; + } + /** + * parseFields + * + * @param string $fullName + * @param string $tableName + * @param array $exploded + * @param string $currPath + * @return void + */ + final public function parseFields($fullName, $tableName, array $exploded, $currPath) + { + $table = $this->tables[$tableName]; + + $fields = array(); + + if(strpos($fullName, '-') === false) { + $fetchmode = $table->getAttribute(Doctrine::ATTR_FETCHMODE); + + if(isset($exploded[1])) { + if(count($exploded) > 2) { + $fields = $this->parseAggregateValues($fullName, $tableName, $exploded, $currPath); + } elseif(count($exploded) == 2) { + $fields = explode(',',substr($exploded[1],0,-1)); + } + } + } else { + if(isset($exploded[1])) { + $fetchmode = $this->parseFetchMode($exploded[1]); + } else + $fetchmode = $table->getAttribute(Doctrine::ATTR_FETCHMODE); + + if(isset($exploded[2])) { + if(substr_count($exploded[2], ')') > 1) { + + } else { + $fields = explode(',', substr($exploded[2],0,-1)); + } + } + + } + if( ! $this->aggregate) + $this->loadFields($table, $fetchmode, $fields, $currPath); + } + /** + * parseAggregateFunction + * + * @param string $func + * @param string $reference + * @return string + */ + public function parseAggregateFunction($func,$reference) + { + $pos = strpos($func, '('); + + if($pos !== false) { + $funcs = array(); + + $name = substr($func, 0, $pos); + $func = substr($func, ($pos + 1), -1); + $params = Doctrine_Query::bracketExplode($func, ',', '(', ')'); + + foreach($params as $k => $param) { + $params[$k] = $this->parseAggregateFunction($param,$reference); + } + + $funcs = $name . '(' . implode(', ', $params). ')'; + + return $funcs; + + } else { + if( ! is_numeric($func)) { + + $func = $this->getTableAlias($reference).'.'.$func; + + return $func; + } else { + + return $func; + } + } + } + /** + * parseAggregateValues + */ + public function parseAggregateValues($fullName, $tableName, array $exploded, $currPath) + { + $this->aggregate = true; + $pos = strpos($fullName, '('); + $name = substr($fullName, 0, $pos); + $string = substr($fullName, ($pos + 1), -1); + + $exploded = Doctrine_Query::bracketExplode($string, ','); + foreach($exploded as $k => $value) { + $func = $this->parseAggregateFunction($value, $currPath); + $exploded[$k] = $func; + + $this->parts['select'][] = $exploded[$k]; + } + } +} + diff --git a/draft/new-core/RawSql.php b/draft/new-core/RawSql.php new file mode 100644 index 000000000..054fb6b11 --- /dev/null +++ b/draft/new-core/RawSql.php @@ -0,0 +1,246 @@ +. + */ +Doctrine::autoload('Doctrine_Hydrate'); +/** + * Doctrine_RawSql + * + * @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: 1181 $ + * @author Konsta Vesterinen + */ +class Doctrine_RawSql extends Doctrine_Hydrate +{ + /** + * @var array $fields + */ + private $fields; + /** + * __call + * method overloader + * + * @param string $name + * @param array $args + * @return Doctrine_RawSql + */ + public function __call($name, $args) + { + if ( ! isset($this->parts[$name])) { + throw new Doctrine_RawSql_Exception("Unknown overload method $name. Availible overload methods are ".implode(" ",array_keys($this->parts))); + } + if ($name == 'select') { + preg_match_all('/{([^}{]*)}/U', $args[0], $m); + + $this->fields = $m[1]; + $this->parts["select"] = array(); + } else { + $this->parts[$name][] = $args[0]; + } + return $this; + } + /** + * get + */ + public function get($name) + { + if ( ! isset($this->parts[$name])) { + throw new Doctrine_RawSql_Exception('Unknown query part '.$name); + } + return $this->parts[$name]; + } + /** + * parseQuery + * + * @param string $query + * @return Doctrine_RawSql + */ + public function parseQuery($query) + { + preg_match_all('/{([^}{]*)}/U', $query, $m); + + $this->fields = $m[1]; + $this->clear(); + + $e = Doctrine_Query::sqlExplode($query,' '); + + foreach ($e as $k => $part) { + $low = strtolower($part); + switch (strtolower($part)) { + case "select": + case "from": + case "where": + case "limit": + case "offset": + case "having": + $p = $low; + if ( ! isset($parts[$low])) { + $parts[$low] = array(); + } + break; + case "order": + case "group": + $i = ($k + 1); + if (isset($e[$i]) && strtolower($e[$i]) === "by") { + $p = $low; + $p .= "by"; + $parts[$low."by"] = array(); + + } else { + $parts[$p][] = $part; + } + break; + case "by": + continue; + default: + if ( ! isset($parts[$p][0])) { + $parts[$p][0] = $part; + } else { + $parts[$p][0] .= ' '.$part; + } + }; + }; + + $this->parts = $parts; + $this->parts["select"] = array(); + + return $this; + } + /** + * getQuery + * + * + * @return string + */ + public function getQuery() + { + foreach ($this->fields as $field) { + $e = explode(".", $field); + if ( ! isset($e[1])) { + throw new Doctrine_RawSql_Exception("All selected fields in Sql query must be in format tableAlias.fieldName"); + } + if ( ! isset($this->tables[$e[0]])) { + try { + $this->addComponent($e[0], ucwords($e[0])); + } catch(Doctrine_Exception $exception) { + throw new Doctrine_RawSql_Exception("The associated component for table alias $e[0] couldn't be found."); + } + } + + if ($e[1] == '*') { + foreach ($this->tables[$e[0]]->getColumnNames() as $name) { + $field = $e[0].".".$name; + $this->parts["select"][$field] = $field." AS ".$e[0]."__".$name; + } + } else { + $field = $e[0].".".$e[1]; + $this->parts["select"][$field] = $field." AS ".$e[0]."__".$e[1]; + } + } + + // force-add all primary key fields + + foreach ($this->tableAliases as $alias) { + foreach ($this->tables[$alias]->getPrimaryKeys() as $key) { + $field = $alias . '.' . $key; + if ( ! isset($this->parts["select"][$field])) { + $this->parts["select"][$field] = $field." AS ".$alias."__".$key; + } + } + } + + $q = 'SELECT '.implode(', ', $this->parts['select']); + + $string = $this->applyInheritance(); + if ( ! empty($string)) { + $this->parts['where'][] = $string; + } + $copy = $this->parts; + unset($copy['select']); + + $q .= ( ! empty($this->parts['from']))? ' FROM ' . implode(' ', $this->parts['from']) : ''; + $q .= ( ! empty($this->parts['where']))? ' WHERE ' . implode(' AND ', $this->parts['where']) : ''; + $q .= ( ! empty($this->parts['groupby']))? ' GROUP BY ' . implode(', ', $this->parts['groupby']) : ''; + $q .= ( ! empty($this->parts['having']))? ' HAVING ' . implode(' ', $this->parts['having']) : ''; + $q .= ( ! empty($this->parts['orderby']))? ' ORDER BY ' . implode(' ', $this->parts['orderby']) : ''; + $q .= ( ! empty($this->parts['limit']))? ' LIMIT ' . implode(' ', $this->parts['limit']) : ''; + $q .= ( ! empty($this->parts['offset']))? ' OFFSET ' . implode(' ', $this->parts['offset']) : ''; + + if ( ! empty($string)) { + array_pop($this->parts['where']); + } + return $q; + } + /** + * getFields + * + * @return array + */ + public function getFields() + { + return $this->fields; + } + /** + * addComponent + * + * @param string $tableAlias + * @param string $componentName + * @return Doctrine_RawSql + */ + public function addComponent($tableAlias, $componentName) + { + $e = explode('.', $componentName); + + $currPath = ''; + $table = null; + + foreach ($e as $k => $component) { + $currPath .= '.' . $component; + if ($k == 0) + $currPath = substr($currPath,1); + + if (isset($this->tableAliases[$currPath])) { + $alias = $this->tableAliases[$currPath]; + } else { + $alias = $tableAlias; + } + + if ($table) { + $tableName = $table->getAliasName($component); + } + $table = $this->conn->getTable($component); + $this->tables[$alias] = $table; + $this->fetchModes[$alias] = Doctrine::FETCH_IMMEDIATE; + $this->tableAliases[$currPath] = $alias; + + if ($k !== 0) { + $this->joins[$alias] = $prevAlias; + } + $prevAlias = $alias; + $prevPath = $currPath; + } + + return $this; + } + +} diff --git a/draft/new-core/Record.php b/draft/new-core/Record.php new file mode 100644 index 000000000..f3b8b2306 --- /dev/null +++ b/draft/new-core/Record.php @@ -0,0 +1,1682 @@ +. + */ +Doctrine::autoload('Doctrine_Access'); +/** + * Doctrine_Record + * All record classes should inherit this super class + * + * @author Konsta Vesterinen + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @package Doctrine + * @category Object Relational Mapping + * @link www.phpdoctrine.com + * @since 1.0 + * @version $Revision: 1298 $ + */ +abstract class Doctrine_Record extends Doctrine_Access implements Countable, IteratorAggregate, Serializable +{ + /** + * STATE CONSTANTS + */ + + /** + * DIRTY STATE + * a Doctrine_Record is in dirty state when its properties are changed + */ + const STATE_DIRTY = 1; + /** + * TDIRTY STATE + * a Doctrine_Record is in transient dirty state when it is created and some of its fields are modified + * but it is NOT yet persisted into database + */ + const STATE_TDIRTY = 2; + /** + * CLEAN STATE + * a Doctrine_Record is in clean state when all of its properties are loaded from the database + * and none of its properties are changed + */ + const STATE_CLEAN = 3; + /** + * PROXY STATE + * a Doctrine_Record is in proxy state when its properties are not fully loaded + */ + const STATE_PROXY = 4; + /** + * NEW TCLEAN + * a Doctrine_Record is in transient clean state when it is created and none of its fields are modified + */ + const STATE_TCLEAN = 5; + /** + * DELETED STATE + * a Doctrine_Record turns into deleted state when it is deleted + */ + const STATE_DELETED = 6; + /** + * the following protected variables use '_' prefixes, the reason for this is to allow child + * classes call for example $this->id, $this->state for getting the values of columns named 'id' and 'state' + * rather than the values of these protected variables + */ + /** + * @var object Doctrine_Table $_table the factory that created this data access object + */ + protected $_table; + /** + * @var Doctrine_Node_ node object + */ + protected $_node; + /** + * @var integer $_id the primary keys of this object + */ + protected $_id = array(); + /** + * @var array $_data the record data + */ + protected $_data = array(); + /** + * @var array $_values the values array, aggregate values and such are mapped into this array + */ + protected $_values = array(); + /** + * @var integer $_state the state of this record + * @see STATE_* constants + */ + protected $_state; + /** + * @var array $_modified an array containing properties that have been modified + */ + protected $_modified = array(); + /** + * @var Doctrine_Validator_ErrorStack error stack object + */ + protected $_errorStack; + /** + * @var array $references an array containing all the references + */ + private $references = array(); + /** + * @var array $originals an array containing all the original references + */ + private $originals = array(); + /** + * @var integer $index this index is used for creating object identifiers + */ + private static $index = 1; + /** + * @var Doctrine_Null $null a Doctrine_Null object used for extremely fast + * null value testing + */ + private static $null; + /** + * @var integer $oid object identifier, each Record object has a unique object identifier + */ + private $oid; + + /** + * constructor + * @param Doctrine_Table|null $table a Doctrine_Table object or null, + * if null the table object is retrieved from current connection + * + * @param boolean $isNewEntry whether or not this record is transient + * + * @throws Doctrine_Connection_Exception if object is created using the new operator and there are no + * open connections + * @throws Doctrine_Record_Exception if the cleanData operation fails somehow + */ + public function __construct($table = null, $isNewEntry = false) + { + if (isset($table) && $table instanceof Doctrine_Table) { + $this->_table = $table; + $exists = ( ! $isNewEntry); + } else { + $class = get_class($this); + // get the table of this class + $this->_table = Doctrine_Manager::getInstance() + ->getTable(get_class($this)); + + $exists = false; + } + + // Check if the current connection has the records table in its registry + // If not this record is only used for creating table definition and setting up + // relations. + + if ($this->_table->getConnection()->hasTable($this->_table->getComponentName())) { + $this->oid = self::$index; + + self::$index++; + + $keys = $this->_table->getPrimaryKeys(); + + if ( ! $exists) { + // listen the onPreCreate event + $this->_table->getAttribute(Doctrine::ATTR_LISTENER)->onPreCreate($this); + } else { + + // listen the onPreLoad event + $this->_table->getAttribute(Doctrine::ATTR_LISTENER)->onPreLoad($this); + } + // get the data array + $this->_data = $this->_table->getData(); + + // get the column count + $count = count($this->_data); + + // clean data array + $this->cleanData(); + + $this->prepareIdentifiers($exists); + + if ( ! $exists) { + if ($count > 0) { + $this->_state = Doctrine_Record::STATE_TDIRTY; + } else { + $this->_state = Doctrine_Record::STATE_TCLEAN; + } + + // set the default values for this record + $this->assignDefaultValues(); + + // listen the onCreate event + $this->_table->getAttribute(Doctrine::ATTR_LISTENER)->onCreate($this); + + } else { + $this->_state = Doctrine_Record::STATE_CLEAN; + + if ($count < $this->_table->getColumnCount()) { + $this->_state = Doctrine_Record::STATE_PROXY; + } + + // listen the onLoad event + $this->_table->getAttribute(Doctrine::ATTR_LISTENER)->onLoad($this); + } + + $this->_errorStack = new Doctrine_Validator_ErrorStack(); + + $repository = $this->_table->getRepository(); + $repository->add($this); + } + $this->construct(); + } + /** + * initNullObject + * + * @param Doctrine_Null $null + * @return void + */ + public static function initNullObject(Doctrine_Null $null) + { + self::$null = $null; + } + /** + * @return Doctrine_Null + */ + public static function getNullObject() + { + return self::$null; + } + /** + * setUp + * this method is used for setting up relations and attributes + * it should be implemented by child classes + * + * @return void + */ + public function setUp() + { } + /** + * construct + * Empty tempalte method to provide concrete Record classes with the possibility + * to hook into the constructor procedure + * + * @return void + */ + public function construct() + { } + /** + * getOID + * returns the object identifier + * + * @return integer + */ + public function getOID() + { + return $this->oid; + } + /** + * isValid + * + * @return boolean whether or not this record passes all column validations + */ + public function isValid() + { + if ( ! $this->_table->getAttribute(Doctrine::ATTR_VLD)) { + return true; + } + // Clear the stack from any previous errors. + $this->_errorStack->clear(); + + // Run validation process + $validator = new Doctrine_Validator(); + $validator->validateRecord($this); + $this->validate(); + if ($this->_state == self::STATE_TDIRTY || $this->_state == self::STATE_TCLEAN) { + $this->validateOnInsert(); + } else { + $this->validateOnUpdate(); + } + + return $this->_errorStack->count() == 0 ? true : false; + } + /** + * Emtpy template method to provide concrete Record classes with the possibility + * to hook into the validation procedure, doing any custom / specialized + * validations that are neccessary. + */ + protected function validate() + {} + /** + * Empty tempalte method to provide concrete Record classes with the possibility + * to hook into the validation procedure only when the record is going to be + * updated. + */ + protected function validateOnUpdate() + {} + /** + * Empty tempalte method to provide concrete Record classes with the possibility + * to hook into the validation procedure only when the record is going to be + * inserted into the data store the first time. + */ + protected function validateOnInsert() + {} + /** + * getErrorStack + * + * @return Doctrine_Validator_ErrorStack returns the errorStack associated with this record + */ + public function getErrorStack() + { + return $this->_errorStack; + } + /** + * errorStack + * assigns / returns record errorStack + * + * @param Doctrine_Validator_ErrorStack errorStack to be assigned for this record + * @return void|Doctrine_Validator_ErrorStack returns the errorStack associated with this record + */ + public function errorStack($stack = null) + { + if($stack !== null) { + if( ! ($stack instanceof Doctrine_Validator_ErrorStack)) { + throw new Doctrine_Record_Exception('Argument should be an instance of Doctrine_Validator_ErrorStack.'); + } + $this->_errorStack = $stack; + } else { + return $this->_errorStack; + } + } + /** + * setDefaultValues + * sets the default values for records internal data + * + * @param boolean $overwrite whether or not to overwrite the already set values + * @return boolean + */ + public function assignDefaultValues($overwrite = false) + { + if ( ! $this->_table->hasDefaultValues()) { + return false; + } + foreach ($this->_data as $column => $value) { + $default = $this->_table->getDefaultValueOf($column); + + if ($default === null) + $default = self::$null; + + if ($value === self::$null || $overwrite) { + $this->_data[$column] = $default; + $this->_modified[] = $column; + $this->_state = Doctrine_Record::STATE_TDIRTY; + } + } + } + /** + * cleanData + * this method does several things to records internal data + * + * 1. It unserializes array and object typed columns + * 2. Uncompresses gzip typed columns + * 3. Gets the appropriate enum values for enum typed columns + * 4. Initializes special null object pointer for null values (for fast column existence checking purposes) + * + * + * example: + * + * $data = array("name"=>"John","lastname"=> null, "id" => 1,"unknown" => "unknown"); + * $names = array("name", "lastname", "id"); + * $data after operation: + * $data = array("name"=>"John","lastname" => Object(Doctrine_Null)); + * + * here column 'id' is removed since its auto-incremented primary key (read-only) + * + * @throws Doctrine_Record_Exception if unserialization of array/object typed column fails or + * if uncompression of gzip typed column fails + * + * @return integer + */ + private function cleanData() + { + $tmp = $this->_data; + + $this->_data = array(); + + $count = 0; + + foreach ($this->_table->getColumnNames() as $name) { + $type = $this->_table->getTypeOf($name); + + if ( ! isset($tmp[$name])) { + $this->_data[$name] = self::$null; + } else { + switch ($type) { + case 'array': + case 'object': + if ($tmp[$name] !== self::$null) { + if (is_string($tmp[$name])) { + $value = unserialize($tmp[$name]); + + if ($value === false) + throw new Doctrine_Record_Exception('Unserialization of ' . $name . ' failed.'); + } else { + $value = $tmp[$name]; + } + $this->_data[$name] = $value; + } + break; + case 'gzip': + if ($tmp[$name] !== self::$null) { + $value = gzuncompress($tmp[$name]); + + if ($value === false) + throw new Doctrine_Record_Exception('Uncompressing of ' . $name . ' failed.'); + + $this->_data[$name] = $value; + } + break; + case 'enum': + $this->_data[$name] = $this->_table->enumValue($name, $tmp[$name]); + break; + default: + $this->_data[$name] = $tmp[$name]; + } + $count++; + } + } + + return $count; + } + /** + * hydrate + * hydrates this object from given array + * + * @param array $data + * @return boolean + */ + public function hydrate(array $data) + { + foreach ($data as $k => $v) { + $this->_data[$k] = $v; + } + $this->cleanData(); + $this->prepareIdentifiers(); + } + /** + * prepareIdentifiers + * prepares identifiers for later use + * + * @param boolean $exists whether or not this record exists in persistent data store + * @return void + */ + private function prepareIdentifiers($exists = true) + { + switch ($this->_table->getIdentifierType()) { + case Doctrine_Identifier::AUTO_INCREMENT: + case Doctrine_Identifier::SEQUENCE: + $name = $this->_table->getIdentifier(); + + if ($exists) { + if (isset($this->_data[$name]) && $this->_data[$name] !== self::$null) { + $this->_id[$name] = $this->_data[$name]; + } + } + + unset($this->_data[$name]); + + break; + case Doctrine_Identifier::NORMAL: + $this->_id = array(); + $name = $this->_table->getIdentifier(); + + if (isset($this->_data[$name]) && $this->_data[$name] !== self::$null) { + $this->_id[$name] = $this->_data[$name]; + } + break; + case Doctrine_Identifier::COMPOSITE: + $names = $this->_table->getIdentifier(); + + foreach ($names as $name) { + if ($this->_data[$name] === self::$null) { + $this->_id[$name] = null; + } else { + $this->_id[$name] = $this->_data[$name]; + } + } + break; + } + } + /** + * serialize + * this method is automatically called when this Doctrine_Record is serialized + * + * @return array + */ + public function serialize() + { + $this->_table->getAttribute(Doctrine::ATTR_LISTENER)->onSleep($this); + + $vars = get_object_vars($this); + + unset($vars['references']); + unset($vars['originals']); + unset($vars['_table']); + unset($vars['_errorStack']); + + $name = $this->_table->getIdentifier(); + $this->_data = array_merge($this->_data, $this->_id); + + foreach ($this->_data as $k => $v) { + if ($v instanceof Doctrine_Record) { + unset($vars['_data'][$k]); + } elseif ($v === self::$null) { + unset($vars['_data'][$k]); + } else { + switch ($this->_table->getTypeOf($k)) { + case "array": + case "object": + $vars['_data'][$k] = serialize($vars['_data'][$k]); + break; + } + } + } + + return serialize($vars); + } + /** + * unseralize + * this method is automatically called everytime a Doctrine_Record object is unserialized + * + * @param string $serialized Doctrine_Record as serialized string + * @throws Doctrine_Record_Exception if the cleanData operation fails somehow + * @return void + */ + public function unserialize($serialized) + { + $manager = Doctrine_Manager::getInstance(); + $connection = $manager->getConnectionForComponent(get_class($this)); + + $this->oid = self::$index; + self::$index++; + + $this->_table = $connection->getTable(get_class($this)); + + $array = unserialize($serialized); + + foreach ($array as $name => $values) { + $this->$name = $values; + } + + $this->_table->getRepository()->add($this); + + $this->cleanData(); + + $this->prepareIdentifiers($this->exists()); + + $this->_table->getAttribute(Doctrine::ATTR_LISTENER)->onWakeUp($this); + } + /** + * getState + * returns the current state of the object + * + * @see Doctrine_Record::STATE_* constants + * @return integer + */ + public function getState() + { + return $this->_state; + } + /** + * state + * returns / assigns the state of this record + * + * @param integer|string $state if set, this method tries to set the record state to $state + * @see Doctrine_Record::STATE_* constants + * + * @throws Doctrine_Record_State_Exception if trying to set an unknown state + * @return null|integer + */ + public function state($state = null) + { + if ($state == null) { + return $this->_state; + } + $err = false; + if (is_integer($state)) { + + if ($state >= 1 && $state <= 6) { + $this->_state = $state; + } else { + $err = true; + } + } elseif (is_string($state)) { + $upper = strtoupper($state); + switch ($upper) { + case 'DIRTY': + case 'CLEAN': + case 'TDIRTY': + case 'TCLEAN': + case 'PROXY': + case 'DELETED': + $this->_state = constant('Doctrine_Record::STATE_' . $upper); + break; + default: + $err = true; + } + } + + if ($err) + throw new Doctrine_Record_State_Exception('Unknown record state ' . $state); + } + /** + * refresh + * refresh internal data from the database + * + * @throws Doctrine_Record_Exception When the refresh operation fails (when the database row + * this record represents does not exist anymore) + * @return boolean + */ + final public function refresh() + { + $id = $this->obtainIdentifier(); + if ( ! is_array($id)) { + $id = array($id); + } + if (empty($id)) { + return false; + } + $id = array_values($id); + + $query = $this->_table->getQuery()." WHERE ".implode(" = ? AND ",$this->_table->getPrimaryKeys())." = ?"; + $stmt = $this->_table->getConnection()->execute($query,$id); + + $this->_data = $stmt->fetch(PDO::FETCH_ASSOC); + + if ( ! $this->_data) + throw new Doctrine_Record_Exception('Failed to refresh. Record does not exist anymore'); + + $this->_data = array_change_key_case($this->_data, CASE_LOWER); + + $this->_modified = array(); + $this->cleanData(true); + + $this->prepareIdentifiers(); + + $this->_state = Doctrine_Record::STATE_CLEAN; + + $this->_table->getAttribute(Doctrine::ATTR_LISTENER)->onLoad($this); + + return true; + } + /** + * factoryRefresh + * refreshes the data from outer source (Doctrine_Table) + * + * @throws Doctrine_Record_Exception When the primary key of this record doesn't match the primary key fetched from a collection + * @return void + */ + final public function factoryRefresh() + { + $this->_data = $this->_table->getData(); + $old = $this->_id; + + $this->cleanData(); + + $this->prepareIdentifiers(); + + if ($this->_id != $old) + throw new Doctrine_Record_Exception("The refreshed primary key doesn't match the one in the record memory.", Doctrine::ERR_REFRESH); + + $this->_state = Doctrine_Record::STATE_CLEAN; + $this->_modified = array(); + + $this->_table->getAttribute(Doctrine::ATTR_LISTENER)->onLoad($this); + } + /** + * getTable + * returns the table object for this record + * + * @return object Doctrine_Table a Doctrine_Table object + */ + final public function getTable() + { + return $this->_table; + } + /** + * getData + * return all the internal data + * + * @return array an array containing all the properties + */ + final public function getData() + { + return $this->_data; + } + /** + * rawGet + * returns the value of a property, if the property is not yet loaded + * this method does NOT load it + * + * @param $name name of the property + * @throws Doctrine_Record_Exception if trying to get an unknown property + * @return mixed + */ + + public function rawGet($name) + { + if ( ! isset($this->_data[$name])) { + throw new Doctrine_Record_Exception('Unknown property '. $name); + } + if ($this->_data[$name] === self::$null) + return null; + + return $this->_data[$name]; + } + + /** + * load + * loads all the unitialized properties from the database + * + * @return boolean + */ + public function load() + { + // only load the data from database if the Doctrine_Record is in proxy state + if ($this->_state == Doctrine_Record::STATE_PROXY) { + $this->refresh(); + + $this->_state = Doctrine_Record::STATE_CLEAN; + + return true; + } + return false; + } + /** + * get + * returns a value of a property or a related component + * + * @param mixed $name name of the property or related component + * @param boolean $invoke whether or not to invoke the onGetProperty listener + * @throws Doctrine_Record_Exception if trying to get a value of unknown property / related component + * @return mixed + */ + public function get($name, $invoke = true) + { + $value = self::$null; + $lower = strtolower($name); + + $lower = $this->_table->getColumnName($lower); + + if (isset($this->_data[$lower])) { + // check if the property is null (= it is the Doctrine_Null object located in self::$null) + if ($this->_data[$lower] === self::$null) { + $this->load(); + } + + if ($this->_data[$lower] === self::$null) { + $value = null; + } else { + $value = $this->_data[$lower]; + } + + } + + if ($value !== self::$null) { + $value = $this->_table->invokeGet($this, $name, $value); + + if ($invoke && $name !== $this->_table->getIdentifier()) { + return $this->_table->getAttribute(Doctrine::ATTR_LISTENER)->onGetProperty($this, $name, $value); + } else { + return $value; + } + } + + if (isset($this->_id[$lower])) { + return $this->_id[$lower]; + } + if ($name === $this->_table->getIdentifier()) { + return null; + } + if (isset($this->_values[$lower])) { + return $this->_values[$lower]; + } + + try { + if ( ! isset($this->references[$name])) { + $this->loadReference($name); + } + } catch(Doctrine_Table_Exception $e) { + throw new Doctrine_Record_Exception("Unknown property / related component '$name'."); + } + + return $this->references[$name]; + } + /** + * mapValue + * This simple method is used for mapping values to $values property. + * Usually this method is used internally by Doctrine for the mapping of + * aggregate values. + * + * @param string $name the name of the mapped value + * @param mixed $value mixed value to be mapped + * @return void + */ + public function mapValue($name, $value) + { + $name = strtolower($name); + $this->_values[$name] = $value; + } + /** + * set + * method for altering properties and Doctrine_Record references + * if the load parameter is set to false this method will not try to load uninitialized record data + * + * @param mixed $name name of the property or reference + * @param mixed $value value of the property or reference + * @param boolean $load whether or not to refresh / load the uninitialized record data + * + * @throws Doctrine_Record_Exception if trying to set a value for unknown property / related component + * @throws Doctrine_Record_Exception if trying to set a value of wrong type for related component + * + * @return Doctrine_Record + */ + public function set($name, $value, $load = true) + { + $lower = strtolower($name); + + $lower = $this->_table->getColumnName($lower); + + if (isset($this->_data[$lower])) { + if ($value instanceof Doctrine_Record) { + $id = $value->getIncremented(); + + if ($id !== null) { + $value = $id; + } + } + + if ($load) { + $old = $this->get($lower, false); + } else { + $old = $this->_data[$lower]; + } + + if ($old !== $value) { + $value = $this->_table->invokeSet($this, $name, $value); + + $value = $this->_table->getAttribute(Doctrine::ATTR_LISTENER)->onSetProperty($this, $name, $value); + + if ($value === null) + $value = self::$null; + + $this->_data[$lower] = $value; + $this->_modified[] = $lower; + switch ($this->_state) { + case Doctrine_Record::STATE_CLEAN: + $this->_state = Doctrine_Record::STATE_DIRTY; + break; + case Doctrine_Record::STATE_TCLEAN: + $this->_state = Doctrine_Record::STATE_TDIRTY; + break; + }; + } + } else { + try { + $this->coreSetRelated($name, $value); + } catch(Doctrine_Table_Exception $e) { + throw new Doctrine_Record_Exception("Unknown property / related component '$name'."); + } + } + } + + public function coreSetRelated($name, $value) + { + $rel = $this->_table->getRelation($name); + + // one-to-many or one-to-one relation + if ($rel instanceof Doctrine_Relation_ForeignKey || + $rel instanceof Doctrine_Relation_LocalKey) { + if ( ! $rel->isOneToOne()) { + // one-to-many relation found + if ( ! ($value instanceof Doctrine_Collection)) { + throw new Doctrine_Record_Exception("Couldn't call Doctrine::set(), second argument should be an instance of Doctrine_Collection when setting one-to-many references."); + } + $value->setReference($this,$rel); + } else { + // one-to-one relation found + if ( ! ($value instanceof Doctrine_Record)) { + throw new Doctrine_Record_Exception("Couldn't call Doctrine::set(), second argument should be an instance of Doctrine_Record when setting one-to-one references."); + } + if ($rel instanceof Doctrine_Relation_LocalKey) { + $this->set($rel->getLocal(), $value, false); + } else { + $value->set($rel->getForeign(), $this, false); + } + } + + } elseif ($rel instanceof Doctrine_Relation_Association) { + // join table relation found + if ( ! ($value instanceof Doctrine_Collection)) { + throw new Doctrine_Record_Exception("Couldn't call Doctrine::set(), second argument should be an instance of Doctrine_Collection when setting many-to-many references."); + } + } + + $this->references[$name] = $value; + } + /** + * contains + * + * @param string $name + * @return boolean + */ + public function contains($name) + { + $lower = strtolower($name); + + if (isset($this->_data[$lower])) { + return true; + } + if (isset($this->_id[$lower])) { + return true; + } + if (isset($this->references[$name])) { + return true; + } + return false; + } + /** + * @param string $name + * @return void + */ + public function __unset($name) + { + if (isset($this->_data[$name])) { + $this->_data[$name] = array(); + } + // todo: what to do with references ? + } + /** + * applies the changes made to this object into database + * this method is smart enough to know if any changes are made + * and whether to use INSERT or UPDATE statement + * + * this method also saves the related components + * + * @param Doctrine_Connection $conn optional connection parameter + * @return void + */ + public function save(Doctrine_Connection $conn = null) + { + if ($conn === null) { + $conn = $this->_table->getConnection(); + } + $conn->beginTransaction(); + + $saveLater = $conn->unitOfWork->saveRelated($this); + + if ($this->isValid()) { + $conn->save($this); + } else { + $conn->transaction->addInvalid($this); + } + + foreach ($saveLater as $fk) { + $table = $fk->getTable(); + $alias = $this->_table->getAlias($table->getComponentName()); + + if (isset($this->references[$alias])) { + $obj = $this->references[$alias]; + $obj->save($conn); + } + } + + // save the MANY-TO-MANY associations + + $conn->unitOfWork->saveAssociations($this); + //$this->saveAssociations(); + + $conn->commit(); + } + /** + * Tries to save the object and all its related components. + * In contrast to Doctrine_Record::save(), this method does not + * throw an exception when validation fails but returns TRUE on + * success or FALSE on failure. + * + * @param Doctrine_Connection $conn optional connection parameter + * @return TRUE if the record was saved sucessfully without errors, FALSE otherwise. + */ + public function trySave(Doctrine_Connection $conn = null) { + try { + $this->save($conn); + return true; + } catch (Doctrine_Validator_Exception $ignored) { + return false; + } + } + /** + * replace + * Execute a SQL REPLACE query. A REPLACE query is identical to a INSERT + * query, except that if there is already a row in the table with the same + * key field values, the REPLACE query just updates its values instead of + * inserting a new row. + * + * The REPLACE type of query does not make part of the SQL standards. Since + * practically only MySQL and SQLIte implement it natively, this type of + * query isemulated through this method for other DBMS using standard types + * of queries inside a transaction to assure the atomicity of the operation. + * + * @param Doctrine_Connection $conn optional connection parameter + * @throws Doctrine_Connection_Exception if some of the key values was null + * @throws Doctrine_Connection_Exception if there were no key fields + * @throws PDOException if something fails at PDO level + * @return integer number of rows affected + */ + public function replace(Doctrine_Connection $conn = null) + { + if ($conn === null) { + $conn = $this->_table->getConnection(); + } + + return $conn->replace($this->_table->getTableName(), $this->getPrepared(), $this->id); + } + /** + * returns an array of modified fields and associated values + * @return array + */ + public function getModified() + { + $a = array(); + + foreach ($this->_modified as $k => $v) { + $a[$v] = $this->_data[$v]; + } + return $a; + } + /** + * getPrepared + * + * returns an array of modified fields and values with data preparation + * adds column aggregation inheritance and converts Records into primary key values + * + * @param array $array + * @return array + */ + public function getPrepared(array $array = array()) { + $a = array(); + + if (empty($array)) { + $array = $this->_modified; + } + foreach ($array as $k => $v) { + $type = $this->_table->getTypeOf($v); + + if ($this->_data[$v] === self::$null) { + $a[$v] = null; + continue; + } + + switch ($type) { + case 'array': + case 'object': + $a[$v] = serialize($this->_data[$v]); + break; + case 'gzip': + $a[$v] = gzcompress($this->_data[$v],5); + break; + case 'boolean': + $a[$v] = $this->getTable()->getConnection()->convertBooleans($this->_data[$v]); + break; + case 'enum': + $a[$v] = $this->_table->enumIndex($v,$this->_data[$v]); + break; + default: + if ($this->_data[$v] instanceof Doctrine_Record) + $this->_data[$v] = $this->_data[$v]->getIncremented(); + + $a[$v] = $this->_data[$v]; + } + } + $map = $this->_table->inheritanceMap; + foreach ($map as $k => $v) { + $old = $this->get($k, false); + + if ((string) $old !== (string) $v || $old === null) { + $a[$k] = $v; + $this->_data[$k] = $v; + } + } + + return $a; + } + /** + * count + * this class implements countable interface + * + * @return integer the number of columns in this record + */ + public function count() + { + return count($this->_data); + } + /** + * alias for count() + * + * @return integer the number of columns in this record + */ + public function columnCount() + { + return $this->count(); + } + /** + * toArray + * returns the record as an array + * + * @return array + */ + public function toArray() + { + $a = array(); + + foreach ($this as $column => $value) { + $a[$column] = $value; + } + if ($this->_table->getIdentifierType() == Doctrine_Identifier::AUTO_INCREMENT) { + $i = $this->_table->getIdentifier(); + $a[$i] = $this->getIncremented(); + } + return $a; + } + /** + * exists + * returns true if this record is persistent, otherwise false + * + * @return boolean + */ + public function exists() + { + return ($this->_state !== Doctrine_Record::STATE_TCLEAN && + $this->_state !== Doctrine_Record::STATE_TDIRTY); + } + /** + * method for checking existence of properties and Doctrine_Record references + * @param mixed $name name of the property or reference + * @return boolean + */ + public function hasRelation($name) + { + if (isset($this->_data[$name]) || isset($this->_id[$name])) { + return true; + } + return $this->_table->hasRelation($name); + } + /** + * getIterator + * @return Doctrine_Record_Iterator a Doctrine_Record_Iterator that iterates through the data + */ + public function getIterator() + { + return new Doctrine_Record_Iterator($this); + } + /** + * getOriginals + * returns an original collection of related component + * + * @return Doctrine_Collection|false + */ + public function obtainOriginals($name) + { + if (isset($this->originals[$name])) { + return $this->originals[$name]; + } + return false; + } + /** + * deletes this data access object and all the related composites + * this operation is isolated by a transaction + * + * this event can be listened by the onPreDelete and onDelete listeners + * + * @return boolean true on success, false on failure + */ + public function delete(Doctrine_Connection $conn = null) + { + if ($conn == null) { + $conn = $this->_table->getConnection(); + } + return $conn->delete($this); + } + /** + * copy + * returns a copy of this object + * + * @return Doctrine_Record + */ + public function copy() + { + $ret = $this->_table->create($this->_data); + $modified = array(); + foreach ($this->_data as $key => $val) { + if ( ! ($val instanceof Doctrine_Null)) { + $ret->_modified[] = $key; + } + } + return $ret; + } + /** + * copyDeep + * returns a copy of this object and all its related objects + * + * @return Doctrine_Record + */ + public function copyDeep(){ + $copy = $this->copy(); + + foreach ($this->references as $key => $value) { + if ($value instanceof Doctrine_Collection) { + foreach ($value as $record) { + $copy->{$key}[] = $record->copyDeep(); + } + } else { + $copy->set($key, $value->copyDeep()); + } + } + return $copy; + } + + /** + * assignIdentifier + * + * @param integer $id + * @return void + */ + final public function assignIdentifier($id = false) + { + if ($id === false) { + $this->_id = array(); + $this->cleanData(); + $this->_state = Doctrine_Record::STATE_TCLEAN; + $this->_modified = array(); + } elseif ($id === true) { + $this->prepareIdentifiers(false); + $this->_state = Doctrine_Record::STATE_CLEAN; + $this->_modified = array(); + } else { + $name = $this->_table->getIdentifier(); + + $this->_id[$name] = $id; + $this->_state = Doctrine_Record::STATE_CLEAN; + $this->_modified = array(); + } + } + /** + * assignOriginals + * + * @param string $alias + * @param Doctrine_Collection $coll + * @return void + */ + public function assignOriginals($alias, Doctrine_Collection $coll) + { + $this->originals[$alias] = $coll; + } + /** + * returns the primary keys of this object + * + * @return array + */ + final public function obtainIdentifier() + { + return $this->_id; + } + /** + * returns the value of autoincremented primary key of this object (if any) + * + * @return integer + */ + final public function getIncremented() + { + $id = current($this->_id); + if ($id === false) + return null; + + return $id; + } + /** + * getLast + * this method is used internally be Doctrine_Query + * it is needed to provide compatibility between + * records and collections + * + * @return Doctrine_Record + */ + public function getLast() + { + return $this; + } + /** + * hasRefence + * @param string $name + * @return boolean + */ + public function hasReference($name) + { + return isset($this->references[$name]); + } + /** + * obtainReference + * + * @param string $name + * @throws Doctrine_Record_Exception if trying to get an unknown related component + */ + public function obtainReference($name) + { + if (isset($this->references[$name])) { + return $this->references[$name]; + } + throw new Doctrine_Record_Exception("Unknown reference $name"); + } + /** + * initalizes a one-to-many / many-to-many relation + * + * @param Doctrine_Collection $coll + * @param Doctrine_Relation $connector + * @return boolean + */ + public function initReference(Doctrine_Collection $coll, Doctrine_Relation $connector) + { + $alias = $connector->getAlias(); + + if (isset($this->references[$alias])) { + return false; + } + if ( ! $connector->isOneToOne()) { + if ( ! ($connector instanceof Doctrine_Relation_Association)) { + $coll->setReference($this, $connector); + } + $this->references[$alias] = $coll; + $this->originals[$alias] = clone $coll; + + return true; + } + return false; + } + + public function lazyInitRelated(Doctrine_Collection $coll, Doctrine_Relation $connector) + { + + } + /** + * addReference + * @param Doctrine_Record $record + * @param mixed $key + * @return void + */ + public function addReference(Doctrine_Record $record, Doctrine_Relation $connector, $key = null) + { + $alias = $connector->getAlias(); + + $this->references[$alias]->add($record, $key); + $this->originals[$alias]->add($record, $key); + } + /** + * getReferences + * @return array all references + */ + public function getReferences() + { + return $this->references; + } + /** + * setRelated + * + * @param string $alias + * @param Doctrine_Access $coll + */ + final public function setRelated($alias, Doctrine_Access $coll) + { + $this->references[$alias] = $coll; + $this->originals[$alias] = $coll; + } + /** + * loadReference + * loads a related component + * + * @throws Doctrine_Table_Exception if trying to load an unknown related component + * @param string $name + * @return void + */ + final public function loadReference($name) + { + + $fk = $this->_table->getRelation($name); + + if ($fk->isOneToOne()) { + $this->references[$name] = $fk->fetchRelatedFor($this); + + } else { + $coll = $fk->fetchRelatedFor($this); + + $this->references[$name] = $coll; + $this->originals[$name] = clone $coll; + } + } + /** + * binds One-to-One composite relation + * + * @param string $objTableName + * @param string $fkField + * @return void + */ + final public function ownsOne($componentName, $foreignKey, $options = null) + { + $this->_table->bind($componentName, $foreignKey, Doctrine_Relation::ONE_COMPOSITE, $options); + } + /** + * binds One-to-Many composite relation + * + * @param string $objTableName + * @param string $fkField + * @return void + */ + final public function ownsMany($componentName, $foreignKey, $options = null) + { + $this->_table->bind($componentName, $foreignKey, Doctrine_Relation::MANY_COMPOSITE, $options); + } + /** + * binds One-to-One aggregate relation + * + * @param string $objTableName + * @param string $fkField + * @return void + */ + final public function hasOne($componentName, $foreignKey, $options = null) + { + $this->_table->bind($componentName, $foreignKey, Doctrine_Relation::ONE_AGGREGATE, $options); + } + /** + * binds One-to-Many aggregate relation + * + * @param string $objTableName + * @param string $fkField + * @return void + */ + final public function hasMany($componentName, $foreignKey, $options = null) + { + $this->_table->bind($componentName, $foreignKey, Doctrine_Relation::MANY_AGGREGATE, $options); + } + /** + * hasColumn + * sets a column definition + * + * @param string $name + * @param string $type + * @param integer $length + * @param mixed $options + * @return void + */ + final public function hasColumn($name, $type, $length = 2147483647, $options = "") + { + $this->_table->setColumn($name, $type, $length, $options); + } + /** + * countRelated + * + * @param string $name the name of the related component + * @return integer + */ + public function countRelated($name) + { + $rel = $this->_table->getRelation($name); + $componentName = $rel->getTable()->getComponentName(); + $alias = $rel->getTable()->getAlias(get_class($this)); + $query = new Doctrine_Query(); + $query->from($componentName. '(' . 'COUNT(1)' . ')')->where($componentName. '.' .$alias. '.' . $this->getTable()->getIdentifier(). ' = ?'); + $array = $query->execute(array($this->getIncremented())); + return $array[0]['COUNT(1)']; + } + /** + * merge + * merges this record with an array of values + * + * @param array $values + * @return void + */ + public function merge(array $values) + { + foreach ($this->_table->getColumnNames() as $value) { + try { + if (isset($values[$value])) { + $this->set($value, $values[$value]); + } + } catch(Exception $e) { + // silence all exceptions + } + } + } + public function setAttribute($attr, $value) + { + $this->_table->setAttribute($attr, $value); + } + public function setTableName($tableName) + { + $this->_table->setOption('tableName', $tableName); + } + public function setInheritanceMap($map) + { + $this->_table->setOption('inheritanceMap', $map); + } + public function setEnumValues($column, $values) + { + $this->_table->setEnumValues($column, $values); + } + /** + * attribute + * sets or retrieves an option + * + * @see Doctrine::ATTR_* constants availible attributes + * @param mixed $attr + * @param mixed $value + * @return mixed + */ + public function attribute($attr, $value) + { + if ($value == null) { + if (is_array($attr)) { + foreach ($attr as $k => $v) { + $this->_table->setAttribute($k, $v); + } + } else { + return $this->_table->getAttribute($attr); + } + } else { + $this->_table->setAttribute($attr, $value); + } + } + /** + * option + * sets or retrieves an option + * + * @see Doctrine_Table::$options availible options + * @param mixed $name the name of the option + * @param mixed $value options value + * @return mixed + */ + public function option($name, $value = null) + { + if ($value == null) { + if (is_array($name)) { + foreach ($name as $k => $v) { + $this->_table->setOption($k, $v); + } + } else { + return $this->_table->getOption($name); + } + } else { + $this->_table->setOption($name, $value); + } + } + /** + * index + * defines a foreignKey + * + * @param array $definition the definition array + * @return void + */ + public function foreignKey(array $definition = array()) + { + return $this->_table->addForeignKey($definition); + } + /** + * index + * defines or retrieves an index + * if the second parameter is set this method defines an index + * if not this method retrieves index named $name + * + * @param string $name the name of the index + * @param array $definition the definition array + * @return mixed + */ + public function index($name, array $definition = array()) + { + if ( ! $definition) { + return $this->_table->getIndex($name); + } else { + return $this->_table->addIndex($name, $definition); + } + } + /** + * addListener + * + * @param Doctrine_Db_EventListener_Interface|Doctrine_Overloadable $listener + * @return Doctrine_Db + */ + public function addListener($listener, $name = null) + { + $this->_table->addListener($listener, $name = null); + return $this; + } + /** + * getListener + * + * @return Doctrine_Db_EventListener_Interface|Doctrine_Overloadable + */ + public function getListener() + { + return $this->_table->getListener(); + } + /** + * setListener + * + * @param Doctrine_Db_EventListener_Interface|Doctrine_Overloadable $listener + * @return Doctrine_Db + */ + public function setListener($listener) + { + $this->_table->setListener($listener); + return $this; + } + /** + * call + * + * @param string|array $callback valid callback + * @param string $column column name + * @param mixed arg1 ... argN optional callback arguments + * @return Doctrine_Record + */ + public function call($callback, $column) + { + $args = func_get_args(); + array_shift($args); + + if (isset($args[0])) { + $column = $args[0]; + $args[0] = $this->get($column); + + $newvalue = call_user_func_array($callback, $args); + + $this->_data[$column] = $newvalue; + } + return $this; + } + /** + * getter for node assciated with this record + * + * @return mixed if tree returns Doctrine_Node otherwise returns false + */ + public function getNode() + { + if ( ! $this->_table->isTree()) { + return false; + } + + if ( ! isset($this->_node)) { + $this->_node = Doctrine_Node::factory($this, + $this->getTable()->getOption('treeImpl'), + $this->getTable()->getOption('treeOptions') + ); + } + + return $this->_node; + } + /** + * used to delete node from tree - MUST BE USE TO DELETE RECORD IF TABLE ACTS AS TREE + * + */ + public function deleteNode() { + $this->getNode()->delete(); + } + /** + * returns a string representation of this object + */ + public function __toString() + { + return Doctrine_Lib::getRecordAsString($this); + } +}