Cache
9 months ago
Decorator
9 months ago
Event
9 months ago
Exception
9 months ago
Id
9 months ago
Internal
9 months ago
Mapping
9 months ago
Persisters
8 months ago
Proxy
9 months ago
Query
9 months ago
Repository
9 months ago
Tools
9 months ago
Utility
9 months ago
AbstractQuery.php
9 months ago
Cache.php
9 months ago
Configuration.php
9 months ago
EntityManager.php
9 months ago
EntityManagerInterface.php
9 months ago
EntityNotFoundException.php
9 months ago
EntityRepository.php
9 months ago
Events.php
9 months ago
LazyCriteriaCollection.php
9 months ago
NativeQuery.php
9 months ago
NoResultException.php
9 months ago
NonUniqueResultException.php
9 months ago
ORMException.php
9 months ago
ORMInvalidArgumentException.php
9 months ago
ORMSetup.php
9 months ago
OptimisticLockException.php
9 months ago
PersistentCollection.php
9 months ago
PessimisticLockException.php
9 months ago
Query.php
9 months ago
QueryBuilder.php
9 months ago
TransactionRequiredException.php
9 months ago
UnexpectedResultException.php
9 months ago
UnitOfWork.php
9 months ago
Version.php
9 months ago
index.php
9 months ago
PersistentCollection.php
350 lines
| 1 | <?php |
| 2 | declare (strict_types=1); |
| 3 | namespace MailPoetVendor\Doctrine\ORM; |
| 4 | if (!defined('ABSPATH')) exit; |
| 5 | use MailPoetVendor\Doctrine\Common\Collections\AbstractLazyCollection; |
| 6 | use MailPoetVendor\Doctrine\Common\Collections\ArrayCollection; |
| 7 | use MailPoetVendor\Doctrine\Common\Collections\Collection; |
| 8 | use MailPoetVendor\Doctrine\Common\Collections\Criteria; |
| 9 | use MailPoetVendor\Doctrine\Common\Collections\Selectable; |
| 10 | use MailPoetVendor\Doctrine\ORM\Internal\CriteriaOrderings; |
| 11 | use MailPoetVendor\Doctrine\ORM\Mapping\ClassMetadata; |
| 12 | use ReturnTypeWillChange; |
| 13 | use RuntimeException; |
| 14 | use UnexpectedValueException; |
| 15 | use function array_combine; |
| 16 | use function array_diff_key; |
| 17 | use function array_map; |
| 18 | use function array_values; |
| 19 | use function array_walk; |
| 20 | use function assert; |
| 21 | use function get_class; |
| 22 | use function is_object; |
| 23 | use function spl_object_id; |
| 24 | final class PersistentCollection extends AbstractLazyCollection implements Selectable |
| 25 | { |
| 26 | use CriteriaOrderings; |
| 27 | private $snapshot = []; |
| 28 | private $owner; |
| 29 | private $association; |
| 30 | private $em; |
| 31 | private $backRefFieldName; |
| 32 | private $typeClass; |
| 33 | private $isDirty = \false; |
| 34 | public function __construct(EntityManagerInterface $em, $class, Collection $collection) |
| 35 | { |
| 36 | $this->collection = $collection; |
| 37 | $this->em = $em; |
| 38 | $this->typeClass = $class; |
| 39 | $this->initialized = \true; |
| 40 | } |
| 41 | public function setOwner($entity, array $assoc) : void |
| 42 | { |
| 43 | $this->owner = $entity; |
| 44 | $this->association = $assoc; |
| 45 | $this->backRefFieldName = $assoc['inversedBy'] ?: $assoc['mappedBy']; |
| 46 | } |
| 47 | public function getOwner() |
| 48 | { |
| 49 | return $this->owner; |
| 50 | } |
| 51 | public function getTypeClass() : Mapping\ClassMetadataInfo |
| 52 | { |
| 53 | assert($this->typeClass !== null); |
| 54 | return $this->typeClass; |
| 55 | } |
| 56 | private function getUnitOfWork() : UnitOfWork |
| 57 | { |
| 58 | assert($this->em !== null); |
| 59 | return $this->em->getUnitOfWork(); |
| 60 | } |
| 61 | public function hydrateAdd($element) : void |
| 62 | { |
| 63 | $this->unwrap()->add($element); |
| 64 | // If _backRefFieldName is set and its a one-to-many association, |
| 65 | // we need to set the back reference. |
| 66 | if ($this->backRefFieldName && $this->getMapping()['type'] === ClassMetadata::ONE_TO_MANY) { |
| 67 | assert($this->typeClass !== null); |
| 68 | // Set back reference to owner |
| 69 | $this->typeClass->reflFields[$this->backRefFieldName]->setValue($element, $this->owner); |
| 70 | $this->getUnitOfWork()->setOriginalEntityProperty(spl_object_id($element), $this->backRefFieldName, $this->owner); |
| 71 | } |
| 72 | } |
| 73 | public function hydrateSet($key, $element) : void |
| 74 | { |
| 75 | $this->unwrap()->set($key, $element); |
| 76 | // If _backRefFieldName is set, then the association is bidirectional |
| 77 | // and we need to set the back reference. |
| 78 | if ($this->backRefFieldName && $this->getMapping()['type'] === ClassMetadata::ONE_TO_MANY) { |
| 79 | assert($this->typeClass !== null); |
| 80 | // Set back reference to owner |
| 81 | $this->typeClass->reflFields[$this->backRefFieldName]->setValue($element, $this->owner); |
| 82 | } |
| 83 | } |
| 84 | public function initialize() : void |
| 85 | { |
| 86 | if ($this->initialized || !$this->association) { |
| 87 | return; |
| 88 | } |
| 89 | $this->doInitialize(); |
| 90 | $this->initialized = \true; |
| 91 | } |
| 92 | public function takeSnapshot() : void |
| 93 | { |
| 94 | $this->snapshot = $this->unwrap()->toArray(); |
| 95 | $this->isDirty = \false; |
| 96 | } |
| 97 | public function getSnapshot() : array |
| 98 | { |
| 99 | return $this->snapshot; |
| 100 | } |
| 101 | public function getDeleteDiff() : array |
| 102 | { |
| 103 | $collectionItems = $this->unwrap()->toArray(); |
| 104 | return array_values(array_diff_key(array_combine(array_map('spl_object_id', $this->snapshot), $this->snapshot), array_combine(array_map('spl_object_id', $collectionItems), $collectionItems))); |
| 105 | } |
| 106 | public function getInsertDiff() : array |
| 107 | { |
| 108 | $collectionItems = $this->unwrap()->toArray(); |
| 109 | return array_values(array_diff_key(array_combine(array_map('spl_object_id', $collectionItems), $collectionItems), array_combine(array_map('spl_object_id', $this->snapshot), $this->snapshot))); |
| 110 | } |
| 111 | public function getMapping() : array |
| 112 | { |
| 113 | if ($this->association === null) { |
| 114 | throw new UnexpectedValueException('The underlying association mapping is null although it should not be'); |
| 115 | } |
| 116 | return $this->association; |
| 117 | } |
| 118 | private function changed() : void |
| 119 | { |
| 120 | if ($this->isDirty) { |
| 121 | return; |
| 122 | } |
| 123 | $this->isDirty = \true; |
| 124 | if ($this->association !== null && $this->getMapping()['isOwningSide'] && $this->getMapping()['type'] === ClassMetadata::MANY_TO_MANY && $this->owner && $this->em !== null && $this->em->getClassMetadata(get_class($this->owner))->isChangeTrackingNotify()) { |
| 125 | $this->getUnitOfWork()->scheduleForDirtyCheck($this->owner); |
| 126 | } |
| 127 | } |
| 128 | public function isDirty() : bool |
| 129 | { |
| 130 | return $this->isDirty; |
| 131 | } |
| 132 | public function setDirty($dirty) : void |
| 133 | { |
| 134 | $this->isDirty = $dirty; |
| 135 | } |
| 136 | public function setInitialized($bool) : void |
| 137 | { |
| 138 | $this->initialized = $bool; |
| 139 | } |
| 140 | public function remove($key) |
| 141 | { |
| 142 | // TODO: If the keys are persistent as well (not yet implemented) |
| 143 | // and the collection is not initialized and orphanRemoval is |
| 144 | // not used we can issue a straight SQL delete/update on the |
| 145 | // association (table). Without initializing the collection. |
| 146 | $removed = parent::remove($key); |
| 147 | if (!$removed) { |
| 148 | return $removed; |
| 149 | } |
| 150 | $this->changed(); |
| 151 | if ($this->association !== null && $this->getMapping()['type'] & ClassMetadata::TO_MANY && $this->owner && $this->getMapping()['orphanRemoval']) { |
| 152 | $this->getUnitOfWork()->scheduleOrphanRemoval($removed); |
| 153 | } |
| 154 | return $removed; |
| 155 | } |
| 156 | public function removeElement($element) : bool |
| 157 | { |
| 158 | $removed = parent::removeElement($element); |
| 159 | if (!$removed) { |
| 160 | return $removed; |
| 161 | } |
| 162 | $this->changed(); |
| 163 | if ($this->association !== null && $this->getMapping()['type'] & ClassMetadata::TO_MANY && $this->owner && $this->getMapping()['orphanRemoval']) { |
| 164 | $this->getUnitOfWork()->scheduleOrphanRemoval($element); |
| 165 | } |
| 166 | return $removed; |
| 167 | } |
| 168 | public function containsKey($key) : bool |
| 169 | { |
| 170 | if (!$this->initialized && $this->getMapping()['fetch'] === ClassMetadata::FETCH_EXTRA_LAZY && isset($this->getMapping()['indexBy'])) { |
| 171 | $persister = $this->getUnitOfWork()->getCollectionPersister($this->getMapping()); |
| 172 | return $this->unwrap()->containsKey($key) || $persister->containsKey($this, $key); |
| 173 | } |
| 174 | return parent::containsKey($key); |
| 175 | } |
| 176 | public function contains($element) : bool |
| 177 | { |
| 178 | if (!$this->initialized && $this->getMapping()['fetch'] === ClassMetadata::FETCH_EXTRA_LAZY) { |
| 179 | $persister = $this->getUnitOfWork()->getCollectionPersister($this->getMapping()); |
| 180 | return $this->unwrap()->contains($element) || $persister->contains($this, $element); |
| 181 | } |
| 182 | return parent::contains($element); |
| 183 | } |
| 184 | public function get($key) |
| 185 | { |
| 186 | if (!$this->initialized && $this->getMapping()['fetch'] === ClassMetadata::FETCH_EXTRA_LAZY && isset($this->getMapping()['indexBy'])) { |
| 187 | assert($this->em !== null); |
| 188 | assert($this->typeClass !== null); |
| 189 | if (!$this->typeClass->isIdentifierComposite && $this->typeClass->isIdentifier($this->getMapping()['indexBy'])) { |
| 190 | return $this->em->find($this->typeClass->name, $key); |
| 191 | } |
| 192 | return $this->getUnitOfWork()->getCollectionPersister($this->getMapping())->get($this, $key); |
| 193 | } |
| 194 | return parent::get($key); |
| 195 | } |
| 196 | public function count() : int |
| 197 | { |
| 198 | if (!$this->initialized && $this->association !== null && $this->getMapping()['fetch'] === ClassMetadata::FETCH_EXTRA_LAZY) { |
| 199 | $persister = $this->getUnitOfWork()->getCollectionPersister($this->association); |
| 200 | return $persister->count($this) + ($this->isDirty ? $this->unwrap()->count() : 0); |
| 201 | } |
| 202 | return parent::count(); |
| 203 | } |
| 204 | public function set($key, $value) : void |
| 205 | { |
| 206 | parent::set($key, $value); |
| 207 | $this->changed(); |
| 208 | if (is_object($value) && $this->em) { |
| 209 | $this->getUnitOfWork()->cancelOrphanRemoval($value); |
| 210 | } |
| 211 | } |
| 212 | public function add($value) : bool |
| 213 | { |
| 214 | $this->unwrap()->add($value); |
| 215 | $this->changed(); |
| 216 | if (is_object($value) && $this->em) { |
| 217 | $this->getUnitOfWork()->cancelOrphanRemoval($value); |
| 218 | } |
| 219 | return \true; |
| 220 | } |
| 221 | public function offsetExists($offset) : bool |
| 222 | { |
| 223 | return $this->containsKey($offset); |
| 224 | } |
| 225 | #[\ReturnTypeWillChange] |
| 226 | public function offsetGet($offset) |
| 227 | { |
| 228 | return $this->get($offset); |
| 229 | } |
| 230 | public function offsetSet($offset, $value) : void |
| 231 | { |
| 232 | if (!isset($offset)) { |
| 233 | $this->add($value); |
| 234 | return; |
| 235 | } |
| 236 | $this->set($offset, $value); |
| 237 | } |
| 238 | #[\ReturnTypeWillChange] |
| 239 | public function offsetUnset($offset) |
| 240 | { |
| 241 | return $this->remove($offset); |
| 242 | } |
| 243 | public function isEmpty() : bool |
| 244 | { |
| 245 | return $this->unwrap()->isEmpty() && $this->count() === 0; |
| 246 | } |
| 247 | public function clear() : void |
| 248 | { |
| 249 | if ($this->initialized && $this->isEmpty()) { |
| 250 | $this->unwrap()->clear(); |
| 251 | return; |
| 252 | } |
| 253 | $uow = $this->getUnitOfWork(); |
| 254 | $association = $this->getMapping(); |
| 255 | if ($association['type'] & ClassMetadata::TO_MANY && $association['orphanRemoval'] && $this->owner) { |
| 256 | // we need to initialize here, as orphan removal acts like implicit cascadeRemove, |
| 257 | // hence for event listeners we need the objects in memory. |
| 258 | $this->initialize(); |
| 259 | foreach ($this->unwrap() as $element) { |
| 260 | $uow->scheduleOrphanRemoval($element); |
| 261 | } |
| 262 | } |
| 263 | $this->unwrap()->clear(); |
| 264 | $this->initialized = \true; |
| 265 | // direct call, {@link initialize()} is too expensive |
| 266 | if ($association['isOwningSide'] && $this->owner) { |
| 267 | $this->changed(); |
| 268 | $uow->scheduleCollectionDeletion($this); |
| 269 | $this->takeSnapshot(); |
| 270 | } |
| 271 | } |
| 272 | public function __sleep() : array |
| 273 | { |
| 274 | return ['collection', 'initialized']; |
| 275 | } |
| 276 | public function slice($offset, $length = null) : array |
| 277 | { |
| 278 | if (!$this->initialized && !$this->isDirty && $this->getMapping()['fetch'] === ClassMetadata::FETCH_EXTRA_LAZY) { |
| 279 | $persister = $this->getUnitOfWork()->getCollectionPersister($this->getMapping()); |
| 280 | return $persister->slice($this, $offset, $length); |
| 281 | } |
| 282 | return parent::slice($offset, $length); |
| 283 | } |
| 284 | public function __clone() |
| 285 | { |
| 286 | if (is_object($this->collection)) { |
| 287 | $this->collection = clone $this->collection; |
| 288 | } |
| 289 | $this->initialize(); |
| 290 | $this->owner = null; |
| 291 | $this->snapshot = []; |
| 292 | $this->changed(); |
| 293 | } |
| 294 | public function matching(Criteria $criteria) : Collection |
| 295 | { |
| 296 | if ($this->isDirty) { |
| 297 | $this->initialize(); |
| 298 | } |
| 299 | if ($this->initialized) { |
| 300 | return $this->unwrap()->matching($criteria); |
| 301 | } |
| 302 | $association = $this->getMapping(); |
| 303 | if ($association['type'] === ClassMetadata::MANY_TO_MANY) { |
| 304 | $persister = $this->getUnitOfWork()->getCollectionPersister($association); |
| 305 | return new ArrayCollection($persister->loadCriteria($this, $criteria)); |
| 306 | } |
| 307 | $builder = Criteria::expr(); |
| 308 | $ownerExpression = $builder->eq($this->backRefFieldName, $this->owner); |
| 309 | $expression = $criteria->getWhereExpression(); |
| 310 | $expression = $expression ? $builder->andX($expression, $ownerExpression) : $ownerExpression; |
| 311 | $criteria = clone $criteria; |
| 312 | $criteria->where($expression); |
| 313 | $criteria->orderBy(self::mapToOrderEnumIfAvailable(self::getCriteriaOrderings($criteria) ?: $association['orderBy'] ?? [])); |
| 314 | $persister = $this->getUnitOfWork()->getEntityPersister($association['targetEntity']); |
| 315 | return $association['fetch'] === ClassMetadata::FETCH_EXTRA_LAZY ? new LazyCriteriaCollection($persister, $criteria) : new ArrayCollection($persister->loadCriteria($criteria)); |
| 316 | } |
| 317 | public function unwrap() : Collection |
| 318 | { |
| 319 | assert($this->collection instanceof Collection); |
| 320 | assert($this->collection instanceof Selectable); |
| 321 | return $this->collection; |
| 322 | } |
| 323 | protected function doInitialize() : void |
| 324 | { |
| 325 | // Has NEW objects added through add(). Remember them. |
| 326 | $newlyAddedDirtyObjects = []; |
| 327 | if ($this->isDirty) { |
| 328 | $newlyAddedDirtyObjects = $this->unwrap()->toArray(); |
| 329 | } |
| 330 | $this->unwrap()->clear(); |
| 331 | $this->getUnitOfWork()->loadCollection($this); |
| 332 | $this->takeSnapshot(); |
| 333 | if ($newlyAddedDirtyObjects) { |
| 334 | $this->restoreNewObjectsInDirtyCollection($newlyAddedDirtyObjects); |
| 335 | } |
| 336 | } |
| 337 | private function restoreNewObjectsInDirtyCollection(array $newObjects) : void |
| 338 | { |
| 339 | $loadedObjects = $this->unwrap()->toArray(); |
| 340 | $newObjectsByOid = array_combine(array_map('spl_object_id', $newObjects), $newObjects); |
| 341 | $loadedObjectsByOid = array_combine(array_map('spl_object_id', $loadedObjects), $loadedObjects); |
| 342 | $newObjectsThatWereNotLoaded = array_diff_key($newObjectsByOid, $loadedObjectsByOid); |
| 343 | if ($newObjectsThatWereNotLoaded) { |
| 344 | // Reattach NEW objects added through add(), if any. |
| 345 | array_walk($newObjectsThatWereNotLoaded, [$this->unwrap(), 'add']); |
| 346 | $this->isDirty = \true; |
| 347 | } |
| 348 | } |
| 349 | } |
| 350 |