[345] | 1 | <?php |
---|
| 2 | /* |
---|
| 3 | * $Id$ |
---|
| 4 | * |
---|
| 5 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
---|
| 6 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
---|
| 7 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
---|
| 8 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
---|
| 9 | * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
---|
| 10 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
---|
| 11 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
---|
| 12 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
---|
| 13 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
---|
| 14 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
---|
| 15 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
---|
| 16 | * |
---|
| 17 | * This software consists of voluntary contributions made by many individuals |
---|
| 18 | * and is licensed under the LGPL. For more information, see |
---|
| 19 | * <http://www.doctrine-project.org>. |
---|
| 20 | */ |
---|
| 21 | |
---|
| 22 | namespace Doctrine\ORM\Persisters; |
---|
| 23 | |
---|
| 24 | use Doctrine\ORM\Mapping\ClassMetadata, |
---|
| 25 | Doctrine\ORM\PersistentCollection, |
---|
| 26 | Doctrine\ORM\UnitOfWork; |
---|
| 27 | |
---|
| 28 | /** |
---|
| 29 | * Persister for many-to-many collections. |
---|
| 30 | * |
---|
| 31 | * @author Roman Borschel <roman@code-factory.org> |
---|
| 32 | * @author Guilherme Blanco <guilhermeblanco@hotmail.com> |
---|
| 33 | * @author Alexander <iam.asm89@gmail.com> |
---|
| 34 | * @since 2.0 |
---|
| 35 | */ |
---|
| 36 | class ManyToManyPersister extends AbstractCollectionPersister |
---|
| 37 | { |
---|
| 38 | /** |
---|
| 39 | * {@inheritdoc} |
---|
| 40 | * |
---|
| 41 | * @override |
---|
| 42 | */ |
---|
| 43 | protected function _getDeleteRowSQL(PersistentCollection $coll) |
---|
| 44 | { |
---|
| 45 | $mapping = $coll->getMapping(); |
---|
| 46 | $class = $this->_em->getClassMetadata(get_class($coll->getOwner())); |
---|
| 47 | |
---|
| 48 | return 'DELETE FROM ' . $class->getQuotedJoinTableName($mapping, $this->_conn->getDatabasePlatform()) |
---|
| 49 | . ' WHERE ' . implode(' = ? AND ', $mapping['joinTableColumns']) . ' = ?'; |
---|
| 50 | } |
---|
| 51 | |
---|
| 52 | /** |
---|
| 53 | * {@inheritdoc} |
---|
| 54 | * |
---|
| 55 | * @override |
---|
| 56 | * @internal Order of the parameters must be the same as the order of the columns in |
---|
| 57 | * _getDeleteRowSql. |
---|
| 58 | */ |
---|
| 59 | protected function _getDeleteRowSQLParameters(PersistentCollection $coll, $element) |
---|
| 60 | { |
---|
| 61 | return $this->_collectJoinTableColumnParameters($coll, $element); |
---|
| 62 | } |
---|
| 63 | |
---|
| 64 | /** |
---|
| 65 | * {@inheritdoc} |
---|
| 66 | * |
---|
| 67 | * @override |
---|
| 68 | */ |
---|
| 69 | protected function _getUpdateRowSQL(PersistentCollection $coll) |
---|
| 70 | {} |
---|
| 71 | |
---|
| 72 | /** |
---|
| 73 | * {@inheritdoc} |
---|
| 74 | * |
---|
| 75 | * @override |
---|
| 76 | * @internal Order of the parameters must be the same as the order of the columns in |
---|
| 77 | * _getInsertRowSql. |
---|
| 78 | */ |
---|
| 79 | protected function _getInsertRowSQL(PersistentCollection $coll) |
---|
| 80 | { |
---|
| 81 | $mapping = $coll->getMapping(); |
---|
| 82 | $columns = $mapping['joinTableColumns']; |
---|
| 83 | $class = $this->_em->getClassMetadata(get_class($coll->getOwner())); |
---|
| 84 | |
---|
| 85 | $joinTable = $class->getQuotedJoinTableName($mapping, $this->_conn->getDatabasePlatform()); |
---|
| 86 | |
---|
| 87 | return 'INSERT INTO ' . $joinTable . ' (' . implode(', ', $columns) . ')' |
---|
| 88 | . ' VALUES (' . implode(', ', array_fill(0, count($columns), '?')) . ')'; |
---|
| 89 | } |
---|
| 90 | |
---|
| 91 | /** |
---|
| 92 | * {@inheritdoc} |
---|
| 93 | * |
---|
| 94 | * @override |
---|
| 95 | * @internal Order of the parameters must be the same as the order of the columns in |
---|
| 96 | * _getInsertRowSql. |
---|
| 97 | */ |
---|
| 98 | protected function _getInsertRowSQLParameters(PersistentCollection $coll, $element) |
---|
| 99 | { |
---|
| 100 | return $this->_collectJoinTableColumnParameters($coll, $element); |
---|
| 101 | } |
---|
| 102 | |
---|
| 103 | /** |
---|
| 104 | * Collects the parameters for inserting/deleting on the join table in the order |
---|
| 105 | * of the join table columns as specified in ManyToManyMapping#joinTableColumns. |
---|
| 106 | * |
---|
| 107 | * @param $coll |
---|
| 108 | * @param $element |
---|
| 109 | * @return array |
---|
| 110 | */ |
---|
| 111 | private function _collectJoinTableColumnParameters(PersistentCollection $coll, $element) |
---|
| 112 | { |
---|
| 113 | $params = array(); |
---|
| 114 | $mapping = $coll->getMapping(); |
---|
| 115 | $isComposite = count($mapping['joinTableColumns']) > 2; |
---|
| 116 | |
---|
| 117 | $identifier1 = $this->_uow->getEntityIdentifier($coll->getOwner()); |
---|
| 118 | $identifier2 = $this->_uow->getEntityIdentifier($element); |
---|
| 119 | |
---|
| 120 | if ($isComposite) { |
---|
| 121 | $class1 = $this->_em->getClassMetadata(get_class($coll->getOwner())); |
---|
| 122 | $class2 = $coll->getTypeClass(); |
---|
| 123 | } |
---|
| 124 | |
---|
| 125 | foreach ($mapping['joinTableColumns'] as $joinTableColumn) { |
---|
| 126 | $isRelationToSource = isset($mapping['relationToSourceKeyColumns'][$joinTableColumn]); |
---|
| 127 | |
---|
| 128 | if ( ! $isComposite) { |
---|
| 129 | $params[] = $isRelationToSource ? array_pop($identifier1) : array_pop($identifier2); |
---|
| 130 | |
---|
| 131 | continue; |
---|
| 132 | } |
---|
| 133 | |
---|
| 134 | if ($isRelationToSource) { |
---|
| 135 | $params[] = $identifier1[$class1->getFieldForColumn($mapping['relationToSourceKeyColumns'][$joinTableColumn])]; |
---|
| 136 | |
---|
| 137 | continue; |
---|
| 138 | } |
---|
| 139 | |
---|
| 140 | $params[] = $identifier2[$class2->getFieldForColumn($mapping['relationToTargetKeyColumns'][$joinTableColumn])]; |
---|
| 141 | } |
---|
| 142 | |
---|
| 143 | return $params; |
---|
| 144 | } |
---|
| 145 | |
---|
| 146 | /** |
---|
| 147 | * {@inheritdoc} |
---|
| 148 | * |
---|
| 149 | * @override |
---|
| 150 | */ |
---|
| 151 | protected function _getDeleteSQL(PersistentCollection $coll) |
---|
| 152 | { |
---|
| 153 | $class = $this->_em->getClassMetadata(get_class($coll->getOwner())); |
---|
| 154 | $mapping = $coll->getMapping(); |
---|
| 155 | |
---|
| 156 | return 'DELETE FROM ' . $class->getQuotedJoinTableName($mapping, $this->_conn->getDatabasePlatform()) |
---|
| 157 | . ' WHERE ' . implode(' = ? AND ', array_keys($mapping['relationToSourceKeyColumns'])) . ' = ?'; |
---|
| 158 | } |
---|
| 159 | |
---|
| 160 | /** |
---|
| 161 | * {@inheritdoc} |
---|
| 162 | * |
---|
| 163 | * @override |
---|
| 164 | * @internal Order of the parameters must be the same as the order of the columns in |
---|
| 165 | * _getDeleteSql. |
---|
| 166 | */ |
---|
| 167 | protected function _getDeleteSQLParameters(PersistentCollection $coll) |
---|
| 168 | { |
---|
| 169 | $identifier = $this->_uow->getEntityIdentifier($coll->getOwner()); |
---|
| 170 | $mapping = $coll->getMapping(); |
---|
| 171 | $params = array(); |
---|
| 172 | |
---|
| 173 | // Optimization for single column identifier |
---|
| 174 | if (count($mapping['relationToSourceKeyColumns']) === 1) { |
---|
| 175 | $params[] = array_pop($identifier); |
---|
| 176 | |
---|
| 177 | return $params; |
---|
| 178 | } |
---|
| 179 | |
---|
| 180 | // Composite identifier |
---|
| 181 | $sourceClass = $this->_em->getClassMetadata(get_class($coll->getOwner())); |
---|
| 182 | |
---|
| 183 | foreach ($mapping['relationToSourceKeyColumns'] as $relColumn => $srcColumn) { |
---|
| 184 | $params[] = $identifier[$sourceClass->fieldNames[$srcColumn]]; |
---|
| 185 | } |
---|
| 186 | |
---|
| 187 | return $params; |
---|
| 188 | } |
---|
| 189 | |
---|
| 190 | /** |
---|
| 191 | * {@inheritdoc} |
---|
| 192 | */ |
---|
| 193 | public function count(PersistentCollection $coll) |
---|
| 194 | { |
---|
| 195 | $mapping = $filterMapping = $coll->getMapping(); |
---|
| 196 | $class = $this->_em->getClassMetadata($mapping['sourceEntity']); |
---|
| 197 | $id = $this->_em->getUnitOfWork()->getEntityIdentifier($coll->getOwner()); |
---|
| 198 | |
---|
| 199 | if ($mapping['isOwningSide']) { |
---|
| 200 | $joinColumns = $mapping['relationToSourceKeyColumns']; |
---|
| 201 | } else { |
---|
| 202 | $mapping = $this->_em->getClassMetadata($mapping['targetEntity'])->associationMappings[$mapping['mappedBy']]; |
---|
| 203 | $joinColumns = $mapping['relationToTargetKeyColumns']; |
---|
| 204 | } |
---|
| 205 | |
---|
| 206 | $whereClauses = array(); |
---|
| 207 | $params = array(); |
---|
| 208 | |
---|
| 209 | foreach ($mapping['joinTableColumns'] as $joinTableColumn) { |
---|
| 210 | if ( ! isset($joinColumns[$joinTableColumn])) { |
---|
| 211 | continue; |
---|
| 212 | } |
---|
| 213 | |
---|
| 214 | $whereClauses[] = $joinTableColumn . ' = ?'; |
---|
| 215 | |
---|
| 216 | $params[] = ($class->containsForeignIdentifier) |
---|
| 217 | ? $id[$class->getFieldForColumn($joinColumns[$joinTableColumn])] |
---|
| 218 | : $id[$class->fieldNames[$joinColumns[$joinTableColumn]]]; |
---|
| 219 | } |
---|
| 220 | |
---|
| 221 | list($joinTargetEntitySQL, $filterSql) = $this->getFilterSql($filterMapping); |
---|
| 222 | if ($filterSql) { |
---|
| 223 | $whereClauses[] = $filterSql; |
---|
| 224 | } |
---|
| 225 | |
---|
| 226 | $sql = 'SELECT COUNT(*)' |
---|
| 227 | . ' FROM ' . $class->getQuotedJoinTableName($mapping, $this->_conn->getDatabasePlatform()) . ' t' |
---|
| 228 | . $joinTargetEntitySQL |
---|
| 229 | . ' WHERE ' . implode(' AND ', $whereClauses); |
---|
| 230 | |
---|
| 231 | return $this->_conn->fetchColumn($sql, $params); |
---|
| 232 | } |
---|
| 233 | |
---|
| 234 | /** |
---|
| 235 | * @param PersistentCollection $coll |
---|
| 236 | * @param int $offset |
---|
| 237 | * @param int $length |
---|
| 238 | * @return array |
---|
| 239 | */ |
---|
| 240 | public function slice(PersistentCollection $coll, $offset, $length = null) |
---|
| 241 | { |
---|
| 242 | $mapping = $coll->getMapping(); |
---|
| 243 | |
---|
| 244 | return $this->_em->getUnitOfWork()->getEntityPersister($mapping['targetEntity'])->getManyToManyCollection($mapping, $coll->getOwner(), $offset, $length); |
---|
| 245 | } |
---|
| 246 | |
---|
| 247 | /** |
---|
| 248 | * @param PersistentCollection $coll |
---|
| 249 | * @param object $element |
---|
| 250 | * @return boolean |
---|
| 251 | */ |
---|
| 252 | public function contains(PersistentCollection $coll, $element) |
---|
| 253 | { |
---|
| 254 | $uow = $this->_em->getUnitOfWork(); |
---|
| 255 | |
---|
| 256 | // Shortcut for new entities |
---|
| 257 | $entityState = $uow->getEntityState($element, UnitOfWork::STATE_NEW); |
---|
| 258 | |
---|
| 259 | if ($entityState === UnitOfWork::STATE_NEW) { |
---|
| 260 | return false; |
---|
| 261 | } |
---|
| 262 | |
---|
| 263 | // Entity is scheduled for inclusion |
---|
| 264 | if ($entityState === UnitOfWork::STATE_MANAGED && $uow->isScheduledForInsert($element)) { |
---|
| 265 | return false; |
---|
| 266 | } |
---|
| 267 | |
---|
| 268 | list($quotedJoinTable, $whereClauses, $params) = $this->getJoinTableRestrictions($coll, $element, true); |
---|
| 269 | |
---|
| 270 | $sql = 'SELECT 1 FROM ' . $quotedJoinTable . ' WHERE ' . implode(' AND ', $whereClauses); |
---|
| 271 | |
---|
| 272 | return (bool) $this->_conn->fetchColumn($sql, $params); |
---|
| 273 | } |
---|
| 274 | |
---|
| 275 | /** |
---|
| 276 | * @param PersistentCollection $coll |
---|
| 277 | * @param object $element |
---|
| 278 | * @return boolean |
---|
| 279 | */ |
---|
| 280 | public function removeElement(PersistentCollection $coll, $element) |
---|
| 281 | { |
---|
| 282 | $uow = $this->_em->getUnitOfWork(); |
---|
| 283 | |
---|
| 284 | // shortcut for new entities |
---|
| 285 | $entityState = $uow->getEntityState($element, UnitOfWork::STATE_NEW); |
---|
| 286 | |
---|
| 287 | if ($entityState === UnitOfWork::STATE_NEW) { |
---|
| 288 | return false; |
---|
| 289 | } |
---|
| 290 | |
---|
| 291 | // If Entity is scheduled for inclusion, it is not in this collection. |
---|
| 292 | // We can assure that because it would have return true before on array check |
---|
| 293 | if ($entityState === UnitOfWork::STATE_MANAGED && $uow->isScheduledForInsert($element)) { |
---|
| 294 | return false; |
---|
| 295 | } |
---|
| 296 | |
---|
| 297 | list($quotedJoinTable, $whereClauses, $params) = $this->getJoinTableRestrictions($coll, $element, false); |
---|
| 298 | |
---|
| 299 | $sql = 'DELETE FROM ' . $quotedJoinTable . ' WHERE ' . implode(' AND ', $whereClauses); |
---|
| 300 | |
---|
| 301 | return (bool) $this->_conn->executeUpdate($sql, $params); |
---|
| 302 | } |
---|
| 303 | |
---|
| 304 | /** |
---|
| 305 | * @param \Doctrine\ORM\PersistentCollection $coll |
---|
| 306 | * @param object $element |
---|
| 307 | * @param boolean $addFilters Whether the filter SQL should be included or not. |
---|
| 308 | * @return array |
---|
| 309 | */ |
---|
| 310 | private function getJoinTableRestrictions(PersistentCollection $coll, $element, $addFilters) |
---|
| 311 | { |
---|
| 312 | $uow = $this->_em->getUnitOfWork(); |
---|
| 313 | $mapping = $filterMapping = $coll->getMapping(); |
---|
| 314 | |
---|
| 315 | if ( ! $mapping['isOwningSide']) { |
---|
| 316 | $sourceClass = $this->_em->getClassMetadata($mapping['targetEntity']); |
---|
| 317 | $targetClass = $this->_em->getClassMetadata($mapping['sourceEntity']); |
---|
| 318 | $sourceId = $uow->getEntityIdentifier($element); |
---|
| 319 | $targetId = $uow->getEntityIdentifier($coll->getOwner()); |
---|
| 320 | |
---|
| 321 | $mapping = $sourceClass->associationMappings[$mapping['mappedBy']]; |
---|
| 322 | } else { |
---|
| 323 | $sourceClass = $this->_em->getClassMetadata($mapping['sourceEntity']); |
---|
| 324 | $targetClass = $this->_em->getClassMetadata($mapping['targetEntity']); |
---|
| 325 | $sourceId = $uow->getEntityIdentifier($coll->getOwner()); |
---|
| 326 | $targetId = $uow->getEntityIdentifier($element); |
---|
| 327 | } |
---|
| 328 | |
---|
| 329 | $quotedJoinTable = $sourceClass->getQuotedJoinTableName($mapping, $this->_conn->getDatabasePlatform()); |
---|
| 330 | $whereClauses = array(); |
---|
| 331 | $params = array(); |
---|
| 332 | |
---|
| 333 | foreach ($mapping['joinTableColumns'] as $joinTableColumn) { |
---|
| 334 | $whereClauses[] = $joinTableColumn . ' = ?'; |
---|
| 335 | |
---|
| 336 | if (isset($mapping['relationToTargetKeyColumns'][$joinTableColumn])) { |
---|
| 337 | $params[] = ($targetClass->containsForeignIdentifier) |
---|
| 338 | ? $targetId[$targetClass->getFieldForColumn($mapping['relationToTargetKeyColumns'][$joinTableColumn])] |
---|
| 339 | : $targetId[$targetClass->fieldNames[$mapping['relationToTargetKeyColumns'][$joinTableColumn]]]; |
---|
| 340 | continue; |
---|
| 341 | } |
---|
| 342 | |
---|
| 343 | // relationToSourceKeyColumns |
---|
| 344 | $params[] = ($sourceClass->containsForeignIdentifier) |
---|
| 345 | ? $sourceId[$sourceClass->getFieldForColumn($mapping['relationToSourceKeyColumns'][$joinTableColumn])] |
---|
| 346 | : $sourceId[$sourceClass->fieldNames[$mapping['relationToSourceKeyColumns'][$joinTableColumn]]]; |
---|
| 347 | } |
---|
| 348 | |
---|
| 349 | if ($addFilters) { |
---|
| 350 | list($joinTargetEntitySQL, $filterSql) = $this->getFilterSql($filterMapping); |
---|
| 351 | if ($filterSql) { |
---|
| 352 | $quotedJoinTable .= ' t ' . $joinTargetEntitySQL; |
---|
| 353 | $whereClauses[] = $filterSql; |
---|
| 354 | } |
---|
| 355 | } |
---|
| 356 | |
---|
| 357 | return array($quotedJoinTable, $whereClauses, $params); |
---|
| 358 | } |
---|
| 359 | |
---|
| 360 | /** |
---|
| 361 | * Generates the filter SQL for a given mapping. |
---|
| 362 | * |
---|
| 363 | * This method is not used for actually grabbing the related entities |
---|
| 364 | * but when the extra-lazy collection methods are called on a filtered |
---|
| 365 | * association. This is why besides the many to many table we also |
---|
| 366 | * have to join in the actual entities table leading to additional |
---|
| 367 | * JOIN. |
---|
| 368 | * |
---|
| 369 | * @param array $mapping Array containing mapping information. |
---|
| 370 | * |
---|
| 371 | * @return string The SQL query part to add to a query. |
---|
| 372 | */ |
---|
| 373 | public function getFilterSql($mapping) |
---|
| 374 | { |
---|
| 375 | $targetClass = $this->_em->getClassMetadata($mapping['targetEntity']); |
---|
| 376 | |
---|
| 377 | if ($mapping['isOwningSide']) { |
---|
| 378 | $joinColumns = $mapping['relationToTargetKeyColumns']; |
---|
| 379 | } else { |
---|
| 380 | $mapping = $targetClass->associationMappings[$mapping['mappedBy']]; |
---|
| 381 | $joinColumns = $mapping['relationToSourceKeyColumns']; |
---|
| 382 | } |
---|
| 383 | |
---|
| 384 | $targetClass = $this->_em->getClassMetadata($targetClass->rootEntityName); |
---|
| 385 | |
---|
| 386 | // A join is needed if there is filtering on the target entity |
---|
| 387 | $joinTargetEntitySQL = ''; |
---|
| 388 | if ($filterSql = $this->generateFilterConditionSQL($targetClass, 'te')) { |
---|
| 389 | $joinTargetEntitySQL = ' JOIN ' |
---|
| 390 | . $targetClass->getQuotedTableName($this->_conn->getDatabasePlatform()) . ' te' |
---|
| 391 | . ' ON'; |
---|
| 392 | |
---|
| 393 | $joinTargetEntitySQLClauses = array(); |
---|
| 394 | foreach ($joinColumns as $joinTableColumn => $targetTableColumn) { |
---|
| 395 | $joinTargetEntitySQLClauses[] = ' t.' . $joinTableColumn . ' = ' . 'te.' . $targetTableColumn; |
---|
| 396 | } |
---|
| 397 | |
---|
| 398 | $joinTargetEntitySQL .= implode(' AND ', $joinTargetEntitySQLClauses); |
---|
| 399 | } |
---|
| 400 | |
---|
| 401 | return array($joinTargetEntitySQL, $filterSql); |
---|
| 402 | } |
---|
| 403 | |
---|
| 404 | /** |
---|
| 405 | * Generates the filter SQL for a given entity and table alias. |
---|
| 406 | * |
---|
| 407 | * @param ClassMetadata $targetEntity Metadata of the target entity. |
---|
| 408 | * @param string $targetTableAlias The table alias of the joined/selected table. |
---|
| 409 | * |
---|
| 410 | * @return string The SQL query part to add to a query. |
---|
| 411 | */ |
---|
| 412 | protected function generateFilterConditionSQL(ClassMetadata $targetEntity, $targetTableAlias) |
---|
| 413 | { |
---|
| 414 | $filterClauses = array(); |
---|
| 415 | |
---|
| 416 | foreach ($this->_em->getFilters()->getEnabledFilters() as $filter) { |
---|
| 417 | if ($filterExpr = $filter->addFilterConstraint($targetEntity, $targetTableAlias)) { |
---|
| 418 | $filterClauses[] = '(' . $filterExpr . ')'; |
---|
| 419 | } |
---|
| 420 | } |
---|
| 421 | |
---|
| 422 | $sql = implode(' AND ', $filterClauses); |
---|
| 423 | return $sql ? "(" . $sql . ")" : ""; |
---|
| 424 | } |
---|
| 425 | } |
---|