From 2eb4a16dd42addbe42d6e8d05d67b6aa11be273c Mon Sep 17 00:00:00 2001 From: romanb Date: Fri, 6 Feb 2009 17:16:39 +0000 Subject: [PATCH] [2.0] More progress on the UnitOfWork and collections. First basic functional many-many test. --- lib/Doctrine/Common/VirtualPropertyObject.php | 260 -------------- lib/Doctrine/Common/VirtualPropertySystem.php | 241 ------------- lib/Doctrine/DBAL/Connection.php | 3 +- lib/Doctrine/ORM/Export/Frontbase.php | 316 ------------------ lib/Doctrine/ORM/Export/Reporter.php | 45 --- .../ORM/Mapping/Driver/AnnotationDriver.php | 4 +- .../ORM/Mapping/ManyToManyMapping.php | 14 + lib/Doctrine/ORM/PersistentCollection.php | 6 +- .../AbstractCollectionPersister.php | 66 ++-- .../Persisters/AbstractEntityPersister.php | 9 +- .../ORM/Persisters/ManyToManyPersister.php | 90 ++++- .../ORM/Persisters/OneToManyPersister.php | 17 +- lib/Doctrine/ORM/UnitOfWork.php | 97 ++++-- .../Tests/ORM/Functional/BasicCRUDTest.php | 68 +++- 14 files changed, 274 insertions(+), 962 deletions(-) delete mode 100644 lib/Doctrine/Common/VirtualPropertyObject.php delete mode 100644 lib/Doctrine/Common/VirtualPropertySystem.php delete mode 100644 lib/Doctrine/ORM/Export/Frontbase.php delete mode 100644 lib/Doctrine/ORM/Export/Reporter.php diff --git a/lib/Doctrine/Common/VirtualPropertyObject.php b/lib/Doctrine/Common/VirtualPropertyObject.php deleted file mode 100644 index da5e07ef0..000000000 --- a/lib/Doctrine/Common/VirtualPropertyObject.php +++ /dev/null @@ -1,260 +0,0 @@ -_entityName = get_class($this); - if ( ! Doctrine_Common_VirtualPropertySystem::isInitialized($this->_entityName)) { - Doctrine_Common_VirtualPropertySystem::initialize($this->_entityName); - } - } - - /** - * Generic getter for virtual properties. - * - * @param string $fieldName Name of the field. - * @return mixed - */ - final public function get($fieldName) - { - if ( ! Doctrine_Common_VirtualPropertySystem::hasProperty($this->_entityName, $fieldName)) { - throw new Doctrine_Exception("Access of undefined property '$fieldName'."); - } - $getter = $this->_getCustomAccessor($fieldName); - if ($getter) { - return $this->$getter(); - } - return $this->_get($fieldName); - } - - /** - * Generic setter for virtual properties. - * - * @param string $name The name of the field to set. - * @param mixed $value The value of the field. - */ - final public function set($fieldName, $value) - { - if ( ! Doctrine_Common_VirtualPropertySystem::hasProperty($this->_entityName, $fieldName)) { - throw new Doctrine_Exception("Access of undefined property '$fieldName'."); - } - if (Doctrine_Common_VirtualPropertySystem::isTypeCheckEnabled()) { - $this->_checkType($fieldName, $value); - } - $setter = $this->_getCustomMutator($fieldName); - if ($setter) { - return $this->$setter($value); - } - $this->_set($fieldName, $value); - } - - /** - * Checks the type of a virtual property. - * - * @param $fieldName - * @param $value - */ - protected function _checkType($fieldName, $value) - { - $type = Doctrine_Common_VirtualPropertySystem::getType($this->_entityName, $fieldName); - if (Doctrine_Common_VirtualPropertySystem::isSimplePHPType($type)) { - $is_type = "is_$type"; - if ( ! $is_type($value)) { - throw new Doctrine_Exception("'$value' is of an invalid type. Expected: $type."); - } - } else if ($type == 'array') { - if ( ! is_array($value)) { - throw new Doctrine_Exception("'$value' is of an invalid type. Expected: array."); - } - } else { - if ( ! $value instanceof $type) { - throw new Doctrine_Exception("'$value' is of an invalid type. Expected: $type."); - } - } - } - - protected function _get($fieldName) - { - return isset($this->_data[$fieldName]) ? $this->_data[$fieldName] : null; - } - - protected function _set($fieldName, $value) - { - $this->_data[$fieldName] = $value; - } - - /** - * Gets the custom mutator method for a virtual property, if it exists. - * - * @param string $fieldName The field name. - * @return mixed The name of the custom mutator or FALSE, if the field does - * not have a custom mutator. - */ - private function _getCustomMutator($fieldName) - { - if (Doctrine_Common_VirtualPropertySystem::getMutator($this->_entityName, $fieldName) === null) { - if (Doctrine_Common_VirtualPropertySystem::isAutoAccessorOverride()) { - $setterMethod = 'set' . Doctrine::classify($fieldName); - if ( ! method_exists($this, $setterMethod)) { - $setterMethod = false; - } - Doctrine_Common_VirtualPropertySystem::setMutator( - $this->_entityName, $fieldName, $setterMethod); - } else { - Doctrine_Common_VirtualPropertySystem::setMutator( - $this->_entityName, $fieldName, false); - } - } - return Doctrine_Common_VirtualPropertySystem::getMutator($this->_entityName, $fieldName); - } - - /** - * Gets the custom accessor method of a virtual property, if it exists. - * - * @param string $fieldName The field name. - * @return mixed The name of the custom accessor method, or FALSE if the - * field does not have a custom accessor. - */ - private function _getCustomAccessor($fieldName) - { - if (Doctrine_Common_VirtualPropertySystem::getAccessor($this->_entityName, $fieldName) === null) { - if (Doctrine_Common_VirtualPropertySystem::isAutoAccessorOverride()) { - $getterMethod = 'get' . Doctrine::classify($fieldName); - if ( ! method_exists($this, $getterMethod)) { - $getterMethod = false; - } - Doctrine_Common_VirtualPropertySystem::setAccessor( - $this->_entityName, $fieldName, $getterMethod); - } else { - Doctrine_Common_VirtualPropertySystem::setAccessor( - $this->_entityName, $fieldName, false); - } - } - - return Doctrine_Common_VirtualPropertySystem::getAccessor($this->_entityName, $fieldName); - } - - protected function _contains($fieldName) - { - return isset($this->_data[$fieldName]); - } - - protected function _unset($fieldName) - { - unset($this->_data[$fieldName]); - } - - /** - * Intercepts mutating calls for virtual properties. - * - * @see set, offsetSet - * @param $name - * @param $value - * @since 1.0 - * @return void - */ - public function __set($name, $value) - { - $this->set($name, $value); - } - - /** - * Intercepts accessing calls for virtual properties. - * - * @see get, offsetGet - * @param mixed $name - * @return mixed - */ - public function __get($name) - { - return $this->get($name); - } - - /** - * Intercepts isset() calls for virtual properties. - * - * @param string $name - * @return boolean whether or not this object contains $name - */ - public function __isset($name) - { - return $this->_contains($name); - } - - /** - * Intercepts unset() calls for virtual properties. - * - * @param string $name - * @return void - */ - public function __unset($name) - { - return $this->_unset($name); - } - - /* ArrayAccess implementation */ - - /** - * Check if an offsetExists. - * - * @param mixed $offset - * @return boolean whether or not this object contains $offset - */ - public function offsetExists($offset) - { - return $this->_contains($offset); - } - - /** - * offsetGet an alias of get() - * - * @see get, __get - * @param mixed $offset - * @return mixed - */ - public function offsetGet($offset) - { - return $this->get($offset); - } - - /** - * sets $offset to $value - * @see set, __set - * @param mixed $offset - * @param mixed $value - * @return void - */ - public function offsetSet($offset, $value) - { - return $this->set($offset, $value); - } - - /** - * unset a given offset - * @see set, offsetSet, __set - * @param mixed $offset - */ - public function offsetUnset($offset) - { - return $this->_unset($offset); - } - - /* END of ArrayAccess implementation */ -} -?> diff --git a/lib/Doctrine/Common/VirtualPropertySystem.php b/lib/Doctrine/Common/VirtualPropertySystem.php deleted file mode 100644 index 1a4a48713..000000000 --- a/lib/Doctrine/Common/VirtualPropertySystem.php +++ /dev/null @@ -1,241 +0,0 @@ - true, - 'string' => true, - 'bool' => true, - 'double' => true - ); - - /** Private constructor. This class cannot be instantiated. */ - private function __construct() {} - - /** - * Gets all properties of a class that are registered with the VirtualPropertySystem. - * - * @param string $class - * @return array - */ - public static function getProperties($class) - { - if ( ! self::isInitialized($class)) { - self::initialize($class); - } - return self::$_properties[$class]; - } - - /** - * Gets whether automatic accessor overrides are enabled. - * - * @return boolean - */ - public static function isAutoAccessorOverride() - { - return self::$_useAutoAccessorOverride; - } - - /** - * Sets whether automatic accessor overrides are enabled. - * - * @param boolean $bool - */ - public static function setAutoAccessorOverride($bool) - { - self::$_useAutoAccessorOverride = (bool)$bool; - } - - /** - * Prepopulates the property system. - * - * @param array $properties - */ - public static function populate(array $properties) - { - self::$_properties = $properties; - } - - /** - * Checks whether the given type is a simple PHP type. - * Simple php types are: int, string, bool, double. - * - * @param string $type The type to check. - * @return boolean - */ - public static function isSimplePHPType($type) - { - return isset(self::$_simplePHPTypes[$type]); - } - - /** - * Gets whether type checks are enabled. - * - * @return boolean - */ - public static function isTypeCheckEnabled() - { - return self::$_checkTypes; - } - - /** - * Sets whether type checks are enabled. - * - * @param boolean $bool - */ - public static function setTypeCheckEnabled($bool) - { - self::$_checkTypes = (bool)$bool; - } - - /** - * Sets the name of the callback method to use for initializing the virtual - * properties of a class. The callback must be static and public. - * - * @param string $callback - */ - public static function setCallback($callback) - { - self::$_callback = $callback; - } - - /** - * Registers a virtual property for a class. - * - * @param string $class - * @param string $propName - * @param string $type - * @param string $accessor - * @param string $mutator - */ - public static function register($class, $propName, $type, $accessor = null, $mutator = null) - { - self::$_properties[$class][$propName] = array( - 'type' => $type, 'accessor' => $accessor, 'mutator' => $mutator - ); - } - - /** - * Gets whether a class has already been initialized by the virtual property system. - * - * @param string $class - * @return boolean - */ - public static function isInitialized($class) - { - return isset(self::$_properties[$class]); - } - - /** - * Initializes a class with the virtual property system. - * - * @param $class - */ - public static function initialize($class) - { - if (method_exists($class, self::$_callback)) { - call_user_func(array($class, self::$_callback)); - } else { - self::$_properties[$class] = false; - } - } - - /** - * Gets whether a class has a virtual property with a certain name. - * - * @param string $class - * @param string $propName - * @return boolean - */ - public static function hasProperty($class, $propName) - { - return isset(self::$_properties[$class][$propName]); - } - - /** - * Gets the accessor for a virtual property. - * - * @param string $class - * @param string $propName - * @return string|null - */ - public static function getAccessor($class, $propName) - { - return isset(self::$_properties[$class][$propName]['accessor']) ? - self::$_properties[$class][$propName]['accessor'] : null; - } - - /** - * Sets the accessor method for a virtual property. - * - * @param $class - * @param $propName - * @param $accessor - */ - public static function setAccessor($class, $propName, $accessor) - { - self::$_properties[$class][$propName]['accessor'] = $accessor; - } - - /** - * Gets the mutator method for a virtual property. - * - * @param $class - * @param $propName - * @return - */ - public static function getMutator($class, $propName) - { - return isset(self::$_properties[$class][$propName]['mutator']) ? - self::$_properties[$class][$propName]['mutator'] : null; - } - - /** - * Sets the mutator method for a virtual property. - * - * @param $class - * @param $propName - * @param $mutator - */ - public static function setMutator($class, $propName, $mutator) - { - self::$_properties[$class][$propName]['mutator'] = $mutator; - } - - /** - * Gets the type of a virtual property. - * - * @param $class - * @param $propName - * @return - */ - public static function getType($class, $propName) - { - return isset(self::$_properties[$class][$propName]['type']) ? - self::$_properties[$class][$propName]['type'] : null; - } - - /** - * Sets the type of a virtual property. - * - * @param $class - * @param $propName - * @param $type - */ - public static function setType($class, $propName, $type) - { - self::$_properties[$class][$propName]['type'] = $type; - } -} -?> diff --git a/lib/Doctrine/DBAL/Connection.php b/lib/Doctrine/DBAL/Connection.php index 43a4aaa7d..6fa541e46 100644 --- a/lib/Doctrine/DBAL/Connection.php +++ b/lib/Doctrine/DBAL/Connection.php @@ -542,8 +542,9 @@ class Connection public function exec($query, array $params = array()) { $this->connect(); try { + echo $query . PHP_EOL; if ( ! empty($params)) { - //var_dump($params); + var_dump($params); $stmt = $this->prepare($query); $stmt->execute($params); return $stmt->rowCount(); diff --git a/lib/Doctrine/ORM/Export/Frontbase.php b/lib/Doctrine/ORM/Export/Frontbase.php deleted file mode 100644 index e10e804e8..000000000 --- a/lib/Doctrine/ORM/Export/Frontbase.php +++ /dev/null @@ -1,316 +0,0 @@ -. - */ - -/** - * Doctrine_Export_Frontbase - * - * @package Doctrine - * @subpackage Export - * @author Konsta Vesterinen - * @author Lukas Smith (PEAR MDB2 library) - * @license http://www.opensource.org/licenses/lgpl-license.php LGPL - * @link www.phpdoctrine.org - * @since 1.0 - * @version $Revision$ - */ -class Doctrine_Export_Frontbase extends Doctrine_Export -{ - /** - * create a new database - * - * @param string $name name of the database that should be created - * @return string - */ - public function createDatabaseSql($name) - { - $name = $this->conn->quoteIdentifier($name, true); - return 'CREATE DATABASE ' . $name; - } - - /** - * drop an existing database - * - * @param string $name name of the database that should be dropped - * @return string - */ - public function dropDatabaseSql($name) - { - $name = $this->conn->quoteIdentifier($name, true); - return 'DELETE DATABASE ' . $name; - } - - /** - * drop an existing table - * - * @param object $this->conns database object that is extended by this class - * @param string $name name of the table that should be dropped - * @return string - */ - public function dropTableSql($name) - { - $name = $this->conn->quoteIdentifier($name, true); - return 'DROP TABLE ' . $name . ' CASCADE'; - } - - /** - * alter an existing table - * - * @param string $name name of the table that is intended to be changed. - * @param array $changes associative array that contains the details of each type - * of change that is intended to be performed. The types of - * changes that are currently supported are defined as follows: - * - * name - * - * New name for the table. - * - * add - * - * Associative array with the names of fields to be added as - * indexes of the array. The value of each entry of the array - * should be set to another associative array with the properties - * of the fields to be added. The properties of the fields should - * be the same as defined by the MDB2 parser. - * - * - * remove - * - * Associative array with the names of fields to be removed as indexes - * of the array. Currently the values assigned to each entry are ignored. - * An empty array should be used for future compatibility. - * - * rename - * - * Associative array with the names of fields to be renamed as indexes - * of the array. The value of each entry of the array should be set to - * another associative array with the entry named name with the new - * field name and the entry named Declaration that is expected to contain - * the portion of the field declaration already in DBMS specific SQL code - * as it is used in the CREATE TABLE statement. - * - * change - * - * Associative array with the names of the fields to be changed as indexes - * of the array. Keep in mind that if it is intended to change either the - * name of a field and any other properties, the change array entries - * should have the new names of the fields as array indexes. - * - * The value of each entry of the array should be set to another associative - * array with the properties of the fields to that are meant to be changed as - * array entries. These entries should be assigned to the new values of the - * respective properties. The properties of the fields should be the same - * as defined by the MDB2 parser. - * - * Example - * array( - * 'name' => 'userlist', - * 'add' => array( - * 'quota' => array( - * 'type' => 'integer', - * 'unsigned' => 1 - * ) - * ), - * 'remove' => array( - * 'file_limit' => array(), - * 'time_limit' => array() - * ), - * 'change' => array( - * 'name' => array( - * 'length' => '20', - * 'definition' => array( - * 'type' => 'text', - * 'length' => 20, - * ), - * ) - * ), - * 'rename' => array( - * 'sex' => array( - * 'name' => 'gender', - * 'definition' => array( - * 'type' => 'text', - * 'length' => 1, - * 'default' => 'M', - * ), - * ) - * ) - * ) - * - * @param boolean $check indicates whether the function should just check if the DBMS driver - * can perform the requested table alterations if the value is true or - * actually perform them otherwise. - * @access public - * - * @return boolean - */ - public function alterTable($name, array $changes, $check = false) - { - foreach ($changes as $changeName => $change) { - switch ($changeName) { - case 'add': - case 'remove': - case 'change': - case 'rename': - case 'name': - break; - default: - throw new Doctrine_Export_Exception('change type "'.$changeName.'" not yet supported'); - } - } - - if ($check) { - return true; - } - - $query = ''; - if ( ! empty($changes['name'])) { - $changeName = $this->conn->quoteIdentifier($changes['name'], true); - $query .= 'RENAME TO ' . $changeName; - } - - if ( ! empty($changes['add']) && is_array($changes['add'])) { - foreach ($changes['add'] as $fieldName => $field) { - if ($query) { - $query.= ', '; - } - $query.= 'ADD ' . $this->conn->getDeclaration($fieldName, $field); - } - } - - if ( ! empty($changes['remove']) && is_array($changes['remove'])) { - foreach ($changes['remove'] as $fieldName => $field) { - if ($query) { - $query.= ', '; - } - $fieldName = $this->conn->quoteIdentifier($fieldName, true); - $query.= 'DROP ' . $fieldName; - } - } - - $rename = array(); - if ( ! empty($changes['rename']) && is_array($changes['rename'])) { - foreach ($changes['rename'] as $fieldName => $field) { - $rename[$field['name']] = $fieldName; - } - } - - if ( ! empty($changes['change']) && is_array($changes['change'])) { - foreach ($changes['change'] as $fieldName => $field) { - if ($query) { - $query.= ', '; - } - if (isset($rename[$fieldName])) { - $oldFieldName = $rename[$fieldName]; - unset($rename[$fieldName]); - } else { - $oldFieldName = $fieldName; - } - $oldFieldName = $this->conn->quoteIdentifier($oldFieldName, true); - $query.= 'CHANGE ' . $oldFieldName . ' ' . $this->conn->getDeclaration($oldFieldName, $field['definition']); - } - } - - if ( ! empty($rename) && is_array($rename)) { - foreach ($rename as $renamedFieldName => $renamed_field) { - if ($query) { - $query.= ', '; - } - $oldFieldName = $rename[$renamedFieldName]; - $field = $changes['rename'][$oldFieldName]; - $query.= 'CHANGE ' . $this->conn->getDeclaration($oldFieldName, $field['definition']); - } - } - - if ( ! $query) { - return true; - } - - $name = $this->conn->quoteIdentifier($name, true); - return $this->conn->exec('ALTER TABLE ' . $name . ' ' . $query); - } - - /** - * create sequence - * - * @param string $seqName name of the sequence to be created - * @param string $start start value of the sequence; default is 1 - * @param array $options An associative array of table options: - * array( - * 'comment' => 'Foo', - * 'charset' => 'utf8', - * 'collate' => 'utf8_unicode_ci', - * ); - * @return void - */ - public function createSequence($sequenceName, $start = 1, array $options = array()) - { - $sequenceName = $this->conn->quoteIdentifier($this->conn->getSequenceName($sequenceName), true); - $seqcolName = $this->conn->quoteIdentifier($this->conn->getAttribute(Doctrine::ATTR_SEQCOL_NAME), true); - - $query = 'CREATE TABLE ' . $sequenceName . ' (' . $seqcolName . ' INTEGER DEFAULT UNIQUE, PRIMARY KEY(' . $seqcolName . '))'; - $res = $this->conn->exec($query); - $res = $this->conn->exec('SET UNIQUE = 1 FOR ' . $sequenceName); - - if ($start == 1) { - return true; - } - - try { - $this->conn->exec('INSERT INTO ' . $sequenceName . ' (' . $seqcolName . ') VALUES (' . ($start-1) . ')'); - } catch(Doctrine_Connection_Exception $e) { - // Handle error - try { - $this->conn->exec('DROP TABLE ' . $sequenceName); - } catch(Doctrine_Connection_Exception $e) { - throw new Doctrine_Export_Exception('could not drop inconsistent sequence table'); - } - - throw new Doctrine_Export_Exception('could not create sequence table'); - } - } - - /** - * drop existing sequence - * - * @param string $seqName name of the sequence to be dropped - * @return string - */ - public function dropSequenceSql($seqName) - { - $sequenceName = $this->conn->quoteIdentifier($this->conn->getSequenceName($seqName), true); - - return 'DROP TABLE ' . $sequenceName . ' CASCADE'; - } - - /** - * drop existing index - * - * @param string $table name of table that should be used in method - * @param string $name name of the index to be dropped - * @return boolean - */ - public function dropIndexSql($table, $name) - { - $table = $this->conn->quoteIdentifier($table, true); - $name = $this->conn->quoteIdentifier($this->conn->getIndexName($name), true); - - return 'ALTER TABLE ' . $table . ' DROP INDEX ' . $name; - } -} \ No newline at end of file diff --git a/lib/Doctrine/ORM/Export/Reporter.php b/lib/Doctrine/ORM/Export/Reporter.php deleted file mode 100644 index 57a9311f6..000000000 --- a/lib/Doctrine/ORM/Export/Reporter.php +++ /dev/null @@ -1,45 +0,0 @@ -. - */ - -/** - * Doctrine_Export_Reporter - * - * @package Doctrine - * @subpackage Export - * @author Konsta Vesterinen - * @license http://www.opensource.org/licenses/lgpl-license.php LGPL - * @link www.phpdoctrine.org - * @since 1.0 - * @version $Revision: 3882 $ - */ -class Doctrine_Export_Reporter implements IteratorAggregate { - protected $messages = array(); - - public function add($code, $message) { - $this->messages[] = array($code, $message); - } - public function pop() { - return array_pop($this->messages); - } - public function getIterator() { - return new ArrayIterator($this->messages); - } -} \ No newline at end of file diff --git a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php index 0556881d2..58627c6ec 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php @@ -31,9 +31,11 @@ if ( ! class_exists('\Addendum', false)) { require __DIR__ . '/DoctrineAnnotations.php'; /** - * The AnnotationDriver reads the mapping metadata from docblock annotations. + * The AnnotationDriver reads the mapping metadata from docblock annotations + * with the help of the Addendum reflection extensions. * * @author robo + * @since 2.0 */ class AnnotationDriver { diff --git a/lib/Doctrine/ORM/Mapping/ManyToManyMapping.php b/lib/Doctrine/ORM/Mapping/ManyToManyMapping.php index ebdd80cec..d1c8d86e6 100644 --- a/lib/Doctrine/ORM/Mapping/ManyToManyMapping.php +++ b/lib/Doctrine/ORM/Mapping/ManyToManyMapping.php @@ -51,6 +51,11 @@ class ManyToManyMapping extends AssociationMapping * Maps the columns in the target table to the columns in the relation table. */ private $_targetToRelationKeyColumns = array(); + + /** + * The columns on the join table. + */ + private $_joinTableColumns = array(); /** * Initializes a new ManyToManyMapping. @@ -76,25 +81,34 @@ class ManyToManyMapping extends AssociationMapping if ( ! isset($mapping['joinTable'])) { throw MappingException::joinTableRequired($mapping['fieldName']); } + // owning side MUST specify joinColumns if ( ! isset($mapping['joinTable']['joinColumns'])) { throw MappingException::invalidMapping($this->_sourceFieldName); } foreach ($mapping['joinTable']['joinColumns'] as $joinColumn) { $this->_sourceToRelationKeyColumns[$joinColumn['referencedColumnName']] = $joinColumn['name']; + $this->_joinTableColumns[] = $joinColumn['name']; } $this->_sourceKeyColumns = array_keys($this->_sourceToRelationKeyColumns); + // owning side MUST specify inverseJoinColumns if ( ! isset($mapping['joinTable']['inverseJoinColumns'])) { throw MappingException::invalidMapping($this->_sourceFieldName); } foreach ($mapping['joinTable']['inverseJoinColumns'] as $inverseJoinColumn) { $this->_targetToRelationKeyColumns[$inverseJoinColumn['referencedColumnName']] = $inverseJoinColumn['name']; + $this->_joinTableColumns[] = $inverseJoinColumn['name']; } $this->_targetKeyColumns = array_keys($this->_targetToRelationKeyColumns); } } + public function getJoinTableColumns() + { + return $this->_joinTableColumns; + } + public function getSourceToRelationKeyColumns() { return $this->_sourceToRelationKeyColumns; diff --git a/lib/Doctrine/ORM/PersistentCollection.php b/lib/Doctrine/ORM/PersistentCollection.php index c869b9f60..6c4fba6db 100644 --- a/lib/Doctrine/ORM/PersistentCollection.php +++ b/lib/Doctrine/ORM/PersistentCollection.php @@ -27,11 +27,11 @@ use Doctrine\ORM\Mapping\AssociationMapping; * A PersistentCollection represents a collection of elements that have persistent state. * Collections of entities represent only the associations (links) to those entities. * That means, if the collection is part of a many-many mapping and you remove - * entities from the collection, only the links in the xref table are removed (on flush). + * entities from the collection, only the links in the relation table are removed (on flush). * Similarly, if you remove entities from a collection that is part of a one-many * mapping this will only result in the nulling out of the foreign keys on flush - * (or removal of the links in the xref table if the one-many is mapped through an - * xref table). If you want entities in a one-many collection to be removed when + * (or removal of the links in the relation table if the one-many is mapped through a + * relation table). If you want entities in a one-many collection to be removed when * they're removed from the collection, use deleteOrphans => true on the one-many * mapping. * diff --git a/lib/Doctrine/ORM/Persisters/AbstractCollectionPersister.php b/lib/Doctrine/ORM/Persisters/AbstractCollectionPersister.php index 8d6655f68..27047fd72 100644 --- a/lib/Doctrine/ORM/Persisters/AbstractCollectionPersister.php +++ b/lib/Doctrine/ORM/Persisters/AbstractCollectionPersister.php @@ -55,54 +55,54 @@ abstract class AbstractCollectionPersister } //... } - - public function delete(PersistentCollection $coll) - { - if ($coll->getRelation()->isInverseSide()) { - return; - } - //... - } - public function update(PersistentCollection $coll) - { - $this->deleteRows($coll); - $this->updateRows($coll); - $this->insertRows($coll); - } - - /* collection update actions */ - - public function deleteRows(PersistentCollection $coll) + /** + * Deletes the persistent state represented by the given collection. + * + * @param PersistentCollection $coll + */ + public function delete(PersistentCollection $coll) { if ($coll->getMapping()->isInverseSide()) { return; // ignore inverse side } - + + $sql = $this->_getDeleteSql($coll); + $this->_conn->exec($sql, $this->_getDeleteSqlParameters($coll)); + } + + abstract protected function _getDeleteSql(PersistentCollection $coll); + abstract protected function _getDeleteSqlParameters(PersistentCollection $coll); + + public function update(PersistentCollection $coll) + { + if ($coll->getMapping()->isInverseSide()) { + return; // ignore inverse side + } + + $this->deleteRows($coll); + //$this->updateRows($coll); + $this->insertRows($coll); + } + + public function deleteRows(PersistentCollection $coll) + { $deleteDiff = $coll->getDeleteDiff(); $sql = $this->_getDeleteRowSql($coll); - $uow = $this->_em->getUnitOfWork(); foreach ($deleteDiff as $element) { - $this->_conn->exec($sql, $this->_getDeleteRowSqlParameters($element)); + $this->_conn->exec($sql, $this->_getDeleteRowSqlParameters($coll, $element)); } } public function updateRows(PersistentCollection $coll) - { - - } + {} public function insertRows(PersistentCollection $coll) { - if ($coll->getMapping()->isInverseSide()) { - return; // ignore inverse side - } - $insertDiff = $coll->getInsertDiff(); $sql = $this->_getInsertRowSql($coll); - $uow = $this->_em->getUnitOfWork(); foreach ($insertDiff as $element) { - $this->_conn->exec($sql/*, $uow->getEntityIdentifier($element)*/); + $this->_conn->exec($sql, $this->_getInsertRowSqlParameters($coll, $element)); } } @@ -120,13 +120,15 @@ abstract class AbstractCollectionPersister * * @param PersistentCollection $coll */ - abstract protected function _getUpdateRowSql(); + abstract protected function _getUpdateRowSql(PersistentCollection $coll); /** * Gets the SQL statement used for inserting a row from to the collection. * * @param PersistentCollection $coll */ - abstract protected function _getInsertRowSql(); + abstract protected function _getInsertRowSql(PersistentCollection $coll); + + abstract protected function _getInsertRowSqlParameters(PersistentCollection $coll, $element); } diff --git a/lib/Doctrine/ORM/Persisters/AbstractEntityPersister.php b/lib/Doctrine/ORM/Persisters/AbstractEntityPersister.php index d767fd33c..f32bae99c 100644 --- a/lib/Doctrine/ORM/Persisters/AbstractEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/AbstractEntityPersister.php @@ -171,13 +171,8 @@ abstract class AbstractEntityPersister protected function _prepareData($entity, array &$result, $isInsert = false) { foreach ($this->_em->getUnitOfWork()->getEntityChangeSet($entity) as $field => $change) { - if (is_array($change)) { - $oldVal = $change[0]; - $newVal = $change[1]; - } else { - $oldVal = null; - $newVal = $change; - } + $oldVal = $change[0]; + $newVal = $change[1]; $type = $this->_classMetadata->getTypeOfField($field); $columnName = $this->_classMetadata->getColumnName($field); diff --git a/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php b/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php index 49b071481..1164cbe31 100644 --- a/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php +++ b/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php @@ -21,40 +21,112 @@ namespace Doctrine\ORM\Persisters; +use Doctrine\ORM\PersistentCollection; + /** * Persister for many-to-many collections. * * @author robo + * @since 2.0 */ class ManyToManyPersister extends AbstractCollectionPersister { /** + * {@inheritdoc} * - * @param $coll * @override - * @todo Identifier quoting. - * @see _getDeleteRowSqlParameters() */ protected function _getDeleteRowSql(PersistentCollection $coll) { $mapping = $coll->getMapping(); $joinTable = $mapping->getJoinTable(); - $columns = array_merge($mapping->getSourceKeyColumns(), $mapping->getTargetKeyColumns()); - return "DELETE FROM $joinTable WHERE " . implode(' = ?, ', $columns) . ' = ?'; + $columns = $mapping->getJoinTableColumns(); + return "DELETE FROM {$joinTable['name']} WHERE " . implode(' = ? AND ', $columns) . ' = ?'; } /** + * {@inheritdoc} * - * @param $element * @override - * @see _getDeleteRowSql() */ protected function _getDeleteRowSqlParameters(PersistentCollection $coll, $element) { - $owner = $coll->getOwner(); + $params = array_merge( + $this->_uow->getEntityIdentifier($coll->getOwner()), + $this->_uow->getEntityIdentifier($element) + ); + //var_dump($params); + return $params; + } + /** + * {@inheritdoc} + * + * @override + */ + protected function _getUpdateRowSql(PersistentCollection $coll) + { - + } + + /** + * {@inheritdoc} + * + * @override + */ + protected function _getInsertRowSql(PersistentCollection $coll) + { + $mapping = $coll->getMapping(); + $joinTable = $mapping->getJoinTable(); + $columns = $mapping->getJoinTableColumns(); + return "INSERT INTO {$joinTable['name']} (" . implode(', ', $columns) . ")" + . " VALUES (" . implode(', ', array_fill(0, count($columns), '?')) . ')'; + } + + /** + * {@inheritdoc} + * + * @override + */ + protected function _getInsertRowSqlParameters(PersistentCollection $coll, $element) + { + //FIXME: This is still problematic for composite keys because we silently + // rely on a specific ordering of the columns. + $params = array_merge( + $this->_uow->getEntityIdentifier($coll->getOwner()), + $this->_uow->getEntityIdentifier($element) + ); + var_dump($params); + return $params; + } + + /** + * {@inheritdoc} + * + * @override + */ + protected function _getDeleteSql(PersistentCollection $coll) + { + $mapping = $coll->getMapping(); + $joinTable = $mapping->getJoinTable(); + $whereClause = ''; + foreach ($mapping->getSourceToRelationKeyColumns() as $relationColumn) { + if ($whereClause !== '') $whereClause .= ' AND '; + $whereClause .= "$relationColumn = ?"; + } + return "DELETE FROM {$joinTable['name']} WHERE $whereClause"; + } + + /** + * {@inheritdoc} + * + * @override + */ + protected function _getDeleteSqlParameters(PersistentCollection $coll) + { + //FIXME: This is still problematic for composite keys because we silently + // rely on a specific ordering of the columns. + return $this->_uow->getEntityIdentifier($coll->getOwner()); } } diff --git a/lib/Doctrine/ORM/Persisters/OneToManyPersister.php b/lib/Doctrine/ORM/Persisters/OneToManyPersister.php index c03b0a570..eb89fa53b 100644 --- a/lib/Doctrine/ORM/Persisters/OneToManyPersister.php +++ b/lib/Doctrine/ORM/Persisters/OneToManyPersister.php @@ -25,8 +25,11 @@ use Doctrine\ORM\PersistentCollection; /** * Persister for one-to-many collections. + * + * This persister is only used for uni-directional one-to-many mappings. * * @since 2.0 + * @author Roman Borschel */ class OneToManyPersister extends AbstractCollectionPersister { @@ -57,19 +60,7 @@ class OneToManyPersister extends AbstractCollectionPersister $whereClause .= "$idColumn = ?"; } - return "UPDATE $table SET $setClause WHERE $whereClause"; - } - - /** - * {@inheritdoc} - * - * @param $element - * @return - * @override - */ - protected function _getDeleteRowSqlParameters(PersistentCollection $coll, $element) - { - return $this->_uow->getEntityIdentifier($element); + return array("UPDATE $table SET $setClause WHERE $whereClause", $this->_uow->getEntityIdentifier($element)); } protected function _getInsertRowSql() diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index e2fe23743..3402550a0 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -24,6 +24,8 @@ namespace Doctrine\ORM; use Doctrine\ORM\Internal\CommitOrderCalculator; use Doctrine\ORM\Internal\CommitOrderNode; use Doctrine\ORM\PersistentCollection; +use Doctrine\ORM\Mapping; +use Doctrine\ORM\Persisters; use Doctrine\ORM\EntityManager; use Doctrine\ORM\Exceptions\UnitOfWorkException; @@ -227,7 +229,8 @@ class UnitOfWork if (empty($this->_newEntities) && empty($this->_deletedEntities) && empty($this->_dirtyEntities) && - empty($this->_collectionUpdates)) { + empty($this->_collectionUpdates) && + empty($this->_collectionDeletions)) { return; // Nothing to do. } @@ -243,8 +246,12 @@ class UnitOfWork $this->_executeUpdates($class); } - //TODO: collection deletions (deletions of complete collections) - //TODO: collection updates (deleteRows, updateRows, insertRows) + // Collection deletions (deletions of complete collections) + foreach ($this->_collectionDeletions as $collectionToDelete) { + $this->getCollectionPersister($collectionToDelete->getMapping()) + ->delete($collectionToDelete); + } + // Collection updates (deleteRows, updateRows, insertRows) foreach ($this->_collectionUpdates as $collectionToUpdate) { $this->getCollectionPersister($collectionToUpdate->getMapping()) ->update($collectionToUpdate); @@ -269,6 +276,7 @@ class UnitOfWork $this->_deletedEntities = array(); $this->_entityChangeSets = array(); $this->_collectionUpdates = array(); + $this->_collectionDeletions = array(); $this->_visitedCollections = array(); } @@ -328,24 +336,31 @@ class UnitOfWork $actualData[$name] = $refProp->getValue($entity); } - if ($class->isCollectionValuedAssociation($name) && ! ($actualData[$name] instanceof PersistentCollection)) { - // Inject PersistentCollection + if ($class->isCollectionValuedAssociation($name) + && ! is_null($actualData[$name]) + && ! ($actualData[$name] instanceof PersistentCollection)) { //TODO: If $actualData[$name] is Collection then unwrap the array $assoc = $class->getAssociationMapping($name); - $coll = new PersistentCollection($this->_em, $assoc->getTargetEntityName(), + if ($assoc->isOwningSide()) { + // Inject PersistentCollection + $coll = new PersistentCollection($this->_em, $assoc->getTargetEntityName(), $actualData[$name] ? $actualData[$name] : array()); - $coll->_setOwner($entity, $assoc); - if ( ! $coll->isEmpty()) $coll->setDirty(true); - $class->getReflectionProperty($name)->setValue($entity, $coll); - $actualData[$name] = $coll; + $coll->_setOwner($entity, $assoc); + if ( ! $coll->isEmpty()) $coll->setDirty(true); + $class->getReflectionProperty($name)->setValue($entity, $coll); + $actualData[$name] = $coll; + } } } if ( ! isset($this->_originalEntityData[$oid])) { // Entity is either NEW or MANAGED but not yet fully persisted // (only has an id). These result in an INSERT. - $this->_entityChangeSets[$oid] = $actualData; $this->_originalEntityData[$oid] = $actualData; + $this->_entityChangeSets[$oid] = array_map( + function($e) { return array(null, $e); }, + $actualData + ); } else { // Entity is "fully" MANAGED: it was already fully persisted before // and we have a copy of the original data @@ -366,11 +381,16 @@ class UnitOfWork $assoc = $class->getAssociationMapping($propName); if ($assoc->isOneToOne() && $assoc->isOwningSide()) { $entityIsDirty = true; + } else if (/*is_null($actualValue) && */$orgValue instanceof PersistentCollection) { + // A PersistentCollection was de-referenced, so delete it. + if ( ! in_array($orgValue, $this->_collectionDeletions, true)) { + $this->_collectionDeletions[] = $orgValue; + } } } else { $entityIsDirty = true; } - } + } } if ($changeSet) { if ($entityIsDirty) { @@ -403,6 +423,10 @@ class UnitOfWork */ private function _computeAssociationChanges($assoc, $value) { + /*if ( ! $assoc->isCascadeSave()) { + return; // "Persistence by reachability" only if save cascade enabled + }*/ + if ($assoc->isOneToOne()) { $value = array($value); } @@ -428,13 +452,27 @@ class UnitOfWork // NEW entities are INSERTed within the current unit of work. $data = array(); + $changeSet = array(); foreach ($targetClass->getReflectionProperties() as $name => $refProp) { $data[$name] = $refProp->getValue($entry); + $changeSet[$name] = array(null, $data[$name]); + // -- + /*if ($targetClass->isCollectionValuedAssociation($name) && ! ($data[$name] instanceof PersistentCollection)) { + // Inject PersistentCollection + //TODO: If $actualData[$name] is Collection then unwrap the array + $assoc = $targetClass->getAssociationMapping($name); + $coll = new PersistentCollection($this->_em, $assoc->getTargetEntityName(), + $data[$name] ? $data[$name] : array()); + $coll->_setOwner($entry, $assoc); + if ( ! $coll->isEmpty()) $coll->setDirty(true); + $targetClass->getReflectionProperty($name)->setValue($entry, $coll); + $data[$name] = $coll; + }*/ + //-- } - $oid = spl_object_hash($entry); $this->_newEntities[$oid] = $entry; - $this->_entityChangeSets[$oid] = $data; + $this->_entityChangeSets[$oid] = $changeSet; $this->_originalEntityData[$oid] = $data; } else if ($state == self::STATE_DELETED) { throw new DoctrineException("Deleted entity in collection detected during flush."); @@ -481,11 +519,6 @@ class UnitOfWork } } - private function _executeCollectionUpdate($collectionToUpdate) - { - //... - } - /** * Executes all entity updates for entities of the specified type. * @@ -784,7 +817,7 @@ class UnitOfWork { $oid = spl_object_hash($entity); if ( ! isset($this->_entityStates[$oid])) { - if (isset($this->_entityIdentifiers[$oid])) { + if (isset($this->_entityIdentifiers[$oid]) && ! isset($this->_newEntities[$oid])) { $this->_entityStates[$oid] = self::STATE_DETACHED; } else { $this->_entityStates[$oid] = self::STATE_NEW; @@ -824,7 +857,7 @@ class UnitOfWork * * @param string $idHash * @param string $rootClassName - * @return Doctrine\ORM\Entity + * @return object */ public function getByIdHash($idHash, $rootClassName) { @@ -1071,8 +1104,8 @@ class UnitOfWork } $relatedEntities = $class->getReflectionProperty($assocMapping->getSourceFieldName()) ->getValue($entity); - if ($relatedEntities instanceof \Doctrine\Common\Collections\Collection && - count($relatedEntities) > 0) { + if ($relatedEntities instanceof \Doctrine\Common\Collections\Collection || is_array($relatedEntities) + && count($relatedEntities) > 0) { foreach ($relatedEntities as $relatedEntity) { $this->_doDelete($relatedEntity, $visited); } @@ -1323,23 +1356,29 @@ class UnitOfWork if ( ! isset($this->_persisters[$entityName])) { $class = $this->_em->getClassMetadata($entityName); if ($class->isInheritanceTypeJoined()) { - $persister = new \Doctrine\ORM\Persisters\JoinedSubclassPersister($this->_em, $class); + $persister = new Persisters\JoinedSubclassPersister($this->_em, $class); } else { - $persister = new \Doctrine\ORM\Persisters\StandardEntityPersister($this->_em, $class); + $persister = new Persisters\StandardEntityPersister($this->_em, $class); } $this->_persisters[$entityName] = $persister; } return $this->_persisters[$entityName]; } + /** + * Gets a collection persister for a collection-valued association. + * + * @param AssociationMapping $association + * @return AbstractCollectionPersister + */ public function getCollectionPersister($association) { $type = get_class($association); if ( ! isset($this->_collectionPersisters[$type])) { - if ($association instanceof \Doctrine\ORM\Mapping\OneToManyMapping) { - $persister = new \Doctrine\ORM\Persisters\OneToManyPersister($this->_em); - } else if ($association instanceof \Doctrine\ORM\Mapping\ManyToManyMapping) { - $persister = new \Doctrine\ORM\Persisters\ManyToManyPersister($this->_em); + if ($association instanceof Mapping\OneToManyMapping) { + $persister = new Persisters\OneToManyPersister($this->_em); + } else if ($association instanceof Mapping\ManyToManyMapping) { + $persister = new Persisters\ManyToManyPersister($this->_em); } $this->_collectionPersisters[$type] = $persister; } diff --git a/tests/Doctrine/Tests/ORM/Functional/BasicCRUDTest.php b/tests/Doctrine/Tests/ORM/Functional/BasicCRUDTest.php index e273d68cc..7ad520829 100644 --- a/tests/Doctrine/Tests/ORM/Functional/BasicCRUDTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/BasicCRUDTest.php @@ -6,8 +6,7 @@ use Doctrine\ORM\Export\ClassExporter; use Doctrine\Tests\Models\CMS\CmsUser; use Doctrine\Tests\Models\CMS\CmsPhonenumber; use Doctrine\Tests\Models\CMS\CmsAddress; -use Doctrine\Tests\Models\Forum\ForumUser; -use Doctrine\Tests\Models\Forum\ForumAvatar; +use Doctrine\Tests\Models\CMS\CmsGroup; require_once __DIR__ . '/../../TestInit.php'; @@ -25,7 +24,8 @@ class BasicCRUDTest extends \Doctrine\Tests\OrmFunctionalTestCase { $exporter->exportClasses(array( $this->_em->getClassMetadata('Doctrine\Tests\Models\CMS\CmsUser'), $this->_em->getClassMetadata('Doctrine\Tests\Models\CMS\CmsPhonenumber'), - $this->_em->getClassMetadata('Doctrine\Tests\Models\CMS\CmsAddress') + $this->_em->getClassMetadata('Doctrine\Tests\Models\CMS\CmsAddress'), + $this->_em->getClassMetadata('Doctrine\Tests\Models\CMS\CmsGroup') )); // Create @@ -48,7 +48,7 @@ class BasicCRUDTest extends \Doctrine\Tests\OrmFunctionalTestCase { $em->flush(); $this->assertTrue($em->contains($ph)); $this->assertTrue($em->contains($user)); - $this->assertTrue($user->phonenumbers instanceof \Doctrine\ORM\PersistentCollection); + //$this->assertTrue($user->phonenumbers instanceof \Doctrine\ORM\PersistentCollection); // Update name $user->name = 'guilherme'; @@ -90,7 +90,7 @@ class BasicCRUDTest extends \Doctrine\Tests\OrmFunctionalTestCase { $this->_em->save($user); $this->_em->flush(); - $this->assertTrue($user->phonenumbers instanceof \Doctrine\ORM\PersistentCollection); + //$this->assertTrue($user->phonenumbers instanceof \Doctrine\ORM\PersistentCollection); // Remove the first element from the collection unset($user->phonenumbers[0]); @@ -125,5 +125,63 @@ class BasicCRUDTest extends \Doctrine\Tests\OrmFunctionalTestCase { array($address->id))->fetchColumn(); $this->assertTrue(is_numeric($userId)); } + + public function testBasicManyToMany() + { + $user = new CmsUser; + $user->name = 'Guilherme'; + $user->username = 'gblanco'; + $user->status = 'developer'; + + $group = new CmsGroup; + $group->name = 'Developers'; + + $user->groups[] = $group; + $group->users[] = $user; + + $this->_em->save($user); + $this->_em->save($group); + + $this->_em->flush(); + + unset($group->users[0]); // inverse side + unset($user->groups[0]); // owning side! + + $this->_em->flush(); + + // Check that the link in the association table has been deleted + $count = $this->_em->getConnection()->execute("SELECT COUNT(*) FROM cms_users_groups", + array())->fetchColumn(); + $this->assertEquals(0, $count); + } + + public function testManyToManyCollectionClearing() + { + $user = new CmsUser; + $user->name = 'Guilherme'; + $user->username = 'gblanco'; + $user->status = 'developer'; + + for ($i=0; $i<10; ++$i) { + $group = new CmsGroup; + $group->name = 'Developers_' . $i; + $user->groups[] = $group; + $group->users[] = $user; + } + + $this->_em->save($user); // Saves the user, cause of post-insert ID + + $this->_em->flush(); // Saves the groups, cause they're attached to a persistent entity ($user) + + //$user->groups->clear(); + unset($user->groups); + + $this->_em->flush(); + + // Check that the links in the association table have been deleted + $count = $this->_em->getConnection()->execute("SELECT COUNT(*) FROM cms_users_groups", + array())->fetchColumn(); + $this->assertEquals(0, $count); + } }