1
0
mirror of synced 2024-12-05 03:06:05 +03:00

Merge pull request #1274 from doctrine/persistent-collection-as-lazy-collection

PersistentCollection now extends AbstractLazyCollection.
This commit is contained in:
Marco Pivetta 2015-03-17 23:32:17 +00:00
commit 3b0fb6b4b8
5 changed files with 108 additions and 330 deletions

View File

@ -20,6 +20,7 @@
namespace Doctrine\ORM;
use Closure;
use Doctrine\Common\Collections\AbstractLazyCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Selectable;
@ -41,9 +42,8 @@ use Doctrine\ORM\Mapping\ClassMetadataInfo;
* @author Roman Borschel <roman@code-factory.org>
* @author Giorgio Sironi <piccoloprincipeazzurro@gmail.com>
* @author Stefano Rodriguez <stefano.rodriguez@fubles.com>
* @todo Design for inheritance to allow custom implementations?
*/
final class PersistentCollection implements Collection, Selectable
final class PersistentCollection extends AbstractLazyCollection implements Selectable
{
/**
* A snapshot of the collection at the moment it was fetched from the database.
@ -98,20 +98,6 @@ final class PersistentCollection implements Collection, Selectable
*/
private $isDirty = false;
/**
* Whether the collection has already been initialized.
*
* @var boolean
*/
private $initialized = true;
/**
* The wrapped Collection instance.
*
* @var Collection
*/
private $coll;
/**
* Creates a new persistent collection.
*
@ -121,9 +107,10 @@ final class PersistentCollection implements Collection, Selectable
*/
public function __construct(EntityManagerInterface $em, $class, $coll)
{
$this->coll = $coll;
$this->em = $em;
$this->typeClass = $class;
$this->collection = $coll;
$this->em = $em;
$this->typeClass = $class;
$this->initialized = true;
}
/**
@ -173,7 +160,7 @@ final class PersistentCollection implements Collection, Selectable
*/
public function hydrateAdd($element)
{
$this->coll->add($element);
$this->collection->add($element);
// If _backRefFieldName is set and its a one-to-many association,
// we need to set the back reference.
@ -200,7 +187,7 @@ final class PersistentCollection implements Collection, Selectable
*/
public function hydrateSet($key, $element)
{
$this->coll->set($key, $element);
$this->collection->set($key, $element);
// If _backRefFieldName is set, then the association is bidirectional
// and we need to set the back reference.
@ -224,25 +211,7 @@ final class PersistentCollection implements Collection, Selectable
return;
}
// Has NEW objects added through add(). Remember them.
$newObjects = array();
if ($this->isDirty) {
$newObjects = $this->coll->toArray();
}
$this->coll->clear();
$this->em->getUnitOfWork()->loadCollection($this);
$this->takeSnapshot();
// Reattach NEW objects added through add(), if any.
if ($newObjects) {
foreach ($newObjects as $obj) {
$this->coll->add($obj);
}
$this->isDirty = true;
}
$this->doInitialize();
$this->initialized = true;
}
@ -255,7 +224,7 @@ final class PersistentCollection implements Collection, Selectable
*/
public function takeSnapshot()
{
$this->snapshot = $this->coll->toArray();
$this->snapshot = $this->collection->toArray();
$this->isDirty = false;
}
@ -280,7 +249,7 @@ final class PersistentCollection implements Collection, Selectable
{
return array_udiff_assoc(
$this->snapshot,
$this->coll->toArray(),
$this->collection->toArray(),
function($a, $b) { return $a === $b ? 0 : 1; }
);
}
@ -294,7 +263,7 @@ final class PersistentCollection implements Collection, Selectable
public function getInsertDiff()
{
return array_udiff_assoc(
$this->coll->toArray(),
$this->collection->toArray(),
$this->snapshot,
function($a, $b) { return $a === $b ? 0 : 1; }
);
@ -367,36 +336,6 @@ final class PersistentCollection implements Collection, Selectable
$this->initialized = $bool;
}
/**
* Checks whether this collection has been initialized.
*
* @return boolean
*/
public function isInitialized()
{
return $this->initialized;
}
/**
* {@inheritdoc}
*/
public function first()
{
$this->initialize();
return $this->coll->first();
}
/**
* {@inheritdoc}
*/
public function last()
{
$this->initialize();
return $this->coll->last();
}
/**
* {@inheritdoc}
*/
@ -406,9 +345,7 @@ final class PersistentCollection implements Collection, Selectable
// and the collection is not initialized and orphanRemoval is
// not used we can issue a straight SQL delete/update on the
// association (table). Without initializing the collection.
$this->initialize();
$removed = $this->coll->remove($key);
$removed = parent::remove($key);
if ( ! $removed) {
return $removed;
@ -432,8 +369,8 @@ final class PersistentCollection implements Collection, Selectable
public function removeElement($element)
{
if ( ! $this->initialized && $this->association['fetch'] === Mapping\ClassMetadataInfo::FETCH_EXTRA_LAZY) {
if ($this->coll->contains($element)) {
return $this->coll->removeElement($element);
if ($this->collection->contains($element)) {
return $this->collection->removeElement($element);
}
$persister = $this->em->getUnitOfWork()->getCollectionPersister($this->association);
@ -445,9 +382,7 @@ final class PersistentCollection implements Collection, Selectable
return null;
}
$this->initialize();
$removed = $this->coll->removeElement($element);
$removed = parent::removeElement($element);
if ( ! $removed) {
return $removed;
@ -470,16 +405,14 @@ final class PersistentCollection implements Collection, Selectable
*/
public function containsKey($key)
{
if (! $this->initialized && $this->association['fetch'] === Mapping\ClassMetadataInfo::FETCH_EXTRA_LAZY
&& isset($this->association['indexBy'])) {
$persister = $this->em->getUnitOfWork()->getCollectionPersister($this->association);
return $this->coll->containsKey($key) || $persister->containsKey($this, $key);
return $this->collection->containsKey($key) || $persister->containsKey($this, $key);
}
$this->initialize();
return $this->coll->containsKey($key);
return parent::containsKey($key);
}
/**
@ -490,32 +423,10 @@ final class PersistentCollection implements Collection, Selectable
if ( ! $this->initialized && $this->association['fetch'] === Mapping\ClassMetadataInfo::FETCH_EXTRA_LAZY) {
$persister = $this->em->getUnitOfWork()->getCollectionPersister($this->association);
return $this->coll->contains($element) || $persister->contains($this, $element);
return $this->collection->contains($element) || $persister->contains($this, $element);
}
$this->initialize();
return $this->coll->contains($element);
}
/**
* {@inheritdoc}
*/
public function exists(Closure $p)
{
$this->initialize();
return $this->coll->exists($p);
}
/**
* {@inheritdoc}
*/
public function indexOf($element)
{
$this->initialize();
return $this->coll->indexOf($element);
return parent::contains($element);
}
/**
@ -534,29 +445,7 @@ final class PersistentCollection implements Collection, Selectable
return $this->em->getUnitOfWork()->getCollectionPersister($this->association)->get($this, $key);
}
$this->initialize();
return $this->coll->get($key);
}
/**
* {@inheritdoc}
*/
public function getKeys()
{
$this->initialize();
return $this->coll->getKeys();
}
/**
* {@inheritdoc}
*/
public function getValues()
{
$this->initialize();
return $this->coll->getValues();
return parent::get($key);
}
/**
@ -567,12 +456,10 @@ final class PersistentCollection implements Collection, Selectable
if ( ! $this->initialized && $this->association['fetch'] === Mapping\ClassMetadataInfo::FETCH_EXTRA_LAZY) {
$persister = $this->em->getUnitOfWork()->getCollectionPersister($this->association);
return $persister->count($this) + ($this->isDirty ? $this->coll->count() : 0);
return $persister->count($this) + ($this->isDirty ? $this->collection->count() : 0);
}
$this->initialize();
return $this->coll->count();
return parent::count();
}
/**
@ -580,9 +467,7 @@ final class PersistentCollection implements Collection, Selectable
*/
public function set($key, $value)
{
$this->initialize();
$this->coll->set($key, $value);
parent::set($key, $value);
$this->changed();
}
@ -592,131 +477,13 @@ final class PersistentCollection implements Collection, Selectable
*/
public function add($value)
{
$this->coll->add($value);
$this->collection->add($value);
$this->changed();
return true;
}
/**
* {@inheritdoc}
*/
public function isEmpty()
{
return $this->coll->isEmpty() && $this->count() === 0;
}
/**
* {@inheritdoc}
*/
public function getIterator()
{
$this->initialize();
return $this->coll->getIterator();
}
/**
* {@inheritdoc}
*/
public function map(Closure $func)
{
$this->initialize();
return $this->coll->map($func);
}
/**
* {@inheritdoc}
*/
public function filter(Closure $p)
{
$this->initialize();
return $this->coll->filter($p);
}
/**
* {@inheritdoc}
*/
public function forAll(Closure $p)
{
$this->initialize();
return $this->coll->forAll($p);
}
/**
* {@inheritdoc}
*/
public function partition(Closure $p)
{
$this->initialize();
return $this->coll->partition($p);
}
/**
* {@inheritdoc}
*/
public function toArray()
{
$this->initialize();
return $this->coll->toArray();
}
/**
* {@inheritdoc}
*/
public function clear()
{
if ($this->initialized && $this->isEmpty()) {
return;
}
$uow = $this->em->getUnitOfWork();
if ($this->association['type'] & ClassMetadata::TO_MANY &&
$this->association['orphanRemoval'] &&
$this->owner) {
// we need to initialize here, as orphan removal acts like implicit cascadeRemove,
// hence for event listeners we need the objects in memory.
$this->initialize();
foreach ($this->coll as $element) {
$uow->scheduleOrphanRemoval($element);
}
}
$this->coll->clear();
$this->initialized = true; // direct call, {@link initialize()} is too expensive
if ($this->association['isOwningSide'] && $this->owner) {
$this->changed();
$uow->scheduleCollectionDeletion($this);
$this->takeSnapshot();
}
}
/**
* Called by PHP when this collection is serialized. Ensures that only the
* elements are properly serialized.
*
* Internal note: Tried to implement Serializable first but that did not work well
* with circular references. This solution seems simpler and works well.
*
* @return array
*/
public function __sleep()
{
return array('coll', 'initialized');
}
/* ArrayAccess implementation */
/**
@ -758,41 +525,59 @@ final class PersistentCollection implements Collection, Selectable
/**
* {@inheritdoc}
*/
public function key()
public function isEmpty()
{
$this->initialize();
return $this->coll->key();
return $this->collection->isEmpty() && $this->count() === 0;
}
/**
* {@inheritdoc}
*/
public function current()
public function clear()
{
$this->initialize();
if ($this->initialized && $this->isEmpty()) {
return;
}
return $this->coll->current();
$uow = $this->em->getUnitOfWork();
if ($this->association['type'] & ClassMetadata::TO_MANY &&
$this->association['orphanRemoval'] &&
$this->owner) {
// we need to initialize here, as orphan removal acts like implicit cascadeRemove,
// hence for event listeners we need the objects in memory.
$this->initialize();
foreach ($this->collection as $element) {
$uow->scheduleOrphanRemoval($element);
}
}
$this->collection->clear();
$this->initialized = true; // direct call, {@link initialize()} is too expensive
if ($this->association['isOwningSide'] && $this->owner) {
$this->changed();
$uow->scheduleCollectionDeletion($this);
$this->takeSnapshot();
}
}
/**
* {@inheritdoc}
*/
public function next()
{
$this->initialize();
return $this->coll->next();
}
/**
* Retrieves the wrapped Collection instance.
* Called by PHP when this collection is serialized. Ensures that only the
* elements are properly serialized.
*
* @return \Doctrine\Common\Collections\Collection
* Internal note: Tried to implement Serializable first but that did not work well
* with circular references. This solution seems simpler and works well.
*
* @return array
*/
public function unwrap()
public function __sleep()
{
return $this->coll;
return array('collection', 'initialized');
}
/**
@ -815,9 +600,7 @@ final class PersistentCollection implements Collection, Selectable
return $persister->slice($this, $offset, $length);
}
$this->initialize();
return $this->coll->slice($offset, $length);
return parent::slice($offset, $length);
}
/**
@ -835,8 +618,8 @@ final class PersistentCollection implements Collection, Selectable
*/
public function __clone()
{
if (is_object($this->coll)) {
$this->coll = clone $this->coll;
if (is_object($this->collection)) {
$this->collection = clone $this->collection;
}
$this->initialize();
@ -864,7 +647,7 @@ final class PersistentCollection implements Collection, Selectable
}
if ($this->initialized) {
return $this->coll->matching($criteria);
return $this->collection->matching($criteria);
}
if ($this->association['type'] === ClassMetadata::MANY_TO_MANY) {
@ -887,4 +670,40 @@ final class PersistentCollection implements Collection, Selectable
? new LazyCriteriaCollection($persister, $criteria)
: new ArrayCollection($persister->loadCriteria($criteria));
}
/**
* Retrieves the wrapped Collection instance.
*
* @return \Doctrine\Common\Collections\Collection
*/
public function unwrap()
{
return $this->collection;
}
/**
* {@inheritdoc}
*/
protected function doInitialize()
{
// Has NEW objects added through add(). Remember them.
$newObjects = array();
if ($this->isDirty) {
$newObjects = $this->collection->toArray();
}
$this->collection->clear();
$this->em->getUnitOfWork()->loadCollection($this);
$this->takeSnapshot();
// Reattach NEW objects added through add(), if any.
if ($newObjects) {
foreach ($newObjects as $obj) {
$this->collection->add($obj);
}
$this->isDirty = true;
}
}
}

View File

@ -12,6 +12,7 @@ class DriverConnectionMock implements \Doctrine\DBAL\Driver\Connection
*/
public function prepare($prepareString)
{
return new StatementMock();
}
/**

View File

@ -390,7 +390,7 @@ abstract class AbstractEntityPersisterTest extends OrmTestCase
{
$mapping = $this->em->getClassMetadata('Doctrine\Tests\Models\Cache\Country');
$assoc = array('type' => 1);
$coll = new PersistentCollection($this->em, 'Foo', $mapping);
$coll = new PersistentCollection($this->em, $mapping, null);
$persister = $this->createPersisterDefault();
$entity = new Country("Foo");
@ -406,7 +406,7 @@ abstract class AbstractEntityPersisterTest extends OrmTestCase
{
$mapping = $this->em->getClassMetadata('Doctrine\Tests\Models\Cache\Country');
$assoc = array('type' => 1);
$coll = new PersistentCollection($this->em, 'Foo', $mapping);
$coll = new PersistentCollection($this->em, $mapping, null);
$persister = $this->createPersisterDefault();
$entity = new Country("Foo");

View File

@ -250,47 +250,5 @@ class IdentityMapTest extends \Doctrine\Tests\OrmFunctionalTestCase
// Now the collection should be refreshed with correct count
$this->assertEquals(4, count($user2->getPhonenumbers()));
}
public function testReusedSplObjectHashDoesNotConfuseUnitOfWork()
{
$hash1 = $this->subRoutine($this->_em);
if (!defined('HHVM_VERSION')) {
// See comment below about PersistentCollection
gc_collect_cycles();
}
$user1 = new CmsUser;
$user1->status = 'dev';
$user1->username = 'jwage';
$user1->name = 'Jonathan W.';
$hash2 = spl_object_hash($user1);
$this->assertEquals($hash1, $hash2); // Hash reused!
$this->_em->persist($user1);
$this->_em->flush();
}
private function subRoutine($em) {
$user = new CmsUser;
$user->status = 'dev';
$user->username = 'romanb';
$user->name = 'Roman B.';
$em->persist($user);
$em->flush();
// The PersistentCollection references its owner, hence without breaking
// the cycle the object will not (yet) be garbage collected and thus
// the object hash is not reused. This is not a memory leak!
if (defined('HHVM_VERSION')) {
$ed = $this->_em->getUnitOfWork()->getOriginalEntityData($user);
$ed['phonenumbers']->setOwner(null, array('inversedBy' => 1));
$ed['articles']->setOwner(null, array('inversedBy' => 1));
$ed['groups']->setOwner(null, array('inversedBy' => 1));
}
$em->remove($user);
$em->flush();
return spl_object_hash($user);
}
}

View File

@ -92,7 +92,7 @@ class DDC1452EntityA
public $id;
/** @Column */
public $title;
/** @ManyToMany(targetEntity="DDC1452EntityB", mappedBy="entityAFrom") */
/** @OneToMany(targetEntity="DDC1452EntityB", mappedBy="entityAFrom") */
public $entitiesB;
public function __construct()