diff --git a/lib/Doctrine/Record.php b/lib/Doctrine/Record.php index 07154c180..7b783c585 100644 --- a/lib/Doctrine/Record.php +++ b/lib/Doctrine/Record.php @@ -1,1393 +1,1398 @@ -. - */ -Doctrine::autoload('Doctrine_Access'); -/** - * Doctrine_Record - * All record classes should inherit this super class - * - * @author Konsta Vesterinen - * @license LGPL - * @package Doctrine - */ - -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; - /** - * @var object Doctrine_Table $table the factory that created this data access object - */ - protected $table; - /** - * @var integer $id the primary keys of this object - */ - protected $id = array(); - /** - * @var array $data the record data - */ - protected $data = 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 array $collections the collections this record is in - */ - private $collections = array(); - /** - * @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 Doctrine_Validator_ErrorStack error stack object - */ - private $errorStack; - /** - * @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 - */ - private $oid; - - /** - * constructor - * @param Doctrine_Table|null $table a Doctrine_Table object or null, - * if null the table object is retrieved from current connection - * - * @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) { - if(isset($table) && $table instanceof Doctrine_Table) { - $this->table = $table; - $exists = ( ! $this->table->isNewEntry()); - } else { - $this->table = Doctrine_Manager::getInstance()->getCurrentConnection()->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->setDefaultValues(); - - // 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); - } - } - /** - * 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() { } - /** - * 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; - - $validator = new Doctrine_Validator(); - - if($validator->validateRecord($this)) - return true; - - $this->errorStack->merge($validator->getErrorStack()); - - return false; - } - /** - * getErrorStack - * - * @return Doctrine_Validator_ErrorStack returns the errorStack associated with this record - */ - public function getErrorStack() { - 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 setDefaultValues($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($debug = false) { - $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. ".var_dump(substr($tmp[$lower],0,30)."...",true)); - } 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]; - endswitch; - $count++; - } - } - - - return $count; - } - /** - * 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; - endswitch; - } - /** - * 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['collections']); - unset($vars['originals']); - unset($vars['table']); - - - $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; - endswitch; - } - } - - 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->getCurrentConnection(); - - $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); - } - - - /** - * addCollection - * - * @param Doctrine_Collection $collection - * @param mixed $key - */ - final public function addCollection(Doctrine_Collection $collection,$key = null) { - if($key !== null) { - $this->collections[$key] = $collection; - } else { - $this->collections[] = $collection; - } - } - /** - * getCollection - * @param integer $key - * @return Doctrine_Collection - */ - final public function getCollection($key) { - return $this->collections[$key]; - } - /** - * hasCollections - * whether or not this record is part of a collection - * - * @return boolean - */ - final public function hasCollections() { - return (! empty($this->collections)); - } - /** - * getState - * returns the current state of the object - * - * @see Doctrine_Record::STATE_* constants - * @return integer - */ - final public function getState() { - return $this->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) { - if( ! empty($this->collections)) { - // delegate the loading operation to collections in which this record resides - foreach($this->collections as $collection) { - $collection->load($this); - - } - } else { - - $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) { - $listener = $this->table->getAttribute(Doctrine::ATTR_LISTENER); - $value = self::$null; - $lower = strtolower($name); - - 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; - - return $value; - } - - - if(isset($this->id[$lower])) - return $this->id[$lower]; - - if($name === $this->table->getIdentifier()) - return null; - - $rel = $this->table->getRelation($name); - - 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]; - } - - /** - * 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); - - 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; - endswitch; - } - } 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 composites - * - * @return void - */ - final public function save(Doctrine_Connection $conn = null) { - if ($conn === null) { - $conn = $this->table->getConnection(); - } - $conn->beginTransaction(); - - $saveLater = $conn->saveRelated($this); - - $this->isValid(); - - if($this->errorStack->count() > 0) { - $conn->getTransaction()->addInvalid($this); - } else { - $conn->save($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(); - } - } - - // save the MANY-TO-MANY associations - - $this->saveAssociations(); - - $conn->commit(); - } - /** - * returns an array of modified fields and associated values - * @return array - */ - final public function getModified() { - $a = array(); - - foreach($this->modified as $k => $v) { - $a[$v] = $this->data[$v]; - } - return $a; - } - /** - * returns an array of modified fields and values with data preparation - * adds column aggregation inheritance and converts Records into primary key values - * - * @return array - */ - final 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] = (int) $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]; - } - } - - foreach($this->table->getInheritanceMap() 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 - */ - public function count() { - return count($this->data); - } - /** - * alias for count() - * - * @return integer - */ - public function getColumnCount() { - 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); - } - /** - * saveAssociations - * - * save the associations of many-to-many relations - * this method also deletes associations that do not exist anymore - * - * @return void - */ - final public function saveAssociations() { - foreach($this->table->getRelations() as $fk) { - $table = $fk->getTable(); - $name = $table->getComponentName(); - $alias = $this->table->getAlias($name); - - if($fk instanceof Doctrine_Relation_Association) { - switch($fk->getType()): - case Doctrine_Relation::MANY_AGGREGATE: - $asf = $fk->getAssociationFactory(); - - if(isset($this->references[$alias])) { - - $new = $this->references[$alias]; - - if( ! isset($this->originals[$alias])) { - $this->loadReference($alias); - } - - $r = Doctrine_Relation::getDeleteOperations($this->originals[$alias],$new); - - foreach($r as $record) { - $query = "DELETE FROM ".$asf->getTableName()." WHERE ".$fk->getForeign()." = ?" - ." AND ".$fk->getLocal()." = ?"; - $this->table->getConnection()->execute($query, array($record->getIncremented(),$this->getIncremented())); - } - - $r = Doctrine_Relation::getInsertOperations($this->originals[$alias],$new); - foreach($r as $record) { - $reldao = $asf->create(); - $reldao->set($fk->getForeign(),$record); - $reldao->set($fk->getLocal(),$this); - $reldao->save(); - - } - $this->originals[$alias] = clone $this->references[$alias]; - } - break; - endswitch; - } elseif($fk instanceof Doctrine_Relation_ForeignKey || - $fk instanceof Doctrine_Relation_LocalKey) { - - if($fk->isOneToOne()) { - if(isset($this->originals[$alias]) && $this->originals[$alias]->obtainIdentifier() != $this->references[$alias]->obtainIdentifier()) - $this->originals[$alias]->delete(); - - } else { - if(isset($this->references[$alias])) { - $new = $this->references[$alias]; - - if( ! isset($this->originals[$alias])) - $this->loadReference($alias); - - $r = Doctrine_Relation::getDeleteOperations($this->originals[$alias], $new); - - foreach($r as $record) { - $record->delete(); - } - - $this->originals[$alias] = clone $this->references[$alias]; - } - } - } - } - } - /** - * getOriginals - * returns an original collection of related component - * - * @return Doctrine_Collection - */ - final public function getOriginals($name) { - if( ! isset($this->originals[$name])) - throw new InvalidKeyException(); - - return $this->originals[$name]; - } - /** - * 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() { - return $this->table->create($this->data); - } - /** - * 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; - } - } - /** - * filterRelated - * lazy initializes a new filter instance for given related component - * - * @param $componentAlias alias of the related component - * @return Doctrine_Filter - */ - final public function filterRelated($componentAlias) { - if( ! isset($this->filters[$componentAlias])) { - $this->filters[$componentAlias] = new Doctrine_Filter($componentAlias); - } - - return $this->filters[$componentAlias]; - } - /** - * binds One-to-One composite relation - * - * @param string $objTableName - * @param string $fkField - * @return void - */ - final public function ownsOne($componentName,$foreignKey, $localKey = null) { - $this->table->bind($componentName,$foreignKey,Doctrine_Relation::ONE_COMPOSITE, $localKey); - } - /** - * binds One-to-Many composite relation - * - * @param string $objTableName - * @param string $fkField - * @return void - */ - final public function ownsMany($componentName,$foreignKey, $localKey = null) { - $this->table->bind($componentName,$foreignKey,Doctrine_Relation::MANY_COMPOSITE, $localKey); - } - /** - * binds One-to-One aggregate relation - * - * @param string $objTableName - * @param string $fkField - * @return void - */ - final public function hasOne($componentName,$foreignKey, $localKey = null) { - $this->table->bind($componentName,$foreignKey,Doctrine_Relation::ONE_AGGREGATE, $localKey); - } - /** - * binds One-to-Many aggregate relation - * - * @param string $objTableName - * @param string $fkField - * @return void - */ - final public function hasMany($componentName,$foreignKey, $localKey = null) { - $this->table->bind($componentName,$foreignKey,Doctrine_Relation::MANY_AGGREGATE, $localKey); - } - /** - * setPrimaryKey - * @param mixed $key - */ - final public function setPrimaryKey($key) { - $this->table->setPrimaryKey($key); - } - /** - * 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 - } - } - } - /** - * __call - * @param string $m - * @param array $a - */ - public function __call($m,$a) { - if(method_exists($this->table, $m)) - return call_user_func_array(array($this->table, $m), $a); - - if( ! function_exists($m)) - throw new Doctrine_Record_Exception("unknown callback '$m'"); - - if(isset($a[0])) { - $column = $a[0]; - $a[0] = $this->get($column); - - $newvalue = call_user_func_array($m, $a); - - $this->data[$column] = $newvalue; - } - return $this; - } - /** - * returns a string representation of this object - */ - public function __toString() { - return Doctrine_Lib::getRecordAsString($this); - } -} - +. + */ +Doctrine::autoload('Doctrine_Access'); +/** + * Doctrine_Record + * All record classes should inherit this super class + * + * @author Konsta Vesterinen + * @license LGPL + * @package Doctrine + */ + +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; + /** + * @var object Doctrine_Table $table the factory that created this data access object + */ + protected $table; + /** + * @var integer $id the primary keys of this object + */ + protected $id = array(); + /** + * @var array $data the record data + */ + protected $data = 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 array $collections the collections this record is in + */ + private $collections = array(); + /** + * @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 Doctrine_Validator_ErrorStack error stack object + */ + protected $errorStack; + /** + * @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 + */ + private $oid; + + /** + * constructor + * @param Doctrine_Table|null $table a Doctrine_Table object or null, + * if null the table object is retrieved from current connection + * + * @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) { + if(isset($table) && $table instanceof Doctrine_Table) { + $this->table = $table; + $exists = ( ! $this->table->isNewEntry()); + } else { + $this->table = Doctrine_Manager::getInstance()->getCurrentConnection()->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->setDefaultValues(); + + // 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); + } + } + /** + * 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() { } + /** + * 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; + + $validator = new Doctrine_Validator(); + // Run validators + $validator->validateRecord($this); + // Run custom validation + $this->validate(); + + return $this->errorStack->count() == 0 ? true : false; + //$this->errorStack->merge($validator->getErrorStack()); + } + /** + * 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() {} + /** + * getErrorStack + * + * @return Doctrine_Validator_ErrorStack returns the errorStack associated with this record + */ + public function getErrorStack() { + 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 setDefaultValues($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($debug = false) { + $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. ".var_dump(substr($tmp[$lower],0,30)."...",true)); + } 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]; + endswitch; + $count++; + } + } + + + return $count; + } + /** + * 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; + endswitch; + } + /** + * 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['collections']); + unset($vars['originals']); + unset($vars['table']); + + + $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; + endswitch; + } + } + + 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->getCurrentConnection(); + + $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); + } + + + /** + * addCollection + * + * @param Doctrine_Collection $collection + * @param mixed $key + */ + final public function addCollection(Doctrine_Collection $collection,$key = null) { + if($key !== null) { + $this->collections[$key] = $collection; + } else { + $this->collections[] = $collection; + } + } + /** + * getCollection + * @param integer $key + * @return Doctrine_Collection + */ + final public function getCollection($key) { + return $this->collections[$key]; + } + /** + * hasCollections + * whether or not this record is part of a collection + * + * @return boolean + */ + final public function hasCollections() { + return (! empty($this->collections)); + } + /** + * getState + * returns the current state of the object + * + * @see Doctrine_Record::STATE_* constants + * @return integer + */ + final public function getState() { + return $this->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) { + if( ! empty($this->collections)) { + // delegate the loading operation to collections in which this record resides + foreach($this->collections as $collection) { + $collection->load($this); + + } + } else { + + $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) { + + $listener = $this->table->getAttribute(Doctrine::ATTR_LISTENER); + $value = self::$null; + $lower = strtolower($name); + + 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; + + return $value; + } + + + if(isset($this->id[$lower])) + return $this->id[$lower]; + + if($name === $this->table->getIdentifier()) + return null; + + $rel = $this->table->getRelation($name); + + 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]; + } + + /** + * 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); + + 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; + endswitch; + } + } 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 composites + * + * @return void + */ + final public function save(Doctrine_Connection $conn = null) { + if ($conn === null) { + $conn = $this->table->getConnection(); + } + $conn->beginTransaction(); + + $saveLater = $conn->saveRelated($this); + + if ($this->isValid()) { + $conn->save($this); + } else { + $conn->getTransaction()->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(); + } + } + + // save the MANY-TO-MANY associations + + $this->saveAssociations(); + + $conn->commit(); + } + /** + * returns an array of modified fields and associated values + * @return array + */ + final public function getModified() { + $a = array(); + + foreach($this->modified as $k => $v) { + $a[$v] = $this->data[$v]; + } + return $a; + } + /** + * returns an array of modified fields and values with data preparation + * adds column aggregation inheritance and converts Records into primary key values + * + * @return array + */ + final 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] = (int) $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]; + } + } + + foreach($this->table->getInheritanceMap() 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 + */ + public function count() { + return count($this->data); + } + /** + * alias for count() + * + * @return integer + */ + public function getColumnCount() { + 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); + } + /** + * saveAssociations + * + * save the associations of many-to-many relations + * this method also deletes associations that do not exist anymore + * + * @return void + */ + final public function saveAssociations() { + foreach($this->table->getRelations() as $fk) { + $table = $fk->getTable(); + $name = $table->getComponentName(); + $alias = $this->table->getAlias($name); + + if($fk instanceof Doctrine_Relation_Association) { + switch($fk->getType()): + case Doctrine_Relation::MANY_AGGREGATE: + $asf = $fk->getAssociationFactory(); + + if(isset($this->references[$alias])) { + + $new = $this->references[$alias]; + + if( ! isset($this->originals[$alias])) { + $this->loadReference($alias); + } + + $r = Doctrine_Relation::getDeleteOperations($this->originals[$alias],$new); + + foreach($r as $record) { + $query = "DELETE FROM ".$asf->getTableName()." WHERE ".$fk->getForeign()." = ?" + ." AND ".$fk->getLocal()." = ?"; + $this->table->getConnection()->execute($query, array($record->getIncremented(),$this->getIncremented())); + } + + $r = Doctrine_Relation::getInsertOperations($this->originals[$alias],$new); + foreach($r as $record) { + $reldao = $asf->create(); + $reldao->set($fk->getForeign(),$record); + $reldao->set($fk->getLocal(),$this); + $reldao->save(); + + } + $this->originals[$alias] = clone $this->references[$alias]; + } + break; + endswitch; + } elseif($fk instanceof Doctrine_Relation_ForeignKey || + $fk instanceof Doctrine_Relation_LocalKey) { + + if($fk->isOneToOne()) { + if(isset($this->originals[$alias]) && $this->originals[$alias]->obtainIdentifier() != $this->references[$alias]->obtainIdentifier()) + $this->originals[$alias]->delete(); + + } else { + if(isset($this->references[$alias])) { + $new = $this->references[$alias]; + + if( ! isset($this->originals[$alias])) + $this->loadReference($alias); + + $r = Doctrine_Relation::getDeleteOperations($this->originals[$alias], $new); + + foreach($r as $record) { + $record->delete(); + } + + $this->originals[$alias] = clone $this->references[$alias]; + } + } + } + } + } + /** + * getOriginals + * returns an original collection of related component + * + * @return Doctrine_Collection + */ + final public function getOriginals($name) { + if( ! isset($this->originals[$name])) + throw new InvalidKeyException(); + + return $this->originals[$name]; + } + /** + * 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() { + return $this->table->create($this->data); + } + /** + * 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; + } + } + /** + * filterRelated + * lazy initializes a new filter instance for given related component + * + * @param $componentAlias alias of the related component + * @return Doctrine_Filter + */ + final public function filterRelated($componentAlias) { + if( ! isset($this->filters[$componentAlias])) { + $this->filters[$componentAlias] = new Doctrine_Filter($componentAlias); + } + + return $this->filters[$componentAlias]; + } + /** + * binds One-to-One composite relation + * + * @param string $objTableName + * @param string $fkField + * @return void + */ + final public function ownsOne($componentName,$foreignKey, $localKey = null) { + $this->table->bind($componentName,$foreignKey,Doctrine_Relation::ONE_COMPOSITE, $localKey); + } + /** + * binds One-to-Many composite relation + * + * @param string $objTableName + * @param string $fkField + * @return void + */ + final public function ownsMany($componentName,$foreignKey, $localKey = null) { + $this->table->bind($componentName,$foreignKey,Doctrine_Relation::MANY_COMPOSITE, $localKey); + } + /** + * binds One-to-One aggregate relation + * + * @param string $objTableName + * @param string $fkField + * @return void + */ + final public function hasOne($componentName,$foreignKey, $localKey = null) { + $this->table->bind($componentName,$foreignKey,Doctrine_Relation::ONE_AGGREGATE, $localKey); + } + /** + * binds One-to-Many aggregate relation + * + * @param string $objTableName + * @param string $fkField + * @return void + */ + final public function hasMany($componentName,$foreignKey, $localKey = null) { + $this->table->bind($componentName,$foreignKey,Doctrine_Relation::MANY_AGGREGATE, $localKey); + } + /** + * setPrimaryKey + * @param mixed $key + */ + final public function setPrimaryKey($key) { + $this->table->setPrimaryKey($key); + } + /** + * 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 + } + } + } + /** + * __call + * @param string $m + * @param array $a + */ + public function __call($m,$a) { + if(method_exists($this->table, $m)) + return call_user_func_array(array($this->table, $m), $a); + + if( ! function_exists($m)) + throw new Doctrine_Record_Exception("unknown callback '$m'"); + + if(isset($a[0])) { + $column = $a[0]; + $a[0] = $this->get($column); + + $newvalue = call_user_func_array($m, $a); + + $this->data[$column] = $newvalue; + } + return $this; + } + /** + * returns a string representation of this object + */ + public function __toString() { + return Doctrine_Lib::getRecordAsString($this); + } +} + diff --git a/lib/Doctrine/Validator.php b/lib/Doctrine/Validator.php index 12e57ec6a..5b4c00f47 100644 --- a/lib/Doctrine/Validator.php +++ b/lib/Doctrine/Validator.php @@ -27,10 +27,6 @@ * @license LGPL */ class Doctrine_Validator { - /** - * @var array $stack error stack - */ - private $stack = array(); /** * @var array $validators an array of validator objects */ @@ -78,6 +74,8 @@ class Doctrine_Validator { $columns = $record->getTable()->getColumns(); $component = $record->getTable()->getComponentName(); + $errorStack = $record->getErrorStack(); + switch($record->getState()): case Doctrine_Record::STATE_TDIRTY: case Doctrine_Record::STATE_TCLEAN: @@ -102,7 +100,7 @@ class Doctrine_Validator { $value = $record->getTable()->enumIndex($key, $value); if($value === false) { - $err[$key] = 'enum'; + $errorStack->add($key, 'enum'); continue; } } @@ -113,7 +111,7 @@ class Doctrine_Validator { $length = strlen($value); if($length > $column[1]) { - $err[$key] = 'length'; + $errorStack->add($key, 'length'); continue; } @@ -144,7 +142,7 @@ class Doctrine_Validator { if( ! $validator->validate($record, $key, $value, $args)) { - $err[$key] = $name; + $errorStack->add($key, $name); //$err[$key] = 'not valid'; @@ -153,17 +151,10 @@ class Doctrine_Validator { } } if( ! self::isValidType($value, $column[0])) { - $err[$key] = 'type'; + $errorStack->add($key, 'type'); continue; } } - - if( ! empty($err)) { - $this->stack = $err; - return false; - } - - return true; } /** * whether or not this validator has errors @@ -173,14 +164,6 @@ class Doctrine_Validator { public function hasErrors() { return (count($this->stack) > 0); } - /** - * returns the error stack - * - * @return array - */ - public function getErrorStack() { - return $this->stack; - } /** * converts a doctrine type to native php type * diff --git a/lib/Doctrine/Validator/ErrorStack.php b/lib/Doctrine/Validator/ErrorStack.php index 9ad42222e..8eaf3de3c 100644 --- a/lib/Doctrine/Validator/ErrorStack.php +++ b/lib/Doctrine/Validator/ErrorStack.php @@ -1,56 +1,156 @@ -. - */ -Doctrine::autoload('Doctrine_Access'); -/** - * Doctrine_Validator_ErrorStack - * - * @author Konsta Vesterinen - * @license LGPL - * @package Doctrine - */ -class Doctrine_Validator_ErrorStack extends Doctrine_Access implements Countable, IteratorAggregate { - - private $errors = array(); - - public function merge($stack) { - if(is_array($stack)) { - $this->errors = array_merge($this->errors, $stack); - } - } - - public function get($name) { - if(isset($this->errors[$name])) - return $this->errors[$name]; - - return null; - } - - public function set($name, $value) { - $this->errors[$name] = $value; - } - - public function getIterator() { - return new ArrayIterator($this->errors); - } - public function count() { - return count($this->errors); - } -} +. + */ + +/** + * Doctrine_Validator_ErrorStack + * + * @author Konsta Vesterinen + * @author Roman Borschel + * @license LGPL + * @package Doctrine + */ +class Doctrine_Validator_ErrorStack implements ArrayAccess, Countable, IteratorAggregate { + + /** + * The errors of the error stack. + * + * @var array + */ + protected $errors = array(); + + /** + * Constructor + * + */ + public function __construct() + {} + + /** + * Adds an error to the stack. + * + * @param string $invalidFieldName + * @param string $errorType + */ + public function add($invalidFieldName, $errorType = 'general') { + $this->errors[$invalidFieldName][] = array('type' => $errorType); + } + + /** + * Removes all existing errors for the specified field from the stack. + * + * @param string $fieldName + */ + public function remove($fieldName) { + if (isset($this->errors[$fieldName])) { + unset($this->errors[$fieldName]); + } + } + + /** + * Enter description here... + * + * @param unknown_type $name + * @return unknown + */ + public function get($name) { + return $this[$name]; + } + + /** ArrayAccess implementation */ + + /** + * Gets all errors that occured for the specified field. + * + * @param string $offset + * @return The array containing the errors or NULL if no errors were found. + */ + public function offsetGet($offset) { + return isset($this->errors[$offset]) ? $this->errors[$offset] : null; + } + + /** + * Enter description here... + * + * @param string $offset + * @param mixed $value + * @throws Doctrine_Validator_ErrorStack_Exception Always thrown since this operation is not allowed. + */ + public function offsetSet($offset, $value) { + throw new Doctrine_Validator_ErrorStack_Exception("Errors can only be added through + Doctrine_Validator_ErrorStack::add()"); + } + + /** + * Enter description here... + * + * @param unknown_type $offset + */ + public function offsetExists($offset) { + return isset($this->errors[$offset]); + } + + /** + * Enter description here... + * + * @param unknown_type $offset + * @throws Doctrine_Validator_ErrorStack_Exception Always thrown since this operation is not allowed. + */ + public function offsetUnset($offset) { + throw new Doctrine_Validator_ErrorStack_Exception("Errors can only be removed + through Doctrine_Validator_ErrorStack::remove()"); + } + + /** + * Enter description here... + * + * @param unknown_type $stack + */ + /* + public function merge($stack) { + if(is_array($stack)) { + $this->errors = array_merge($this->errors, $stack); + } + }*/ + + + /** IteratorAggregate implementation */ + + /** + * Enter description here... + * + * @return unknown + */ + public function getIterator() { + return new ArrayIterator($this->errors); + } + + + /** Countable implementation */ + + /** + * Enter description here... + * + * @return unknown + */ + public function count() { + return count($this->errors); + } +} diff --git a/tests/EnumTestCase.php b/tests/EnumTestCase.php index 36bb37d4b..2b7b64612 100644 --- a/tests/EnumTestCase.php +++ b/tests/EnumTestCase.php @@ -1,86 +1,86 @@ -tables = array("EnumTest"); - parent::prepareTables(); - } - - public function testParameterConversion() { - $test = new EnumTest(); - $test->status = 'open'; - $this->assertEqual($test->status, 'open'); - $test->save(); - - $query = new Doctrine_Query($this->connection); - $ret = $query->query('FROM EnumTest WHERE EnumTest.status = ?', array('open')); - $this->assertEqual(count($ret), 1); - - $query = new Doctrine_Query($this->connection); - $ret = $query->query('FROM EnumTest WHERE EnumTest.status = open'); - $this->assertEqual(count($ret), 1); - - } - public function testEnumType() { - - $enum = new EnumTest(); - $enum->status = "open"; - $this->assertEqual($enum->status, "open"); - $enum->save(); - $this->assertEqual($enum->status, "open"); - $enum->refresh(); - $this->assertEqual($enum->status, "open"); - - $enum->status = "closed"; - - $this->assertEqual($enum->status, "closed"); - - $enum->save(); - $this->assertEqual($enum->status, "closed"); - $this->assertTrue(is_numeric($enum->id)); - $enum->refresh(); - $this->assertEqual($enum->status, "closed"); - } - - public function testEnumTypeWithCaseConversion() { - $this->dbh->setAttribute(PDO::ATTR_CASE, PDO::CASE_UPPER); - - $enum = new EnumTest(); - - $enum->status = "open"; - $this->assertEqual($enum->status, "open"); - - $enum->save(); - $this->assertEqual($enum->status, "open"); - - $enum->refresh(); - $this->assertEqual($enum->status, "open"); - - $enum->status = "closed"; - - $this->assertEqual($enum->status, "closed"); - - $enum->save(); - $this->assertEqual($enum->status, "closed"); - - $enum->refresh(); - $this->assertEqual($enum->status, "closed"); - - $this->dbh->setAttribute(PDO::ATTR_CASE, PDO::CASE_NATURAL); - } - - public function testFailingRefresh() { - $enum = $this->connection->getTable('EnumTest')->find(1); - - $this->dbh->query('DELETE FROM enum_test WHERE id = 1'); - - $f = false; - try { - $enum->refresh(); - } catch(Doctrine_Record_Exception $e) { - $f = true; - } - $this->assertTrue($f); - } -} -?> +tables = array("EnumTest"); + parent::prepareTables(); + } + + public function testParameterConversion() { + $test = new EnumTest(); + $test->status = 'open'; + $this->assertEqual($test->status, 'open'); + $test->save(); + + $query = new Doctrine_Query($this->connection); + $ret = $query->query('FROM EnumTest WHERE EnumTest.status = ?', array('open')); + $this->assertEqual(count($ret), 1); + + $query = new Doctrine_Query($this->connection); + $ret = $query->query('FROM EnumTest WHERE EnumTest.status = open'); + $this->assertEqual(count($ret), 1); + + } + public function testEnumType() { + + $enum = new EnumTest(); + $enum->status = "open"; + $this->assertEqual($enum->status, "open"); + $enum->save(); + $this->assertEqual($enum->status, "open"); + $enum->refresh(); + $this->assertEqual($enum->status, "open"); + + $enum->status = "closed"; + + $this->assertEqual($enum->status, "closed"); + + $enum->save(); + $this->assertEqual($enum->status, "closed"); + $this->assertTrue(is_numeric($enum->id)); + $enum->refresh(); + $this->assertEqual($enum->status, "closed"); + } + + public function testEnumTypeWithCaseConversion() { + $this->dbh->setAttribute(PDO::ATTR_CASE, PDO::CASE_UPPER); + + $enum = new EnumTest(); + + $enum->status = "open"; + $this->assertEqual($enum->status, "open"); + + $enum->save(); + $this->assertEqual($enum->status, "open"); + + $enum->refresh(); + $this->assertEqual($enum->status, "open"); + + $enum->status = "closed"; + + $this->assertEqual($enum->status, "closed"); + + $enum->save(); + $this->assertEqual($enum->status, "closed"); + + $enum->refresh(); + $this->assertEqual($enum->status, "closed"); + + $this->dbh->setAttribute(PDO::ATTR_CASE, PDO::CASE_NATURAL); + } + + public function testFailingRefresh() { + $enum = $this->connection->getTable('EnumTest')->find(1); + + $this->dbh->query('DELETE FROM enum_test WHERE id = 1'); + + $f = false; + try { + $enum->refresh(); + } catch(Doctrine_Record_Exception $e) { + $f = true; + } + $this->assertTrue($f); + } +} +?> diff --git a/tests/ValidatorTestCase.php b/tests/ValidatorTestCase.php index 372f6d025..45a5ea5b2 100644 --- a/tests/ValidatorTestCase.php +++ b/tests/ValidatorTestCase.php @@ -1,188 +1,227 @@ -tables[] = "ValidatorTest"; - parent::prepareTables(); - } - - public function testIsValidType() { - $var = "123"; - $this->assertTrue(Doctrine_Validator::isValidType($var,"string")); - $this->assertTrue(Doctrine_Validator::isValidType($var,"integer")); - $this->assertTrue(Doctrine_Validator::isValidType($var,"float")); - $this->assertFalse(Doctrine_Validator::isValidType($var,"array")); - $this->assertFalse(Doctrine_Validator::isValidType($var,"object")); - - $var = 123; - $this->assertTrue(Doctrine_Validator::isValidType($var,"string")); - $this->assertTrue(Doctrine_Validator::isValidType($var,"integer")); - $this->assertTrue(Doctrine_Validator::isValidType($var,"float")); - $this->assertFalse(Doctrine_Validator::isValidType($var,"array")); - $this->assertFalse(Doctrine_Validator::isValidType($var,"object")); - - $var = 123.12; - $this->assertTrue(Doctrine_Validator::isValidType($var,"string")); - $this->assertFalse(Doctrine_Validator::isValidType($var,"integer")); - $this->assertTrue(Doctrine_Validator::isValidType($var,"float")); - $this->assertFalse(Doctrine_Validator::isValidType($var,"array")); - $this->assertFalse(Doctrine_Validator::isValidType($var,"object")); - - $var = '123.12'; - $this->assertTrue(Doctrine_Validator::isValidType($var,"string")); - $this->assertFalse(Doctrine_Validator::isValidType($var,"integer")); - $this->assertTrue(Doctrine_Validator::isValidType($var,"float")); - $this->assertFalse(Doctrine_Validator::isValidType($var,"array")); - $this->assertFalse(Doctrine_Validator::isValidType($var,"object")); - - $var = ''; - $this->assertTrue(Doctrine_Validator::isValidType($var,"string")); - $this->assertFalse(Doctrine_Validator::isValidType($var,"integer")); - $this->assertFalse(Doctrine_Validator::isValidType($var,"float")); - $this->assertFalse(Doctrine_Validator::isValidType($var,"array")); - $this->assertFalse(Doctrine_Validator::isValidType($var,"object")); - - $var = null; - $this->assertTrue(Doctrine_Validator::isValidType($var,"string")); - $this->assertTrue(Doctrine_Validator::isValidType($var,"integer")); - $this->assertTrue(Doctrine_Validator::isValidType($var,"float")); - $this->assertTrue(Doctrine_Validator::isValidType($var,"array")); - $this->assertTrue(Doctrine_Validator::isValidType($var,"object")); - - $var = 'str'; - $this->assertTrue(Doctrine_Validator::isValidType($var,"string")); - $this->assertFalse(Doctrine_Validator::isValidType($var,"integer")); - $this->assertFalse(Doctrine_Validator::isValidType($var,"float")); - $this->assertFalse(Doctrine_Validator::isValidType($var,"array")); - $this->assertFalse(Doctrine_Validator::isValidType($var,"object")); - - $var = array(); - $this->assertFalse(Doctrine_Validator::isValidType($var,"string")); - $this->assertFalse(Doctrine_Validator::isValidType($var,"integer")); - $this->assertFalse(Doctrine_Validator::isValidType($var,"float")); - $this->assertTrue(Doctrine_Validator::isValidType($var,"array")); - $this->assertFalse(Doctrine_Validator::isValidType($var,"object")); - - $var = new Exception(); - $this->assertFalse(Doctrine_Validator::isValidType($var,"string")); - $this->assertFalse(Doctrine_Validator::isValidType($var,"integer")); - $this->assertFalse(Doctrine_Validator::isValidType($var,"float")); - $this->assertFalse(Doctrine_Validator::isValidType($var,"array")); - $this->assertTrue(Doctrine_Validator::isValidType($var,"object")); - } - - public function testValidate2() { - $test = new ValidatorTest(); - $test->mymixed = "message"; - $test->myrange = 1; - $test->myregexp = '123a'; - - $validator = new Doctrine_Validator(); - $validator->validateRecord($test); - - $stack = $validator->getErrorStack(); - - $this->assertTrue(is_array($stack)); - - $this->assertEqual($stack['mystring'], 'notnull'); - $this->assertEqual($stack['myemail2'], 'notblank'); - $this->assertEqual($stack['myrange'], 'range'); - $this->assertEqual($stack['myregexp'], 'regexp'); - $test->mystring = 'str'; - - - $test->save(); - } - public function testEmailValidation() { - } - - public function testValidate() { - $user = $this->connection->getTable("User")->find(4); - - $set = array("password" => "this is an example of too long password", - "loginname" => "this is an example of too long loginname", - "name" => "valid name", - "created" => "invalid"); - $user->setArray($set); - $email = $user->Email; - $email->address = "zYne@invalid"; - - $this->assertTrue($user->getModified() == $set); - - $validator = new Doctrine_Validator(); - $validator->validateRecord($user); - - - $stack = $validator->getErrorStack(); - - $this->assertTrue(is_array($stack)); - $this->assertEqual($stack["loginname"], 'length'); - $this->assertEqual($stack["password"], 'length'); - $this->assertEqual($stack["created"], 'type'); - - - $validator->validateRecord($email); - $stack = $validator->getErrorStack(); - $this->assertEqual($stack["address"], 'email'); - $email->address = "arnold@example.com"; - - $validator->validateRecord($email); - $stack = $validator->getErrorStack(); - - $this->assertEqual($stack["address"], 'unique'); - - $email->isValid(); - - $this->assertTrue($email->getErrorStack() instanceof Doctrine_Validator_ErrorStack); - } - - public function testIsValidEmail() { - - $validator = new Doctrine_Validator_Email(); - - $email = $this->connection->create("Email"); - $this->assertFalse($validator->validate($email,"address","example@example",null)); - $this->assertFalse($validator->validate($email,"address","example@@example",null)); - $this->assertFalse($validator->validate($email,"address","example@example.",null)); - $this->assertFalse($validator->validate($email,"address","example@e..",null)); - - $this->assertFalse($validator->validate($email,"address","example@e..",null)); - - $this->assertTrue($validator->validate($email,"address","null@pookey.co.uk",null)); - $this->assertTrue($validator->validate($email,"address","null@pookey.com",null)); - $this->assertTrue($validator->validate($email,"address","null@users.doctrine.pengus.net",null)); - - } - - public function testSave() { - $this->manager->setAttribute(Doctrine::ATTR_VLD, true); - $user = $this->connection->getTable("User")->find(4); - try { - $user->name = "this is an example of too long name not very good example but an example nevertheless"; - $user->save(); - } catch(Doctrine_Validator_Exception $e) { - $this->assertEqual($e->count(), 1); - } - - try { - $user = $this->connection->create("User"); - $user->Email->address = "jackdaniels@drinkmore.info..."; - $user->name = "this is an example of too long user name not very good example but an example nevertheles"; - $user->save(); - $this->fail(); - } catch(Doctrine_Validator_Exception $e) { - $this->pass(); - $a = $e->getInvalidRecords(); - } - - $this->assertTrue(is_array($a)); - - $emailStack = $a[array_search($user->Email, $a)]->getErrorStack(); - $userStack = $a[array_search($user, $a)]->getErrorStack(); - - $this->assertEqual($emailStack["address"], 'email'); - $this->assertEqual($userStack["name"], 'length'); - $this->manager->setAttribute(Doctrine::ATTR_VLD, false); - } - -} -?> +tables[] = "ValidatorTest"; + parent::prepareTables(); + } + + /** + * Tests correct type detection. + */ + public function testIsValidType() { + $var = "123"; + $this->assertTrue(Doctrine_Validator::isValidType($var,"string")); + $this->assertTrue(Doctrine_Validator::isValidType($var,"integer")); + $this->assertTrue(Doctrine_Validator::isValidType($var,"float")); + $this->assertFalse(Doctrine_Validator::isValidType($var,"array")); + $this->assertFalse(Doctrine_Validator::isValidType($var,"object")); + + $var = 123; + $this->assertTrue(Doctrine_Validator::isValidType($var,"string")); + $this->assertTrue(Doctrine_Validator::isValidType($var,"integer")); + $this->assertTrue(Doctrine_Validator::isValidType($var,"float")); + $this->assertFalse(Doctrine_Validator::isValidType($var,"array")); + $this->assertFalse(Doctrine_Validator::isValidType($var,"object")); + + $var = 123.12; + $this->assertTrue(Doctrine_Validator::isValidType($var,"string")); + $this->assertFalse(Doctrine_Validator::isValidType($var,"integer")); + $this->assertTrue(Doctrine_Validator::isValidType($var,"float")); + $this->assertFalse(Doctrine_Validator::isValidType($var,"array")); + $this->assertFalse(Doctrine_Validator::isValidType($var,"object")); + + $var = '123.12'; + $this->assertTrue(Doctrine_Validator::isValidType($var,"string")); + $this->assertFalse(Doctrine_Validator::isValidType($var,"integer")); + $this->assertTrue(Doctrine_Validator::isValidType($var,"float")); + $this->assertFalse(Doctrine_Validator::isValidType($var,"array")); + $this->assertFalse(Doctrine_Validator::isValidType($var,"object")); + + $var = ''; + $this->assertTrue(Doctrine_Validator::isValidType($var,"string")); + $this->assertFalse(Doctrine_Validator::isValidType($var,"integer")); + $this->assertFalse(Doctrine_Validator::isValidType($var,"float")); + $this->assertFalse(Doctrine_Validator::isValidType($var,"array")); + $this->assertFalse(Doctrine_Validator::isValidType($var,"object")); + + $var = null; + $this->assertTrue(Doctrine_Validator::isValidType($var,"string")); + $this->assertTrue(Doctrine_Validator::isValidType($var,"integer")); + $this->assertTrue(Doctrine_Validator::isValidType($var,"float")); + $this->assertTrue(Doctrine_Validator::isValidType($var,"array")); + $this->assertTrue(Doctrine_Validator::isValidType($var,"object")); + + $var = 'str'; + $this->assertTrue(Doctrine_Validator::isValidType($var,"string")); + $this->assertFalse(Doctrine_Validator::isValidType($var,"integer")); + $this->assertFalse(Doctrine_Validator::isValidType($var,"float")); + $this->assertFalse(Doctrine_Validator::isValidType($var,"array")); + $this->assertFalse(Doctrine_Validator::isValidType($var,"object")); + + $var = array(); + $this->assertFalse(Doctrine_Validator::isValidType($var,"string")); + $this->assertFalse(Doctrine_Validator::isValidType($var,"integer")); + $this->assertFalse(Doctrine_Validator::isValidType($var,"float")); + $this->assertTrue(Doctrine_Validator::isValidType($var,"array")); + $this->assertFalse(Doctrine_Validator::isValidType($var,"object")); + + $var = new Exception(); + $this->assertFalse(Doctrine_Validator::isValidType($var,"string")); + $this->assertFalse(Doctrine_Validator::isValidType($var,"integer")); + $this->assertFalse(Doctrine_Validator::isValidType($var,"float")); + $this->assertFalse(Doctrine_Validator::isValidType($var,"array")); + $this->assertTrue(Doctrine_Validator::isValidType($var,"object")); + } + + /** + * Tests Doctrine_Validator::validateRecord() + */ + public function testValidate2() { + $test = new ValidatorTest(); + $test->mymixed = "message"; + $test->myrange = 1; + $test->myregexp = '123a'; + + $validator = new Doctrine_Validator(); + $validator->validateRecord($test); + + $stack = $test->getErrorStack(); + + $this->assertTrue($stack instanceof Doctrine_Validator_ErrorStack); + + $this->assertTrue(in_array(array('type' => 'notnull'), $stack['mystring'])); + $this->assertTrue(in_array(array('type' => 'notblank'), $stack['myemail2'])); + $this->assertTrue(in_array(array('type' => 'range'), $stack['myrange'])); + $this->assertTrue(in_array(array('type' => 'regexp'), $stack['myregexp'])); + $test->mystring = 'str'; + + + $test->save(); + } + + /** + * Tests Doctrine_Validator::validateRecord() + */ + public function testValidate() { + $user = $this->connection->getTable("User")->find(4); + + $set = array("password" => "this is an example of too long password", + "loginname" => "this is an example of too long loginname", + "name" => "valid name", + "created" => "invalid"); + $user->setArray($set); + $email = $user->Email; + $email->address = "zYne@invalid"; + + $this->assertTrue($user->getModified() == $set); + + $validator = new Doctrine_Validator(); + $validator->validateRecord($user); + + + $stack = $user->getErrorStack(); + + $this->assertTrue($stack instanceof Doctrine_Validator_ErrorStack); + $this->assertTrue(in_array(array('type' => 'length'), $stack['loginname'])); + $this->assertTrue(in_array(array('type' => 'length'), $stack['password'])); + $this->assertTrue(in_array(array('type' => 'type'), $stack['created'])); + + $validator->validateRecord($email); + $stack = $email->getErrorStack(); + $this->assertTrue(in_array(array('type' => 'email'), $stack['address'])); + $email->address = "arnold@example.com"; + + $validator->validateRecord($email); + $stack = $email->getErrorStack(); + + $this->assertTrue(in_array(array('type' => 'unique'), $stack['address'])); + } + + /** + * Tests the Email validator. (Doctrine_Validator_Email) + */ + public function testIsValidEmail() { + + $validator = new Doctrine_Validator_Email(); + + $email = $this->connection->create("Email"); + $this->assertFalse($validator->validate($email,"address","example@example",null)); + $this->assertFalse($validator->validate($email,"address","example@@example",null)); + $this->assertFalse($validator->validate($email,"address","example@example.",null)); + $this->assertFalse($validator->validate($email,"address","example@e..",null)); + + $this->assertFalse($validator->validate($email,"address","example@e..",null)); + + $this->assertTrue($validator->validate($email,"address","null@pookey.co.uk",null)); + $this->assertTrue($validator->validate($email,"address","null@pookey.com",null)); + $this->assertTrue($validator->validate($email,"address","null@users.doctrine.pengus.net",null)); + + } + + /** + * Tests saving records with invalid attributes. + */ + public function testSave() { + $this->manager->setAttribute(Doctrine::ATTR_VLD, true); + $user = $this->connection->getTable("User")->find(4); + try { + $user->name = "this is an example of too long name not very good example but an example nevertheless"; + $user->save(); + } catch(Doctrine_Validator_Exception $e) { + $this->assertEqual($e->count(), 1); + $invalidRecords = $e->getInvalidRecords(); + $this->assertEqual(count($invalidRecords), 1); + $stack = $invalidRecords[0]->getErrorStack(); + $this->assertTrue(in_array(array('type' => 'length'), $stack['name'])); + } + + try { + $user = $this->connection->create("User"); + $user->Email->address = "jackdaniels@drinkmore.info..."; + $user->name = "this is an example of too long user name not very good example but an example nevertheles"; + $user->save(); + $this->fail(); + } catch(Doctrine_Validator_Exception $e) { + $this->pass(); + $a = $e->getInvalidRecords(); + } + + $this->assertTrue(is_array($a)); + + $emailStack = $a[array_search($user->Email, $a)]->getErrorStack(); + $userStack = $a[array_search($user, $a)]->getErrorStack(); + + $this->assertTrue(in_array(array('type' => 'email'), $emailStack['address'])); + $this->assertTrue(in_array(array('type' => 'length'), $userStack['name'])); + $this->manager->setAttribute(Doctrine::ATTR_VLD, false); + } + + /** + * Tests whether custom validation through template methods works correctly + * in descendants of Doctrine_Record. + */ + public function testCustomValidation() { + $this->manager->setAttribute(Doctrine::ATTR_VLD, true); + $user = $this->connection->getTable("User")->find(4); + try { + $user->name = "I'm not The Saint"; + $user->save(); + } catch(Doctrine_Validator_Exception $e) { + $this->assertEqual($e->count(), 1); + $invalidRecords = $e->getInvalidRecords(); + $this->assertEqual(count($invalidRecords), 1); + + $stack = $invalidRecords[0]->getErrorStack(); + + $this->assertEqual($stack->count(), 1); + $this->assertTrue(in_array(array('type' => 'notTheSaint'), $stack['name'])); + } + $this->manager->setAttribute(Doctrine::ATTR_VLD, false); + } +} +?> diff --git a/tests/classes.php b/tests/classes.php index 99344f7b1..a92980dfa 100644 --- a/tests/classes.php +++ b/tests/classes.php @@ -97,6 +97,13 @@ class User extends Entity { $this->hasMany("Group","Groupuser.group_id"); $this->setInheritanceMap(array("type"=>0)); } + /** Custom validation */ + public function validate() { + // Allow only one name! + if ($this->name !== 'The Saint') { + $this->errorStack->add('name', 'notTheSaint'); + } + } } class Groupuser extends Doctrine_Record { public function setTableDefinition() { @@ -541,4 +548,5 @@ class BoardWithPosition extends Doctrine_Record { $this->hasOne("CategoryWithPosition as Category", "BoardWithPosition.category_id"); } } + ?>