Commit e0488ff8 authored by romanb's avatar romanb

[2.0] First draft of EntityManager#merge(). First draft of DynamicProxyGenerator.

parent 1da8f672
......@@ -95,6 +95,7 @@ class ClassLoader
$prefix = substr($className, 0, strpos($className, $this->_namespaceSeparator));
$class = '';
if (isset($this->_basePaths[$prefix])) {
$class .= $this->_basePaths[$prefix] . DIRECTORY_SEPARATOR;
}
......
......@@ -398,7 +398,7 @@ abstract class AbstractQuery
if (count($result) > 1) {
throw QueryException::nonUniqueResult();
}
return $result->getFirst();
return $result->first();
}
return $result;
}
......
......@@ -45,10 +45,21 @@ class Configuration extends \Doctrine\DBAL\Configuration
'queryCacheImpl' => null,
'metadataCacheImpl' => null,
'metadataDriverImpl' => new AnnotationDriver(),
'dqlClassAliasMap' => array()
'dqlClassAliasMap' => array(),
'cacheDir' => null
));
}
public function setCacheDir($dir)
{
$this->_attributes['cacheDir'] = $dir;
}
public function getCacheDir()
{
return $this->_attributes['cacheDir'];
}
public function getDqlClassAliasMap()
{
return $this->_attributes['dqlClassAliasMap'];
......
<?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.doctrine-project.org>.
*/
namespace Doctrine\ORM;
/**
* The DynamicProxyGenerator is used to generate proxy objects for entities.
* For that purpose he generates proxy class files on the fly as needed.
*
* @author Roman Borschel <roman@code-factory.org>
*/
class DynamicProxyGenerator
{
private $_cacheDir = '/Users/robo/dev/php/tmp/gen/';
private $_em;
public function __construct(EntityManager $em, $cacheDir = null)
{
$this->_em = $em;
if ($cacheDir === null) {
$cacheDir = sys_get_tmp_dir();
}
$this->_cacheDir = $cacheDir;
}
/**
*
*
* @param <type> $className
* @param <type> $identifier
* @return <type>
*/
public function getProxy($className, $identifier)
{
$proxyClassName = str_replace('\\', '_', $className) . 'Proxy';
if ( ! class_exists($proxyClassName, false)) {
$fileName = $this->_cacheDir . $proxyClassName . '.g.php';
if ( ! file_exists($fileName)) {
$this->_generateProxyClass($className, $identifier, $proxyClassName, $fileName);
}
require $fileName;
}
$proxyClassName = '\Doctrine\Generated\Proxies\\' . $proxyClassName;
return new $proxyClassName($this->_em, $this->_em->getClassMetadata($className), $identifier);
}
/**
* Generates a proxy class.
*
* @param <type> $className
* @param <type> $id
* @param <type> $proxyClassName
* @param <type> $fileName
*/
private function _generateProxyClass($className, $id, $proxyClassName, $fileName)
{
$class = $this->_em->getClassMetadata($className);
$file = self::$_proxyClassTemplate;
if (is_array($id) && count($id) > 1) {
// it's a composite key. keys = field names, values = values.
$values = array_values($id);
$keys = array_keys($id);
} else {
$values = is_array($id) ? array_values($id) : array($id);
$keys = $class->getIdentifierFieldNames();
}
$paramIndex = 1;
$identifierCondition = 'prx.' . $keys[0] . ' = ?' . $paramIndex++;
for ($i=1, $c=count($keys); $i < $c; ++$i) {
$identifierCondition .= ' AND prx.' . $keys[$i] . ' = ?' . $paramIndex++;
}
$parameters = 'array(';
$first = true;
foreach ($values as $value) {
if ($first) {
$first = false;
} else {
$parameters = ', ';
}
$parameters .= "'" . $value . "'";
}
$parameters .= ')';
$hydrationSetters = '';
foreach ($class->getReflectionProperties() as $name => $prop) {
if ( ! $class->hasAssociation($name)) {
$hydrationSetters .= '$this->_class->setValue($this, \'' . $name . '\', $scalar[0][\'prx_' . $name . '\']);' . PHP_EOL;
}
}
$methods = '';
foreach ($class->getReflectionClass()->getMethods() as $method) {
if ($method->isPublic() && ! $method->isFinal()) {
$methods .= PHP_EOL . 'public function ' . $method->getName() . '(';
$firstParam = true;
$parameterString = '';
foreach ($method->getParameters() as $param) {
if ($firstParam) {
$firstParam = false;
} else {
$parameterString .= ', ';
}
$parameterString .= '$' . $param->getName();
}
$methods .= $parameterString . ') {' . PHP_EOL;
$methods .= '$this->_load();' . PHP_EOL;
$methods .= 'return parent::' . $method->getName() . '(' . $parameterString . ');';
$methods .= '}' . PHP_EOL;
}
}
$sleepImpl = '';
if ($class->getReflectionClass()->hasMethod('__sleep')) {
$sleepImpl .= 'return parent::__sleep();';
} else {
$sleepImpl .= 'return array(';
$first = true;
foreach ($class->getReflectionProperties() as $name => $prop) {
if ($first) {
$first = false;
} else {
$sleepImpl .= ', ';
}
$sleepImpl .= "'" . $name . "'";
}
$sleepImpl .= ');';
}
$placeholders = array(
'<proxyClassName>', '<className>', '<identifierCondition>',
'<parameters>', '<hydrationSetters>', '<methods>', '<sleepImpl>'
);
$replacements = array(
$proxyClassName, $className, $identifierCondition, $parameters,
$hydrationSetters, $methods, $sleepImpl
);
$file = str_replace($placeholders, $replacements, $file);
file_put_contents($fileName, $file);
}
/** Proxy class code template */
private static $_proxyClassTemplate =
'<?php
/** This class was generated by the Doctrine ORM. DO NOT EDIT THIS FILE. */
namespace Doctrine\Generated\Proxies {
class <proxyClassName> extends \<className> {
private $_em;
private $_class;
private $_loaded = false;
public function __construct($em, $class, $identifier) {
$this->_em = $em;
$this->_class = $class;
$this->_class->setIdentifierValues($this, $identifier);
}
private function _load() {
if ( ! $this->_loaded) {
$scalar = $this->_em->createQuery(\'select prx from <className> prx where <identifierCondition>\')->execute(<parameters>, \Doctrine\ORM\Query::HYDRATE_SCALAR);
<hydrationSetters>
unset($this->_em);
unset($this->_class);
$this->_loaded = true;
}
}
<methods>
public function __sleep() {
if (!$this->_loaded) {
throw new RuntimeException("Not fully loaded proxy can not be serialized.");
}
<sleepImpl>
}
}
}';
}
......@@ -42,22 +42,22 @@ class EntityManager
* IMMEDIATE: Flush occurs automatically after each operation that issues database
* queries. No operations are queued.
*/
const FLUSHMODE_IMMEDIATE = 'immediate';
const FLUSHMODE_IMMEDIATE = 1;
/**
* AUTO: Flush occurs automatically in the following situations:
* - Before any query executions (to prevent getting stale data)
* - On EntityManager#commit()
*/
const FLUSHMODE_AUTO = 'auto';
const FLUSHMODE_AUTO = 2;
/**
* COMMIT: Flush occurs automatically only on EntityManager#commit().
*/
const FLUSHMODE_COMMIT = 'commit';
const FLUSHMODE_COMMIT = 3;
/**
* MANUAL: Flush occurs never automatically. The only way to flush is
* through EntityManager#flush().
*/
const FLUSHMODE_MANUAL = 'manual';
const FLUSHMODE_MANUAL = 4;
/**
* The used Configuration.
......@@ -92,7 +92,7 @@ class EntityManager
*
* @var string
*/
private $_flushMode = 'commit';
private $_flushMode = self::FLUSHMODE_COMMIT;
/**
* The UnitOfWork used to coordinate object-level transactions.
......@@ -115,6 +115,13 @@ class EntityManager
*/
private $_hydrators = array();
/**
* The proxy generator.
*
* @var DynamicProxyGenerator
*/
private $_proxyGenerator;
/**
* Whether the EntityManager is closed or not.
*/
......@@ -211,17 +218,6 @@ class EntityManager
return $query;
}
/**
* Detaches an entity from the manager. It's lifecycle is no longer managed.
*
* @param object $entity
* @return boolean
*/
public function detach($entity)
{
return $this->_unitOfWork->removeFromIdentityMap($entity);
}
/**
* Creates a DQL query with the specified name.
*
......@@ -237,6 +233,7 @@ class EntityManager
* Creates a native SQL query.
*
* @param string $sql
* @param ResultSetMapping $rsm The ResultSetMapping to use.
* @return Query
*/
public function createNativeQuery($sql, \Doctrine\ORM\Query\ResultSetMapping $rsm)
......@@ -287,6 +284,23 @@ class EntityManager
{
return $this->getRepository($entityName)->find($identifier);
}
/**
* Gets a reference to the entity identified by the given type and identifier
* without actually loading it. Only the identifier of the returned entity
* will be populated.
*
* NOTE: There is currently no magic proxying in place, that means the full state
* of the entity will not be loaded upon accessing it.
*
* @return object The entity reference.
*/
public function getReference($entityName, $identifier)
{
$entity = new $entityName;
$this->getClassMetadata($entityName)->setEntityIdentifier($entity, $identifier);
return $entity;
}
/**
* Sets the flush mode to use.
......@@ -309,10 +323,7 @@ class EntityManager
*/
private function _isFlushMode($value)
{
return $value == self::FLUSHMODE_AUTO ||
$value == self::FLUSHMODE_COMMIT ||
$value == self::FLUSHMODE_IMMEDIATE ||
$value == self::FLUSHMODE_MANUAL;
return $value >= 1 && $value <= 4;
}
/**
......@@ -326,7 +337,10 @@ class EntityManager
}
/**
* Clears the persistence context, effectively detaching all managed entities.
* Clears the EntityManager. All entities that are currently managed
* by this EntityManager become detached.
*
* @param string $entityName
*/
public function clear($entityName = null)
{
......@@ -334,14 +348,18 @@ class EntityManager
$this->_unitOfWork->detachAll();
} else {
//TODO
throw DoctrineException::notImplemented();
}
}
/**
* Closes the EntityManager.
* Closes the EntityManager. All entities that are currently managed
* by this EntityManager become detached. The EntityManager may no longer
* be used after it is closed.
*/
public function close()
{
$this->clear();
$this->_closed = true;
}
......@@ -378,52 +396,51 @@ class EntityManager
* overriding any local changes that have not yet been persisted.
*
* @param object $entity
* @todo FIX Impl
* @todo Implemntation
*/
public function refresh($entity)
{
throw DoctrineException::notImplemented();
/*$this->_mergeData($entity, $this->getRepository(get_class($entity))->find(
$entity->identifier(), Query::HYDRATE_ARRAY),
true);*/
}
/**
* Detaches an entity from the EntityManager. Its lifecycle is no longer managed.
*
* @param object $entity The entity to detach.
* @return boolean
*/
public function detach($entity)
{
return $this->_unitOfWork->removeFromIdentityMap($entity);
}
/**
* Merges the state of a detached entity into the persistence context
* of this EntityManager.
*
* @param object $entity The entity to merge into the persistence context.
* @return object The managed copy of the entity.
*/
public function merge($entity)
{
return $this->_unitOfWork->merge($entity);
}
/**
* Creates a copy of the given entity. Can create a shallow or a deep copy.
*
* @param object $entity The entity to copy.
* @return object The new entity.
* @todo Implementation or remove.
*/
public function copy($entity, $deep = false)
{
//...
throw DoctrineException::notImplemented();
}
/*
public function toArray($entity, $deep = false)
{
$array = array();
foreach ($entity as $key => $value) {
if ($deep && is_object($value)) {
$array[$key] = $this->toArray($value, $deep);
} else if ( ! is_object($value)) {
$array[$key] = $value;
}
}
return $array;
}
public function fromArray($entity, array $array, $deep = false)
{
foreach ($array as $key => $value) {
if ($deep && is_array($value)) {
$entity->$key = $this->fromArray($entity, $value, $deep);
} else if ( ! is_array($value)) {
$entity->$key = $value;
}
}
}
*/
/**
* Gets the repository for an entity class.
*
......@@ -528,11 +545,11 @@ class EntityManager
$this->_hydrators[$hydrationMode] = new \Doctrine\ORM\Internal\Hydration\NoneHydrator($this);
break;
default:
\Doctrine\Common\DoctrineException::updateMe("No hydrator found for hydration mode '$hydrationMode'.");
throw DoctrineException::updateMe("No hydrator found for hydration mode '$hydrationMode'.");
}
} else if ($this->_hydrators[$hydrationMode] instanceof Closure) {
}/* else if ($this->_hydrators[$hydrationMode] instanceof Closure) {
$this->_hydrators[$hydrationMode] = $this->_hydrators[$hydrationMode]($this);
}
}*/
return $this->_hydrators[$hydrationMode];
}
......@@ -540,13 +557,13 @@ class EntityManager
* Sets a hydrator for a hydration mode.
*
* @param mixed $hydrationMode
* @param object $hydrator Either a hydrator instance or a closure that creates a
* @param object $hydrator Either a hydrator instance or a Closure that creates a
* hydrator instance.
*/
public function setHydrator($hydrationMode, $hydrator)
/*public function setHydrator($hydrationMode, $hydrator)
{
$this->_hydrators[$hydrationMode] = $hydrator;
}
}*/
/**
* Factory method to create EntityManager instances.
......@@ -563,7 +580,7 @@ class EntityManager
if (is_array($conn)) {
$conn = \Doctrine\DBAL\DriverManager::getConnection($conn, $config, $eventManager);
} else if ( ! $conn instanceof Connection) {
\Doctrine\Common\DoctrineException::updateMe("Invalid parameter '$conn'.");
throw DoctrineException::updateMe("Invalid parameter '$conn'.");
}
if ($config === null) {
......
......@@ -16,7 +16,7 @@
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the LGPL. For more information, see
* <http://www.phpdoctrine.org>.
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM;
......@@ -80,10 +80,6 @@ class EntityRepository
*/
public function find($id, $hydrationMode = null)
{
if ($id === null) {
return false;
}
if (is_array($id) && count($id) > 1) {
// it's a composite key. keys = field names, values = values.
$values = array_values($id);
......@@ -98,9 +94,21 @@ class EntityRepository
return $entity; // Hit!
}
return $this->_createQuery()
->where(implode(' = ? AND ', $keys) . ' = ?')
->fetchOne($values, $hydrationMode);
$dql = 'select e from ' . $this->_classMetadata->getClassName() . ' e where ';
$conditionDql = '';
$paramIndex = 1;
foreach ($keys as $key) {
if ($conditionDql != '') $conditionDql .= ' and ';
$conditionDql .= 'e.' . $key . ' = ?' . $paramIndex++;
}
$dql .= $conditionDql;
$q = $this->_em->createQuery($dql);
foreach ($values as $index => $value) {
$q->setParameter($index, $value);
}
return $q->getSingleResult($hydrationMode);
}
/**
......@@ -172,7 +180,7 @@ class EntityRepository
*/
public function findByDql($dql, array $params = array(), $hydrationMode = null)
{
$query = new Doctrine_Query($this->_em);
$query = new Query($this->_em);
$component = $this->getComponentName();
$dql = 'FROM ' . $component . ' WHERE ' . $dql;
......
......@@ -103,6 +103,7 @@ class ObjectHydrator extends AbstractHydrator
$this->_classMetadatas = array();
$e = microtime(true);
echo 'Hydration took: ' . ($e - $s) . PHP_EOL;
return $result;
......
......@@ -50,6 +50,7 @@ abstract class AssociationMapping
protected $_isCascadeDelete;
protected $_isCascadeSave;
protected $_isCascadeRefresh;
protected $_isCascadeMerge;
/**
* The fetch mode used for the association.
......@@ -207,6 +208,20 @@ abstract class AssociationMapping
}
return $this->_isCascadeRefresh;
}
/**
* Whether the association cascades merge() operations from the source entity
* to the target entity/entities.
*
* @return boolean
*/
public function isCascadeMerge()
{
if ($this->_isCascadeMerge === null) {
$this->_isCascadeMerge = in_array('merge', $this->_cascades);
}
return $this->_isCascadeMerge;
}
/**
* Whether the target entity/entities of the association are eagerly fetched.
......@@ -262,6 +277,7 @@ abstract class AssociationMapping
* Whether the association is optional (0..X), or not (1..X).
*
* @return boolean TRUE if the association is optional, FALSE otherwise.
* @todo Only applicable to OneToOne. Move there.
*/
public function isOptional()
{
......@@ -319,26 +335,51 @@ abstract class AssociationMapping
{
return $this->_mappedByFieldName;
}
/**
* Whether the association is a one-to-one association.
*
* @return boolean
*/
public function isOneToOne()
{
return false;
}
/**
* Whether the association is a one-to-many association.
*
* @return boolean
*/
public function isOneToMany()
{
return false;
}
/**
* Whether the association is a many-to-many association.
*
* @return boolean
*/
public function isManyToMany()
{
return false;
}
/**
* Whether the association uses a join table for the mapping.
*
* @return boolean
*/
public function usesJoinTable()
{
return (bool)$this->_joinTable;
}
/**
*
* @param <type> $entity
* @param <type> $entityManager
*/
abstract public function lazyLoadFor($entity, $entityManager);
}
\ No newline at end of file
......@@ -885,6 +885,45 @@ final class ClassMetadata
$this->_reflectionProperties[$field]->setValue($entity, $value);
}
}
/**
* Extracts the identifier values of an entity of this class.
*
* @param object $entity
* @return mixed
*/
public function getIdentifierValues($entity)
{
if ($this->_isIdentifierComposite) {
$id = array();
foreach ($this->_identifier as $idField) {
$value = $this->_reflectionProperties[$idField]->getValue($entity);
if ($value !== null) {
$id[] = $value;
}
}
return $id;
} else {
return $this->_reflectionProperties[$this->_identifier[0]]->getValue($entity);
}
}
/**
* Populates the entity identifier of an entity.
*
* @param object $entity
* @param mixed $id
*/
public function setIdentifierValues($entity, $id)
{
if ($this->_isIdentifierComposite) {
foreach ((array)$id as $idField => $idValue) {
$this->_reflectionProperties[$idField]->setValue($entity, $idValue);
}
} else {
$this->_reflectionProperties[$this->_identifier[0]]->setValue($entity, $id);
}
}
/**
* Gets all field mappings.
......
......@@ -58,20 +58,26 @@ final class DoctrineOneToOne extends \Addendum\Annotation {
public $targetEntity;
public $mappedBy;
public $cascade;
public $fetch;
public $optional;
}
final class DoctrineOneToMany extends \Addendum\Annotation {
public $mappedBy;
public $targetEntity;
public $cascade;
public $fetch;
}
final class DoctrineManyToOne extends \Addendum\Annotation {
public $targetEntity;
public $cascade;
public $fetch;
public $optional;
}
final class DoctrineManyToMany extends \Addendum\Annotation {
public $targetEntity;
public $mappedBy;
public $cascade;
public $fetch;
}
final class DoctrineElementCollection extends \Addendum\Annotation {
public $tableName;
......
......@@ -146,7 +146,7 @@ abstract class AbstractCollectionPersister
abstract protected function _getUpdateRowSql(PersistentCollection $coll);
/**
* Gets the SQL statement used for inserting a row from to the collection.
* Gets the SQL statement used for inserting a row in the collection.
*
* @param PersistentCollection $coll
*/
......
......@@ -919,7 +919,7 @@ class Query extends AbstractQuery
$this->_dqlParts[$queryPartName] = array($queryPart);
}
$this->_state = Doctrine_ORM_Query::STATE_DIRTY;
$this->_state = self::STATE_DIRTY;
return $this;
}
......
......@@ -398,12 +398,14 @@ class UnitOfWork implements PropertyChangedListener
) {
//TODO: If $actualData[$name] is Collection then unwrap the array
$assoc = $class->getAssociationMapping($name);
echo PHP_EOL . "INJECTING PCOLL into $name" . PHP_EOL;
//echo PHP_EOL . "INJECTING PCOLL into $name" . PHP_EOL;
// Inject PersistentCollection
$coll = new PersistentCollection($this->_em, $assoc->getTargetEntityName(),
$actualData[$name] ? $actualData[$name] : array());
$coll->setOwner($entity, $assoc);
if ( ! $coll->isEmpty()) $coll->setDirty(true);
if ( ! $coll->isEmpty()) {
$coll->setDirty(true);
}
$class->getReflectionProperty($name)->setValue($entity, $coll);
$actualData[$name] = $coll;
}
......@@ -428,7 +430,7 @@ class UnitOfWork implements PropertyChangedListener
$orgValue = isset($originalData[$propName]) ? $originalData[$propName] : null;
if (is_object($orgValue) && $orgValue !== $actualValue) {
$changeSet[$propName] = array($orgValue, $actualValue);
} else if ($orgValue != $actualValue || ($orgValue === null xor $actualValue === null)) {
} else if ($orgValue != $actualValue || ($orgValue === null ^ $actualValue === null)) {
$changeSet[$propName] = array($orgValue, $actualValue);
}
......@@ -764,8 +766,7 @@ class UnitOfWork implements PropertyChangedListener
* Detaches an entity from the persistence management. It's persistence will
* no longer be managed by Doctrine.
*
* @param integer $oid object identifier
* @return boolean whether ot not the operation was successful
* @param object $entity The entity to detach.
*/
public function detach($entity)
{
......@@ -802,7 +803,6 @@ class UnitOfWork implements PropertyChangedListener
*/
public function detachAll($entityName = null)
{
//TODO: what do do with new/dirty/removed lists?
$numDetached = 0;
if ($entityName !== null && isset($this->_identityMap[$entityName])) {
$numDetached = count($this->_identityMap[$entityName]);
......@@ -852,13 +852,20 @@ class UnitOfWork implements PropertyChangedListener
/**
* Gets the state of an entity within the current unit of work.
*
* @param Doctrine\ORM\Entity $entity
* @param object $entity
* @return int
*/
public function getEntityState($entity)
{
$oid = spl_object_hash($entity);
if ( ! isset($this->_entityStates[$oid])) {
/*if (isset($this->_entityInsertions[$oid])) {
$this->_entityStates[$oid] = self::STATE_NEW;
} else if ( ! isset($this->_entityIdentifiers[$oid])) {
// Either NEW (if no ID) or DETACHED (if ID)
} else {
$this->_entityStates[$oid] = self::STATE_DETACHED;
}*/
if (isset($this->_entityIdentifiers[$oid]) && ! isset($this->_entityInsertions[$oid])) {
$this->_entityStates[$oid] = self::STATE_DETACHED;
} else {
......@@ -872,7 +879,7 @@ class UnitOfWork implements PropertyChangedListener
* Removes an entity from the identity map. This effectively detaches the
* entity from the persistence management of Doctrine.
*
* @param Doctrine\ORM\Entity $entity
* @param object $entity
* @return boolean
*/
public function removeFromIdentityMap($entity)
......@@ -1000,18 +1007,18 @@ class UnitOfWork implements PropertyChangedListener
return; // Prevent infinite recursion
}
$visited[$oid] = $entity; // mark visited
$visited[$oid] = $entity; // Mark visited
$class = $this->_em->getClassMetadata(get_class($entity));
switch ($this->getEntityState($entity)) {
case self::STATE_MANAGED:
// nothing to do, except if policy is "deferred explicit"
// Nothing to do, except if policy is "deferred explicit"
if ($class->isChangeTrackingDeferredExplicit()) {
$this->scheduleForDirtyCheck($entity);
}
break;
case self::STATE_NEW:
//TODO: Better defer insert for post-insert ID generators also.
//TODO: Better defer insert for post-insert ID generators also?
$idGen = $class->getIdGenerator();
if ($idGen->isPostInsertGenerator()) {
$insertNow[$oid] = $entity;
......@@ -1020,22 +1027,19 @@ class UnitOfWork implements PropertyChangedListener
$this->_entityStates[$oid] = self::STATE_MANAGED;
if ( ! $idGen instanceof \Doctrine\ORM\Id\Assigned) {
$this->_entityIdentifiers[$oid] = array($idValue);
$class->getSingleIdReflectionProperty()->setValue($entity, $idValue);
$class->setIdentifierValues($entity, $idValue);
} else {
$this->_entityIdentifiers[$oid] = $idValue;
}
}
//TODO: Calculate changeSet now instead of later to allow some optimizations
// in calculateChangeSets() (ie no need to consider NEW objects) ?
$this->registerNew($entity);
break;
case self::STATE_DETACHED:
throw DoctrineException::updateMe("Behavior of save() for a detached entity "
. "is not yet defined.");
case self::STATE_DELETED:
// entity becomes managed again
// Entity becomes managed again
if ($this->isRegisteredRemoved($entity)) {
//TODO: better a method for this?
unset($this->_entityDeletions[$oid]);
} else {
//FIXME: There's more to think of here...
......@@ -1093,6 +1097,93 @@ class UnitOfWork implements PropertyChangedListener
$this->_cascadeDelete($entity, $visited);
}
/**
* Merges the state of the given detached entity into this UnitOfWork.
*
* @param object $entity
* @return object The managed copy of the entity.
*/
public function merge($entity)
{
$visited = array();
return $this->_doMerge($entity, $visited);
}
/**
* Executes a merge operation on an entity.
*
* @param object $entity
* @param array $visited
* @return object The managed copy of the entity.
*/
private function _doMerge($entity, array &$visited, $prevManagedCopy = null, $assoc = null)
{
$class = $this->_em->getClassMetadata(get_class($entity));
$id = $class->getIdentifierValues($entity);
if ( ! $id) {
throw new InvalidArgumentException('New entity passed to merge().');
}
$managedCopy = $this->tryGetById($id, $class->getRootClassName());
if ($managedCopy) {
if ($this->getEntityState($managedCopy) == self::STATE_DELETED) {
throw new InvalidArgumentException('Can not merge with a deleted entity.');
}
} else {
$managedCopy = $this->_em->find($class->getClassName(), $id);
}
// Merge state of $entity into existing (managed) entity
foreach ($class->getReflectionProperties() as $name => $prop) {
if ( ! $class->hasAssociation($name)) {
$prop->setValue($managedCopy, $prop->getValue($entity));
}
if ($class->isChangeTrackingNotify()) {
//TODO
}
}
if ($class->isChangeTrackingDeferredExplicit()) {
//TODO
}
if ($prevManagedCopy !== null) {
$assocField = $assoc->getSourceFieldName();
$prevClass = $this->_em->getClassMetadata(get_class($prevManagedCopy));
if ($assoc->isOneToOne()) {
$prevClass->getReflectionProperty($assocField)->setValue($prevManagedCopy, $managedCopy);
} else {
$prevClass->getReflectionProperty($assocField)->getValue($prevManagedCopy)->add($managedCopy);
}
}
$this->_cascadeMerge($entity, $managedCopy, $visited);
return $managedCopy;
}
/**
* Cascades a merge operation to associated entities.
*/
private function _cascadeMerge($entity, $managedCopy, array &$visited)
{
$class = $this->_em->getClassMetadata(get_class($entity));
foreach ($class->getAssociationMappings() as $assocMapping) {
if ( ! $assocMapping->isCascadeMerge()) {
continue;
}
$relatedEntities = $class->getReflectionProperty($assocMapping->getSourceFieldName())
->getValue($entity);
if (($relatedEntities instanceof Collection) && count($relatedEntities) > 0) {
foreach ($relatedEntities as $relatedEntity) {
$this->_doMerge($relatedEntity, $visited, $managedCopy, $assocMapping);
}
} else if (is_object($relatedEntities)) {
$this->_doMerge($relatedEntities, $visited, $managedCopy, $assocMapping);
}
}
}
/**
* Cascades the save operation to associated entities.
*
......@@ -1155,11 +1246,22 @@ class UnitOfWork implements PropertyChangedListener
}
/**
* Closes the UnitOfWork.
* Clears the UnitOfWork.
*/
public function close()
public function clear()
{
//...
$this->_identityMap = array();
$this->_entityIdentifiers = array();
$this->_originalEntityData = array();
$this->_entityChangeSets = array();
$this->_entityStates = array();
$this->_scheduledForDirtyCheck = array();
$this->_entityInsertions = array();
$this->_entityUpdates = array();
$this->_entityDeletions = array();
$this->_collectionDeletions = array();
$this->_collectionCreations = array();
$this->_collectionUpdates = array();
$this->_commitOrderCalculator->clear();
}
......@@ -1221,8 +1323,8 @@ class UnitOfWork implements PropertyChangedListener
$entity = $this->tryGetByIdHash($idHash, $class->getRootClassName());
if ($entity) {
$oid = spl_object_hash($entity);
$this->_mergeData($entity, $data, $class/*, $query->getHint('doctrine.refresh')*/);
return $entity;
$overrideLocalChanges = false;
//$overrideLocalChanges = $query->getHint('doctrine.refresh');
} else {
$entity = new $className;
$oid = spl_object_hash($entity);
......@@ -1233,33 +1335,18 @@ class UnitOfWork implements PropertyChangedListener
$prop->setValue($entity, new \Doctrine\ORM\VirtualProxy($entity, $lazyAssoc, $prop));
}
}*/
$this->_mergeData($entity, $data, $class, true);
$this->_entityIdentifiers[$oid] = $id;
$this->_entityStates[$oid] = self::STATE_MANAGED;
$this->_originalEntityData[$oid] = $data;
$this->addToIdentityMap($entity);
$overrideLocalChanges = true;
}
$this->_originalEntityData[$oid] = $data;
return $entity;
}
/**
* Merges the given data into the given entity, optionally overriding
* local changes.
*
* @param object $entity
* @param array $data
* @param boolean $overrideLocalChanges
* @todo Consider moving to ClassMetadata for a little performance improvement.
*/
private function _mergeData($entity, array $data, $class, $overrideLocalChanges = false) {
if ($overrideLocalChanges) {
foreach ($data as $field => $value) {
$class->setValue($entity, $field, $value);
}
} else {
$oid = spl_object_hash($entity);
foreach ($data as $field => $value) {
if ($class->hasField($field)) {
$currentValue = $class->getReflectionProperty($field)->getValue($entity);
......@@ -1270,6 +1357,8 @@ class UnitOfWork implements PropertyChangedListener
}
}
}
return $entity;
}
/**
......@@ -1300,9 +1389,9 @@ class UnitOfWork implements PropertyChangedListener
/**
* INTERNAL:
* For hydration purposes only.
* For internal purposes only.
*
* Sets a property of the original data array of an entity.
* Sets a property value of the original data array of an entity.
*
* @param string $oid
* @param string $property
......@@ -1328,7 +1417,13 @@ class UnitOfWork implements PropertyChangedListener
}
/**
* Tries to find an entity with the given identifier in the identity map of
* this UnitOfWork.
*
* @param mixed $id The entity identifier to look for.
* @param string $rootClassName The name of the root class of the mapped entity hierarchy.
* @return mixed Returns the entity with the specified identifier if it exists in
* this UnitOfWork, FALSE otherwise.
*/
public function tryGetById($id, $rootClassName)
{
......@@ -1429,23 +1524,25 @@ class UnitOfWork implements PropertyChangedListener
*/
public function propertyChanged($entity, $propertyName, $oldValue, $newValue)
{
$oid = spl_object_hash($entity);
$class = $this->_em->getClassMetadata(get_class($entity));
$this->_entityChangeSets[$oid][$propertyName] = array($oldValue, $newValue);
if ($this->getEntityState($entity) == self::STATE_MANAGED) {
$oid = spl_object_hash($entity);
$class = $this->_em->getClassMetadata(get_class($entity));
if ($class->hasAssociation($propertyName)) {
$assoc = $class->getAssociationMapping($name);
if ($assoc->isOneToOne() && $assoc->isOwningSide()) {
$this->_entityUpdates[$oid] = $entity;
} else if ($oldValue instanceof PersistentCollection) {
// A PersistentCollection was de-referenced, so delete it.
if ( ! in_array($orgValue, $this->_collectionDeletions, true)) {
$this->_collectionDeletions[] = $orgValue;
$this->_entityChangeSets[$oid][$propertyName] = array($oldValue, $newValue);
if ($class->hasAssociation($propertyName)) {
$assoc = $class->getAssociationMapping($name);
if ($assoc->isOneToOne() && $assoc->isOwningSide()) {
$this->_entityUpdates[$oid] = $entity;
} else if ($oldValue instanceof PersistentCollection) {
// A PersistentCollection was de-referenced, so delete it.
if ( ! in_array($orgValue, $this->_collectionDeletions, true)) {
$this->_collectionDeletions[] = $orgValue;
}
}
} else {
$this->_entityUpdates[$oid] = $entity;
}
} else {
$this->_entityUpdates[$oid] = $entity;
}
}
}
\ No newline at end of file
......@@ -26,7 +26,7 @@ use Doctrine\ORM\Mapping\AssociationMapping;
/**
* Represents a virtual proxy that is used for lazy to-one associations.
*
* @author robo
* @author Roman Borschel <roman@code-factory.org>
* @since 2.0
*/
class VirtualProxy
......
......@@ -46,6 +46,22 @@ class CmsUser
*/
public $groups;
public function getId() {
return $this->id;
}
public function getStatus() {
return $this->status;
}
public function getUsername() {
return $this->username;
}
public function getName() {
return $this->name;
}
/**
* Adds a phonenumber to the user.
*
......@@ -58,6 +74,10 @@ class CmsUser
}
}
public function getPhonenumbers() {
return $this->phonenumbers;
}
public function addArticle(CmsArticle $article) {
$this->articles[] = $article;
if ($article->user !== $this) {
......
......@@ -22,6 +22,7 @@ class AllTests
$suite->addTestSuite('Doctrine\Tests\ORM\Functional\BasicFunctionalTest');
$suite->addTestSuite('Doctrine\Tests\ORM\Functional\NativeQueryTest');
$suite->addTestSuite('Doctrine\Tests\ORM\Functional\SingleTableInheritanceTest');
$suite->addTestSuite('Doctrine\Tests\ORM\Functional\DetachedEntityTest');
return $suite;
}
......
......@@ -306,5 +306,14 @@ class BasicFunctionalTest extends \Doctrine\Tests\OrmFunctionalTestCase
$query = $this->_em->createQuery("select u, g from Doctrine\Tests\Models\CMS\CmsUser u inner join u.groups g");
$this->assertEquals(0, count($query->getResultList()));
/* RB: TEST
\Doctrine\ORM\DynamicProxyGenerator::configure($this->_em);
$proxy = \Doctrine\ORM\DynamicProxyGenerator::getReferenceProxy('Doctrine\Tests\Models\CMS\CmsUser', 1);
echo $proxy->getId();
var_dump(serialize($proxy));
*/
}
}
\ No newline at end of file
<?php
namespace Doctrine\Tests\ORM\Functional;
use Doctrine\Tests\Models\CMS\CmsUser;
use Doctrine\ORM\UnitOfWork;
require_once __DIR__ . '/../../TestInit.php';
/**
* Description of DetachedEntityTest
*
* @author robo
*/
class DetachedEntityTest extends \Doctrine\Tests\OrmFunctionalTestCase
{
protected function setUp() {
$this->useModelSet('cms');
parent::setUp();
}
public function testSimpleDetachMerge() {
$user = new CmsUser;
$user->name = 'Roman';
$user->username = 'romanb';
$user->status = 'dev';
$this->_em->save($user);
$this->_em->flush();
$this->_em->clear();
// $user is now detached
$this->assertFalse($this->_em->contains($user));
$user->name = 'Roman B.';
//$this->assertEquals(UnitOfWork::STATE_DETACHED, $this->_em->getUnitOfWork()->getEntityState($user));
$user2 = $this->_em->merge($user);
$this->assertFalse($user === $user2);
$this->assertTrue($this->_em->contains($user2));
$this->assertEquals('Roman B.', $user2->name);
}
}
......@@ -693,16 +693,18 @@ class ObjectHydratorTest extends HydrationTest
{
$rsm = new ResultSetMapping;
$rsm->addEntityResult($this->_em->getClassMetadata('Doctrine\Tests\Models\CMS\CmsUser'), 'u');
$rsm->addJoinedEntityResult(
/*$rsm->addJoinedEntityResult(
$this->_em->getClassMetadata('Doctrine\Tests\Models\CMS\CmsPhonenumber'),
'p',
'u',
$this->_em->getClassMetadata('Doctrine\Tests\Models\CMS\CmsUser')->getAssociationMapping('phonenumbers')
);
);*/
$rsm->addFieldResult('u', 'u__id', 'id');
$rsm->addFieldResult('u', 'u__status', 'status');
$rsm->addScalarResult('sclr0', 'nameUpper');
$rsm->addFieldResult('p', 'p__phonenumber', 'phonenumber');
$rsm->addFieldResult('u', 'u__username', 'username');
$rsm->addFieldResult('u', 'u__name', 'name');
//$rsm->addScalarResult('sclr0', 'nameUpper');
//$rsm->addFieldResult('p', 'p__phonenumber', 'phonenumber');
// Faked result set
$resultSet = array(
......@@ -710,29 +712,37 @@ class ObjectHydratorTest extends HydrationTest
array(
'u__id' => '1',
'u__status' => 'developer',
'sclr0' => 'ROMANB',
'p__phonenumber' => '42',
'u__username' => 'romanb',
'u__name' => 'Roman',
//'sclr0' => 'ROMANB',
//'p__phonenumber' => '42',
),
array(
'u__id' => '1',
'u__status' => 'developer',
'sclr0' => 'ROMANB',
'p__phonenumber' => '43',
'u__username' => 'romanb',
'u__name' => 'Roman',
//'sclr0' => 'ROMANB',
//'p__phonenumber' => '43',
),
array(
'u__id' => '2',
'u__status' => 'developer',
'sclr0' => 'JWAGE',
'p__phonenumber' => '91'
'u__username' => 'romanb',
'u__name' => 'Roman',
//'sclr0' => 'JWAGE',
//'p__phonenumber' => '91'
)
);
for ($i = 4; $i < 300; $i++) {
for ($i = 4; $i < 1000; ++$i) {
$resultSet[] = array(
'u__id' => $i,
'u__status' => 'developer',
'sclr0' => 'JWAGE' . $i,
'p__phonenumber' => '91'
'u__username' => 'jwage',
'u__name' => 'Jonathan',
//'sclr0' => 'JWAGE' . $i,
//'p__phonenumber' => '91'
);
}
......
......@@ -20,13 +20,3 @@ set_include_path(
. PATH_SEPARATOR . $modelDir . DIRECTORY_SEPARATOR . 'forum'
);
// Some of these classes depend on Doctrine_* classes
/*require_once 'DoctrineTestCase.php';
require_once 'TestUtil.php';
require_once 'DbalTestCase.php';
require_once 'OrmTestCase.php';
require_once 'OrmFunctionalTestCase.php';
require_once 'DoctrineTestSuite.php';
require_once 'OrmTestSuite.php';
require_once 'OrmFunctionalTestSuite.php';
require_once 'DbalTestSuite.php';*/
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