diff options
Diffstat (limited to 'vendor/doctrine/orm/src/Persisters/Collection/OneToManyPersister.php')
-rw-r--r-- | vendor/doctrine/orm/src/Persisters/Collection/OneToManyPersister.php | 264 |
1 files changed, 264 insertions, 0 deletions
diff --git a/vendor/doctrine/orm/src/Persisters/Collection/OneToManyPersister.php b/vendor/doctrine/orm/src/Persisters/Collection/OneToManyPersister.php new file mode 100644 index 0000000..0727b1f --- /dev/null +++ b/vendor/doctrine/orm/src/Persisters/Collection/OneToManyPersister.php | |||
@@ -0,0 +1,264 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Persisters\Collection; | ||
6 | |||
7 | use BadMethodCallException; | ||
8 | use Doctrine\Common\Collections\Criteria; | ||
9 | use Doctrine\DBAL\Exception as DBALException; | ||
10 | use Doctrine\DBAL\Types\Type; | ||
11 | use Doctrine\ORM\EntityNotFoundException; | ||
12 | use Doctrine\ORM\Mapping\MappingException; | ||
13 | use Doctrine\ORM\Mapping\OneToManyAssociationMapping; | ||
14 | use Doctrine\ORM\PersistentCollection; | ||
15 | use Doctrine\ORM\Utility\PersisterHelper; | ||
16 | |||
17 | use function array_reverse; | ||
18 | use function array_values; | ||
19 | use function assert; | ||
20 | use function implode; | ||
21 | use function is_int; | ||
22 | use function is_string; | ||
23 | |||
24 | /** | ||
25 | * Persister for one-to-many collections. | ||
26 | */ | ||
27 | class OneToManyPersister extends AbstractCollectionPersister | ||
28 | { | ||
29 | public function delete(PersistentCollection $collection): void | ||
30 | { | ||
31 | // The only valid case here is when you have weak entities. In this | ||
32 | // scenario, you have @OneToMany with orphanRemoval=true, and replacing | ||
33 | // the entire collection with a new would trigger this operation. | ||
34 | $mapping = $this->getMapping($collection); | ||
35 | |||
36 | if (! $mapping->orphanRemoval) { | ||
37 | // Handling non-orphan removal should never happen, as @OneToMany | ||
38 | // can only be inverse side. For owning side one to many, it is | ||
39 | // required to have a join table, which would classify as a ManyToManyPersister. | ||
40 | return; | ||
41 | } | ||
42 | |||
43 | $targetClass = $this->em->getClassMetadata($mapping->targetEntity); | ||
44 | |||
45 | $targetClass->isInheritanceTypeJoined() | ||
46 | ? $this->deleteJoinedEntityCollection($collection) | ||
47 | : $this->deleteEntityCollection($collection); | ||
48 | } | ||
49 | |||
50 | public function update(PersistentCollection $collection): void | ||
51 | { | ||
52 | // This can never happen. One to many can only be inverse side. | ||
53 | // For owning side one to many, it is required to have a join table, | ||
54 | // then classifying it as a ManyToManyPersister. | ||
55 | return; | ||
56 | } | ||
57 | |||
58 | public function get(PersistentCollection $collection, mixed $index): object|null | ||
59 | { | ||
60 | $mapping = $this->getMapping($collection); | ||
61 | |||
62 | if (! $mapping->isIndexed()) { | ||
63 | throw new BadMethodCallException('Selecting a collection by index is only supported on indexed collections.'); | ||
64 | } | ||
65 | |||
66 | $persister = $this->uow->getEntityPersister($mapping->targetEntity); | ||
67 | |||
68 | return $persister->load( | ||
69 | [ | ||
70 | $mapping->mappedBy => $collection->getOwner(), | ||
71 | $mapping->indexBy() => $index, | ||
72 | ], | ||
73 | null, | ||
74 | $mapping, | ||
75 | [], | ||
76 | null, | ||
77 | 1, | ||
78 | ); | ||
79 | } | ||
80 | |||
81 | public function count(PersistentCollection $collection): int | ||
82 | { | ||
83 | $mapping = $this->getMapping($collection); | ||
84 | $persister = $this->uow->getEntityPersister($mapping->targetEntity); | ||
85 | |||
86 | // only works with single id identifier entities. Will throw an | ||
87 | // exception in Entity Persisters if that is not the case for the | ||
88 | // 'mappedBy' field. | ||
89 | $criteria = new Criteria(Criteria::expr()->eq($mapping->mappedBy, $collection->getOwner())); | ||
90 | |||
91 | return $persister->count($criteria); | ||
92 | } | ||
93 | |||
94 | /** | ||
95 | * {@inheritDoc} | ||
96 | */ | ||
97 | public function slice(PersistentCollection $collection, int $offset, int|null $length = null): array | ||
98 | { | ||
99 | $mapping = $this->getMapping($collection); | ||
100 | $persister = $this->uow->getEntityPersister($mapping->targetEntity); | ||
101 | |||
102 | return $persister->getOneToManyCollection($mapping, $collection->getOwner(), $offset, $length); | ||
103 | } | ||
104 | |||
105 | public function containsKey(PersistentCollection $collection, mixed $key): bool | ||
106 | { | ||
107 | $mapping = $this->getMapping($collection); | ||
108 | |||
109 | if (! $mapping->isIndexed()) { | ||
110 | throw new BadMethodCallException('Selecting a collection by index is only supported on indexed collections.'); | ||
111 | } | ||
112 | |||
113 | $persister = $this->uow->getEntityPersister($mapping->targetEntity); | ||
114 | |||
115 | // only works with single id identifier entities. Will throw an | ||
116 | // exception in Entity Persisters if that is not the case for the | ||
117 | // 'mappedBy' field. | ||
118 | $criteria = new Criteria(); | ||
119 | |||
120 | $criteria->andWhere(Criteria::expr()->eq($mapping->mappedBy, $collection->getOwner())); | ||
121 | $criteria->andWhere(Criteria::expr()->eq($mapping->indexBy(), $key)); | ||
122 | |||
123 | return (bool) $persister->count($criteria); | ||
124 | } | ||
125 | |||
126 | public function contains(PersistentCollection $collection, object $element): bool | ||
127 | { | ||
128 | if (! $this->isValidEntityState($element)) { | ||
129 | return false; | ||
130 | } | ||
131 | |||
132 | $mapping = $this->getMapping($collection); | ||
133 | $persister = $this->uow->getEntityPersister($mapping->targetEntity); | ||
134 | |||
135 | // only works with single id identifier entities. Will throw an | ||
136 | // exception in Entity Persisters if that is not the case for the | ||
137 | // 'mappedBy' field. | ||
138 | $criteria = new Criteria(Criteria::expr()->eq($mapping->mappedBy, $collection->getOwner())); | ||
139 | |||
140 | return $persister->exists($element, $criteria); | ||
141 | } | ||
142 | |||
143 | /** | ||
144 | * {@inheritDoc} | ||
145 | */ | ||
146 | public function loadCriteria(PersistentCollection $collection, Criteria $criteria): array | ||
147 | { | ||
148 | throw new BadMethodCallException('Filtering a collection by Criteria is not supported by this CollectionPersister.'); | ||
149 | } | ||
150 | |||
151 | /** | ||
152 | * @throws DBALException | ||
153 | * @throws EntityNotFoundException | ||
154 | * @throws MappingException | ||
155 | */ | ||
156 | private function deleteEntityCollection(PersistentCollection $collection): int | ||
157 | { | ||
158 | $mapping = $this->getMapping($collection); | ||
159 | $identifier = $this->uow->getEntityIdentifier($collection->getOwner()); | ||
160 | $sourceClass = $this->em->getClassMetadata($mapping->sourceEntity); | ||
161 | $targetClass = $this->em->getClassMetadata($mapping->targetEntity); | ||
162 | $columns = []; | ||
163 | $parameters = []; | ||
164 | $types = []; | ||
165 | |||
166 | foreach ($this->em->getMetadataFactory()->getOwningSide($mapping)->joinColumns as $joinColumn) { | ||
167 | $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform); | ||
168 | $parameters[] = $identifier[$sourceClass->getFieldForColumn($joinColumn->referencedColumnName)]; | ||
169 | $types[] = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $sourceClass, $this->em); | ||
170 | } | ||
171 | |||
172 | $statement = 'DELETE FROM ' . $this->quoteStrategy->getTableName($targetClass, $this->platform) | ||
173 | . ' WHERE ' . implode(' = ? AND ', $columns) . ' = ?'; | ||
174 | |||
175 | if ($targetClass->isInheritanceTypeSingleTable()) { | ||
176 | $discriminatorColumn = $targetClass->getDiscriminatorColumn(); | ||
177 | $statement .= ' AND ' . $discriminatorColumn->name . ' = ?'; | ||
178 | $parameters[] = $targetClass->discriminatorValue; | ||
179 | $types[] = $discriminatorColumn->type; | ||
180 | } | ||
181 | |||
182 | $numAffected = $this->conn->executeStatement($statement, $parameters, $types); | ||
183 | |||
184 | assert(is_int($numAffected)); | ||
185 | |||
186 | return $numAffected; | ||
187 | } | ||
188 | |||
189 | /** | ||
190 | * Delete Class Table Inheritance entities. | ||
191 | * A temporary table is needed to keep IDs to be deleted in both parent and child class' tables. | ||
192 | * | ||
193 | * Thanks Steve Ebersole (Hibernate) for idea on how to tackle reliably this scenario, we owe him a beer! =) | ||
194 | * | ||
195 | * @throws DBALException | ||
196 | */ | ||
197 | private function deleteJoinedEntityCollection(PersistentCollection $collection): int | ||
198 | { | ||
199 | $mapping = $this->getMapping($collection); | ||
200 | $sourceClass = $this->em->getClassMetadata($mapping->sourceEntity); | ||
201 | $targetClass = $this->em->getClassMetadata($mapping->targetEntity); | ||
202 | $rootClass = $this->em->getClassMetadata($targetClass->rootEntityName); | ||
203 | |||
204 | // 1) Build temporary table DDL | ||
205 | $tempTable = $this->platform->getTemporaryTableName($rootClass->getTemporaryIdTableName()); | ||
206 | $idColumnNames = $rootClass->getIdentifierColumnNames(); | ||
207 | $idColumnList = implode(', ', $idColumnNames); | ||
208 | $columnDefinitions = []; | ||
209 | |||
210 | foreach ($idColumnNames as $idColumnName) { | ||
211 | $columnDefinitions[$idColumnName] = [ | ||
212 | 'name' => $idColumnName, | ||
213 | 'notnull' => true, | ||
214 | 'type' => Type::getType(PersisterHelper::getTypeOfColumn($idColumnName, $rootClass, $this->em)), | ||
215 | ]; | ||
216 | } | ||
217 | |||
218 | $statement = $this->platform->getCreateTemporaryTableSnippetSQL() . ' ' . $tempTable | ||
219 | . ' (' . $this->platform->getColumnDeclarationListSQL($columnDefinitions) . ')'; | ||
220 | |||
221 | $this->conn->executeStatement($statement); | ||
222 | |||
223 | // 2) Build insert table records into temporary table | ||
224 | $query = $this->em->createQuery( | ||
225 | ' SELECT t0.' . implode(', t0.', $rootClass->getIdentifierFieldNames()) | ||
226 | . ' FROM ' . $targetClass->name . ' t0 WHERE t0.' . $mapping->mappedBy . ' = :owner', | ||
227 | )->setParameter('owner', $collection->getOwner()); | ||
228 | |||
229 | $sql = $query->getSQL(); | ||
230 | assert(is_string($sql)); | ||
231 | $statement = 'INSERT INTO ' . $tempTable . ' (' . $idColumnList . ') ' . $sql; | ||
232 | $parameters = array_values($sourceClass->getIdentifierValues($collection->getOwner())); | ||
233 | $numDeleted = $this->conn->executeStatement($statement, $parameters); | ||
234 | |||
235 | // 3) Delete records on each table in the hierarchy | ||
236 | $classNames = [...$targetClass->parentClasses, ...[$targetClass->name], ...$targetClass->subClasses]; | ||
237 | |||
238 | foreach (array_reverse($classNames) as $className) { | ||
239 | $tableName = $this->quoteStrategy->getTableName($this->em->getClassMetadata($className), $this->platform); | ||
240 | $statement = 'DELETE FROM ' . $tableName . ' WHERE (' . $idColumnList . ')' | ||
241 | . ' IN (SELECT ' . $idColumnList . ' FROM ' . $tempTable . ')'; | ||
242 | |||
243 | $this->conn->executeStatement($statement); | ||
244 | } | ||
245 | |||
246 | // 4) Drop temporary table | ||
247 | $statement = $this->platform->getDropTemporaryTableSQL($tempTable); | ||
248 | |||
249 | $this->conn->executeStatement($statement); | ||
250 | |||
251 | assert(is_int($numDeleted)); | ||
252 | |||
253 | return $numDeleted; | ||
254 | } | ||
255 | |||
256 | private function getMapping(PersistentCollection $collection): OneToManyAssociationMapping | ||
257 | { | ||
258 | $mapping = $collection->getMapping(); | ||
259 | |||
260 | assert($mapping->isOneToMany()); | ||
261 | |||
262 | return $mapping; | ||
263 | } | ||
264 | } | ||