234 lines
7.7 KiB
PHP
Executable File
234 lines
7.7 KiB
PHP
Executable File
<?php
|
|
|
|
namespace Mautic\LeadBundle\Model;
|
|
|
|
use Doctrine\Common\Collections\ArrayCollection;
|
|
use Doctrine\ORM\PersistentCollection;
|
|
use Mautic\CacheBundle\Cache\CacheProvider;
|
|
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
|
use Mautic\CoreBundle\Model\MauticModelInterface;
|
|
use Mautic\LeadBundle\Entity\DoNotContact as DNC;
|
|
use Mautic\LeadBundle\Entity\DoNotContactRepository;
|
|
use Mautic\LeadBundle\Entity\Lead;
|
|
|
|
class DoNotContact implements MauticModelInterface
|
|
{
|
|
public function __construct(
|
|
protected LeadModel $leadModel,
|
|
protected DoNotContactRepository $dncRepo,
|
|
protected CoreParametersHelper $coreParametersHelper,
|
|
protected CacheProvider $cacheProvider,
|
|
) {
|
|
}
|
|
|
|
/**
|
|
* Remove a Lead's DNC entry based on channel.
|
|
*
|
|
* @param int|Lead $contact
|
|
* @param string $channel
|
|
* @param bool|true $persist
|
|
* @param int|null $reason
|
|
*/
|
|
public function removeDncForContact($contact, $channel, $persist = true, $reason = null): bool
|
|
{
|
|
if (is_numeric($contact)) {
|
|
$contact = $this->leadModel->getEntity($contact);
|
|
}
|
|
|
|
if (null === $contact) {
|
|
return false;
|
|
}
|
|
|
|
/** @var DNC $dnc */
|
|
foreach ($contact->getDoNotContact() as $dnc) {
|
|
if ($dnc->getChannel() === $channel) {
|
|
// Skip if reason doesn't match
|
|
// Some integrations (Sugar CRM) can use both reasons (unsubscribed, bounced)
|
|
if ($reason && $dnc->getReason() != $reason) {
|
|
continue;
|
|
}
|
|
$contact->removeDoNotContactEntry($dnc);
|
|
|
|
if ($persist) {
|
|
$this->leadModel->saveEntity($contact);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Create a DNC entry for a lead.
|
|
*
|
|
* @param Lead|int|null $contact
|
|
* @param string|mixed[] $channel If an array with an ID, use the structure ['email' => 123]
|
|
* @param string $comments
|
|
* @param int $reason Must be a class constant from the DoNotContact class
|
|
* @param bool $persist
|
|
* @param bool $checkCurrentStatus
|
|
* @param bool $allowUnsubscribeOverride
|
|
*
|
|
* @return bool|DNC If a DNC entry is added or updated, returns the DoNotContact object. If a DNC is already present
|
|
* and has the specified reason, nothing is done and this returns false
|
|
*/
|
|
public function addDncForContact(
|
|
$contact,
|
|
$channel,
|
|
$reason = DNC::BOUNCED,
|
|
$comments = '',
|
|
$persist = true,
|
|
$checkCurrentStatus = true,
|
|
$allowUnsubscribeOverride = false,
|
|
) {
|
|
$dnc = false;
|
|
if (is_numeric($contact)) {
|
|
$contact = $this->leadModel->getEntity($contact);
|
|
}
|
|
|
|
if (null === $contact) {
|
|
// Contact not found, nothing to do
|
|
return false;
|
|
}
|
|
|
|
// if !$checkCurrentStatus, assume is contactable due to already being validated
|
|
$isContactable = ($checkCurrentStatus) ? $this->isContactable($contact, $channel) : DNC::IS_CONTACTABLE;
|
|
|
|
/** @var ArrayCollection<int, DNC> $dncEntities */
|
|
$dncEntities = new ArrayCollection();
|
|
// If they don't have a DNC entry yet
|
|
if (DNC::IS_CONTACTABLE === $isContactable) {
|
|
$dnc = $dncEntities[] = $this->createDncRecord($contact, $channel, $reason, $comments);
|
|
} elseif ($isContactable !== $reason) {
|
|
// Or if the given reason is different than the stated reason
|
|
|
|
$dncEntities = $contact->getDoNotContact();
|
|
foreach ($dncEntities as $dnc) {
|
|
// Only update if the contact did not unsubscribe themselves or if the code forces it
|
|
$allowOverride = ($allowUnsubscribeOverride || DNC::UNSUBSCRIBED !== $dnc->getReason());
|
|
|
|
// Only update if the contact did not unsubscribe themselves
|
|
if ($allowOverride && $dnc->getChannel() === $channel) {
|
|
// Note the outdated entry for listeners
|
|
$contact->removeDoNotContactEntry($dnc);
|
|
|
|
// Update the entry with the latest
|
|
$this->updateDncRecord($dnc, $contact, $channel, $reason, $comments);
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (null !== $dnc && $persist) {
|
|
// Use model saveEntity to trigger events for DNC change
|
|
$this->leadModel->saveEntity($contact);
|
|
$this->dncRepo->detachEntities($dncEntities->toArray());
|
|
// need to force a collection to load items in the next call.
|
|
$collection = $contact->getDoNotContact();
|
|
if ($collection instanceof PersistentCollection) {
|
|
$collection->setInitialized(false);
|
|
}
|
|
}
|
|
|
|
return $dnc;
|
|
}
|
|
|
|
/**
|
|
* @param string $channel
|
|
*
|
|
* @return int
|
|
*
|
|
* @see DNC This method can return boolean false, so be
|
|
* sure to always compare the return value against
|
|
* the class constants of DoNotContact
|
|
*/
|
|
public function isContactable(Lead $contact, $channel)
|
|
{
|
|
if (is_array($channel)) {
|
|
$channel = key($channel);
|
|
}
|
|
|
|
$dncEntries = $this->dncRepo->getEntriesByLeadAndChannel($contact, $channel);
|
|
|
|
// If the lead has no entries in the DNC table, we're good to go
|
|
if (empty($dncEntries)) {
|
|
return DNC::IS_CONTACTABLE;
|
|
}
|
|
|
|
foreach ($dncEntries as $dnc) {
|
|
if (DNC::IS_CONTACTABLE !== $dnc->getReason()) {
|
|
return $dnc->getReason();
|
|
}
|
|
}
|
|
|
|
return DNC::IS_CONTACTABLE;
|
|
}
|
|
|
|
public function createDncRecord(Lead $contact, $channel, $reason, $comments = null): DNC
|
|
{
|
|
$dnc = new DNC();
|
|
|
|
if (is_array($channel)) {
|
|
$channelId = reset($channel);
|
|
$channel = key($channel);
|
|
|
|
$dnc->setChannelId((int) $channelId);
|
|
}
|
|
|
|
$dnc->setChannel($channel);
|
|
$dnc->setReason($reason);
|
|
$dnc->setLead($contact);
|
|
$dnc->setDateAdded(new \DateTime());
|
|
$dnc->setComments($comments);
|
|
|
|
$contact->addDoNotContactEntry($dnc);
|
|
|
|
return $dnc;
|
|
}
|
|
|
|
public function updateDncRecord(DNC $dnc, Lead $contact, $channel, $reason, $comments = null): void
|
|
{
|
|
// Update the DNC entry
|
|
$dnc->setChannel($channel);
|
|
$dnc->setReason($reason);
|
|
$dnc->setLead($contact);
|
|
$dnc->setDateAdded(new \DateTime());
|
|
$dnc->setComments($comments);
|
|
|
|
// Re-add the entry to the lead
|
|
$contact->addDoNotContactEntry($dnc);
|
|
}
|
|
|
|
/**
|
|
* Get all available reason-channel combinations.
|
|
*
|
|
* @return array<array{reason: int, channel: string}>
|
|
*/
|
|
public function getReasonChannelCombinations(bool $useCache = true): array
|
|
{
|
|
$cacheTimeout = (int) $this->coreParametersHelper->get('cached_data_timeout');
|
|
$cacheItem = $this->cacheProvider->getItem('dnc.reason_channel_combinations');
|
|
if ($useCache && $cacheItem->isHit()) {
|
|
return $cacheItem->get();
|
|
} else {
|
|
$reasonChannelCombinations = $this->dncRepo->getReasonChannelCombinations();
|
|
$cacheItem->set($reasonChannelCombinations);
|
|
$cacheItem->expiresAfter($cacheTimeout * 60);
|
|
$this->cacheProvider->save($cacheItem);
|
|
|
|
return $reasonChannelCombinations;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return DoNotContactRepository
|
|
*/
|
|
public function getDncRepo()
|
|
{
|
|
return $this->dncRepo;
|
|
}
|
|
}
|