Commit 2eb4a16d authored by romanb's avatar romanb

[2.0] More progress on the UnitOfWork and collections. First basic functional many-many test.

parent deb095f2
<?php
#namespace Doctrine\Common;
#use \ArrayAccess;
/**
* Base class for classes that use the virtual property system.
*
* @author robo
*/
class Doctrine_Common_VirtualPropertyObject implements ArrayAccess
{
protected $_data = array();
protected $_entityName;
/**
* Initializes a new instance of a class derived from VirtualPropertyObject.
*/
public function __construct()
{
$this->_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 <type> $fieldName
* @param <type> $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 */
}
?>
<?php
/**
* The VirtualPropertySystem class is a class consisting solely of static methods and
* serves as a generic virtual property registry system.
* Classes register their (virtual) properties with the property system, optionally specifying
* property features/options. These can then be evaluated by other code.
*
* @author robo
* @since 2.0
*/
class Doctrine_Common_VirtualPropertySystem {
private static $_properties = array();
private static $_callback = 'construct';
private static $_checkTypes = false;
private static $_useAutoAccessorOverride = true;
private static $_simplePHPTypes = array(
'int' => 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 <type> $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 <type> $class
* @param <type> $propName
* @param <type> $accessor
*/
public static function setAccessor($class, $propName, $accessor)
{
self::$_properties[$class][$propName]['accessor'] = $accessor;
}
/**
* Gets the mutator method for a virtual property.
*
* @param <type> $class
* @param <type> $propName
* @return <type>
*/
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 <type> $class
* @param <type> $propName
* @param <type> $mutator
*/
public static function setMutator($class, $propName, $mutator)
{
self::$_properties[$class][$propName]['mutator'] = $mutator;
}
/**
* Gets the type of a virtual property.
*
* @param <type> $class
* @param <type> $propName
* @return <type>
*/
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 <type> $class
* @param <type> $propName
* @param <type> $type
*/
public static function setType($class, $propName, $type)
{
self::$_properties[$class][$propName]['type'] = $type;
}
}
?>
......@@ -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();
......
<?php
/*
* $Id$
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the LGPL. For more information, see
* <http://www.phpdoctrine.org>.
*/
/**
* Doctrine_Export_Frontbase
*
* @package Doctrine
* @subpackage Export
* @author Konsta Vesterinen <kvesteri@cc.hut.fi>
* @author Lukas Smith <smith@pooteeweet.org> (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
<?php
/*
* $Id: Reporter.php 3882 2008-02-22 18:11:35Z jwage $
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the LGPL. For more information, see
* <http://www.phpdoctrine.org>.
*/
/**
* Doctrine_Export_Reporter
*
* @package Doctrine
* @subpackage Export
* @author Konsta Vesterinen <kvesteri@cc.hut.fi>
* @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
......@@ -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
{
......
......@@ -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;
......
......@@ -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.
*
......
......@@ -55,54 +55,54 @@ abstract class AbstractCollectionPersister
}
//...
}
/**
* Deletes the persistent state represented by the given collection.
*
* @param PersistentCollection $coll
*/
public function delete(PersistentCollection $coll)
{
if ($coll->getRelation()->isInverseSide()) {
return;
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->updateRows($coll);
$this->insertRows($coll);
}
/* collection update actions */
public function deleteRows(PersistentCollection $coll)
{
if ($coll->getMapping()->isInverseSide()) {
return; // ignore inverse side
}
{
$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);
}
......@@ -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);
......
......@@ -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 <type> $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 <type> $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());
}
}
......@@ -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 <roman@code-factory.org>
*/
class OneToManyPersister extends AbstractCollectionPersister
{
......@@ -57,19 +60,7 @@ class OneToManyPersister extends AbstractCollectionPersister
$whereClause .= "$idColumn = ?";
}
return "UPDATE $table SET $setClause WHERE $whereClause";
}
/**
* {@inheritdoc}
*
* @param <type> $element
* @return <type>
* @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()
......
......@@ -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;
}
......
......@@ -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);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment