Complex classes like ManyToManyPersister often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use ManyToManyPersister, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 37 | class ManyToManyPersister extends AbstractCollectionPersister |
||
| 38 | { |
||
| 39 | /** |
||
| 40 | * {@inheritdoc} |
||
| 41 | */ |
||
| 42 | 17 | public function delete(PersistentCollection $collection) |
|
| 43 | { |
||
| 44 | 17 | $mapping = $collection->getMapping(); |
|
| 45 | |||
| 46 | 17 | if ( ! $mapping['isOwningSide']) { |
|
| 47 | return; // ignore inverse side |
||
| 48 | } |
||
| 49 | |||
| 50 | 17 | $types = []; |
|
| 51 | 17 | $class = $this->em->getClassMetadata($mapping['sourceEntity']); |
|
| 52 | |||
| 53 | 17 | foreach ($mapping['joinTable']['joinColumns'] as $joinColumn) { |
|
| 54 | 17 | $types[] = PersisterHelper::getTypeOfColumn($joinColumn['referencedColumnName'], $class, $this->em); |
|
|
|
|||
| 55 | } |
||
| 56 | |||
| 57 | 17 | $this->conn->executeUpdate($this->getDeleteSQL($collection), $this->getDeleteSQLParameters($collection), $types); |
|
| 58 | 17 | } |
|
| 59 | |||
| 60 | /** |
||
| 61 | * {@inheritdoc} |
||
| 62 | */ |
||
| 63 | 326 | public function update(PersistentCollection $collection) |
|
| 64 | { |
||
| 65 | 326 | $mapping = $collection->getMapping(); |
|
| 66 | |||
| 67 | 326 | if ( ! $mapping['isOwningSide']) { |
|
| 68 | 231 | return; // ignore inverse side |
|
| 69 | } |
||
| 70 | |||
| 71 | 325 | list($deleteSql, $deleteTypes) = $this->getDeleteRowSQL($collection); |
|
| 72 | 325 | list($insertSql, $insertTypes) = $this->getInsertRowSQL($collection); |
|
| 73 | |||
| 74 | 325 | foreach ($collection->getDeleteDiff() as $element) { |
|
| 75 | 11 | $this->conn->executeUpdate( |
|
| 76 | $deleteSql, |
||
| 77 | 11 | $this->getDeleteRowSQLParameters($collection, $element), |
|
| 78 | 11 | $deleteTypes |
|
| 79 | ); |
||
| 80 | } |
||
| 81 | |||
| 82 | 325 | foreach ($collection->getInsertDiff() as $element) { |
|
| 83 | 325 | $this->conn->executeUpdate( |
|
| 84 | $insertSql, |
||
| 85 | 325 | $this->getInsertRowSQLParameters($collection, $element), |
|
| 86 | 325 | $insertTypes |
|
| 87 | ); |
||
| 88 | } |
||
| 89 | 325 | } |
|
| 90 | |||
| 91 | /** |
||
| 92 | * {@inheritdoc} |
||
| 93 | */ |
||
| 94 | 3 | public function get(PersistentCollection $collection, $index) |
|
| 95 | { |
||
| 96 | 3 | $mapping = $collection->getMapping(); |
|
| 97 | |||
| 98 | 3 | if ( ! isset($mapping['indexBy'])) { |
|
| 99 | throw new \BadMethodCallException("Selecting a collection by index is only supported on indexed collections."); |
||
| 100 | } |
||
| 101 | |||
| 102 | 3 | $persister = $this->uow->getEntityPersister($mapping['targetEntity']); |
|
| 103 | 3 | $mappedKey = $mapping['isOwningSide'] |
|
| 104 | 2 | ? $mapping['inversedBy'] |
|
| 105 | 3 | : $mapping['mappedBy']; |
|
| 106 | |||
| 107 | 3 | return $persister->load([$mappedKey => $collection->getOwner(), $mapping['indexBy'] => $index], null, $mapping, [], 0, 1); |
|
| 108 | } |
||
| 109 | |||
| 110 | /** |
||
| 111 | * {@inheritdoc} |
||
| 112 | */ |
||
| 113 | 18 | public function count(PersistentCollection $collection) |
|
| 169 | |||
| 170 | /** |
||
| 171 | * {@inheritDoc} |
||
| 172 | */ |
||
| 173 | 8 | public function slice(PersistentCollection $collection, $offset, $length = null) |
|
| 180 | /** |
||
| 181 | * {@inheritdoc} |
||
| 182 | */ |
||
| 183 | 7 | public function containsKey(PersistentCollection $collection, $key) |
|
| 184 | { |
||
| 185 | 7 | $mapping = $collection->getMapping(); |
|
| 186 | |||
| 187 | 7 | if ( ! isset($mapping['indexBy'])) { |
|
| 188 | throw new \BadMethodCallException("Selecting a collection by index is only supported on indexed collections."); |
||
| 189 | } |
||
| 190 | |||
| 191 | 7 | list($quotedJoinTable, $whereClauses, $params, $types) = $this->getJoinTableRestrictionsWithKey($collection, $key, true); |
|
| 192 | |||
| 193 | 7 | $sql = 'SELECT 1 FROM ' . $quotedJoinTable . ' WHERE ' . implode(' AND ', $whereClauses); |
|
| 194 | |||
| 195 | 7 | return (bool) $this->conn->fetchColumn($sql, $params, 0, $types); |
|
| 196 | } |
||
| 197 | |||
| 198 | /** |
||
| 199 | * {@inheritDoc} |
||
| 200 | */ |
||
| 201 | 7 | public function contains(PersistentCollection $collection, $element) |
|
| 213 | |||
| 214 | /** |
||
| 215 | * {@inheritDoc} |
||
| 216 | */ |
||
| 217 | 2 | public function removeElement(PersistentCollection $collection, $element) |
|
| 229 | |||
| 230 | /** |
||
| 231 | * {@inheritDoc} |
||
| 232 | */ |
||
| 233 | 7 | public function loadCriteria(PersistentCollection $collection, Criteria $criteria) |
|
| 234 | { |
||
| 235 | 7 | $mapping = $collection->getMapping(); |
|
| 236 | 7 | $owner = $collection->getOwner(); |
|
| 237 | 7 | $ownerMetadata = $this->em->getClassMetadata(get_class($owner)); |
|
| 238 | 7 | $id = $this->uow->getEntityIdentifier($owner); |
|
| 239 | 7 | $targetClass = $this->em->getClassMetadata($mapping['targetEntity']); |
|
| 240 | 7 | $onConditions = $this->getOnConditionSQL($mapping); |
|
| 241 | 7 | $whereClauses = $params = []; |
|
| 242 | |||
| 243 | 7 | if ( ! $mapping['isOwningSide']) { |
|
| 244 | 1 | $associationSourceClass = $targetClass; |
|
| 245 | 1 | $mapping = $targetClass->associationMappings[$mapping['mappedBy']]; |
|
| 246 | 1 | $sourceRelationMode = 'relationToTargetKeyColumns'; |
|
| 247 | } else { |
||
| 248 | 6 | $associationSourceClass = $ownerMetadata; |
|
| 249 | 6 | $sourceRelationMode = 'relationToSourceKeyColumns'; |
|
| 250 | } |
||
| 251 | |||
| 252 | 7 | foreach ($mapping[$sourceRelationMode] as $key => $value) { |
|
| 253 | 7 | $whereClauses[] = sprintf('t.%s = ?', $key); |
|
| 254 | 7 | $params[] = $ownerMetadata->containsForeignIdentifier |
|
| 255 | ? $id[$ownerMetadata->getFieldForColumn($value)] |
||
| 256 | 7 | : $id[$ownerMetadata->fieldNames[$value]]; |
|
| 257 | } |
||
| 258 | |||
| 259 | 7 | $parameters = $this->expandCriteriaParameters($criteria); |
|
| 260 | |||
| 261 | 7 | foreach ($parameters as $parameter) { |
|
| 262 | 2 | list($name, $value) = $parameter; |
|
| 263 | 2 | $field = $this->quoteStrategy->getColumnName($name, $targetClass, $this->platform); |
|
| 264 | 2 | $whereClauses[] = sprintf('te.%s = ?', $field); |
|
| 265 | 2 | $params[] = $value; |
|
| 266 | } |
||
| 267 | |||
| 268 | 7 | $tableName = $this->quoteStrategy->getTableName($targetClass, $this->platform); |
|
| 269 | 7 | $joinTable = $this->quoteStrategy->getJoinTableName($mapping, $associationSourceClass, $this->platform); |
|
| 270 | |||
| 271 | 7 | $rsm = new Query\ResultSetMappingBuilder($this->em); |
|
| 272 | 7 | $rsm->addRootEntityFromClassMetadata($targetClass->name, 'te'); |
|
| 273 | |||
| 274 | 7 | $sql = 'SELECT ' . $rsm->generateSelectClause() |
|
| 275 | 7 | . ' FROM ' . $tableName . ' te' |
|
| 276 | 7 | . ' JOIN ' . $joinTable . ' t ON' |
|
| 277 | 7 | . implode(' AND ', $onConditions) |
|
| 278 | 7 | . ' WHERE ' . implode(' AND ', $whereClauses); |
|
| 279 | |||
| 280 | 7 | $sql .= $this->getOrderingSql($criteria, $targetClass); |
|
| 281 | |||
| 282 | 7 | $sql .= $this->getLimitSql($criteria); |
|
| 283 | |||
| 284 | 7 | $stmt = $this->conn->executeQuery($sql, $params); |
|
| 285 | |||
| 286 | return $this |
||
| 287 | 7 | ->em |
|
| 288 | 7 | ->newHydrator(Query::HYDRATE_OBJECT) |
|
| 289 | 7 | ->hydrateAll($stmt, $rsm); |
|
| 290 | } |
||
| 291 | |||
| 292 | /** |
||
| 293 | * Generates the filter SQL for a given mapping. |
||
| 294 | * |
||
| 295 | * This method is not used for actually grabbing the related entities |
||
| 296 | * but when the extra-lazy collection methods are called on a filtered |
||
| 297 | * association. This is why besides the many to many table we also |
||
| 298 | * have to join in the actual entities table leading to additional |
||
| 299 | * JOIN. |
||
| 300 | * |
||
| 301 | * @param array $mapping Array containing mapping information. |
||
| 302 | * |
||
| 303 | * @return string[] ordered tuple: |
||
| 304 | * - JOIN condition to add to the SQL |
||
| 305 | * - WHERE condition to add to the SQL |
||
| 306 | */ |
||
| 307 | 32 | public function getFilterSql($mapping) |
|
| 324 | |||
| 325 | /** |
||
| 326 | * Generates the filter SQL for a given entity and table alias. |
||
| 327 | * |
||
| 328 | * @param ClassMetadata $targetEntity Metadata of the target entity. |
||
| 329 | * @param string $targetTableAlias The table alias of the joined/selected table. |
||
| 330 | * |
||
| 331 | * @return string The SQL query part to add to a query. |
||
| 332 | */ |
||
| 333 | 32 | protected function generateFilterConditionSQL(ClassMetadata $targetEntity, $targetTableAlias) |
|
| 347 | |||
| 348 | /** |
||
| 349 | * Generate ON condition |
||
| 350 | * |
||
| 351 | * @param array $mapping |
||
| 352 | * |
||
| 353 | * @return array |
||
| 354 | */ |
||
| 355 | 13 | protected function getOnConditionSQL($mapping) |
|
| 377 | |||
| 378 | /** |
||
| 379 | * {@inheritdoc} |
||
| 380 | * |
||
| 381 | * @override |
||
| 382 | */ |
||
| 383 | 17 | protected function getDeleteSQL(PersistentCollection $collection) |
|
| 397 | |||
| 398 | /** |
||
| 399 | * {@inheritdoc} |
||
| 400 | * |
||
| 401 | * Internal note: Order of the parameters must be the same as the order of the columns in getDeleteSql. |
||
| 402 | * @override |
||
| 403 | */ |
||
| 404 | 17 | protected function getDeleteSQLParameters(PersistentCollection $collection) |
|
| 426 | |||
| 427 | /** |
||
| 428 | * Gets the SQL statement used for deleting a row from the collection. |
||
| 429 | * |
||
| 430 | * @param \Doctrine\ORM\PersistentCollection $collection |
||
| 431 | * |
||
| 432 | * @return string[]|string[][] ordered tuple containing the SQL to be executed and an array |
||
| 433 | * of types for bound parameters |
||
| 434 | */ |
||
| 435 | 325 | protected function getDeleteRowSQL(PersistentCollection $collection) |
|
| 459 | |||
| 460 | /** |
||
| 461 | * Gets the SQL parameters for the corresponding SQL statement to delete the given |
||
| 462 | * element from the given collection. |
||
| 463 | * |
||
| 464 | * Internal note: Order of the parameters must be the same as the order of the columns in getDeleteRowSql. |
||
| 465 | * |
||
| 466 | * @param \Doctrine\ORM\PersistentCollection $collection |
||
| 467 | * @param mixed $element |
||
| 468 | * |
||
| 469 | * @return array |
||
| 470 | */ |
||
| 471 | 11 | protected function getDeleteRowSQLParameters(PersistentCollection $collection, $element) |
|
| 475 | |||
| 476 | /** |
||
| 477 | * Gets the SQL statement used for inserting a row in the collection. |
||
| 478 | * |
||
| 479 | * @param \Doctrine\ORM\PersistentCollection $collection |
||
| 480 | * |
||
| 481 | * @return string[]|string[][] ordered tuple containing the SQL to be executed and an array |
||
| 482 | * of types for bound parameters |
||
| 483 | */ |
||
| 484 | 325 | protected function getInsertRowSQL(PersistentCollection $collection) |
|
| 510 | |||
| 511 | /** |
||
| 512 | * Gets the SQL parameters for the corresponding SQL statement to insert the given |
||
| 513 | * element of the given collection into the database. |
||
| 514 | * |
||
| 515 | * Internal note: Order of the parameters must be the same as the order of the columns in getInsertRowSql. |
||
| 516 | * |
||
| 517 | * @param \Doctrine\ORM\PersistentCollection $collection |
||
| 518 | * @param mixed $element |
||
| 519 | * |
||
| 520 | * @return array |
||
| 521 | */ |
||
| 522 | 325 | protected function getInsertRowSQLParameters(PersistentCollection $collection, $element) |
|
| 526 | |||
| 527 | /** |
||
| 528 | * Collects the parameters for inserting/deleting on the join table in the order |
||
| 529 | * of the join table columns as specified in ManyToManyMapping#joinTableColumns. |
||
| 530 | * |
||
| 531 | * @param \Doctrine\ORM\PersistentCollection $collection |
||
| 532 | * @param object $element |
||
| 533 | * |
||
| 534 | * @return array |
||
| 535 | */ |
||
| 536 | 325 | private function collectJoinTableColumnParameters(PersistentCollection $collection, $element) |
|
| 570 | |||
| 571 | /** |
||
| 572 | * @param \Doctrine\ORM\PersistentCollection $collection |
||
| 573 | * @param string $key |
||
| 574 | * @param boolean $addFilters Whether the filter SQL should be included or not. |
||
| 575 | * |
||
| 576 | * @return array ordered vector: |
||
| 577 | * - quoted join table name |
||
| 578 | * - where clauses to be added for filtering |
||
| 579 | * - parameters to be bound for filtering |
||
| 580 | * - types of the parameters to be bound for filtering |
||
| 581 | */ |
||
| 582 | 7 | private function getJoinTableRestrictionsWithKey(PersistentCollection $collection, $key, $addFilters) |
|
| 583 | { |
||
| 584 | 7 | $filterMapping = $collection->getMapping(); |
|
| 585 | 7 | $mapping = $filterMapping; |
|
| 586 | 7 | $indexBy = $mapping['indexBy']; |
|
| 587 | 7 | $id = $this->uow->getEntityIdentifier($collection->getOwner()); |
|
| 588 | 7 | $sourceClass = $this->em->getClassMetadata($mapping['sourceEntity']); |
|
| 589 | 7 | $targetClass = $this->em->getClassMetadata($mapping['targetEntity']); |
|
| 590 | |||
| 591 | 7 | if (! $mapping['isOwningSide']) { |
|
| 592 | 3 | $associationSourceClass = $this->em->getClassMetadata($mapping['targetEntity']); |
|
| 593 | 3 | $mapping = $associationSourceClass->associationMappings[$mapping['mappedBy']]; |
|
| 594 | 3 | $joinColumns = $mapping['joinTable']['joinColumns']; |
|
| 595 | 3 | $sourceRelationMode = 'relationToTargetKeyColumns'; |
|
| 596 | 3 | $targetRelationMode = 'relationToSourceKeyColumns'; |
|
| 597 | } else { |
||
| 598 | 4 | $associationSourceClass = $this->em->getClassMetadata($mapping['sourceEntity']); |
|
| 599 | 4 | $joinColumns = $mapping['joinTable']['inverseJoinColumns']; |
|
| 600 | 4 | $sourceRelationMode = 'relationToSourceKeyColumns'; |
|
| 601 | 4 | $targetRelationMode = 'relationToTargetKeyColumns'; |
|
| 602 | } |
||
| 603 | |||
| 604 | 7 | $quotedJoinTable = $this->quoteStrategy->getJoinTableName($mapping, $associationSourceClass, $this->platform). ' t'; |
|
| 605 | 7 | $whereClauses = []; |
|
| 606 | 7 | $params = []; |
|
| 607 | 7 | $types = []; |
|
| 608 | |||
| 609 | 7 | $joinNeeded = ! in_array($indexBy, $targetClass->identifier); |
|
| 610 | |||
| 611 | 7 | if ($joinNeeded) { // extra join needed if indexBy is not a @id |
|
| 612 | 3 | $joinConditions = []; |
|
| 613 | |||
| 614 | 3 | foreach ($joinColumns as $joinTableColumn) { |
|
| 615 | 3 | $joinConditions[] = 't.' . $joinTableColumn['name'] . ' = tr.' . $joinTableColumn['referencedColumnName']; |
|
| 616 | } |
||
| 617 | |||
| 618 | 3 | $tableName = $this->quoteStrategy->getTableName($targetClass, $this->platform); |
|
| 619 | 3 | $quotedJoinTable .= ' JOIN ' . $tableName . ' tr ON ' . implode(' AND ', $joinConditions); |
|
| 620 | 3 | $columnName = $targetClass->getColumnName($indexBy); |
|
| 621 | |||
| 622 | 3 | $whereClauses[] = 'tr.' . $columnName . ' = ?'; |
|
| 623 | 3 | $params[] = $key; |
|
| 624 | 3 | $types[] = PersisterHelper::getTypeOfColumn($columnName, $targetClass, $this->em); |
|
| 625 | } |
||
| 626 | |||
| 627 | 7 | foreach ($mapping['joinTableColumns'] as $joinTableColumn) { |
|
| 628 | 7 | if (isset($mapping[$sourceRelationMode][$joinTableColumn])) { |
|
| 629 | 7 | $column = $mapping[$sourceRelationMode][$joinTableColumn]; |
|
| 630 | 7 | $whereClauses[] = 't.' . $joinTableColumn . ' = ?'; |
|
| 631 | 7 | $params[] = $sourceClass->containsForeignIdentifier |
|
| 632 | ? $id[$sourceClass->getFieldForColumn($column)] |
||
| 633 | 7 | : $id[$sourceClass->fieldNames[$column]]; |
|
| 634 | 7 | $types[] = PersisterHelper::getTypeOfColumn($column, $sourceClass, $this->em); |
|
| 635 | 7 | } elseif ( ! $joinNeeded) { |
|
| 636 | 4 | $column = $mapping[$targetRelationMode][$joinTableColumn]; |
|
| 637 | |||
| 638 | 4 | $whereClauses[] = 't.' . $joinTableColumn . ' = ?'; |
|
| 639 | 4 | $params[] = $key; |
|
| 640 | 7 | $types[] = PersisterHelper::getTypeOfColumn($column, $targetClass, $this->em); |
|
| 641 | } |
||
| 642 | } |
||
| 643 | |||
| 644 | 7 | if ($addFilters) { |
|
| 645 | 7 | list($joinTargetEntitySQL, $filterSql) = $this->getFilterSql($filterMapping); |
|
| 646 | |||
| 647 | 7 | if ($filterSql) { |
|
| 648 | $quotedJoinTable .= ' ' . $joinTargetEntitySQL; |
||
| 649 | $whereClauses[] = $filterSql; |
||
| 650 | } |
||
| 651 | } |
||
| 652 | |||
| 653 | 7 | return [$quotedJoinTable, $whereClauses, $params, $types]; |
|
| 654 | } |
||
| 655 | |||
| 656 | /** |
||
| 657 | * @param \Doctrine\ORM\PersistentCollection $collection |
||
| 658 | * @param object $element |
||
| 659 | * @param boolean $addFilters Whether the filter SQL should be included or not. |
||
| 660 | * |
||
| 661 | * @return array ordered vector: |
||
| 662 | * - quoted join table name |
||
| 663 | * - where clauses to be added for filtering |
||
| 664 | * - parameters to be bound for filtering |
||
| 665 | * - types of the parameters to be bound for filtering |
||
| 666 | */ |
||
| 667 | 9 | private function getJoinTableRestrictions(PersistentCollection $collection, $element, $addFilters) |
|
| 721 | |||
| 722 | /** |
||
| 723 | * Expands Criteria Parameters by walking the expressions and grabbing all |
||
| 724 | * parameters and types from it. |
||
| 725 | * |
||
| 726 | * @param \Doctrine\Common\Collections\Criteria $criteria |
||
| 727 | * |
||
| 728 | * @return array |
||
| 729 | */ |
||
| 730 | 7 | private function expandCriteriaParameters(Criteria $criteria) |
|
| 746 | |||
| 747 | /** |
||
| 748 | * @param Criteria $criteria |
||
| 749 | * @param ClassMetadata $targetClass |
||
| 750 | * @return string |
||
| 751 | */ |
||
| 752 | 7 | private function getOrderingSql(Criteria $criteria, ClassMetadata $targetClass) |
|
| 753 | { |
||
| 754 | 7 | $orderings = $criteria->getOrderings(); |
|
| 755 | 7 | if ($orderings) { |
|
| 756 | 2 | $orderBy = []; |
|
| 757 | 2 | foreach ($orderings as $name => $direction) { |
|
| 758 | 2 | $field = $this->quoteStrategy->getColumnName( |
|
| 759 | $name, |
||
| 760 | $targetClass, |
||
| 761 | 2 | $this->platform |
|
| 762 | ); |
||
| 763 | 2 | $orderBy[] = $field . ' ' . $direction; |
|
| 764 | } |
||
| 765 | |||
| 766 | 2 | return ' ORDER BY ' . implode(', ', $orderBy); |
|
| 767 | } |
||
| 768 | 5 | return ''; |
|
| 769 | } |
||
| 770 | |||
| 771 | /** |
||
| 772 | * @param Criteria $criteria |
||
| 773 | * @return string |
||
| 774 | * @throws \Doctrine\DBAL\DBALException |
||
| 775 | */ |
||
| 776 | 7 | private function getLimitSql(Criteria $criteria) |
|
| 785 | } |
||
| 786 |
This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.
Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.