Complex classes like ModelManager 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 ModelManager, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 42 | class ModelManager implements ModelManagerInterface, LockInterface |
||
| 43 | { |
||
| 44 | public const ID_SEPARATOR = '~'; |
||
| 45 | |||
| 46 | /** |
||
| 47 | * @var ManagerRegistry |
||
| 48 | */ |
||
| 49 | protected $registry; |
||
| 50 | |||
| 51 | /** |
||
| 52 | * @var PropertyAccessorInterface |
||
| 53 | */ |
||
| 54 | protected $propertyAccessor; |
||
| 55 | |||
| 56 | /** |
||
| 57 | * @var EntityManager[] |
||
| 58 | */ |
||
| 59 | protected $cache = []; |
||
| 60 | |||
| 61 | /** |
||
| 62 | * NEXT_MAJOR: Make $propertyAccessor mandatory. |
||
| 63 | */ |
||
| 64 | public function __construct(ManagerRegistry $registry, ?PropertyAccessorInterface $propertyAccessor = null) |
||
| 82 | |||
| 83 | /** |
||
| 84 | * @param string $class |
||
| 85 | * |
||
| 86 | * @return ClassMetadata |
||
| 87 | */ |
||
| 88 | public function getMetadata($class) |
||
| 92 | |||
| 93 | /** |
||
| 94 | * Returns the model's metadata holding the fully qualified property, and the last |
||
| 95 | * property name. |
||
| 96 | * |
||
| 97 | * @param string $baseClass The base class of the model holding the fully qualified property |
||
| 98 | * @param string $propertyFullName The name of the fully qualified property (dot ('.') separated |
||
| 99 | * property string) |
||
| 100 | * |
||
| 101 | * @return array( |
||
| 102 | * \Doctrine\ORM\Mapping\ClassMetadata $parentMetadata, |
||
| 103 | * string $lastPropertyName, |
||
| 104 | * array $parentAssociationMappings |
||
| 105 | * ) |
||
| 106 | */ |
||
| 107 | public function getParentMetadataForProperty($baseClass, $propertyFullName) |
||
| 132 | |||
| 133 | /** |
||
| 134 | * @param string $class |
||
| 135 | * |
||
| 136 | * @return bool |
||
| 137 | */ |
||
| 138 | public function hasMetadata($class) |
||
| 142 | |||
| 143 | public function getNewFieldDescriptionInstance($class, $name, array $options = []) |
||
| 174 | |||
| 175 | public function create($object): void |
||
| 195 | |||
| 196 | public function update($object): void |
||
| 216 | |||
| 217 | public function delete($object): void |
||
| 237 | |||
| 238 | public function getLockVersion($object) |
||
| 248 | |||
| 249 | public function lock($object, $expectedVersion): void |
||
| 264 | |||
| 265 | public function find($class, $id) |
||
| 280 | |||
| 281 | public function findBy($class, array $criteria = []) |
||
| 285 | |||
| 286 | public function findOneBy($class, array $criteria = []) |
||
| 290 | |||
| 291 | /** |
||
| 292 | * @param string|object $class |
||
| 293 | * |
||
| 294 | * @return EntityManager |
||
| 295 | */ |
||
| 296 | public function getEntityManager($class) |
||
| 314 | |||
| 315 | /** |
||
| 316 | * NEXT_MAJOR: Remove this method. |
||
| 317 | * |
||
| 318 | * @deprecated since sonata-project/doctrine-orm-admin-bundle 3.x and will be removed in version 4.0 |
||
| 319 | */ |
||
| 320 | public function getParentFieldDescription($parentAssociationMapping, $class) |
||
| 321 | { |
||
| 322 | @trigger_error(sprintf( |
||
| 323 | 'Method %s() is deprecated since sonata-project/doctrine-orm-admin-bundle 3.x and will be removed in 4.0', |
||
| 324 | __METHOD__ |
||
| 325 | ), E_USER_DEPRECATED); |
||
| 326 | |||
| 327 | $fieldName = $parentAssociationMapping['fieldName']; |
||
| 328 | |||
| 329 | $metadata = $this->getMetadata($class); |
||
| 330 | |||
| 331 | $associatingMapping = $metadata->associationMappings[$parentAssociationMapping]; |
||
| 332 | |||
| 333 | $fieldDescription = $this->getNewFieldDescriptionInstance($class, $fieldName); |
||
| 334 | $fieldDescription->setName($parentAssociationMapping); |
||
| 335 | $fieldDescription->setAssociationMapping($associatingMapping); |
||
| 336 | |||
| 337 | return $fieldDescription; |
||
| 338 | } |
||
| 339 | |||
| 340 | public function createQuery($class, $alias = 'o') |
||
| 341 | { |
||
| 342 | $repository = $this->getEntityManager($class)->getRepository($class); |
||
| 343 | |||
| 344 | return new ProxyQuery($repository->createQueryBuilder($alias)); |
||
| 345 | } |
||
| 346 | |||
| 347 | public function executeQuery($query) |
||
| 348 | { |
||
| 349 | if ($query instanceof QueryBuilder) { |
||
| 350 | return $query->getQuery()->execute(); |
||
| 351 | } |
||
| 352 | |||
| 353 | return $query->execute(); |
||
| 354 | } |
||
| 355 | |||
| 356 | /** |
||
| 357 | * NEXT_MAJOR: Remove this function. |
||
| 358 | * |
||
| 359 | * @deprecated since sonata-project/doctrine-orm-admin-bundle 3.18. To be removed in 4.0. |
||
| 360 | */ |
||
| 361 | public function getModelIdentifier($class) |
||
| 362 | { |
||
| 363 | return $this->getMetadata($class)->identifier; |
||
| 364 | } |
||
| 365 | |||
| 366 | public function getIdentifierValues($entity) |
||
| 367 | { |
||
| 368 | // Fix code has an impact on performance, so disable it ... |
||
| 369 | //$entityManager = $this->getEntityManager($entity); |
||
| 370 | //if (!$entityManager->getUnitOfWork()->isInIdentityMap($entity)) { |
||
| 371 | // throw new \RuntimeException('Entities passed to the choice field must be managed'); |
||
| 372 | //} |
||
| 373 | |||
| 374 | $class = ClassUtils::getClass($entity); |
||
| 375 | $metadata = $this->getMetadata($class); |
||
| 376 | $platform = $this->getEntityManager($class)->getConnection()->getDatabasePlatform(); |
||
| 377 | |||
| 378 | $identifiers = []; |
||
| 379 | |||
| 380 | foreach ($metadata->getIdentifierValues($entity) as $name => $value) { |
||
| 381 | if (!\is_object($value)) { |
||
| 382 | $identifiers[] = $value; |
||
| 383 | |||
| 384 | continue; |
||
| 385 | } |
||
| 386 | |||
| 387 | $fieldType = $metadata->getTypeOfField($name); |
||
| 388 | $type = $fieldType && Type::hasType($fieldType) ? Type::getType($fieldType) : null; |
||
| 389 | if ($type) { |
||
| 390 | $identifiers[] = $this->getValueFromType($value, $type, $fieldType, $platform); |
||
| 391 | |||
| 392 | continue; |
||
| 393 | } |
||
| 394 | |||
| 395 | $identifierMetadata = $this->getMetadata(ClassUtils::getClass($value)); |
||
| 396 | |||
| 397 | foreach ($identifierMetadata->getIdentifierValues($value) as $value) { |
||
| 398 | $identifiers[] = $value; |
||
| 399 | } |
||
| 400 | } |
||
| 401 | |||
| 402 | return $identifiers; |
||
| 403 | } |
||
| 404 | |||
| 405 | public function getIdentifierFieldNames($class) |
||
| 406 | { |
||
| 407 | return $this->getMetadata($class)->getIdentifierFieldNames(); |
||
| 408 | } |
||
| 409 | |||
| 410 | public function getNormalizedIdentifier($entity) |
||
| 411 | { |
||
| 412 | // NEXT_MAJOR: Remove the following 2 checks and declare "object" as type for argument 1. |
||
| 413 | if (null === $entity) { |
||
| 414 | @trigger_error(sprintf( |
||
| 415 | 'Passing null as argument 1 for %s() is deprecated since sonata-project/doctrine-orm-admin-bundle 3.20 and will be not allowed in version 4.0.', |
||
| 416 | __METHOD__ |
||
| 417 | ), E_USER_DEPRECATED); |
||
| 418 | |||
| 419 | return null; |
||
| 420 | } |
||
| 421 | |||
| 422 | if (!\is_object($entity)) { |
||
| 423 | throw new \RuntimeException('Invalid argument, object or null required'); |
||
| 424 | } |
||
| 425 | |||
| 426 | if (\in_array($this->getEntityManager($entity)->getUnitOfWork()->getEntityState($entity), [ |
||
| 427 | UnitOfWork::STATE_NEW, |
||
| 428 | UnitOfWork::STATE_REMOVED, |
||
| 429 | ], true)) { |
||
| 430 | // NEXT_MAJOR: Uncomment the following exception, remove the deprecation and the return statement inside this conditional block. |
||
| 431 | // throw new \InvalidArgumentException(sprintf( |
||
| 432 | // 'Can not get the normalized identifier for %s since it is in state %u.', |
||
| 433 | // ClassUtils::getClass($entity), |
||
| 434 | // $this->getEntityManager($entity)->getUnitOfWork()->getEntityState($entity) |
||
| 435 | // )); |
||
| 436 | |||
| 437 | @trigger_error(sprintf( |
||
| 438 | 'Passing an object which is in state %u (new) or %u (removed) as argument 1 for %s() is deprecated since sonata-project/doctrine-orm-admin-bundle 3.20' |
||
| 439 | .'and will be not allowed in version 4.0.', |
||
| 440 | UnitOfWork::STATE_NEW, |
||
| 441 | UnitOfWork::STATE_REMOVED, |
||
| 442 | __METHOD__ |
||
| 443 | ), E_USER_DEPRECATED); |
||
| 444 | |||
| 445 | return null; |
||
| 446 | } |
||
| 447 | |||
| 448 | $values = $this->getIdentifierValues($entity); |
||
| 449 | |||
| 450 | if (0 === \count($values)) { |
||
| 451 | return null; |
||
| 452 | } |
||
| 453 | |||
| 454 | return implode(self::ID_SEPARATOR, $values); |
||
| 455 | } |
||
| 456 | |||
| 457 | /** |
||
| 458 | * {@inheritdoc} |
||
| 459 | * |
||
| 460 | * The ORM implementation does nothing special but you still should use |
||
| 461 | * this method when using the id in a URL to allow for future improvements. |
||
| 462 | */ |
||
| 463 | public function getUrlSafeIdentifier($entity) |
||
| 464 | { |
||
| 465 | // NEXT_MAJOR: Remove the following check and declare "object" as type for argument 1. |
||
| 466 | if (!\is_object($entity)) { |
||
| 467 | @trigger_error(sprintf( |
||
| 468 | 'Passing other type than object for argument 1 for %s() is deprecated since sonata-project/doctrine-orm-admin-bundle 3.20 and will be not allowed in version 4.0.', |
||
| 469 | __METHOD__ |
||
| 470 | ), E_USER_DEPRECATED); |
||
| 471 | |||
| 472 | return null; |
||
| 473 | } |
||
| 474 | |||
| 475 | return $this->getNormalizedIdentifier($entity); |
||
| 476 | } |
||
| 477 | |||
| 478 | public function addIdentifiersToQuery($class, ProxyQueryInterface $queryProxy, array $idx): void |
||
| 479 | { |
||
| 480 | $fieldNames = $this->getIdentifierFieldNames($class); |
||
| 481 | $qb = $queryProxy->getQueryBuilder(); |
||
| 482 | |||
| 483 | $prefix = uniqid(); |
||
| 484 | $sqls = []; |
||
| 485 | foreach ($idx as $pos => $id) { |
||
| 486 | $ids = explode(self::ID_SEPARATOR, $id); |
||
| 487 | |||
| 488 | $ands = []; |
||
| 489 | foreach ($fieldNames as $posName => $name) { |
||
| 490 | $parameterName = sprintf('field_%s_%s_%d', $prefix, $name, $pos); |
||
| 491 | $ands[] = sprintf('%s.%s = :%s', current($qb->getRootAliases()), $name, $parameterName); |
||
| 492 | $qb->setParameter($parameterName, $ids[$posName]); |
||
| 493 | } |
||
| 494 | |||
| 495 | $sqls[] = implode(' AND ', $ands); |
||
| 496 | } |
||
| 497 | |||
| 498 | $qb->andWhere(sprintf('( %s )', implode(' OR ', $sqls))); |
||
| 499 | } |
||
| 500 | |||
| 501 | public function batchDelete($class, ProxyQueryInterface $queryProxy): void |
||
| 502 | { |
||
| 503 | $queryProxy->select('DISTINCT '.current($queryProxy->getRootAliases())); |
||
| 504 | |||
| 505 | try { |
||
| 506 | $entityManager = $this->getEntityManager($class); |
||
| 507 | |||
| 508 | $i = 0; |
||
| 509 | foreach ($queryProxy->getQuery()->iterate() as $pos => $object) { |
||
| 510 | $entityManager->remove($object[0]); |
||
| 511 | |||
| 512 | if (0 === (++$i % 20)) { |
||
| 513 | $entityManager->flush(); |
||
| 514 | $entityManager->clear(); |
||
| 515 | } |
||
| 516 | } |
||
| 517 | |||
| 518 | $entityManager->flush(); |
||
| 519 | $entityManager->clear(); |
||
| 520 | } catch (\PDOException | DBALException $e) { |
||
| 521 | throw new ModelManagerException('', 0, $e); |
||
| 522 | } |
||
| 523 | } |
||
| 524 | |||
| 525 | public function getDataSourceIterator(DatagridInterface $datagrid, array $fields, $firstResult = null, $maxResult = null) |
||
| 526 | { |
||
| 527 | $datagrid->buildPager(); |
||
| 528 | $query = $datagrid->getQuery(); |
||
| 529 | |||
| 530 | $query->select('DISTINCT '.current($query->getRootAliases())); |
||
| 531 | $query->setFirstResult($firstResult); |
||
| 532 | $query->setMaxResults($maxResult); |
||
| 533 | |||
| 534 | if ($query instanceof ProxyQueryInterface) { |
||
| 535 | $sortBy = $query->getSortBy(); |
||
| 536 | |||
| 537 | if (!empty($sortBy)) { |
||
| 538 | $query->addOrderBy($sortBy, $query->getSortOrder()); |
||
| 539 | $query = $query->getQuery(); |
||
| 540 | $query->setHint(Query::HINT_CUSTOM_TREE_WALKERS, [OrderByToSelectWalker::class]); |
||
| 541 | } else { |
||
| 542 | $query = $query->getQuery(); |
||
| 543 | } |
||
| 544 | } |
||
| 545 | |||
| 546 | return new DoctrineORMQuerySourceIterator($query, $fields); |
||
| 547 | } |
||
| 548 | |||
| 549 | public function getExportFields($class) |
||
| 550 | { |
||
| 551 | $metadata = $this->getEntityManager($class)->getClassMetadata($class); |
||
| 552 | |||
| 553 | return $metadata->getFieldNames(); |
||
| 554 | } |
||
| 555 | |||
| 556 | public function getModelInstance($class) |
||
| 557 | { |
||
| 558 | $r = new \ReflectionClass($class); |
||
| 559 | if ($r->isAbstract()) { |
||
| 560 | throw new \RuntimeException(sprintf('Cannot initialize abstract class: %s', $class)); |
||
| 561 | } |
||
| 562 | |||
| 563 | $constructor = $r->getConstructor(); |
||
| 564 | |||
| 565 | if (null !== $constructor && (!$constructor->isPublic() || $constructor->getNumberOfRequiredParameters() > 0)) { |
||
| 566 | return $r->newInstanceWithoutConstructor(); |
||
| 567 | } |
||
| 568 | |||
| 569 | return new $class(); |
||
| 570 | } |
||
| 571 | |||
| 572 | public function getSortParameters(FieldDescriptionInterface $fieldDescription, DatagridInterface $datagrid) |
||
| 573 | { |
||
| 574 | $values = $datagrid->getValues(); |
||
| 575 | |||
| 576 | if ($this->isFieldAlreadySorted($fieldDescription, $datagrid)) { |
||
| 577 | if ('ASC' === $values['_sort_order']) { |
||
| 578 | $values['_sort_order'] = 'DESC'; |
||
| 579 | } else { |
||
| 580 | $values['_sort_order'] = 'ASC'; |
||
| 581 | } |
||
| 582 | } else { |
||
| 583 | $values['_sort_order'] = 'ASC'; |
||
| 584 | } |
||
| 585 | |||
| 586 | $values['_sort_by'] = \is_string($fieldDescription->getOption('sortable')) ? $fieldDescription->getOption('sortable') : $fieldDescription->getName(); |
||
| 587 | |||
| 588 | return ['filter' => $values]; |
||
| 589 | } |
||
| 590 | |||
| 591 | public function getPaginationParameters(DatagridInterface $datagrid, $page) |
||
| 592 | { |
||
| 593 | $values = $datagrid->getValues(); |
||
| 594 | |||
| 595 | if (isset($values['_sort_by']) && $values['_sort_by'] instanceof FieldDescriptionInterface) { |
||
| 596 | $values['_sort_by'] = $values['_sort_by']->getName(); |
||
| 597 | } |
||
| 598 | $values['_page'] = $page; |
||
| 599 | |||
| 600 | return ['filter' => $values]; |
||
| 601 | } |
||
| 602 | |||
| 603 | public function getDefaultSortValues($class) |
||
| 604 | { |
||
| 605 | return [ |
||
| 606 | '_page' => 1, |
||
| 607 | '_per_page' => 25, |
||
| 608 | ]; |
||
| 609 | } |
||
| 610 | |||
| 611 | public function getDefaultPerPageOptions(string $class): array |
||
| 612 | { |
||
| 613 | return [10, 25, 50, 100, 250]; |
||
| 614 | } |
||
| 615 | |||
| 616 | public function modelTransform($class, $instance) |
||
| 617 | { |
||
| 618 | return $instance; |
||
| 619 | } |
||
| 620 | |||
| 621 | public function modelReverseTransform($class, array $array = []) |
||
| 622 | { |
||
| 623 | $instance = $this->getModelInstance($class); |
||
| 624 | $metadata = $this->getMetadata($class); |
||
| 625 | |||
| 626 | foreach ($array as $name => $value) { |
||
| 627 | $property = $this->getFieldName($metadata, $name); |
||
| 628 | $this->propertyAccessor->setValue($instance, $property, $value); |
||
| 629 | } |
||
| 630 | |||
| 631 | return $instance; |
||
| 632 | } |
||
| 633 | |||
| 634 | public function getModelCollectionInstance($class) |
||
| 638 | |||
| 639 | public function collectionClear(&$collection) |
||
| 643 | |||
| 644 | public function collectionHasElement(&$collection, &$element) |
||
| 648 | |||
| 649 | public function collectionAddElement(&$collection, &$element) |
||
| 653 | |||
| 654 | public function collectionRemoveElement(&$collection, &$element) |
||
| 658 | |||
| 659 | /** |
||
| 660 | * NEXT_MAJOR: Remove this method. |
||
| 661 | * |
||
| 662 | * @param string $property |
||
| 663 | * |
||
| 664 | * @return mixed |
||
| 665 | */ |
||
| 666 | protected function camelize($property) |
||
| 675 | |||
| 676 | private function getFieldName(ClassMetadata $metadata, string $name): string |
||
| 677 | { |
||
| 678 | if (\array_key_exists($name, $metadata->fieldMappings)) { |
||
| 679 | return $metadata->fieldMappings[$name]['fieldName']; |
||
| 680 | } |
||
| 681 | |||
| 682 | if (\array_key_exists($name, $metadata->associationMappings)) { |
||
| 683 | return $metadata->associationMappings[$name]['fieldName']; |
||
| 684 | } |
||
| 685 | |||
| 686 | return $name; |
||
| 687 | } |
||
| 688 | |||
| 689 | private function isFieldAlreadySorted(FieldDescriptionInterface $fieldDescription, DatagridInterface $datagrid): bool |
||
| 690 | { |
||
| 691 | $values = $datagrid->getValues(); |
||
| 692 | |||
| 693 | if (!isset($values['_sort_by']) || !$values['_sort_by'] instanceof FieldDescriptionInterface) { |
||
| 694 | return false; |
||
| 695 | } |
||
| 700 | |||
| 701 | /** |
||
| 702 | * @param mixed $value |
||
| 703 | */ |
||
| 704 | private function getValueFromType($value, Type $type, string $fieldType, AbstractPlatform $platform): string |
||
| 724 | } |
||
| 725 |
If you suppress an error, we recommend checking for the error condition explicitly: