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; | |
} | |
} | |