Spaces:
No application file
No application file
namespace Mautic\LeadBundle\Model; | |
use Doctrine\DBAL\Exception as DBALException; | |
use Doctrine\ORM\EntityManager; | |
use Doctrine\ORM\NonUniqueResultException; | |
use Doctrine\ORM\Tools\Pagination\Paginator; | |
use Mautic\CategoryBundle\Entity\Category; | |
use Mautic\CategoryBundle\Model\CategoryModel; | |
use Mautic\ChannelBundle\Helper\ChannelListHelper; | |
use Mautic\CoreBundle\Cache\ResultCacheOptions; | |
use Mautic\CoreBundle\Entity\IpAddress; | |
use Mautic\CoreBundle\Form\RequestTrait; | |
use Mautic\CoreBundle\Helper\Chart\ChartQuery; | |
use Mautic\CoreBundle\Helper\Chart\LineChart; | |
use Mautic\CoreBundle\Helper\Chart\PieChart; | |
use Mautic\CoreBundle\Helper\CoreParametersHelper; | |
use Mautic\CoreBundle\Helper\DateTimeHelper; | |
use Mautic\CoreBundle\Helper\InputHelper; | |
use Mautic\CoreBundle\Helper\IpLookupHelper; | |
use Mautic\CoreBundle\Helper\PathsHelper; | |
use Mautic\CoreBundle\Helper\UserHelper; | |
use Mautic\CoreBundle\Model\FormModel; | |
use Mautic\CoreBundle\Security\Permissions\CorePermissions; | |
use Mautic\CoreBundle\Translation\Translator; | |
use Mautic\EmailBundle\Entity\Stat; | |
use Mautic\EmailBundle\Entity\StatRepository; | |
use Mautic\EmailBundle\Helper\EmailValidator; | |
use Mautic\LeadBundle\DataObject\LeadManipulator; | |
use Mautic\LeadBundle\Entity\Company; | |
use Mautic\LeadBundle\Entity\CompanyLead; | |
use Mautic\LeadBundle\Entity\DoNotContact as DNC; | |
use Mautic\LeadBundle\Entity\FrequencyRule; | |
use Mautic\LeadBundle\Entity\Lead; | |
use Mautic\LeadBundle\Entity\LeadCategory; | |
use Mautic\LeadBundle\Entity\LeadEventLog; | |
use Mautic\LeadBundle\Entity\LeadField; | |
use Mautic\LeadBundle\Entity\LeadList; | |
use Mautic\LeadBundle\Entity\LeadRepository; | |
use Mautic\LeadBundle\Entity\OperatorListTrait; | |
use Mautic\LeadBundle\Entity\PointsChangeLog; | |
use Mautic\LeadBundle\Entity\StagesChangeLog; | |
use Mautic\LeadBundle\Entity\Tag; | |
use Mautic\LeadBundle\Entity\UtmTag; | |
use Mautic\LeadBundle\Event\CategoryChangeEvent; | |
use Mautic\LeadBundle\Event\DoNotContactAddEvent; | |
use Mautic\LeadBundle\Event\DoNotContactRemoveEvent; | |
use Mautic\LeadBundle\Event\LeadEvent; | |
use Mautic\LeadBundle\Event\LeadTimelineEvent; | |
use Mautic\LeadBundle\Exception\ImportFailedException; | |
use Mautic\LeadBundle\Form\Type\LeadType; | |
use Mautic\LeadBundle\Helper\IdentifyCompanyHelper; | |
use Mautic\LeadBundle\LeadEvents; | |
use Mautic\LeadBundle\Tracker\ContactTracker; | |
use Mautic\LeadBundle\Tracker\DeviceTracker; | |
use Mautic\PluginBundle\Helper\IntegrationHelper; | |
use Mautic\PointBundle\Entity\GroupContactScore; | |
use Mautic\PointBundle\Entity\GroupContactScoreRepository; | |
use Mautic\StageBundle\Entity\Stage; | |
use Mautic\UserBundle\Entity\User; | |
use Mautic\UserBundle\Security\Provider\UserProvider; | |
use Psr\Log\LoggerInterface; | |
use Symfony\Component\EventDispatcher\EventDispatcherInterface; | |
use Symfony\Component\Form\FormFactoryInterface; | |
use Symfony\Component\HttpFoundation\RequestStack; | |
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException; | |
use Symfony\Component\Intl\Countries; | |
use Symfony\Component\Routing\Generator\UrlGeneratorInterface; | |
use Symfony\Contracts\EventDispatcher\Event; | |
use Tightenco\Collect\Support\Collection; | |
/** | |
* @extends FormModel<Lead> | |
*/ | |
class LeadModel extends FormModel | |
{ | |
use DefaultValueTrait; | |
use OperatorListTrait; | |
use RequestTrait; | |
public const CHANNEL_FEATURE = 'contact_preference'; | |
/** | |
* @var FieldModel | |
*/ | |
protected $leadFieldModel; | |
/** | |
* @var array | |
*/ | |
protected $leadFields = []; | |
protected $leadTrackingId; | |
/** | |
* @var bool | |
*/ | |
protected $leadTrackingCookieGenerated = false; | |
/** | |
* @var array | |
*/ | |
protected $availableLeadFields = []; | |
private bool $repoSetup = false; | |
private array $flattenedFields = []; | |
private array $fieldsByGroup = []; | |
public function __construct( | |
protected RequestStack $requestStack, | |
protected IpLookupHelper $ipLookupHelper, | |
protected PathsHelper $pathsHelper, | |
protected IntegrationHelper $integrationHelper, | |
FieldModel $leadFieldModel, | |
protected ListModel $leadListModel, | |
protected FormFactoryInterface $formFactory, | |
protected CompanyModel $companyModel, | |
protected CategoryModel $categoryModel, | |
protected ChannelListHelper $channelListHelper, | |
CoreParametersHelper $coreParametersHelper, | |
protected EmailValidator $emailValidator, | |
protected UserProvider $userProvider, | |
private ContactTracker $contactTracker, | |
private DeviceTracker $deviceTracker, | |
private IpAddressModel $ipAddressModel, | |
EntityManager $em, | |
CorePermissions $security, | |
EventDispatcherInterface $dispatcher, | |
UrlGeneratorInterface $router, | |
Translator $translator, | |
UserHelper $userHelper, | |
LoggerInterface $mauticLogger | |
) { | |
$this->leadFieldModel = $leadFieldModel; | |
parent::__construct($em, $security, $dispatcher, $router, $translator, $userHelper, $mauticLogger, $coreParametersHelper); | |
} | |
/** | |
* @return LeadRepository | |
*/ | |
public function getRepository() | |
{ | |
/** @var LeadRepository $repo */ | |
$repo = $this->em->getRepository(Lead::class); | |
$repo->setDispatcher($this->dispatcher); | |
if (!$this->repoSetup) { | |
$this->repoSetup = true; | |
// set the point trigger model in order to get the color code for the lead | |
$fields = $this->leadFieldModel->getFieldList(true, false); | |
$socialFields = (!empty($fields['social'])) ? array_keys($fields['social']) : []; | |
$repo->setAvailableSocialFields($socialFields); | |
$searchFields = []; | |
foreach ($fields as $groupFields) { | |
$searchFields = array_merge($searchFields, array_keys($groupFields)); | |
} | |
$repo->setAvailableSearchFields($searchFields); | |
} | |
return $repo; | |
} | |
/** | |
* Get the tags repository. | |
* | |
* @return \Mautic\LeadBundle\Entity\TagRepository | |
*/ | |
public function getTagRepository() | |
{ | |
return $this->em->getRepository(Tag::class); | |
} | |
/** | |
* @return \Mautic\LeadBundle\Entity\PointsChangeLogRepository | |
*/ | |
public function getPointLogRepository() | |
{ | |
return $this->em->getRepository(PointsChangeLog::class); | |
} | |
/** | |
* Get the tags repository. | |
* | |
* @return \Mautic\LeadBundle\Entity\UtmTagRepository | |
*/ | |
public function getUtmTagRepository() | |
{ | |
return $this->em->getRepository(UtmTag::class); | |
} | |
/** | |
* Get the tags repository. | |
* | |
* @return \Mautic\LeadBundle\Entity\LeadDeviceRepository | |
*/ | |
public function getDeviceRepository() | |
{ | |
return $this->em->getRepository(\Mautic\LeadBundle\Entity\LeadDevice::class); | |
} | |
/** | |
* Get the lead event log repository. | |
* | |
* @return \Mautic\LeadBundle\Entity\LeadEventLogRepository | |
*/ | |
public function getEventLogRepository() | |
{ | |
return $this->em->getRepository(LeadEventLog::class); | |
} | |
/** | |
* Get the frequency rules repository. | |
* | |
* @return \Mautic\LeadBundle\Entity\FrequencyRuleRepository | |
*/ | |
public function getFrequencyRuleRepository() | |
{ | |
return $this->em->getRepository(FrequencyRule::class); | |
} | |
/** | |
* Get the Stages change log repository. | |
* | |
* @return \Mautic\LeadBundle\Entity\StagesChangeLogRepository | |
*/ | |
public function getStagesChangeLogRepository() | |
{ | |
return $this->em->getRepository(StagesChangeLog::class); | |
} | |
/** | |
* Get the lead categories repository. | |
* | |
* @return \Mautic\LeadBundle\Entity\LeadCategoryRepository | |
*/ | |
public function getLeadCategoryRepository() | |
{ | |
return $this->em->getRepository(LeadCategory::class); | |
} | |
/** | |
* @return \Mautic\LeadBundle\Entity\MergeRecordRepository | |
*/ | |
public function getMergeRecordRepository() | |
{ | |
return $this->em->getRepository(\Mautic\LeadBundle\Entity\MergeRecord::class); | |
} | |
/** | |
* @return \Mautic\LeadBundle\Entity\LeadListRepository | |
*/ | |
public function getLeadListRepository() | |
{ | |
return $this->em->getRepository(LeadList::class); | |
} | |
public function getGroupContactScoreRepository(): GroupContactScoreRepository | |
{ | |
return $this->em->getRepository(GroupContactScore::class); | |
} | |
public function getPermissionBase(): string | |
{ | |
return 'lead:leads'; | |
} | |
public function getNameGetter(): string | |
{ | |
return 'getPrimaryIdentifier'; | |
} | |
/** | |
* @param Lead $entity | |
* @param string|null $action | |
* @param array $options | |
* | |
* @return \Symfony\Component\Form\FormInterface<Lead> | |
* | |
* @throws MethodNotAllowedHttpException | |
*/ | |
public function createForm($entity, FormFactoryInterface $formFactory, $action = null, $options = []): \Symfony\Component\Form\FormInterface | |
{ | |
if (!$entity instanceof Lead) { | |
throw new MethodNotAllowedHttpException(['Lead'], 'Entity must be of class Lead()'); | |
} | |
if (!empty($action)) { | |
$options['action'] = $action; | |
} | |
return $formFactory->create(LeadType::class, $entity, $options); | |
} | |
/** | |
* Get a specific entity or generate a new one if id is empty. | |
*/ | |
public function getEntity($id = null): ?Lead | |
{ | |
if (null === $id) { | |
return new Lead(); | |
} | |
$entity = parent::getEntity($id); | |
if (null === $entity) { | |
// Check if this contact was merged into another and if so, return the new contact | |
if ($entity = $this->getMergeRecordRepository()->findMergedContact($id)) { | |
// Hydrate fields with custom field data | |
$fields = $this->getRepository()->getFieldValues($entity->getId()); | |
$entity->setFields($fields); | |
} | |
} | |
return $entity; | |
} | |
/** | |
* @throws MethodNotAllowedHttpException | |
*/ | |
protected function dispatchEvent($action, &$entity, $isNew = false, ?Event $event = null): ?Event | |
{ | |
if (!$entity instanceof Lead) { | |
throw new MethodNotAllowedHttpException(['Lead'], 'Entity must be of class Lead()'); | |
} | |
switch ($action) { | |
case 'pre_save': | |
$name = LeadEvents::LEAD_PRE_SAVE; | |
break; | |
case 'post_save': | |
$name = LeadEvents::LEAD_POST_SAVE; | |
break; | |
case 'pre_delete': | |
$name = LeadEvents::LEAD_PRE_DELETE; | |
break; | |
case 'post_delete': | |
$name = LeadEvents::LEAD_POST_DELETE; | |
break; | |
default: | |
return null; | |
} | |
if ($this->dispatcher->hasListeners($name)) { | |
if (empty($event)) { | |
$event = new LeadEvent($entity, $isNew); | |
$event->setEntityManager($this->em); | |
} | |
$this->dispatcher->dispatch($event, $name); | |
return $event; | |
} else { | |
return null; | |
} | |
} | |
/** | |
* @param Lead $entity | |
* @param bool $unlock | |
*/ | |
public function saveEntity($entity, $unlock = true): void | |
{ | |
$companyFieldMatches = []; | |
$fields = $entity->getFields(); | |
$company = null; | |
// check to see if we can glean information from ip address | |
if (!$entity->imported && count($ips = $entity->getIpAddresses())) { | |
$details = $ips->first()->getIpDetails(); | |
// Only update with IP details if none of the following are set to prevent wrong combinations | |
if (empty($fields['core']['city']['value']) && empty($fields['core']['state']['value']) && empty($fields['core']['country']['value']) && empty($fields['core']['zipcode']['value'])) { | |
if ($this->coreParametersHelper->get('anonymize_ip') && $this->ipLookupHelper->getRealIp()) { | |
$details = $this->ipLookupHelper->getIpDetails($this->ipLookupHelper->getRealIp()); | |
} | |
if (!empty($details['city'])) { | |
$entity->addUpdatedField('city', $details['city']); | |
$companyFieldMatches['city'] = $details['city']; | |
} | |
if (!empty($details['region'])) { | |
$entity->addUpdatedField('state', $details['region']); | |
$companyFieldMatches['state'] = $details['region']; | |
} | |
if (!empty($details['country'])) { | |
$entity->addUpdatedField('country', $details['country']); | |
$companyFieldMatches['country'] = $details['country']; | |
} | |
if (!empty($details['zipcode'])) { | |
$entity->addUpdatedField('zipcode', $details['zipcode']); | |
} | |
} | |
if (!$entity->getCompany() && !empty($details['organization']) && $this->coreParametersHelper->get('ip_lookup_create_organization', false)) { | |
$entity->addUpdatedField('company', $details['organization']); | |
} | |
} | |
$updatedFields = $entity->getUpdatedFields(); | |
$changeLogEntity = null; | |
if (isset($updatedFields['company'])) { | |
$companyFieldMatches['company'] = $updatedFields['company']; | |
[$company, $leadAdded, $companyEntity] = IdentifyCompanyHelper::identifyLeadsCompany($companyFieldMatches, $entity, $this->companyModel); | |
if ($leadAdded) { | |
$changeLogEntity = $entity->addCompanyChangeLogEntry('form', 'Identify Company', 'Lead added to the company, '.$company['companyname'], $company['id']); | |
} | |
} | |
$this->processManipulator($entity); | |
$this->setEntityDefaultValues($entity); | |
$this->ipAddressModel->saveIpAddressesReferencesForContact($entity); | |
parent::saveEntity($entity, $unlock); | |
if (!empty($company)) { | |
// Save after the lead in for new leads created through the API and maybe other places | |
$this->companyModel->addLeadToCompany($companyEntity, $entity); | |
$this->setPrimaryCompany($companyEntity->getId(), $entity->getId()); | |
} elseif (array_key_exists('company', $updatedFields) && empty($updatedFields['company'])) { | |
$this->companyModel->getCompanyLeadRepository()->removeContactPrimaryCompany($entity->getId()); | |
} | |
if (null !== $changeLogEntity) { | |
$this->em->detach($changeLogEntity); | |
} | |
} | |
/** | |
* @param object $entity | |
*/ | |
public function deleteEntity($entity): void | |
{ | |
// Delete custom avatar if one exists | |
$imageDir = $this->pathsHelper->getSystemPath('images', true); | |
$avatar = $imageDir.'/lead_avatars/avatar'.$entity->getId(); | |
if (file_exists($avatar)) { | |
unlink($avatar); | |
} | |
parent::deleteEntity($entity); | |
} | |
/** | |
* Populates custom field values for updating the lead. Also retrieves social media data. | |
* | |
* @param bool|false $overwriteWithBlank | |
* @param bool|true $fetchSocialProfiles | |
* @param bool|false $bindWithForm Send $data through the Lead form and only use valid data (should be used with request data) | |
* | |
* @throws ImportFailedException | |
*/ | |
public function setFieldValues(Lead $lead, array $data, $overwriteWithBlank = false, $fetchSocialProfiles = true, $bindWithForm = false): void | |
{ | |
if ($fetchSocialProfiles) { | |
// @todo - add a catch to NOT do social gleaning if a lead is created via a form, etc as we do not want the user to experience the wait | |
// generate the social cache | |
[$socialCache, $socialFeatureSettings] = $this->integrationHelper->getUserProfiles( | |
$lead, | |
$data, | |
true, | |
null, | |
false, | |
true | |
); | |
// set the social cache while we have it | |
if (!empty($socialCache)) { | |
$lead->setSocialCache($socialCache); | |
} | |
} | |
if (isset($data['stage'])) { | |
$stagesChangeLogRepo = $this->getStagesChangeLogRepository(); | |
$currentLeadStageId = $stagesChangeLogRepo->getCurrentLeadStage($lead->getId()); | |
$currentLeadStageName = null; | |
if ($currentLeadStageId) { | |
/** @var Stage|null $currentStage */ | |
$currentStage = $this->em->getRepository(Stage::class)->findByIdOrName($currentLeadStageId); | |
if ($currentStage) { | |
$currentLeadStageName = $currentStage->getName(); | |
} | |
} | |
$newLeadStageIdOrName = is_object($data['stage']) ? $data['stage']->getId() : $data['stage']; | |
if ((int) $newLeadStageIdOrName !== $currentLeadStageId && $newLeadStageIdOrName !== $currentLeadStageName) { | |
/** @var Stage|null $newStage */ | |
$newStage = $this->em->getRepository(Stage::class)->findByIdOrName($newLeadStageIdOrName); | |
if ($newStage) { | |
$lead->stageChangeLogEntry( | |
$newStage, | |
$newStage->getId().':'.$newStage->getName(), | |
$this->translator->trans('mautic.stage.event.changed') | |
); | |
} else { | |
throw new ImportFailedException($this->translator->trans('mautic.lead.import.stage.not.exists', ['id' => $newLeadStageIdOrName])); | |
} | |
} | |
} | |
// save the field values | |
$fieldValues = $lead->getFields(); | |
if (empty($fieldValues) || $bindWithForm) { | |
// Lead is new or they haven't been populated so let's build the fields now | |
if (empty($this->flattenedFields)) { | |
/** @var Paginator<mixed[]> $paginator */ | |
$paginator = $this->leadFieldModel->getEntities( | |
[ | |
'filter' => ['isPublished' => true, 'object' => 'lead'], | |
'hydration_mode' => 'HYDRATE_ARRAY', | |
'result_cache' => new ResultCacheOptions(LeadField::CACHE_NAMESPACE), | |
] | |
); | |
$this->flattenedFields = iterator_to_array($paginator->getIterator()); | |
$this->fieldsByGroup = $this->organizeFieldsByGroup($this->flattenedFields); | |
} | |
if (empty($fieldValues)) { | |
$fieldValues = $this->fieldsByGroup; | |
} | |
} | |
if ($bindWithForm) { | |
// Cleanup the field values | |
$form = $this->createForm( | |
new Lead(), // use empty lead to prevent binding errors | |
$this->formFactory, | |
null, | |
['fields' => $this->flattenedFields, 'csrf_protection' => false, 'allow_extra_fields' => true] | |
); | |
// Unset stage and owner from the form because it's already been handled | |
unset($data['stage'], $data['owner'], $data['tags']); | |
// Prepare special fields | |
$this->prepareParametersFromRequest($form, $data, $lead, [], $this->fieldsByGroup); | |
// Submit the data | |
$form->submit($data); | |
if ($form->getErrors()->count()) { | |
$this->logger->debug('LEAD: form validation failed with an error of '.$form->getErrors()); | |
} | |
foreach ($form as $field => $formField) { | |
if (isset($data[$field])) { | |
if ($formField->getErrors()->count()) { | |
$this->logger->debug('LEAD: '.$field.' failed form validation with an error of '.$formField->getErrors()); | |
// Don't save bad data | |
unset($data[$field]); | |
} else { | |
$data[$field] = $formField->getData(); | |
} | |
} | |
} | |
} | |
// update existing values | |
foreach ($fieldValues as $group => &$groupFields) { | |
if ('all' === $group) { | |
continue; | |
} | |
foreach ($groupFields as $alias => &$field) { | |
if (!isset($field['value'])) { | |
$field['value'] = null; | |
} | |
// Only update fields that are part of the passed $data array | |
if (array_key_exists($alias, $data)) { | |
if (!$bindWithForm) { | |
$this->cleanFields($data, $field); | |
} | |
$curValue = $field['value']; | |
$newValue = $data[$alias] ?? ''; | |
if (is_array($newValue)) { | |
$newValue = implode('|', $newValue); | |
} | |
$isEmpty = (null == $newValue || '' == $newValue); | |
if ($curValue !== $newValue && (!$isEmpty || ($isEmpty && $overwriteWithBlank))) { | |
$field['value'] = $newValue; | |
$lead->addUpdatedField($alias, $newValue, $curValue); | |
} | |
// if empty, check for social media data to plug the hole | |
if (empty($newValue) && !empty($socialCache)) { | |
foreach ($socialCache as $service => $details) { | |
// check to see if a field has been assigned | |
if (!empty($socialFeatureSettings[$service]['leadFields']) | |
&& in_array($field['alias'], $socialFeatureSettings[$service]['leadFields']) | |
) { | |
// check to see if the data is available | |
$key = array_search($field['alias'], $socialFeatureSettings[$service]['leadFields']); | |
if (isset($details['profile'][$key])) { | |
// Found!! | |
$field['value'] = $details['profile'][$key]; | |
$lead->addUpdatedField($alias, $details['profile'][$key]); | |
break; | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
$lead->setFields($fieldValues); | |
} | |
/** | |
* Disassociates a user from leads. | |
*/ | |
public function disassociateOwner($userId): void | |
{ | |
$leads = $this->getRepository()->findByOwner($userId); | |
foreach ($leads as $lead) { | |
$lead->setOwner(null); | |
$this->saveEntity($lead); | |
} | |
} | |
/** | |
* Get list of entities for autopopulate fields. | |
* | |
* @return array | |
*/ | |
public function getLookupResults($type, $filter = '', $limit = 10, $start = 0) | |
{ | |
$results = []; | |
switch ($type) { | |
case 'user': | |
$results = $this->em->getRepository(User::class)->getUserList($filter, $limit, $start, ['lead' => 'leads']); | |
break; | |
case 'contact': | |
$fetchResults = $this->getEntities([ | |
'start' => $start, | |
'limit' => $limit, | |
'filter' => ['string' => $filter], | |
]); | |
$results = []; | |
/** @var Lead $fetchResult */ | |
foreach ($fetchResults as $fetchResult) { | |
$results[] = [ | |
'value' => $fetchResult->getName() ?: $fetchResult->getEmail(), | |
'id' => $fetchResult->getId(), | |
]; | |
} | |
break; | |
} | |
return $results; | |
} | |
/** | |
* Obtain an array of users for api lead edits. | |
* | |
* @return array<mixed> | |
*/ | |
public function getOwnerList() | |
{ | |
return $this->em->getRepository(User::class)->getUserList('', 0); | |
} | |
/** | |
* Obtains a list of leads based off IP. | |
* | |
* @return array<mixed> | |
*/ | |
public function getLeadsByIp($ip) | |
{ | |
return $this->getRepository()->getLeadsByIp($ip); | |
} | |
/** | |
* Obtains a list of leads based a list of IDs. | |
* | |
* @return Paginator | |
*/ | |
public function getLeadsByIds(array $ids) | |
{ | |
return $this->getEntities([ | |
'filter' => [ | |
'force' => [ | |
[ | |
'column' => 'l.id', | |
'expr' => 'in', | |
'value' => $ids, | |
], | |
], | |
], | |
]); | |
} | |
/** | |
* @return bool | |
*/ | |
public function canEditContact(Lead $contact) | |
{ | |
return $this->security->hasEntityAccess('lead:leads:editown', 'lead:leads:editother', $contact->getPermissionUser()); | |
} | |
/** | |
* Gets the details of a lead if not already set. | |
* | |
* @return array<mixed> | |
*/ | |
public function getLeadDetails($lead) | |
{ | |
if ($lead instanceof Lead) { | |
$fields = $lead->getFields(); | |
if (!empty($fields)) { | |
return $fields; | |
} | |
} | |
$leadId = ($lead instanceof Lead) ? $lead->getId() : (int) $lead; | |
return $this->getRepository()->getFieldValues($leadId); | |
} | |
/** | |
* Reorganizes a field list to be keyed by field's group then alias. | |
*/ | |
public function organizeFieldsByGroup($fields): array | |
{ | |
$array = []; | |
foreach ($fields as $field) { | |
if ($field instanceof LeadField) { | |
$alias = $field->getAlias(); | |
if ($field->isPublished() and 'Lead' === $field->getObject()) { | |
$group = $field->getGroup(); | |
$array[$group][$alias]['id'] = $field->getId(); | |
$array[$group][$alias]['group'] = $group; | |
$array[$group][$alias]['label'] = $field->getLabel(); | |
$array[$group][$alias]['alias'] = $alias; | |
$array[$group][$alias]['type'] = $field->getType(); | |
$array[$group][$alias]['properties'] = $field->getProperties(); | |
} | |
} else { | |
$alias = $field['alias']; | |
if ($field['isPublished'] and 'lead' === $field['object']) { | |
$group = $field['group']; | |
$array[$group][$alias]['id'] = $field['id']; | |
$array[$group][$alias]['group'] = $group; | |
$array[$group][$alias]['label'] = $field['label']; | |
$array[$group][$alias]['alias'] = $alias; | |
$array[$group][$alias]['type'] = $field['type']; | |
$array[$group][$alias]['properties'] = $field['properties'] ?? []; | |
} | |
} | |
} | |
// make sure each group key is present | |
$groups = ['core', 'social', 'personal', 'professional']; | |
foreach ($groups as $g) { | |
if (!isset($array[$g])) { | |
$array[$g] = []; | |
} | |
} | |
return $array; | |
} | |
/** | |
* Returns flat array for single lead. | |
* | |
* @return array | |
*/ | |
public function getLead($leadId) | |
{ | |
return $this->getRepository()->getLead($leadId); | |
} | |
/** | |
* @param bool $returnWithQueryFields | |
* | |
* @return array|Lead | |
*/ | |
public function checkForDuplicateContact(array $queryFields, $returnWithQueryFields = false, $onlyPubliclyUpdateable = false) | |
{ | |
// Search for lead by request and/or update lead fields if some data were sent in the URL query | |
if (empty($this->availableLeadFields)) { | |
$filter = ['isPublished' => true, 'object' => 'lead']; | |
if ($onlyPubliclyUpdateable) { | |
$filter['isPubliclyUpdatable'] = true; | |
} | |
$this->availableLeadFields = $this->leadFieldModel->getFieldList( | |
false, | |
false, | |
$filter | |
); | |
} | |
$lead = new Lead(); | |
$uniqueFields = $this->leadFieldModel->getUniqueIdentifierFields(); | |
$uniqueFieldData = []; | |
$inQuery = array_intersect_key($queryFields, $this->availableLeadFields); | |
$values = $onlyPubliclyUpdateable ? $inQuery : $queryFields; | |
// Run values through setFieldValues to clean them first | |
$this->setFieldValues($lead, $values, false, false); | |
$cleanFields = $lead->getFields(); | |
foreach ($inQuery as $k => $v) { | |
if (empty($queryFields[$k])) { | |
unset($inQuery[$k]); | |
} | |
} | |
foreach ($cleanFields as $group) { | |
foreach ($group as $key => $field) { | |
if (array_key_exists($key, $uniqueFields) && !empty($field['value'])) { | |
$uniqueFieldData[$key] = $field['value']; | |
} | |
} | |
} | |
// Check for leads using unique identifier | |
if (count($uniqueFieldData)) { | |
$existingLeads = $this->getRepository()->getLeadsByUniqueFields($uniqueFieldData); | |
if (!empty($existingLeads)) { | |
$this->logger->debug("LEAD: Existing contact ID# {$existingLeads[0]->getId()} found through query identifiers."); | |
$lead = $existingLeads[0]; | |
} | |
} | |
return $returnWithQueryFields ? [$lead, $inQuery] : $lead; | |
} | |
/** | |
* Get a list of segments this lead belongs to. | |
* | |
* @param bool $forLists | |
* @param bool $arrayHydration | |
* @param bool $isPublic | |
* | |
* @return mixed | |
*/ | |
public function getLists(Lead $lead, $forLists = false, $arrayHydration = false, $isPublic = false, $isPreferenceCenter = false) | |
{ | |
$repo = $this->em->getRepository(LeadList::class); | |
return $repo->getLeadLists($lead->getId(), $forLists, $arrayHydration, $isPublic, $isPreferenceCenter); | |
} | |
/** | |
* Get a list of companies this contact belongs to. | |
* | |
* @return array<mixed> | |
*/ | |
public function getCompanies(Lead $lead) | |
{ | |
$repo = $this->em->getRepository(CompanyLead::class); | |
return $repo->getCompaniesByLeadId($lead->getId()); | |
} | |
/** | |
* Add lead to lists. | |
* | |
* @param array|Lead|int $lead | |
* @param array|LeadList $lists | |
* @param bool $manuallyAdded | |
*/ | |
public function addToLists($lead, $lists, $manuallyAdded = true): void | |
{ | |
$this->leadListModel->addLead($lead, $lists, $manuallyAdded); | |
} | |
/** | |
* Remove lead from lists. | |
* | |
* @param bool $manuallyRemoved | |
*/ | |
public function removeFromLists($lead, $lists, $manuallyRemoved = true): void | |
{ | |
$this->leadListModel->removeLead($lead, $lists, $manuallyRemoved); | |
} | |
/** | |
* Add lead to Stage. | |
* | |
* @param array|Lead $lead | |
* @param array|Stage $stage | |
* @param bool $manuallyAdded | |
* | |
* @return $this | |
*/ | |
public function addToStages($lead, $stage, $manuallyAdded = true) | |
{ | |
if (!$lead instanceof Lead) { | |
$leadId = (is_array($lead) && isset($lead['id'])) ? $lead['id'] : $lead; | |
$lead = $this->em->getReference(Lead::class, $leadId); | |
} | |
$lead->setStage($stage); | |
$lead->stageChangeLogEntry( | |
$stage, | |
$stage->getId().': '.$stage->getName(), | |
$this->translator->trans('mautic.stage.event.added.batch') | |
); | |
return $this; | |
} | |
/** | |
* Remove lead from Stage. | |
* | |
* @param bool $manuallyRemoved | |
* | |
* @return $this | |
*/ | |
public function removeFromStages($lead, $stage, $manuallyRemoved = true) | |
{ | |
$lead->setStage(null); | |
$lead->stageChangeLogEntry( | |
$stage, | |
$stage->getId().': '.$stage->getName(), | |
$this->translator->trans('mautic.stage.event.removed.batch') | |
); | |
return $this; | |
} | |
/** | |
* @param string $channel | |
* | |
* @return array<mixed> | |
*/ | |
public function getFrequencyRules(Lead $lead, $channel = null) | |
{ | |
if (is_array($channel)) { | |
$channel = key($channel); | |
} | |
/** @var \Mautic\LeadBundle\Entity\FrequencyRuleRepository $frequencyRuleRepo */ | |
$frequencyRuleRepo = $this->em->getRepository(FrequencyRule::class); | |
$frequencyRules = $frequencyRuleRepo->getFrequencyRules($channel, $lead->getId()); | |
if (empty($frequencyRules)) { | |
return []; | |
} | |
return $frequencyRules; | |
} | |
/** | |
* Set frequency rules for lead per channel. | |
* | |
* @param array<mixed> $data | |
* @param array<LeadList> $leadLists | |
* | |
* @return bool Returns true | |
*/ | |
public function setFrequencyRules(Lead $lead, $data, $leadLists, $persist = true): bool | |
{ | |
// One query to get all the lead's current frequency rules and go ahead and create entities for them | |
$frequencyRules = $lead->getFrequencyRules()->toArray(); | |
$entities = []; | |
$channels = $this->getPreferenceChannels(); | |
foreach ($channels as $ch) { | |
if (empty($data['lead_channels']['preferred_channel'])) { | |
$data['lead_channels']['preferred_channel'] = $ch; | |
} | |
$frequencyRule = $frequencyRules[$ch] ?? new FrequencyRule(); | |
$frequencyRule->setChannel($ch); | |
$frequencyRule->setLead($lead); | |
$frequencyRule->setDateAdded(new \DateTime()); | |
if (!empty($data['lead_channels']['frequency_number_'.$ch]) && !empty($data['lead_channels']['frequency_time_'.$ch])) { | |
$frequencyRule->setFrequencyNumber($data['lead_channels']['frequency_number_'.$ch]); | |
$frequencyRule->setFrequencyTime($data['lead_channels']['frequency_time_'.$ch]); | |
} else { | |
$frequencyRule->setFrequencyNumber(null); | |
$frequencyRule->setFrequencyTime(null); | |
} | |
$frequencyRule->setPauseFromDate(!empty($data['lead_channels']['contact_pause_start_date_'.$ch]) ? $data['lead_channels']['contact_pause_start_date_'.$ch] : null); | |
$frequencyRule->setPauseToDate(!empty($data['lead_channels']['contact_pause_end_date_'.$ch]) ? $data['lead_channels']['contact_pause_end_date_'.$ch] : null); | |
$frequencyRule->setLead($lead); | |
$frequencyRule->setPreferredChannel($data['lead_channels']['preferred_channel'] === $ch); | |
if ($persist) { | |
$entities[$ch] = $frequencyRule; | |
} else { | |
$lead->addFrequencyRule($frequencyRule); | |
} | |
} | |
if (!empty($entities)) { | |
$this->em->getRepository(FrequencyRule::class)->saveEntities($entities); | |
} | |
foreach ($data['lead_lists'] as $leadList) { | |
if (!isset($leadLists[$leadList])) { | |
$this->addToLists($lead, [$leadList]); | |
} | |
} | |
// Delete lists that were removed | |
$deletedLists = array_diff(array_keys($leadLists), $data['lead_lists']); | |
if (!empty($deletedLists)) { | |
$this->removeFromLists($lead, $deletedLists); | |
} | |
if (!empty($data['global_categories'])) { | |
$this->addToCategory($lead, $data['global_categories']); | |
} | |
$leadCategories = $this->getLeadCategories($lead); | |
// Update categories relations as removed those are removed. | |
$unsubscribedCategories = array_diff($leadCategories, $data['global_categories']); | |
if (!empty($unsubscribedCategories)) { | |
$this->unsubscribeCategories($unsubscribedCategories); | |
} | |
// Delete channels that were removed | |
$deleted = array_diff_key($frequencyRules, $entities); | |
if (!empty($deleted)) { | |
$this->em->getRepository(FrequencyRule::class)->deleteEntities($deleted); | |
} | |
return true; | |
} | |
/** | |
* @param bool $manuallyAdded | |
*/ | |
public function addToCategory(Lead $lead, $categories, $manuallyAdded = true): array | |
{ | |
$leadCategories = $this->getLeadCategoryRepository()->getLeadCategories($lead); | |
$results = []; | |
foreach ($categories as $category) { | |
if (!isset($leadCategories[$category])) { | |
$newLeadCategory = new LeadCategory(); | |
$newLeadCategory->setLead($lead); | |
if (!$category instanceof Category) { | |
$category = $this->categoryModel->getEntity($category); | |
} | |
$newLeadCategory->setCategory($category); | |
$newLeadCategory->setDateAdded(new \DateTime()); | |
$newLeadCategory->setManuallyAdded($manuallyAdded); | |
$results[$category->getId()] = $newLeadCategory; | |
if ($this->dispatcher->hasListeners(LeadEvents::LEAD_CATEGORY_CHANGE)) { | |
$this->dispatcher->dispatch(new CategoryChangeEvent($lead, $category), LeadEvents::LEAD_CATEGORY_CHANGE); | |
} | |
} | |
} | |
if (!empty($results)) { | |
$this->getLeadCategoryRepository()->saveEntities($results); | |
} | |
return $results; | |
} | |
/** | |
* @param mixed[] $categories | |
*/ | |
private function unsubscribeCategories(array $categories): void | |
{ | |
$unsubscribedCats = []; | |
foreach ($categories as $key => $category) { | |
/** @var LeadCategory $category */ | |
$category = $this->getLeadCategoryRepository()->getEntity($key); | |
$category->setManuallyRemoved(true); | |
$category->setManuallyAdded(false); | |
$unsubscribedCats[] = $category; | |
if ($this->dispatcher->hasListeners(LeadEvents::LEAD_CATEGORY_CHANGE)) { | |
$this->dispatcher->dispatch(new CategoryChangeEvent($category->getLead(), $category->getCategory(), false), LeadEvents::LEAD_CATEGORY_CHANGE); | |
} | |
} | |
if (!empty($unsubscribedCats)) { | |
$this->getLeadCategoryRepository()->saveEntities($unsubscribedCats); | |
} | |
} | |
public function removeFromCategories($categories): void | |
{ | |
$deleteCats = []; | |
if (is_array($categories)) { | |
foreach ($categories as $key => $category) { | |
/** @var LeadCategory $category */ | |
$category = $this->getLeadCategoryRepository()->getEntity($key); | |
$deleteCats[] = $category; | |
if ($this->dispatcher->hasListeners(LeadEvents::LEAD_CATEGORY_CHANGE)) { | |
$this->dispatcher->dispatch(new CategoryChangeEvent($category->getLead(), $category->getCategory(), false), LeadEvents::LEAD_CATEGORY_CHANGE); | |
} | |
} | |
} elseif ($categories instanceof LeadCategory) { | |
$deleteCats[] = $categories; | |
if ($this->dispatcher->hasListeners(LeadEvents::LEAD_CATEGORY_CHANGE)) { | |
$this->dispatcher->dispatch(new CategoryChangeEvent($categories->getLead(), $categories->getCategory(), false), LeadEvents::LEAD_CATEGORY_CHANGE); | |
} | |
} | |
if (!empty($deleteCats)) { | |
$this->getLeadCategoryRepository()->deleteEntities($deleteCats); | |
} | |
} | |
public function getLeadCategories(Lead $lead): array | |
{ | |
$leadCategories = $this->getLeadCategoryRepository()->getLeadCategories($lead); | |
$leadCategoryList = []; | |
foreach ($leadCategories as $category) { | |
$leadCategoryList[$category['id']] = $category['category_id']; | |
} | |
return $leadCategoryList; | |
} | |
/** | |
* @return mixed[] | |
*/ | |
public function getUnsubscribedLeadCategoriesIds(Lead $lead): array | |
{ | |
$leadCategories = $this->getLeadCategoryRepository()->getUnsubscribedLeadCategories($lead); | |
$leadCategoryList = []; | |
foreach ($leadCategories as $category) { | |
$leadCategoryList[$category['id']] = $category['category_id']; | |
} | |
return $leadCategoryList; | |
} | |
/** | |
* @param array $fields | |
* @param array $data | |
* @param bool $persist | |
* @param bool $skipIfExists | |
* | |
* @throws \Exception | |
*/ | |
public function import($fields, $data, $owner = null, $list = null, $tags = null, $persist = true, ?LeadEventLog $eventLog = null, $importId = null, $skipIfExists = false): bool | |
{ | |
$fields = array_flip($fields); | |
$fieldData = []; | |
// Extract company data and import separately | |
// Modifies the data array | |
$company = null; | |
[$companyFields, $companyData] = $this->companyModel->extractCompanyDataFromImport($fields, $data); | |
if (!empty($companyData)) { | |
$company = $this->companyModel->importCompany(array_flip($companyFields), $companyData); | |
} | |
foreach ($fields as $leadField => $importField) { | |
// Prevent overwriting existing data with empty data | |
if (array_key_exists($importField, $data) && !is_null($data[$importField]) && '' != $data[$importField]) { | |
$fieldData[$leadField] = InputHelper::_($data[$importField], 'string'); | |
} | |
} | |
if (array_key_exists('id', $fieldData)) { | |
$lead = $this->getEntity($fieldData['id']); | |
} | |
$lead ??= $this->checkForDuplicateContact($fieldData); | |
$merged = (bool) $lead->getId(); | |
if (!empty($fields['dateAdded']) && !empty($data[$fields['dateAdded']])) { | |
$dateAdded = new DateTimeHelper($data[$fields['dateAdded']]); | |
$lead->setDateAdded($dateAdded->getUtcDateTime()); | |
} | |
unset($fieldData['dateAdded']); | |
if (!empty($fields['dateModified']) && !empty($data[$fields['dateModified']])) { | |
$dateModified = new DateTimeHelper($data[$fields['dateModified']]); | |
$lead->setDateModified($dateModified->getUtcDateTime()); | |
} | |
unset($fieldData['dateModified']); | |
if (!empty($fields['lastActive']) && !empty($data[$fields['lastActive']])) { | |
$lastActive = new DateTimeHelper($data[$fields['lastActive']]); | |
$lead->setLastActive($lastActive->getUtcDateTime()); | |
} | |
unset($fieldData['lastActive']); | |
if (!empty($fields['dateIdentified']) && !empty($data[$fields['dateIdentified']])) { | |
$dateIdentified = new DateTimeHelper($data[$fields['dateIdentified']]); | |
$lead->setDateIdentified($dateIdentified->getUtcDateTime()); | |
} | |
unset($fieldData['dateIdentified']); | |
if (!empty($fields['createdByUser']) && !empty($data[$fields['createdByUser']])) { | |
$userRepo = $this->em->getRepository(User::class); | |
$createdByUser = $userRepo->findByIdentifier($data[$fields['createdByUser']]); | |
if (null !== $createdByUser) { | |
$lead->setCreatedBy($createdByUser); | |
} | |
} | |
unset($fieldData['createdByUser']); | |
if (!empty($fields['modifiedByUser']) && !empty($data[$fields['modifiedByUser']])) { | |
$userRepo = $this->em->getRepository(User::class); | |
$modifiedByUser = $userRepo->findByIdentifier($data[$fields['modifiedByUser']]); | |
if (null !== $modifiedByUser) { | |
$lead->setModifiedBy($modifiedByUser); | |
} | |
} | |
unset($fieldData['modifiedByUser']); | |
if (!empty($fields['ip']) && !empty($data[$fields['ip']])) { | |
$addresses = explode(',', $data[$fields['ip']]); | |
foreach ($addresses as $address) { | |
$address = trim($address); | |
if (!$ipAddress = $this->ipAddressModel->findOneByIpAddress($address)) { | |
$ipAddress = new IpAddress(); | |
$ipAddress->setIpAddress($address); | |
} | |
$lead->addIpAddress($ipAddress); | |
} | |
} | |
unset($fieldData['ip']); | |
if (!empty($fields['points']) && !empty($data[$fields['points']]) && null === $lead->getId()) { | |
// Add points only for new leads | |
$lead->setPoints($data[$fields['points']]); | |
// add a lead point change log | |
$log = new PointsChangeLog(); | |
$log->setDelta($data[$fields['points']]); | |
$log->setLead($lead); | |
$log->setType('lead'); | |
$log->setEventName($this->translator->trans('mautic.lead.import.event.name')); | |
$log->setActionName($this->translator->trans('mautic.lead.import.action.name', [ | |
'%name%' => $this->userHelper->getUser()->getUsername(), | |
])); | |
$log->setIpAddress($this->ipLookupHelper->getIpAddress()); | |
$log->setDateAdded(new \DateTime()); | |
$lead->addPointsChangeLog($log); | |
} | |
if (!empty($fields['stage']) && !empty($data[$fields['stage']])) { | |
static $stages = []; | |
$stageName = $data[$fields['stage']]; | |
if (!array_key_exists($stageName, $stages)) { | |
// Set stage for contact | |
$stage = $this->em->getRepository(Stage::class)->getStageByName($stageName); | |
if (empty($stage)) { | |
$stage = new Stage(); | |
$stage->setName($stageName); | |
$stages[$stageName] = $stage; | |
} | |
} else { | |
$stage = $stages[$stageName]; | |
} | |
$lead->setStage($stage); | |
// add a contact stage change log | |
$log = new StagesChangeLog(); | |
$log->setStage($stage); | |
$log->setEventName($stage->getId().':'.$stage->getName()); | |
$log->setLead($lead); | |
$log->setActionName( | |
$this->translator->trans( | |
'mautic.stage.import.action.name', | |
[ | |
'%name%' => $this->userHelper->getUser()->getUsername(), | |
] | |
) | |
); | |
$log->setDateAdded(new \DateTime()); | |
$lead->stageChangeLog($log); | |
} | |
unset($fieldData['stage']); | |
// Set unsubscribe status | |
if (!empty($fields['doNotEmail']) && isset($data[$fields['doNotEmail']]) && (!empty($fields['email']) && !empty($data[$fields['email']]))) { | |
$doNotEmail = filter_var($data[$fields['doNotEmail']], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); | |
if (null !== $doNotEmail) { | |
$reason = $this->translator->trans('mautic.lead.import.by.user', [ | |
'%user%' => $this->userHelper->getUser()->getUsername(), | |
]); | |
// The email must be set for successful unsubscribtion | |
$lead->addUpdatedField('email', $data[$fields['email']]); | |
if ($doNotEmail) { | |
$event = new DoNotContactAddEvent($lead, 'email', $reason, DNC::MANUAL); | |
$this->dispatcher->dispatch($event, DoNotContactAddEvent::ADD_DONOT_CONTACT); | |
} else { | |
$event = new DoNotContactRemoveEvent($lead, 'email'); | |
$this->dispatcher->dispatch($event, DoNotContactRemoveEvent::REMOVE_DONOT_CONTACT); | |
} | |
} | |
} | |
unset($fieldData['doNotEmail']); | |
if (!empty($fields['ownerusername']) && !empty($data[$fields['ownerusername']])) { | |
try { | |
$newOwner = $this->userProvider->loadUserByIdentifier($data[$fields['ownerusername']]); | |
$lead->setOwner($newOwner); | |
// reset default import owner if exists owner for contact | |
$owner = null; | |
} catch (NonUniqueResultException) { | |
// user not found | |
} | |
} | |
unset($fieldData['ownerusername']); | |
if (!empty($fields['tags']) && !empty($data[$fields['tags']])) { | |
$leadTags = explode('|', $data[$fields['tags']]); | |
$this->modifyTags($lead, $leadTags, null, false); | |
} | |
unset($fieldData['tags']); | |
if (null !== $owner) { | |
$lead->setOwner($this->em->getReference(User::class, $owner)); | |
} | |
if (null !== $tags) { | |
$this->modifyTags($lead, $tags, null, false); | |
} | |
if (empty($this->leadFields)) { | |
$this->leadFields = $this->leadFieldModel->getEntities( | |
[ | |
'filter' => [ | |
'force' => [ | |
[ | |
'column' => 'f.isPublished', | |
'expr' => 'eq', | |
'value' => true, | |
], | |
[ | |
'column' => 'f.object', | |
'expr' => 'eq', | |
'value' => 'lead', | |
], | |
], | |
], | |
'hydration_mode' => 'HYDRATE_ARRAY', | |
'result_cache' => new ResultCacheOptions(LeadField::CACHE_NAMESPACE), | |
] | |
); | |
} | |
$fieldErrors = []; | |
foreach ($this->leadFields as $leadField) { | |
// Skip If value already exists | |
if ($skipIfExists && !$lead->isNew() && !empty($lead->getFieldValue($leadField['alias']))) { | |
unset($fieldData[$leadField['alias']]); | |
continue; | |
} | |
if (isset($fieldData[$leadField['alias']])) { | |
if ('NULL' === $fieldData[$leadField['alias']]) { | |
$fieldData[$leadField['alias']] = null; | |
continue; | |
} | |
try { | |
$this->cleanFields($fieldData, $leadField); | |
} catch (\Exception $exception) { | |
$fieldErrors[] = $leadField['alias'].': '.$exception->getMessage(); | |
} | |
if ('email' === $leadField['type'] && !empty($fieldData[$leadField['alias']])) { | |
try { | |
$this->emailValidator->validate($fieldData[$leadField['alias']], false); | |
} catch (\Exception $exception) { | |
$fieldErrors[] = $leadField['alias'].': '.$exception->getMessage(); | |
} | |
} | |
// Skip if the value is in the CSV row | |
continue; | |
} elseif ($lead->isNew() && $leadField['defaultValue']) { | |
// Fill in the default value if any | |
$fieldData[$leadField['alias']] = ('multiselect' === $leadField['type']) ? [$leadField['defaultValue']] : $leadField['defaultValue']; | |
} | |
} | |
if ($fieldErrors) { | |
$fieldErrors = implode("\n", $fieldErrors); | |
throw new \Exception($fieldErrors); | |
} | |
// All clear | |
foreach ($fieldData as $field => $value) { | |
$lead->addUpdatedField($field, $value); | |
} | |
$lead->imported = true; | |
if ($eventLog) { | |
$action = $merged ? 'updated' : 'inserted'; | |
$eventLog->setAction($action); | |
} | |
if ($persist) { | |
$lead->setManipulator(new LeadManipulator( | |
'lead', | |
'import', | |
$importId, | |
$this->userHelper->getUser()->getName() | |
)); | |
$this->saveEntity($lead); | |
if (null !== $list) { | |
$this->addToLists($lead, [$list]); | |
} | |
if (null !== $company) { | |
$this->companyModel->addLeadToCompany($company, $lead); | |
$this->setPrimaryCompany($company->getId(), $lead->getId()); | |
} | |
if ($eventLog) { | |
$lead->addEventLog($eventLog); | |
} | |
} | |
return $merged; | |
} | |
/** | |
* Update a leads tags. | |
* | |
* @param bool|false $removeOrphans | |
*/ | |
public function setTags(Lead $lead, array $tags, $removeOrphans = false): void | |
{ | |
/** @var Tag[] $currentTags */ | |
$currentTags = $lead->getTags(); | |
$leadModified = $tagsDeleted = false; | |
foreach ($currentTags as $tag) { | |
if (!in_array($tag->getId(), $tags)) { | |
// Tag has been removed | |
$lead->removeTag($tag); | |
$leadModified = $tagsDeleted = true; | |
} else { | |
// Remove tag so that what's left are new tags | |
$key = array_search($tag->getId(), $tags); | |
unset($tags[$key]); | |
} | |
} | |
if (!empty($tags)) { | |
foreach ($tags as $tag) { | |
if (is_numeric($tag)) { | |
// Existing tag being added to this lead | |
$lead->addTag( | |
$this->em->getReference(Tag::class, $tag) | |
); | |
} else { | |
$lead->addTag( | |
$this->getTagRepository()->getTagByNameOrCreateNewOne($tag) | |
); | |
} | |
} | |
$leadModified = true; | |
} | |
if ($leadModified) { | |
$this->saveEntity($lead); | |
// Delete orphaned tags | |
if ($tagsDeleted && $removeOrphans) { | |
$this->getTagRepository()->deleteOrphans(); | |
} | |
} | |
} | |
/** | |
* Update a leads UTM tags. | |
*/ | |
public function setUtmTags(Lead $lead, UtmTag $utmTags): void | |
{ | |
$lead->setUtmTags($utmTags); | |
$this->saveEntity($lead); | |
} | |
/** | |
* Add leads UTM tags via API. | |
* | |
* @param array $params | |
*/ | |
public function addUTMTags(Lead $lead, $params): void | |
{ | |
// known "synonym" fields expected | |
$synonyms = ['useragent' => 'user_agent', | |
'remotehost' => 'remote_host', ]; | |
// convert 'query' option to an array if necessary | |
if (isset($params['query']) && !is_array($params['query'])) { | |
// assume it's a query string; convert it to array | |
parse_str($params['query'], $queryResult); | |
if (!empty($queryResult)) { | |
$params['query'] = $queryResult; | |
} else { | |
// Something wrong with, remove it | |
unset($params['query']); | |
} | |
} | |
// Fix up known synonym/mismatch field names | |
foreach ($synonyms as $expected => $replace) { | |
if (array_key_exists($expected, $params) && !isset($params[$replace])) { | |
// add expected key name | |
$params[$replace] = $params[$expected]; | |
} | |
} | |
// see if active date set, so we can use it | |
$updateLastActive = false; | |
$lastActive = new \DateTime(); | |
// should be: yyyy-mm-ddT00:00:00+00:00 | |
if (isset($params['lastActive'])) { | |
$lastActive = new \DateTime($params['lastActive']); | |
$updateLastActive = true; | |
} | |
$params['date_added'] = $lastActive; | |
// New utmTag | |
$utmTags = new UtmTag(); | |
// get available fields and their setter. | |
$fields = $utmTags->getFieldSetterList(); | |
// cycle through calling appropriate setter | |
foreach ($fields as $q => $setter) { | |
if (isset($params[$q])) { | |
$utmTags->$setter($params[$q]); | |
} | |
} | |
// create device | |
if (!empty($params['useragent'])) { | |
$this->deviceTracker->createDeviceFromUserAgent($lead, $params['useragent']); | |
} | |
// add the lead | |
$utmTags->setLead($lead); | |
if ($updateLastActive) { | |
$lead->setLastActive($lastActive); | |
} | |
$this->setUtmTags($lead, $utmTags); | |
} | |
/** | |
* Removes a UtmTag set from a Lead. | |
* | |
* @param int $utmId | |
*/ | |
public function removeUtmTags(Lead $lead, $utmId): bool | |
{ | |
/** @var UtmTag $utmTag */ | |
foreach ($lead->getUtmTags() as $utmTag) { | |
if ($utmTag->getId() === $utmId) { | |
$lead->removeUtmTagEntry($utmTag); | |
$this->saveEntity($lead); | |
return true; | |
} | |
} | |
return false; | |
} | |
/** | |
* Modify tags with support to remove via a prefixed minus sign. | |
* | |
* @param bool $persist True if tags modified | |
*/ | |
public function modifyTags(Lead $lead, $tags, ?array $removeTags = null, $persist = true): bool | |
{ | |
$tagsModified = false; | |
$leadTags = $lead->getTags(); | |
if (!$leadTags->isEmpty()) { | |
$this->logger->debug('CONTACT: Contact currently has tags '.implode(', ', $leadTags->getKeys())); | |
} else { | |
$this->logger->debug('CONTACT: Contact currently does not have any tags'); | |
} | |
if (!is_array($tags)) { | |
$tags = explode(',', $tags); | |
} | |
if (empty($tags) && empty($removeTags)) { | |
return false; | |
} | |
$this->logger->debug('CONTACT: Adding '.implode(', ', $tags).' to contact ID# '.$lead->getId()); | |
array_walk($tags, function (&$val): void { | |
$val = html_entity_decode(trim($val), ENT_QUOTES); | |
$val = InputHelper::clean($val); | |
}); | |
// See which tags already exist | |
$foundTags = $this->getTagRepository()->getTagsByName($tags); | |
foreach ($tags as $tag) { | |
if (str_starts_with($tag, '-')) { | |
// Tag to be removed | |
$tag = substr($tag, 1); | |
if (array_key_exists($tag, $foundTags) && $leadTags->contains($foundTags[$tag])) { | |
$tagsModified = true; | |
$lead->removeTag($foundTags[$tag]); | |
$this->logger->debug('CONTACT: Removed '.$tag); | |
} | |
} else { | |
$tagToBeAdded = null; | |
if (!array_key_exists($tag, $foundTags)) { | |
$tagToBeAdded = new Tag($tag, false); | |
} elseif (!$leadTags->contains($foundTags[$tag])) { | |
$tagToBeAdded = $foundTags[$tag]; | |
} | |
if ($tagToBeAdded) { | |
$lead->addTag($tagToBeAdded); | |
$tagsModified = true; | |
$this->logger->debug('CONTACT: Added '.$tag); | |
} | |
} | |
} | |
if (!empty($removeTags)) { | |
$this->logger->debug('CONTACT: Removing '.implode(', ', $removeTags).' for contact ID# '.$lead->getId()); | |
array_walk($removeTags, function (&$val): void { | |
$val = html_entity_decode(trim($val), ENT_QUOTES); | |
$val = InputHelper::clean($val); | |
}); | |
// See which tags really exist | |
$foundRemoveTags = $this->getTagRepository()->getTagsByName($removeTags); | |
foreach ($removeTags as $tag) { | |
// Tag to be removed | |
if (array_key_exists($tag, $foundRemoveTags) && $leadTags->contains($foundRemoveTags[$tag])) { | |
$lead->removeTag($foundRemoveTags[$tag]); | |
$tagsModified = true; | |
$this->logger->debug('CONTACT: Removed '.$tag); | |
} | |
} | |
} | |
if ($persist) { | |
$this->saveEntity($lead); | |
} | |
return $tagsModified; | |
} | |
/** | |
* Modify companies for lead. | |
* | |
* @param int[] $companies | |
*/ | |
public function modifyCompanies(Lead $lead, array $companies): void | |
{ | |
// See which companies belong to the lead already | |
$leadCompanies = $this->companyModel->getCompanyLeadRepository()->getCompaniesByLeadId($lead->getId()); | |
$requestedCompanies = new Collection($companies); | |
$currentCompanies = (new Collection($leadCompanies))->keyBy('company_id'); | |
// Add companies that are not in the array of found companies | |
$addCompanies = $requestedCompanies->reject( | |
// Reject if the lead is already in the given company | |
fn ($companyId) => $currentCompanies->has($companyId) | |
); | |
if ($addCompanies->count()) { | |
$this->companyModel->addLeadToCompany($addCompanies->toArray(), $lead); | |
} | |
// Remove companies that are not in the array of given companies | |
$removeCompanies = $currentCompanies->reject( | |
fn (array $company) => | |
// Reject if the found company is still in the list of companies given | |
$requestedCompanies->contains($company['company_id']) | |
); | |
if ($removeCompanies->count()) { | |
$this->companyModel->removeLeadFromCompany($removeCompanies->keys()->toArray(), $lead); | |
} | |
} | |
/** | |
* Get array of available lead tags. | |
* | |
* @return mixed[] | |
*/ | |
public function getTagList(): array | |
{ | |
return $this->getTagRepository()->getSimpleList(null, [], 'tag', 'id'); | |
} | |
/** | |
* Get bar chart data of contacts. | |
* | |
* @param string $unit {@link php.net/manual/en/function.date.php#refsect1-function.date-parameters} | |
* @param \DateTime $dateFrom | |
* @param \DateTime $dateTo | |
* @param string $dateFormat | |
* @param array $filter | |
* @param bool $canViewOthers | |
*/ | |
public function getLeadsLineChartData($unit, $dateFrom, $dateTo, $dateFormat = null, $filter = [], $canViewOthers = true): array | |
{ | |
$flag = null; | |
$topLists = null; | |
$allLeadsT = $this->translator->trans('mautic.lead.all.leads'); | |
$identifiedT = $this->translator->trans('mautic.lead.identified'); | |
$anonymousT = $this->translator->trans('mautic.lead.lead.anonymous'); | |
if (isset($filter['flag'])) { | |
$flag = $filter['flag']; | |
unset($filter['flag']); | |
} | |
if (!$canViewOthers) { | |
$filter['owner_id'] = $this->userHelper->getUser()->getId(); | |
} | |
$chart = new LineChart($unit, $dateFrom, $dateTo, $dateFormat); | |
$query = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo); | |
$anonymousFilter = $filter; | |
$anonymousFilter['date_identified'] = [ | |
'expression' => 'isNull', | |
]; | |
$identifiedFilter = $filter; | |
$identifiedFilter['date_identified'] = [ | |
'expression' => 'isNotNull', | |
]; | |
if ('top' == $flag) { | |
$topLists = $this->leadListModel->getTopLists(6, $dateFrom, $dateTo); | |
foreach ($topLists as $list) { | |
$filter['leadlist_id'] = [ | |
'value' => $list['id'], | |
'list_column_name' => 't.id', | |
]; | |
$all = $query->fetchTimeData('leads', 'date_added', $filter); | |
$chart->setDataset($list['name'].': '.$allLeadsT, $all); | |
} | |
} elseif ('topIdentifiedVsAnonymous' == $flag) { | |
$topLists = $this->leadListModel->getTopLists(3, $dateFrom, $dateTo); | |
foreach ($topLists as $list) { | |
$anonymousFilter['leadlist_id'] = [ | |
'value' => $list['id'], | |
'list_column_name' => 't.id', | |
]; | |
$identifiedFilter['leadlist_id'] = [ | |
'value' => $list['id'], | |
'list_column_name' => 't.id', | |
]; | |
$identified = $query->fetchTimeData('leads', 'date_added', $identifiedFilter); | |
$anonymous = $query->fetchTimeData('leads', 'date_added', $anonymousFilter); | |
$chart->setDataset($list['name'].': '.$identifiedT, $identified); | |
$chart->setDataset($list['name'].': '.$anonymousT, $anonymous); | |
} | |
} elseif ('identified' == $flag) { | |
$identified = $query->fetchTimeData('leads', 'date_added', $identifiedFilter); | |
$chart->setDataset($identifiedT, $identified); | |
} elseif ('anonymous' == $flag) { | |
$anonymous = $query->fetchTimeData('leads', 'date_added', $anonymousFilter); | |
$chart->setDataset($anonymousT, $anonymous); | |
} elseif ('identifiedVsAnonymous' == $flag) { | |
$identified = $query->fetchTimeData('leads', 'date_added', $identifiedFilter); | |
$anonymous = $query->fetchTimeData('leads', 'date_added', $anonymousFilter); | |
$chart->setDataset($identifiedT, $identified); | |
$chart->setDataset($anonymousT, $anonymous); | |
} else { | |
$all = $query->fetchTimeData('leads', 'date_added', $filter); | |
$chart->setDataset($allLeadsT, $all); | |
} | |
return $chart->render(); | |
} | |
/** | |
* Get pie chart data of dwell times. | |
* | |
* @param string $dateFrom | |
* @param string $dateTo | |
* @param array $filters | |
* @param bool $canViewOthers | |
*/ | |
public function getAnonymousVsIdentifiedPieChartData($dateFrom, $dateTo, $filters = [], $canViewOthers = true): array | |
{ | |
$chart = new PieChart(); | |
$query = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo); | |
if (!$canViewOthers) { | |
$filter['owner_id'] = $this->userHelper->getUser()->getId(); | |
} | |
$identified = $query->count('leads', 'date_identified', 'date_added', $filters); | |
$all = $query->count('leads', 'id', 'date_added', $filters); | |
$chart->setDataset($this->translator->trans('mautic.lead.identified'), $identified); | |
$chart->setDataset($this->translator->trans('mautic.lead.lead.anonymous'), $all - $identified); | |
return $chart->render(); | |
} | |
/** | |
* Get leads count per country name. | |
* Can't use entity, because country is a custom field. | |
* | |
* @param \DateTime $dateFrom | |
* @param \DateTime $dateTo | |
* @param mixed[] $filters | |
* @param bool $canViewOthers | |
*/ | |
public function getLeadMapData($dateFrom, $dateTo, $filters = [], $canViewOthers = true): array | |
{ | |
if (!$canViewOthers) { | |
$filter['owner_id'] = $this->userHelper->getUser()->getId(); | |
} | |
$q = $this->em->getConnection()->createQueryBuilder(); | |
$q->select('COUNT(t.id) as quantity, t.country') | |
->from(MAUTIC_TABLE_PREFIX.'leads', 't') | |
->groupBy('t.country') | |
->where($q->expr()->isNotNull('t.country')); | |
$chartQuery = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo); | |
$chartQuery->applyFilters($q, $filters); | |
$chartQuery->applyDateFilters($q, 'date_added'); | |
$results = $q->executeQuery()->fetchAllAssociative(); | |
$countries = array_flip(Countries::getNames('en')); | |
$mapData = []; | |
// Convert country names to 2-char code | |
if ($results) { | |
foreach ($results as $leadCountry) { | |
if (isset($countries[$leadCountry['country']])) { | |
$mapData[$countries[$leadCountry['country']]] = $leadCountry['quantity']; | |
} | |
} | |
} | |
return $mapData; | |
} | |
/** | |
* @param string[] $aliases | |
* | |
* @return mixed[] | |
* | |
* @throws DBALException | |
*/ | |
public function getCustomLeadFieldLength(array $aliases): array | |
{ | |
$columns = []; | |
foreach ($aliases as $alias) { | |
$columns[] = sprintf('max(CHAR_LENGTH(%s)) %s', $alias, $alias); | |
} | |
$query = $this->em->getConnection()->createQueryBuilder(); | |
$query->select(implode(', ', $columns)) | |
->from(MAUTIC_TABLE_PREFIX.'leads'); | |
return $query->executeQuery()->fetchAssociative(); | |
} | |
/** | |
* Get a list of top (by leads owned) users. | |
* | |
* @param int $limit | |
* @param string $dateFrom | |
* @param string $dateTo | |
* @param array $filters | |
* | |
* @return array | |
*/ | |
public function getTopOwners($limit = 10, $dateFrom = null, $dateTo = null, $filters = []) | |
{ | |
$q = $this->em->getConnection()->createQueryBuilder(); | |
$q->select('COUNT(t.id) AS leads, t.owner_id, u.first_name, u.last_name') | |
->from(MAUTIC_TABLE_PREFIX.'leads', 't') | |
->join('t', MAUTIC_TABLE_PREFIX.'users', 'u', 'u.id = t.owner_id') | |
->where($q->expr()->isNotNull('t.owner_id')) | |
->orderBy('leads', 'DESC') | |
->groupBy('t.owner_id, u.first_name, u.last_name') | |
->setMaxResults($limit); | |
$chartQuery = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo); | |
$chartQuery->applyFilters($q, $filters); | |
$chartQuery->applyDateFilters($q, 'date_added'); | |
return $q->executeQuery()->fetchAllAssociative(); | |
} | |
/** | |
* Get a list of top (by leads owned) users. | |
* | |
* @param int $limit | |
* @param string $dateFrom | |
* @param string $dateTo | |
* @param array $filters | |
* | |
* @return array | |
*/ | |
public function getTopCreators($limit = 10, $dateFrom = null, $dateTo = null, $filters = []) | |
{ | |
$q = $this->em->getConnection()->createQueryBuilder(); | |
$q->select('COUNT(t.id) AS leads, t.created_by, t.created_by_user') | |
->from(MAUTIC_TABLE_PREFIX.'leads', 't') | |
->where($q->expr()->isNotNull('t.created_by')) | |
->andWhere($q->expr()->isNotNull('t.created_by_user')) | |
->orderBy('leads', 'DESC') | |
->groupBy('t.created_by, t.created_by_user') | |
->setMaxResults($limit); | |
$chartQuery = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo); | |
$chartQuery->applyFilters($q, $filters); | |
$chartQuery->applyDateFilters($q, 'date_added'); | |
return $q->executeQuery()->fetchAllAssociative(); | |
} | |
/** | |
* Get a list of leads in a date range. | |
* | |
* @param int $limit | |
* @param array $filters | |
* @param array $options | |
* | |
* @return array | |
*/ | |
public function getLeadList($limit = 10, ?\DateTime $dateFrom = null, ?\DateTime $dateTo = null, $filters = [], $options = []) | |
{ | |
if (!empty($options['canViewOthers'])) { | |
$filter['owner_id'] = $this->userHelper->getUser()->getId(); | |
} | |
$q = $this->em->getConnection()->createQueryBuilder(); | |
$q->select('t.id, t.firstname, t.lastname, t.email, t.date_added, t.date_modified') | |
->from(MAUTIC_TABLE_PREFIX.'leads', 't') | |
->setMaxResults($limit); | |
$chartQuery = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo); | |
$chartQuery->applyFilters($q, $filters); | |
$chartQuery->applyDateFilters($q, 'date_added'); | |
if (empty($options['includeAnonymous'])) { | |
$q->andWhere($q->expr()->isNotNull('t.date_identified')); | |
} | |
$results = $q->executeQuery()->fetchAllAssociative(); | |
if ($results) { | |
foreach ($results as &$result) { | |
if ($result['firstname'] || $result['lastname']) { | |
$result['name'] = trim($result['firstname'].' '.$result['lastname']); | |
} elseif ($result['email']) { | |
$result['name'] = $result['email']; | |
} else { | |
$result['name'] = 'anonymous'; | |
} | |
unset($result['firstname']); | |
unset($result['lastname']); | |
unset($result['email']); | |
} | |
} | |
return $results; | |
} | |
/** | |
* @param array<mixed, mixed>|null $filters | |
*/ | |
public function getEngagements(?Lead $lead = null, ?array $filters = null, ?array $orderBy = null, int $page = 1, int $limit = 25, bool $forTimeline = true): array | |
{ | |
$event = $this->dispatcher->dispatch( | |
new LeadTimelineEvent($lead, $filters, $orderBy, $page, $limit, $forTimeline, $this->coreParametersHelper->get('site_url')), | |
LeadEvents::TIMELINE_ON_GENERATE | |
); | |
$payload = [ | |
'events' => $event->getEvents(), | |
'filters' => $filters, | |
'order' => $orderBy, | |
'types' => $event->getEventTypes(), | |
'total' => $event->getEventCounter()['total'], | |
'page' => $page, | |
'limit' => $limit, | |
'maxPages' => $event->getMaxPage(), | |
]; | |
return ($forTimeline) ? $payload : [$payload, $event->getSerializerGroups()]; | |
} | |
/** | |
* @return array | |
*/ | |
public function getEngagementTypes() | |
{ | |
$event = new LeadTimelineEvent(); | |
$event->fetchTypesOnly(); | |
$this->dispatcher->dispatch($event, LeadEvents::TIMELINE_ON_GENERATE); | |
return $event->getEventTypes(); | |
} | |
/** | |
* Get engagement counts by time unit. | |
* | |
* @param string $unit | |
*/ | |
public function getEngagementCount(Lead $lead, ?\DateTime $dateFrom = null, ?\DateTime $dateTo = null, $unit = 'm', ?ChartQuery $chartQuery = null): array | |
{ | |
$event = new LeadTimelineEvent($lead); | |
$event->setCountOnly($dateFrom, $dateTo, $unit, $chartQuery); | |
$this->dispatcher->dispatch($event, LeadEvents::TIMELINE_ON_GENERATE); | |
return $event->getEventCounter(); | |
} | |
public function addToCompany(Lead $lead, $company): bool | |
{ | |
// check if lead is in company already | |
if (!$company instanceof Company) { | |
$company = $this->companyModel->getEntity($company); | |
} | |
// company does not exist anymore | |
if (null === $company) { | |
return false; | |
} | |
$companyLead = $this->companyModel->getCompanyLeadRepository()->getCompaniesByLeadId($lead->getId(), $company->getId()); | |
if (empty($companyLead)) { | |
$this->companyModel->addLeadToCompany($company, $lead); | |
return true; | |
} | |
return false; | |
} | |
/** | |
* Get contact channels. | |
*/ | |
public function getContactChannels(Lead $lead): array | |
{ | |
$allChannels = $this->getPreferenceChannels(); | |
$channels = []; | |
foreach ($allChannels as $channel) { | |
if (DNC::IS_CONTACTABLE === $this->isContactable($lead, $channel)) { | |
$channels[$channel] = $channel; | |
} | |
} | |
return $channels; | |
} | |
/** | |
* Get contact channels. | |
*/ | |
public function getDoNotContactChannels(Lead $lead): array | |
{ | |
$allChannels = $this->getPreferenceChannels(); | |
$channels = []; | |
foreach ($allChannels as $channel) { | |
if (DNC::IS_CONTACTABLE !== $this->isContactable($lead, $channel)) { | |
$channels[$channel] = $channel; | |
} | |
} | |
return $channels; | |
} | |
public function getPreferenceChannels(): array | |
{ | |
return $this->channelListHelper->getFeatureChannels(self::CHANNEL_FEATURE, true); | |
} | |
/** | |
* @return array | |
*/ | |
public function getPreferredChannel(Lead $lead) | |
{ | |
$preferredChannel = $this->getFrequencyRuleRepository()->getPreferredChannel($lead->getId()); | |
if (!empty($preferredChannel)) { | |
return $preferredChannel[0]; | |
} | |
return []; | |
} | |
/** | |
* @return mixed[] | |
*/ | |
public function setPrimaryCompany($companyId, $leadId) | |
{ | |
$companyArray = []; | |
$oldPrimaryCompany = $newPrimaryCompany = false; | |
$lead = $this->getEntity($leadId); | |
$companyLeads = $this->companyModel->getCompanyLeadRepository()->getEntitiesByLead($lead); | |
/** @var CompanyLead $companyLead */ | |
foreach ($companyLeads as $companyLead) { | |
$company = $companyLead->getCompany(); | |
if ($companyLead) { | |
if ($companyLead->getPrimary() && !$oldPrimaryCompany) { | |
$oldPrimaryCompany = $companyLead->getCompany()->getId(); | |
} | |
if ($company->getId() === (int) $companyId) { | |
$companyLead->setPrimary(true); | |
$newPrimaryCompany = $companyId; | |
$lead->addUpdatedField('company', $company->getName()); | |
} else { | |
$companyLead->setPrimary(false); | |
} | |
$companyArray[] = $companyLead; | |
} | |
} | |
if (!$newPrimaryCompany) { | |
$latestCompany = $this->companyModel->getCompanyLeadRepository()->getLatestCompanyForLead($leadId); | |
if (!empty($latestCompany)) { | |
$lead->addUpdatedField('company', $latestCompany['companyname']) | |
->setDateModified(new \DateTime()); | |
} | |
} | |
if (!empty($companyArray)) { | |
$this->em->getRepository(Lead::class)->saveEntity($lead); | |
$this->companyModel->getCompanyLeadRepository()->saveEntities($companyArray, false); | |
} | |
// Clear CompanyLead entities from Doctrine memory | |
$this->companyModel->getCompanyLeadRepository()->detachEntities($companyLeads); | |
return ['oldPrimary' => $oldPrimaryCompany, 'newPrimary' => $companyId]; | |
} | |
public function scoreContactsCompany(Lead $lead, $score): bool | |
{ | |
$success = false; | |
$entities = []; | |
$contactCompanies = $this->companyModel->getCompanyLeadRepository()->getCompaniesByLeadId($lead->getId()); | |
foreach ($contactCompanies as $contactCompany) { | |
$company = $this->companyModel->getEntity($contactCompany['company_id']); | |
$oldScore = $company->getScore(); | |
$newScore = $score + $oldScore; | |
$company->setScore($newScore); | |
$entities[] = $company; | |
$success = true; | |
} | |
if (!empty($entities)) { | |
$this->companyModel->getRepository()->saveEntities($entities); | |
} | |
return $success; | |
} | |
public function updateLeadOwner(Lead $lead, $ownerId): void | |
{ | |
$owner = $this->em->getReference(User::class, $ownerId); | |
$lead->setOwner($owner); | |
parent::saveEntity($lead); | |
} | |
private function processManipulator(Lead $lead): void | |
{ | |
if ($lead->isNewlyCreated() || $lead->wasAnonymous()) { | |
// Only store an entry once for created and once for identified, not every time the lead is saved | |
$manipulator = $lead->getManipulator(); | |
if (null !== $manipulator && !$manipulator->wasLogged()) { | |
$manipulationLog = new LeadEventLog(); | |
$manipulationLog->setLead($lead) | |
->setBundle($manipulator->getBundleName()) | |
->setObject($manipulator->getObjectName()) | |
->setObjectId($manipulator->getObjectId()); | |
if ($lead->isAnonymous()) { | |
$manipulationLog->setAction('created_contact'); | |
} else { | |
$manipulationLog->setAction('identified_contact'); | |
} | |
$description = $manipulator->getObjectDescription(); | |
$manipulationLog->setProperties(['object_description' => $description]); | |
$lead->addEventLog($manipulationLog); | |
$manipulator->setAsLogged(); | |
} | |
} | |
} | |
/** | |
* @param bool $persist | |
*/ | |
protected function createNewContact(IpAddress $ip, $persist = true): Lead | |
{ | |
// let's create a lead | |
$lead = new Lead(); | |
$lead->addIpAddress($ip); | |
$lead->setNewlyCreated(true); | |
if ($persist && !defined('MAUTIC_NON_TRACKABLE_REQUEST')) { | |
// Set to prevent loops | |
$this->contactTracker->setTrackedContact($lead); | |
// Note ignoring a lead manipulator object here on purpose to not falsely record entries | |
$this->saveEntity($lead, false); | |
$fields = $this->getLeadDetails($lead); | |
$lead->setFields($fields); | |
} | |
if ($leadId = $lead->getId()) { | |
$this->logger->debug("LEAD: New lead created with ID# $leadId."); | |
} | |
return $lead; | |
} | |
/** | |
* @deprecated 2.12.0 to be removed in 3.0; use Mautic\LeadBundle\Model\DoNotContact instead | |
* | |
* @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 $lead, $channel) | |
{ | |
if (is_array($channel)) { | |
$channel = key($channel); | |
} | |
/** @var \Mautic\LeadBundle\Entity\DoNotContactRepository $dncRepo */ | |
$dncRepo = $this->em->getRepository(DNC::class); | |
$dncEntries = $dncRepo->getEntriesByLeadAndChannel($lead, $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 getAvailableLeadFields(): array | |
{ | |
return $this->availableLeadFields; | |
} | |
/** | |
* @return array<string, int|float> | |
*/ | |
public function getLeadEmailStats(Lead $lead): array | |
{ | |
/** @var StatRepository $statRepository */ | |
$statRepository = $this->em->getRepository(Stat::class); | |
return $statRepository->getStatsSummaryForContacts([$lead->getId()])[$lead->getId()]; | |
} | |
public function removeTagFromLead(int $leadId, int $tagId): void | |
{ | |
$lead = $this->getEntity($leadId); | |
$tag = $this->getTagRepository()->find($tagId); | |
if ($lead && $tag) { | |
$lead->removeTag($tag); | |
$this->saveEntity($lead); | |
} | |
} | |
} | |