diff --git a/src/Bundle/ChillPersonBundle/Service/AccompanyingPeriodWork/AccompanyingPeriodWorkMergeService.php b/src/Bundle/ChillPersonBundle/Service/AccompanyingPeriodWork/AccompanyingPeriodWorkMergeService.php index 155a180a7..4b48ffcbf 100644 --- a/src/Bundle/ChillPersonBundle/Service/AccompanyingPeriodWork/AccompanyingPeriodWorkMergeService.php +++ b/src/Bundle/ChillPersonBundle/Service/AccompanyingPeriodWork/AccompanyingPeriodWorkMergeService.php @@ -12,7 +12,7 @@ declare(strict_types=1); namespace Chill\PersonBundle\Service\AccompanyingPeriodWork; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork; -use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Exception; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\ClassMetadata; @@ -20,6 +20,9 @@ class AccompanyingPeriodWorkMergeService { public function __construct(private readonly EntityManagerInterface $em) {} + /** + * @throws Exception + */ public function merge(AccompanyingPeriodWork $toKeep, AccompanyingPeriodWork $toDelete): void { // Transfer non-duplicate data @@ -32,96 +35,140 @@ class AccompanyingPeriodWorkMergeService $this->em->flush(); } + /** + * @throws Exception + */ private function transferData(AccompanyingPeriodWork $toKeep, AccompanyingPeriodWork $toDelete): void { - $excludedProperties = ['id', 'createdAt', 'createdBy', 'updatedAt', 'updatedBy']; - $reflection = new \ReflectionClass(AccompanyingPeriodWork::class); + $conn = $this->em->getConnection(); - foreach ($reflection->getProperties() as $property) { - if (in_array($property->getName(), $excludedProperties, true)) { - continue; - } - - $toKeepValue = $property->getValue($toKeep); - $toDeleteValue = $property->getValue($toDelete); - - if (null === $toKeepValue && null !== $toDeleteValue) { - $property->setValue($toKeep, $toDeleteValue); - } - - if ($toKeepValue instanceof Collection - && $toDeleteValue instanceof Collection) { - foreach ($toDeleteValue as $item) { - if (!$toKeepValue->contains($item)) { - $toKeepValue->add($item); - } - } + $sqlStatements = [ + $this->generateStartDateSQL(), + $this->generateEndDateSQL(), + $this->generateCommentSQL(), + ]; + + $conn->beginTransaction(); + + try { + foreach ($sqlStatements as $sql) { + $conn->executeQuery($sql, ['toDelete' => $toDelete->getId(), 'toKeep' => $toKeep->getId()]); } + $conn->commit(); + } catch (\Exception $e) { + $conn->rollBack(); + throw $e; } } + private function generateStartDateSQL(): string + { + return ' + UPDATE chill_person_accompanying_period_work + SET startdate = LEAST( + COALESCE((SELECT startdate FROM chill_person_accompanying_period_work WHERE id = :toDelete), startdate), + startdate + ) + WHERE id = :toKeep'; + } + + private function generateEndDateSQL(): string + { + return ' + UPDATE chill_person_accompanying_period_work + SET enddate = + CASE + WHEN (SELECT enddate FROM chill_person_accompanying_period_work WHERE id = :toDelete) IS NULL + OR enddate IS NULL + THEN NULL + ELSE GREATEST( + COALESCE((SELECT enddate FROM chill_person_accompanying_period_work WHERE id = :toDelete), enddate), + enddate + ) + END + WHERE id = :toKeep'; + } + + private function generateCommentSQL(): string + { + return " + UPDATE chill_person_accompanying_period_work + SET note = CONCAT_WS( + '\n', + NULLIF(TRIM(note), ''), + NULLIF(TRIM((SELECT note FROM chill_person_accompanying_period_work WHERE id = :toDelete)), '') + ) + WHERE id = :toKeep"; + } + + /** + * @throws Exception + */ private function updateReferences(AccompanyingPeriodWork $toKeep, AccompanyingPeriodWork $toDelete): void { $allMeta = $this->em->getMetadataFactory()->getAllMetadata(); + $conn = $this->em->getConnection(); + $sqlStatements = []; foreach ($allMeta as $meta) { + if ($meta->isMappedSuperclass) { + continue; + } + foreach ($meta->getAssociationMappings() as $assoc) { if (AccompanyingPeriodWork::class !== $assoc['targetEntity']) { - continue; // Skip unrelated associations + continue; } - $entityClass = $meta->getName(); - $associationField = $assoc['fieldName']; - if ($assoc['type'] & ClassMetadata::TO_ONE) { - // Handle ManyToOne or OneToOne - $qb = $this->em->createQueryBuilder(); - $qb->update($entityClass, 'e') - ->set("e.{$associationField}", ':toKeep') - ->where("e.{$associationField} = :toDelete") - ->setParameter('toKeep', $toKeep) - ->setParameter('toDelete', $toDelete) - ->getQuery() - ->execute(); + if ('handlingThirdparty' === $assoc['fieldName']) { + continue; + } + $sqlStatements[] = $this->generateToOneUpdateQuery($meta, $assoc); } if ($assoc['type'] & ClassMetadata::TO_MANY) { - // Handle ManyToMany or OneToMany (inverse side) - $repo = $this->em->getRepository($entityClass); - $linkedEntities = $repo->createQueryBuilder('e') - ->join("e.{$associationField}", 't') - ->where('t = :toDelete') - ->setParameter('toDelete', $toDelete) - ->getQuery() - ->getResult(); - - foreach ($linkedEntities as $entity) { - $getter = 'get'.ucfirst($associationField); - $setter = 'set'.ucfirst($associationField); - $adder = 'add'.ucfirst(rtrim($associationField, 's')); - $remover = 'remove'.ucfirst(rtrim($associationField, 's')); - - if (method_exists($entity, $getter) && method_exists($entity, $setter)) { - // For OneToMany owning side - $collection = $entity->{$getter}(); - if ($collection->contains($toDelete)) { - $collection->removeElement($toDelete); - if (!$collection->contains($toKeep)) { - $collection->add($toKeep); - } - } - } elseif (method_exists($entity, $adder) && method_exists($entity, $remover)) { - // For ManyToMany - $entity->{$remover}($toDelete); - $entity->{$adder}($toKeep); - } - - $this->em->persist($entity); + if (!isset($assoc['joinTable'])) { + continue; } + $sqlStatements = array_merge($sqlStatements, $this->generateToManyUpdateQueries($assoc)); } } } - $this->em->flush(); + $conn->beginTransaction(); + try { + foreach ($sqlStatements as $sql) { + $conn->executeStatement($sql, ['toDelete' => $toDelete->getId(), 'toKeep' => $toKeep->getId()]); + } + $conn->commit(); + } catch (\Exception $e) { + $conn->rollBack(); + throw $e; + } + } + + private function generateToOneUpdateQuery(ClassMetadata $meta, array $assoc): string + { + $tableName = $meta->getTableName(); + $joinColumn = $meta->getSingleAssociationJoinColumnName($assoc['fieldName']); + + return "UPDATE {$tableName} SET {$joinColumn} = :toKeep WHERE {$joinColumn} = :toDelete"; + } + + private function generateToManyUpdateQueries(array $assoc): array + { + $sqls = []; + $joinTable = $assoc['joinTable']['name']; + $owningColumn = $assoc['joinTable']['joinColumns'][0]['name']; + $inverseColumn = $assoc['joinTable']['inverseJoinColumns'][0]['name']; + + // Insert relations, skip already existing ones + $sqls[] = "INSERT IGNORE INTO {$joinTable} ({$owningColumn}, {$inverseColumn}) + SELECT :toKeep, {$inverseColumn} FROM {$joinTable} WHERE {$owningColumn} = :toDelete"; + // Delete old references + $sqls[] = "DELETE FROM {$joinTable} WHERE {$owningColumn} = :toDelete"; + + return $sqls; } }