diff --git a/UPGRADE_TO_2_1 b/UPGRADE_TO_2_1 index 8b289ecd4..f42244493 100644 --- a/UPGRADE_TO_2_1 +++ b/UPGRADE_TO_2_1 @@ -9,6 +9,7 @@ The EntityRepository now has an interface Doctrine\Common\Persistence\ObjectRepo The annotation reader was heavily refactored between 2.0 and 2.1-RC1. In theory the operation of the new reader should be backwards compatible, but it has to be setup differently to work that way: + // new call to the AnnotationRegistry \Doctrine\Common\Annotations\AnnotationRegistry::registerFile('/doctrine-src/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php'); $reader = new \Doctrine\Common\Annotations\AnnotationReader(); diff --git a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php index 23cdb4b91..7d87f1d89 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php @@ -499,7 +499,7 @@ class AnnotationDriver implements Driver new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS), \RecursiveIteratorIterator::LEAVES_ONLY ), - '/^.+\\' . $this->_fileExtension . '$/i', + '/^.+' . str_replace('.', '\.', $this->_fileExtension) . '$/i', \RecursiveRegexIterator::GET_MATCH ); diff --git a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php index 2de0611cd..19da2e200 100644 --- a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php @@ -633,7 +633,11 @@ class BasicEntityPersister // TRICKY: since the association is specular source and target are flipped foreach ($owningAssoc['targetToSourceKeyColumns'] as $sourceKeyColumn => $targetKeyColumn) { if (isset($sourceClass->fieldNames[$sourceKeyColumn])) { - $identifier[$targetKeyColumn] = $sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity); + // unset the old value and set the new sql aliased value here. By definition + // unset($identifier[$targetKeyColumn] works here with how UnitOfWork::createEntity() calls this method. + $identifier[$this->_getSQLTableAlias($targetClass->name) . "." . $targetKeyColumn] = + $sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity); + unset($identifier[$targetKeyColumn]); } else { throw MappingException::joinColumnMustPointToMappedField( $sourceClass->name, $sourceKeyColumn @@ -1214,7 +1218,7 @@ class BasicEntityPersister } else if ($assoc !== null && strpos($field, " ") === false && strpos($field, "(") === false) { // very careless developers could potentially open up this normally hidden api for userland attacks, // therefore checking for spaces and function calls which are not allowed. - + // found a join column condition, not really a "field" $conditionSql .= $field; } else { diff --git a/lib/Doctrine/ORM/Proxy/ProxyFactory.php b/lib/Doctrine/ORM/Proxy/ProxyFactory.php index 52a4791a9..90a3a8d5b 100644 --- a/lib/Doctrine/ORM/Proxy/ProxyFactory.php +++ b/lib/Doctrine/ORM/Proxy/ProxyFactory.php @@ -274,6 +274,14 @@ class extends \ implements \Doctrine\ORM\Proxy\Proxy { if (!$this->__isInitialized__ && $this->_entityPersister) { $this->__isInitialized__ = true; + + if (method_exists($this, "__wakeup")) { + // call this after __isInitialized__to avoid infinite recursion + // but before loading to emulate what ClassMetadata::newInstance() + // provides. + $this->__wakeup(); + } + if ($this->_entityPersister->load($this->_identifier, $this) === null) { throw new \Doctrine\ORM\EntityNotFoundException(); } diff --git a/lib/Doctrine/ORM/Tools/DebugUnitOfWorkListener.php b/lib/Doctrine/ORM/Tools/DebugUnitOfWorkListener.php new file mode 100644 index 000000000..5475d998f --- /dev/null +++ b/lib/Doctrine/ORM/Tools/DebugUnitOfWorkListener.php @@ -0,0 +1,151 @@ +. + */ + +namespace Doctrine\ORM\Tools; + +use Doctrine\ORM\Event\OnFlushEventArgs; +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\PersistentCollection; +use Doctrine\ORM\UnitOfWork; + +/** + * Use this logger to dump the identity map during the onFlush event. This is useful for debugging + * weird UnitOfWork behavior with complex operations. + */ +class DebugUnitOfWorkListener +{ + private $file; + private $context; + + /** + * Pass a stream and contet information for the debugging session. + * + * The stream can be php://output to print to the screen. + * + * @param string $file + * @param string $context + */ + public function __construct($file = 'php://output', $context = '') + { + $this->file = $file; + $this->context = $context; + } + + public function onFlush(OnFlushEventArgs $args) + { + $this->dumpIdentityMap($args->getEntityManager()); + } + + /** + * Dump the contents of the identity map into a stream. + * + * @param EntityManager $em + * @return void + */ + public function dumpIdentityMap(EntityManager $em) + { + $uow = $em->getUnitOfWork(); + $identityMap = $uow->getIdentityMap(); + + $fh = fopen($this->file, "x+"); + if (count($identityMap) == 0) { + fwrite($fh, "Flush Operation [".$this->context."] - Empty identity map.\n"); + return; + } + + fwrite($fh, "Flush Operation [".$this->context."] - Dumping identity map:\n"); + foreach ($identityMap AS $className => $map) { + fwrite($fh, "Class: ". $className . "\n"); + foreach ($map AS $idHash => $entity) { + fwrite($fh, " Entity: " . $this->getIdString($entity, $uow) . " " . spl_object_hash($entity)."\n"); + fwrite($fh, " Associations:\n"); + + $cm = $em->getClassMetadata($className); + foreach ($cm->associationMappings AS $field => $assoc) { + fwrite($fh, " " . $field . " "); + $value = $cm->reflFields[$field]->getValue($entity); + + if ($assoc['type'] & ClassMetadata::TO_ONE) { + if ($value === null) { + fwrite($fh, " NULL\n"); + } else { + if ($value instanceof Proxy && !$value->__isInitialized__) { + fwrite($fh, "[PROXY] "); + } + + fwrite($fh, $this->getIdString($value, $uow) . " " . spl_object_hash($value) . "\n"); + } + } else { + $initialized = !($value instanceof PersistentCollection) || $value->isInitialized(); + if ($value === null) { + fwrite($fh, " NULL\n"); + } else if ($initialized) { + fwrite($fh, "[INITIALIZED] " . $this->getType($value). " " . count($value) . " elements\n"); + foreach ($value AS $obj) { + fwrite($fh, " " . $this->getIdString($obj, $uow) . " " . spl_object_hash($obj)."\n"); + } + } else { + fwrite($fh, "[PROXY] " . $this->getType($value) . " unknown element size\n"); + foreach ($value->unwrap() AS $obj) { + fwrite($fh, " " . $this->getIdString($obj, $uow) . " " . spl_object_hash($obj)."\n"); + } + } + } + } + } + } + fclose($fh); + } + + private function getType($var) + { + if (is_object($var)) { + $refl = new \ReflectionObject($var); + return $refl->getShortname(); + } else { + return gettype($var); + } + } + + private function getIdString($entity, $uow) + { + if ($uow->isInIdentityMap($entity)) { + $ids = $uow->getEntityIdentifier($entity); + $idstring = ""; + foreach ($ids AS $k => $v) { + $idstring .= $k."=".$v; + } + } else { + $idstring = "NEWOBJECT "; + } + + $state = $uow->getEntityState($entity); + if ($state == UnitOfWork::STATE_NEW) { + $idstring .= " [NEW]"; + } else if ($state == UnitOfWork::STATE_REMOVED) { + $idstring .= " [REMOVED]"; + } else if ($state == UnitOfWork::STATE_MANAGED) { + $idstring .= " [MANAGED]"; + } else if ($state == UnitOfwork::STATE_DETACHED) { + $idstring .= " [DETACHED]"; + } + + return $idstring; + } +} \ No newline at end of file diff --git a/lib/Doctrine/ORM/Tools/EntityGenerator.php b/lib/Doctrine/ORM/Tools/EntityGenerator.php index 50b0f18ed..c9fc21136 100644 --- a/lib/Doctrine/ORM/Tools/EntityGenerator.php +++ b/lib/Doctrine/ORM/Tools/EntityGenerator.php @@ -451,7 +451,7 @@ public function () } else if ($token[0] == T_FUNCTION) { if ($tokens[$i+2][0] == T_STRING) { $this->_staticReflection[$lastSeenClass]['methods'][] = $tokens[$i+2][1]; - } else if ($tokens[$i+2][0] == T_AMPERSAND && $tokens[$i+3][0] == T_STRING) { + } else if ($tokens[$i+2] == "&" && $tokens[$i+3][0] == T_STRING) { $this->_staticReflection[$lastSeenClass]['methods'][] = $tokens[$i+3][1]; } } else if (in_array($token[0], array(T_VAR, T_PUBLIC, T_PRIVATE, T_PROTECTED)) && $tokens[$i+2][0] != T_FUNCTION) { @@ -691,6 +691,7 @@ public function () if ($this->_hasMethod($methodName, $metadata)) { return; } + $this->_staticReflection[$metadata->name]['methods'][] = $methodName; $var = sprintf('_%sMethodTemplate', $type); $template = self::$$var; @@ -723,6 +724,7 @@ public function () if ($this->_hasMethod($methodName, $metadata)) { return; } + $this->_staticReflection[$metadata->name]['methods'][] = $methodName; $replacements = array( '' => $this->_annotationsPrefix . $name, diff --git a/lib/Doctrine/ORM/Version.php b/lib/Doctrine/ORM/Version.php index 5e811edd0..183887147 100644 --- a/lib/Doctrine/ORM/Version.php +++ b/lib/Doctrine/ORM/Version.php @@ -36,7 +36,7 @@ class Version /** * Current Doctrine Version */ - const VERSION = '2.1.0RC4-DEV'; + const VERSION = '2.2.0-DEV'; /** * Compares a Doctrine version with the current one. diff --git a/lib/vendor/doctrine-common b/lib/vendor/doctrine-common index 74a2c924c..40f1bf16e 160000 --- a/lib/vendor/doctrine-common +++ b/lib/vendor/doctrine-common @@ -1 +1 @@ -Subproject commit 74a2c924cd08b30785877808b1fb519b4b2e60b1 +Subproject commit 40f1bf16e84ddc5291a6a63aa00b9879c40e3500 diff --git a/lib/vendor/doctrine-dbal b/lib/vendor/doctrine-dbal index be3790059..0127ee98a 160000 --- a/lib/vendor/doctrine-dbal +++ b/lib/vendor/doctrine-dbal @@ -1 +1 @@ -Subproject commit be3790059cc43b674a55548eb42d5d25846ea6a9 +Subproject commit 0127ee98a4301f2f6e3463c824adc3a3687f901f diff --git a/tests/Doctrine/Tests/Models/ECommerce/ECommerceCustomer.php b/tests/Doctrine/Tests/Models/ECommerce/ECommerceCustomer.php index fff4c9b82..0144b0998 100644 --- a/tests/Doctrine/Tests/Models/ECommerce/ECommerceCustomer.php +++ b/tests/Doctrine/Tests/Models/ECommerce/ECommerceCustomer.php @@ -34,7 +34,7 @@ class ECommerceCustomer * only one customer at the time, while a customer can choose only one * mentor. Not properly appropriate but it works. * - * @OneToOne(targetEntity="ECommerceCustomer", cascade={"persist"}) + * @OneToOne(targetEntity="ECommerceCustomer", cascade={"persist"}, fetch="EAGER") * @JoinColumn(name="mentor_id", referencedColumnName="id") */ private $mentor; diff --git a/tests/Doctrine/Tests/Models/ECommerce/ECommerceProduct.php b/tests/Doctrine/Tests/Models/ECommerce/ECommerceProduct.php index 198e16720..d159a51da 100644 --- a/tests/Doctrine/Tests/Models/ECommerce/ECommerceProduct.php +++ b/tests/Doctrine/Tests/Models/ECommerce/ECommerceProduct.php @@ -56,6 +56,7 @@ class ECommerceProduct private $related; public $isCloned = false; + public $wakeUp = false; public function __construct() { @@ -166,4 +167,12 @@ class ECommerceProduct { $this->isCloned = true; } + + /** + * Testing docblock contents here + */ + public function __wakeup() + { + $this->wakeUp = true; + } } diff --git a/tests/Doctrine/Tests/ORM/Functional/OneToOneSelfReferentialAssociationTest.php b/tests/Doctrine/Tests/ORM/Functional/OneToOneSelfReferentialAssociationTest.php index 321f66340..d7132ed60 100644 --- a/tests/Doctrine/Tests/ORM/Functional/OneToOneSelfReferentialAssociationTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/OneToOneSelfReferentialAssociationTest.php @@ -49,6 +49,14 @@ class OneToOneSelfReferentialAssociationTest extends \Doctrine\Tests\OrmFunction $this->assertForeignKeyIs(null); } + public function testFind() + { + $id = $this->_createFixture(); + + $customer = $this->_em->find('Doctrine\Tests\Models\ECommerce\ECommerceCustomer', $id); + $this->assertNotInstanceOf('Doctrine\ORM\Proxy\Proxy', $customer->getMentor()); + } + public function testEagerLoadsAssociation() { $this->_createFixture(); @@ -127,6 +135,8 @@ class OneToOneSelfReferentialAssociationTest extends \Doctrine\Tests\OrmFunction $this->_em->flush(); $this->_em->clear(); + + return $customer->getId(); } } diff --git a/tests/Doctrine/Tests/ORM/Functional/ReferenceProxyTest.php b/tests/Doctrine/Tests/ORM/Functional/ReferenceProxyTest.php index 3b6e1f546..8ecb389af 100644 --- a/tests/Doctrine/Tests/ORM/Functional/ReferenceProxyTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/ReferenceProxyTest.php @@ -130,4 +130,21 @@ class ReferenceProxyTest extends \Doctrine\Tests\OrmFunctionalTestCase $entity = $this->_em->getReference('Doctrine\Tests\Models\ECommerce\ECommerceProduct' , $id); $this->assertEquals('Doctrine 2 Cookbook', $entity->getName()); } + + /** + * @group DDC-1022 + */ + public function testWakeupCalledOnProxy() + { + $id = $this->createProduct(); + + /* @var $entity Doctrine\Tests\Models\ECommerce\ECommerceProduct */ + $entity = $this->_em->getReference('Doctrine\Tests\Models\ECommerce\ECommerceProduct' , $id); + + $this->assertFalse($entity->wakeUp); + + $entity->setName('Doctrine 2 Cookbook'); + + $this->assertTrue($entity->wakeUp, "Loading the proxy should call __wakeup()."); + } } diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1250Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1250Test.php new file mode 100644 index 000000000..3a756da9a --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1250Test.php @@ -0,0 +1,96 @@ +_schemaTool->createSchema(array( + $this->_em->getClassMetadata(__NAMESPACE__ . '\\DDC1250ClientHistory'), + )); + } catch(\PDOException $e) { + + } + } + + public function testIssue() + { + $c1 = new DDC1250ClientHistory; + $c2 = new DDC1250ClientHistory; + $c1->declinedClientsHistory = $c2; + $c1->declinedBy = $c2; + $c2->declinedBy = $c1; + $c2->declinedClientsHistory= $c1; + + $this->_em->persist($c1); + $this->_em->persist($c2); + $this->_em->flush(); + $this->_em->clear(); + + $history = $this->_em->createQuery('SELECT h FROM ' . __NAMESPACE__ . '\\DDC1250ClientHistory h WHERE h.id = ?1') + ->setParameter(1, $c2->id)->getSingleResult(); + + $this->assertInstanceOf(__NAMESPACE__ . '\\DDC1250ClientHistory', $history); + } +} + +/** + * @Entity + */ +class DDC1250ClientHistory +{ + /** @Id @GeneratedValue @Column(type="integer") */ + public $id; + + /** @OneToOne(targetEntity="DDC1250ClientHistory", inversedBy="declinedBy") + * @JoinColumn(name="declined_clients_history_id", referencedColumnName="id") + */ + public $declinedClientsHistory; + + /** + * @OneToOne(targetEntity="DDC1250ClientHistory", mappedBy="declinedClientsHistory") + * @var + */ + public $declinedBy; +} + +/** + * +Entities\ClientsHistory: +type: entity +table: clients_history +fields: +id: +id: true +type: integer +unsigned: false +nullable: false +generator: +strategy: IDENTITY +[...skiped...] +oneToOne: +declinedClientsHistory: +targetEntity: Entities\ClientsHistory +joinColumn: +name: declined_clients_history_id +referencedColumnName: id +inversedBy: declinedBy +declinedBy: +targetEntity: Entities\ClientsHistory +mappedBy: declinedClientsHistory +lifecycleCallbacks: { } +repositoryClass: Entities\ClientsHistoryRepository + + + */ \ No newline at end of file diff --git a/tests/Doctrine/Tests/OrmFunctionalTestCase.php b/tests/Doctrine/Tests/OrmFunctionalTestCase.php index 92320e3a4..1f8032aeb 100644 --- a/tests/Doctrine/Tests/OrmFunctionalTestCase.php +++ b/tests/Doctrine/Tests/OrmFunctionalTestCase.php @@ -303,6 +303,10 @@ abstract class OrmFunctionalTestCase extends OrmTestCase $evm->addEventSubscriber($subscriberInstance); } } + + if (isset($GLOBALS['debug_uow_listener'])) { + $evm->addEventListener(array('onFlush'), new \Doctrine\ORM\Tools\DebugUnitOfWorkListener()); + } return \Doctrine\ORM\EntityManager::create($conn, $config); }