Spaces:
No application file
No application file
| namespace Mautic\LeadBundle\Deduplicate; | |
| use Mautic\CoreBundle\Helper\ArrayHelper; | |
| use Mautic\LeadBundle\Deduplicate\Exception\SameContactException; | |
| use Mautic\LeadBundle\Deduplicate\Exception\ValueNotMergeableException; | |
| use Mautic\LeadBundle\Deduplicate\Helper\MergeValueHelper; | |
| use Mautic\LeadBundle\Entity\Lead; | |
| use Mautic\LeadBundle\Entity\MergeRecord; | |
| use Mautic\LeadBundle\Entity\MergeRecordRepository; | |
| use Mautic\LeadBundle\Event\LeadMergeEvent; | |
| use Mautic\LeadBundle\LeadEvents; | |
| use Mautic\LeadBundle\Model\LeadModel; | |
| use Psr\Log\LoggerInterface; | |
| use Symfony\Component\EventDispatcher\EventDispatcherInterface; | |
| class ContactMerger | |
| { | |
| /** | |
| * @var Lead | |
| */ | |
| protected $winner; | |
| /** | |
| * @var Lead | |
| */ | |
| protected $loser; | |
| public function __construct( | |
| protected LeadModel $leadModel, | |
| protected MergeRecordRepository $repo, | |
| protected EventDispatcherInterface $dispatcher, | |
| protected LoggerInterface $logger | |
| ) { | |
| } | |
| /** | |
| * @throws SameContactException | |
| */ | |
| public function merge(Lead $winner, Lead $loser): Lead | |
| { | |
| if ($winner->getId() === $loser->getId()) { | |
| throw new SameContactException(); | |
| } | |
| $this->logger->debug('CONTACT: ID# '.$loser->getId().' will be merged into ID# '.$winner->getId()); | |
| // Dispatch pre merge event | |
| $event = new LeadMergeEvent($winner, $loser); | |
| $this->dispatcher->dispatch($event, LeadEvents::LEAD_PRE_MERGE); | |
| // Merge everything | |
| $this->updateMergeRecords($winner, $loser) | |
| ->mergeTimestamps($winner, $loser) | |
| ->mergeIpAddressHistory($winner, $loser) | |
| ->mergeFieldData($winner, $loser) | |
| ->mergeOwners($winner, $loser) | |
| ->mergePoints($winner, $loser) | |
| ->mergeTags($winner, $loser); | |
| // Save the updated contact | |
| $this->leadModel->saveEntity($winner, false); | |
| // Dispatch post merge event | |
| $this->dispatcher->dispatch($event, LeadEvents::LEAD_POST_MERGE); | |
| // Delete the loser | |
| $this->leadModel->deleteEntity($loser); | |
| return $winner; | |
| } | |
| /** | |
| * Merge timestamps. | |
| * | |
| * @return $this | |
| */ | |
| public function mergeTimestamps(Lead $winner, Lead $loser) | |
| { | |
| // The winner should keep the most recent last active timestamp of the two | |
| if ($loser->getLastActive() > $winner->getLastActive()) { | |
| $winner->setLastActive($loser->getLastActive()); | |
| } | |
| /* | |
| * The winner should keep the oldest date identified timestamp | |
| * as long as the loser's date identified is not null. | |
| * Alternatively, if the winner's date identified is null, | |
| * use the loser's date identified (doesn't matter if it is null). | |
| */ | |
| if ((null !== $loser->getDateIdentified() && $loser->getDateIdentified() < $winner->getDateIdentified()) || null === $winner->getDateIdentified()) { | |
| $winner->setDateIdentified($loser->getDateIdentified()); | |
| } | |
| return $this; | |
| } | |
| /** | |
| * Merge IP history into the winner. | |
| * | |
| * @return $this | |
| */ | |
| public function mergeIpAddressHistory(Lead $winner, Lead $loser) | |
| { | |
| $ipAddresses = $loser->getIpAddresses(); | |
| foreach ($ipAddresses as $ip) { | |
| $winner->addIpAddress($ip); | |
| $this->logger->debug('CONTACT: Associating '.$winner->getId().' with IP '.$ip->getIpAddress()); | |
| } | |
| return $this; | |
| } | |
| /** | |
| * Merge custom field data into winner. | |
| * | |
| * @return $this | |
| */ | |
| public function mergeFieldData(Lead $winner, Lead $loser) | |
| { | |
| // Use the modified date if applicable or date added if the contact has never been edited | |
| $loserDate = $loser->getDateModified() ?: $loser->getDateAdded(); | |
| $winnerDate = $winner->getDateModified() ?: $winner->getDateAdded(); | |
| // When it comes to data, keep the newest value regardless of the winner/loser | |
| $newest = ($loserDate > $winnerDate) ? $loser : $winner; | |
| $oldest = ($newest->getId() === $winner->getId()) ? $loser : $winner; | |
| // It may happen that the Lead entities doesn't have fields fill in. Fill them in if not. | |
| if (!$newest->hasFields()) { | |
| $newest->setFields($this->leadModel->getRepository()->getFieldValues($newest->getId())); | |
| } | |
| if (!$oldest->hasFields()) { | |
| $oldest->setFields($this->leadModel->getRepository()->getFieldValues($oldest->getId())); | |
| } | |
| $newestFields = $newest->getProfileFields(); | |
| $oldestFields = $oldest->getProfileFields(); | |
| foreach (array_keys($newestFields) as $field) { | |
| if (in_array($field, ['id', 'points'])) { | |
| // Let mergePoints() take care of this | |
| continue; | |
| } | |
| try { | |
| $fromValue = empty($oldestFields[$field]) ? 'empty' : $oldestFields[$field]; | |
| $fieldDetails = $winner->getField($field); | |
| if (false === $fieldDetails) { | |
| throw new ValueNotMergeableException($fromValue, false); | |
| } | |
| $defaultValue = ArrayHelper::getValue('default_value', $fieldDetails); | |
| $newValue = MergeValueHelper::getMergeValue( | |
| $newestFields[$field], | |
| $oldestFields[$field], | |
| $winner->getFieldValue($field), | |
| $defaultValue, | |
| $newest->isAnonymous() | |
| ); | |
| $winner->addUpdatedField($field, $newValue); | |
| $this->logger->debug("CONTACT: Updated {$field} from {$fromValue} to {$newValue} for {$winner->getId()}"); | |
| } catch (ValueNotMergeableException $exception) { | |
| $this->logger->info("CONTACT: {$field} is not mergeable for {$winner->getId()} - {$exception->getMessage()}"); | |
| } | |
| } | |
| return $this; | |
| } | |
| /** | |
| * Merge owners if the winner isn't already assigned an owner. | |
| * | |
| * @return $this | |
| */ | |
| public function mergeOwners(Lead $winner, Lead $loser) | |
| { | |
| $oldOwner = $winner->getOwner(); | |
| $newOwner = $loser->getOwner(); | |
| if (null === $oldOwner && null !== $newOwner) { | |
| $winner->setOwner($newOwner); | |
| $this->logger->debug("CONTACT: New owner of {$winner->getId()} is {$newOwner->getId()}"); | |
| } | |
| return $this; | |
| } | |
| /** | |
| * Sum points from both contacts. | |
| * | |
| * @return $this | |
| */ | |
| public function mergePoints(Lead $winner, Lead $loser) | |
| { | |
| $winnerPoints = (int) $winner->getPoints(); | |
| $loserPoints = (int) $loser->getPoints(); | |
| $winner->adjustPoints($loserPoints); | |
| $this->logger->debug( | |
| 'CONTACT: Adding '.$loserPoints.' points from contact ID #'.$loser->getId().' to contact ID #'.$winner->getId().' with '.$winnerPoints | |
| .' points' | |
| ); | |
| return $this; | |
| } | |
| /** | |
| * Merge tags from loser into winner. | |
| * | |
| * @return $this | |
| */ | |
| public function mergeTags(Lead $winner, Lead $loser) | |
| { | |
| $loserTags = $loser->getTags(); | |
| $addTags = $loserTags->getKeys(); | |
| $this->leadModel->modifyTags($winner, $addTags, null, false); | |
| return $this; | |
| } | |
| /** | |
| * Merge past merge records into the winner. | |
| * | |
| * @return $this | |
| */ | |
| private function updateMergeRecords(Lead $winner, Lead $loser) | |
| { | |
| // Update merge records for the lead about to be deleted | |
| $this->repo->moveMergeRecord($loser->getId(), $winner->getId()); | |
| // Create an entry this contact was merged | |
| $mergeRecord = new MergeRecord(); | |
| $mergeRecord->setContact($winner) | |
| ->setDateAdded() | |
| ->setName($loser->getPrimaryIdentifier()) | |
| ->setMergedId($loser->getId()); | |
| $this->repo->saveEntity($mergeRecord); | |
| return $this; | |
| } | |
| } | |