Merge pull request #1274 from doctrine/persistent-collection-as-lazy-collection
PersistentCollection now extends AbstractLazyCollection.
This commit is contained in:
commit
3b0fb6b4b8
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ class DriverConnectionMock implements \Doctrine\DBAL\Driver\Connection
|
||||
*/
|
||||
public function prepare($prepareString)
|
||||
{
|
||||
return new StatementMock();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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");
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
|
Loading…
Reference in New Issue
Block a user