Spaces:
No application file
No application file
namespace Mautic\LeadBundle\Entity; | |
use Doctrine\Common\Collections\ArrayCollection; | |
use Doctrine\Common\Collections\Collection; | |
use Doctrine\Common\Collections\Criteria; | |
use Doctrine\ORM\Mapping as ORM; | |
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver; | |
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder; | |
use Mautic\CoreBundle\Entity\FormEntity; | |
use Mautic\CoreBundle\Entity\IpAddress; | |
use Mautic\LeadBundle\DataObject\LeadManipulator; | |
use Mautic\LeadBundle\Form\Validator\Constraints\UniqueCustomField; | |
use Mautic\LeadBundle\Model\FieldModel; | |
use Mautic\NotificationBundle\Entity\PushID; | |
use Mautic\PointBundle\Entity\Group; | |
use Mautic\PointBundle\Entity\GroupContactScore; | |
use Mautic\StageBundle\Entity\Stage; | |
use Mautic\UserBundle\Entity\User; | |
use Symfony\Component\Validator\Mapping\ClassMetadata; | |
class Lead extends FormEntity implements CustomFieldEntityInterface, IdentifierFieldEntityInterface | |
{ | |
use CustomFieldEntityTrait; | |
public const FIELD_ALIAS = ''; | |
public const POINTS_ADD = 'plus'; | |
public const POINTS_SUBTRACT = 'minus'; | |
public const POINTS_MULTIPLY = 'times'; | |
public const POINTS_DIVIDE = 'divide'; | |
public const POINTS_SET = 'set'; | |
public const DEFAULT_ALIAS = 'l'; | |
/** | |
* Used to determine social identity. | |
* | |
* @var array | |
*/ | |
private $availableSocialFields = []; | |
/** | |
* @var string | |
*/ | |
private $id; | |
private $title; | |
private $firstname; | |
private $lastname; | |
private $company; | |
private $position; | |
private $email; | |
private $phone; | |
private $mobile; | |
private $address1; | |
private $address2; | |
private $city; | |
private $state; | |
private $zipcode; | |
/** | |
* @var string|null | |
*/ | |
private $timezone; | |
private $country; | |
/** | |
* @var User|null | |
*/ | |
private $owner; | |
/** | |
* @var int | |
*/ | |
private $points = 0; | |
/** | |
* @var array | |
*/ | |
private $pointChanges = []; | |
/** | |
* @var int|null | |
*/ | |
private $updatedPoints; | |
/** | |
* @var \Doctrine\Common\Collections\Collection<int, \Mautic\LeadBundle\Entity\PointsChangeLog> | |
*/ | |
private $pointsChangeLog; | |
/** | |
* @var null | |
*/ | |
private $actualPoints; | |
/** | |
* @var \Doctrine\Common\Collections\Collection<int, \Mautic\LeadBundle\Entity\CompanyChangeLog> | |
*/ | |
private $companyChangeLog; | |
/** | |
* @var \Doctrine\Common\Collections\Collection<int, \Mautic\LeadBundle\Entity\DoNotContact> | |
*/ | |
private $doNotContact; | |
/** | |
* @var \Doctrine\Common\Collections\Collection<int, \Mautic\CoreBundle\Entity\IpAddress> | |
*/ | |
private $ipAddresses; | |
/** | |
* @var \Doctrine\Common\Collections\Collection<int, \Mautic\NotificationBundle\Entity\PushID> | |
*/ | |
private $pushIds; | |
/** | |
* @var ArrayCollection<int, LeadEventLog> | |
*/ | |
private $eventLog; | |
/** | |
* @var \DateTimeInterface | |
*/ | |
private $lastActive; | |
/** | |
* @var array | |
*/ | |
private $internal = []; | |
/** | |
* @var array | |
*/ | |
private $socialCache = []; | |
/** | |
* Used to populate trigger color. | |
* | |
* @var string | |
*/ | |
private $color; | |
/** | |
* @var LeadManipulator | |
*/ | |
private $manipulator; | |
/** | |
* @var bool | |
*/ | |
private $newlyCreated = false; | |
/** | |
* @var \DateTimeInterface | |
*/ | |
private $dateIdentified; | |
/** | |
* @var \Doctrine\Common\Collections\Collection<int, \Mautic\LeadBundle\Entity\LeadNote> | |
*/ | |
private $notes; | |
/** | |
* @var string|null | |
*/ | |
private $preferredProfileImage = 'gravatar'; | |
/** | |
* @var bool | |
*/ | |
public $imported = false; | |
/** | |
* @var \Doctrine\Common\Collections\Collection<int, \Mautic\LeadBundle\Entity\Tag> | |
*/ | |
private $tags; | |
/** | |
* @var Stage|null | |
*/ | |
private $stage; | |
/** | |
* @var \Doctrine\Common\Collections\Collection<int, \Mautic\LeadBundle\Entity\StagesChangeLog> | |
*/ | |
private $stageChangeLog; | |
/** | |
* @var \Doctrine\Common\Collections\Collection<int, \Mautic\LeadBundle\Entity\UtmTag> | |
*/ | |
private $utmtags; | |
/** | |
* @var \Doctrine\Common\Collections\Collection<int, \Mautic\LeadBundle\Entity\FrequencyRule> | |
*/ | |
private $frequencyRules; | |
/** | |
* @var ArrayCollection<int,GroupContactScore> | |
*/ | |
private $groupScores; | |
private $primaryCompany; | |
/** | |
* Used to determine order of preferred channels. | |
* | |
* @var array | |
*/ | |
private $channelRules = []; | |
public function __construct() | |
{ | |
$this->ipAddresses = new ArrayCollection(); | |
$this->pushIds = new ArrayCollection(); | |
$this->eventLog = new ArrayCollection(); | |
$this->doNotContact = new ArrayCollection(); | |
$this->pointsChangeLog = new ArrayCollection(); | |
$this->tags = new ArrayCollection(); | |
$this->stageChangeLog = new ArrayCollection(); | |
$this->frequencyRules = new ArrayCollection(); | |
$this->companyChangeLog = new ArrayCollection(); | |
$this->groupScores = new ArrayCollection(); | |
} | |
public static function loadMetadata(ORM\ClassMetadata $metadata): void | |
{ | |
$builder = new ClassMetadataBuilder($metadata); | |
$builder->setTable('leads') | |
->setCustomRepositoryClass(LeadRepository::class) | |
->addLifecycleEvent('checkDateIdentified', 'preUpdate') | |
->addLifecycleEvent('checkDateIdentified', 'prePersist') | |
->addLifecycleEvent('checkAttributionDate', 'preUpdate') | |
->addLifecycleEvent('checkAttributionDate', 'prePersist') | |
->addLifecycleEvent('checkDateAdded', 'prePersist') | |
->addIndex(['date_added'], 'lead_date_added') | |
->addIndex(['date_identified'], 'date_identified'); | |
$builder->addBigIntIdField(); | |
$builder->createManyToOne('owner', User::class) | |
->fetchLazy() | |
->addJoinColumn('owner_id', 'id', true, false, 'SET NULL') | |
->build(); | |
$builder->createField('points', 'integer') | |
->build(); | |
$builder->createOneToMany('pointsChangeLog', 'PointsChangeLog') | |
->orphanRemoval() | |
->setOrderBy(['dateAdded' => 'DESC']) | |
->mappedBy('lead') | |
->cascadeAll() | |
->fetchExtraLazy() | |
->build(); | |
$builder->createOneToMany('companyChangeLog', 'CompanyChangeLog') | |
->orphanRemoval() | |
->setOrderBy(['dateAdded' => 'DESC']) | |
->mappedBy('lead') | |
->cascadeAll() | |
->fetchExtraLazy() | |
->build(); | |
$builder->createOneToMany('doNotContact', DoNotContact::class) | |
->orphanRemoval() | |
->mappedBy('lead') | |
->cascadePersist() | |
->cascadeDetach() | |
->cascadeMerge() | |
->fetchExtraLazy() | |
->build(); | |
$builder->createManyToMany('ipAddresses', IpAddress::class) | |
->setJoinTable('lead_ips_xref') | |
->addInverseJoinColumn('ip_id', 'id', true, false, 'CASCADE') | |
->addJoinColumn('lead_id', 'id', false, false, 'CASCADE') | |
->setIndexBy('ipAddress') | |
->cascadeDetach() | |
->cascadeMerge() | |
->cascadePersist() | |
->build(); | |
$builder->createOneToMany('pushIds', PushID::class) | |
->orphanRemoval() | |
->mappedBy('lead') | |
->cascadeAll() | |
->fetchExtraLazy() | |
->build(); | |
$builder->createOneToMany('eventLog', LeadEventLog::class) | |
->mappedBy('lead') | |
->cascadePersist() | |
->cascadeMerge() | |
->cascadeDetach() | |
->fetchExtraLazy() | |
->build(); | |
$builder->createField('lastActive', 'datetime') | |
->columnName('last_active') | |
->nullable() | |
->build(); | |
$builder->createField('internal', 'array') | |
->nullable() | |
->build(); | |
$builder->createField('socialCache', 'array') | |
->columnName('social_cache') | |
->nullable() | |
->build(); | |
$builder->createField('dateIdentified', 'datetime') | |
->columnName('date_identified') | |
->nullable() | |
->build(); | |
$builder->createOneToMany('notes', 'LeadNote') | |
->orphanRemoval() | |
->setOrderBy(['dateAdded' => 'DESC']) | |
->mappedBy('lead') | |
->cascadeDetach() | |
->cascadeMerge() | |
->fetchExtraLazy() | |
->build(); | |
$builder->createField('preferredProfileImage', 'string') | |
->columnName('preferred_profile_image') | |
->nullable() | |
->build(); | |
$builder->createManyToMany('tags', Tag::class) | |
->setJoinTable('lead_tags_xref') | |
->addInverseJoinColumn('tag_id', 'id', false) | |
->addJoinColumn('lead_id', 'id', false, false, 'CASCADE') | |
->setOrderBy(['tag' => 'ASC']) | |
->setIndexBy('tag') | |
->fetchLazy() | |
->cascadeMerge() | |
->cascadePersist() | |
->cascadeDetach() | |
->build(); | |
$builder->createManyToOne('stage', Stage::class) | |
->cascadePersist() | |
->cascadeMerge() | |
->cascadeDetach() | |
->addJoinColumn('stage_id', 'id', true, false, 'SET NULL') | |
->build(); | |
$builder->createOneToMany('stageChangeLog', 'StagesChangeLog') | |
->orphanRemoval() | |
->setOrderBy(['dateAdded' => 'DESC']) | |
->mappedBy('lead') | |
->cascadeAll() | |
->fetchExtraLazy() | |
->build(); | |
$builder->createOneToMany('utmtags', UtmTag::class) | |
->orphanRemoval() | |
->mappedBy('lead') | |
->cascadeAll() | |
->fetchExtraLazy() | |
->build(); | |
$builder->createOneToMany('frequencyRules', FrequencyRule::class) | |
->orphanRemoval() | |
->setIndexBy('channel') | |
->setOrderBy(['dateAdded' => 'DESC']) | |
->mappedBy('lead') | |
->cascadeAll() | |
->fetchExtraLazy() | |
->build(); | |
$builder->createOneToMany('groupScores', GroupContactScore::class) | |
->mappedBy('contact') | |
->cascadeAll() | |
->fetchExtraLazy() | |
->build(); | |
self::loadFixedFieldMetadata( | |
$builder, | |
[ | |
'title', | |
'firstname', | |
'lastname', | |
'company', | |
'position', | |
'email', | |
'phone', | |
'mobile', | |
'address1', | |
'address2', | |
'city', | |
'state', | |
'zipcode', | |
'timezone', | |
'country', | |
], | |
FieldModel::$coreFields | |
); | |
} | |
/** | |
* Prepares the metadata for API usage. | |
*/ | |
public static function loadApiMetadata(ApiMetadataDriver $metadata): void | |
{ | |
$metadata->setRoot('lead') | |
->setGroupPrefix('leadBasic') | |
->addListProperties( | |
[ | |
'id', | |
'points', | |
'color', | |
'title', | |
'firstname', | |
'lastname', | |
'company', | |
'position', | |
'email', | |
'phone', | |
'mobile', | |
'address1', | |
'address2', | |
'city', | |
'state', | |
'zipcode', | |
'timezone', | |
'country', | |
] | |
) | |
->setGroupPrefix('lead') | |
->addListProperties( | |
[ | |
'id', | |
'points', | |
'color', | |
'fields', | |
] | |
) | |
->addProperties( | |
[ | |
'lastActive', | |
'owner', | |
'ipAddresses', | |
'tags', | |
'utmtags', | |
'stage', | |
'dateIdentified', | |
'preferredProfileImage', | |
'doNotContact', | |
'frequencyRules', | |
] | |
) | |
->build(); | |
} | |
public static function loadValidatorMetadata(ClassMetadata $metadata): void | |
{ | |
$metadata->addConstraint(new UniqueCustomField(['object' => 'lead'])); | |
} | |
public static function getDefaultIdentifierFields(): array | |
{ | |
return [ | |
'firstname', | |
'lastname', | |
'company', | |
'email', | |
]; | |
} | |
/** | |
* @param string $prop | |
* @param mixed $val | |
* @param mixed|null $oldValue | |
*/ | |
protected function isChanged($prop, $val, $oldValue = null) | |
{ | |
$getter = 'get'.ucfirst($prop); | |
$current = $oldValue ?? $this->$getter(); | |
if ('owner' == $prop) { | |
if ($current && !$val) { | |
$this->changes['owner'] = [$current->getId(), $val]; | |
} elseif (!$current && $val) { | |
$this->changes['owner'] = [$current, $val->getId()]; | |
} elseif ($current && $val && $current->getId() != $val->getId()) { | |
$this->changes['owner'] = [$current->getId(), $val->getId()]; | |
} | |
} elseif ('ipAddresses' == $prop) { | |
$this->changes['ipAddresses'] = ['', $val->getIpAddress()]; // Kept for BC. Not a good way to track changes on a collection | |
if (empty($this->changes['ipAddressList'])) { | |
$this->changes['ipAddressList'] = []; | |
} | |
$this->changes['ipAddressList'][$val->getIpAddress()] = $val; | |
} elseif ('tags' == $prop) { | |
if ($val instanceof Tag) { | |
$this->changes['tags']['added'][] = $val->getTag(); | |
} else { | |
$this->changes['tags']['removed'][] = $val; | |
} | |
} elseif ('utmtags' == $prop) { | |
if ($val instanceof UtmTag) { | |
if ($val->getUtmContent()) { | |
$this->changes['utmtags'] = ['utm_content', $val->getUtmContent()]; | |
} | |
if ($val->getUtmMedium()) { | |
$this->changes['utmtags'] = ['utm_medium', $val->getUtmMedium()]; | |
} | |
if ($val->getUtmCampaign()) { | |
$this->changes['utmtags'] = ['utm_campaign', $val->getUtmCampaign()]; | |
} | |
if ($val->getUtmTerm()) { | |
$this->changes['utmtags'] = ['utm_term', $val->getUtmTerm()]; | |
} | |
if ($val->getUtmSource()) { | |
$this->changes['utmtags'] = ['utm_source', $val->getUtmSource()]; | |
} | |
} | |
} elseif ('frequencyRules' == $prop) { | |
if (!isset($this->changes['frequencyRules'])) { | |
$this->changes['frequencyRules'] = []; | |
} | |
if ($val instanceof FrequencyRule) { | |
$channel = $val->getChannel(); | |
$this->changes['frequencyRules'][$channel] = $val->getChanges(); | |
} else { | |
$this->changes['frequencyRules']['removed'][] = $val; | |
} | |
} elseif ('stage' == $prop) { | |
if ($current && !$val) { | |
$this->changes['stage'] = [$current->getId(), $val]; | |
} elseif (!$current && $val) { | |
$this->changes['stage'] = [$current, $val->getId()]; | |
} elseif ($current && $val && $current->getId() != $val->getId()) { | |
$this->changes['stage'] = [$current->getId(), $val->getId()]; | |
} | |
} elseif ('points' == $prop && $current != $val) { | |
$this->changes['points'] = [$current, $val]; | |
} else { | |
parent::isChanged($prop, $val); | |
} | |
} | |
public function convertToArray(): array | |
{ | |
return get_object_vars($this); | |
} | |
/** | |
* Set id. | |
* | |
* @param int $id | |
* | |
* @return Lead | |
*/ | |
public function setId($id) | |
{ | |
$this->id = (string) $id; | |
return $this; | |
} | |
/** | |
* Get id. | |
*/ | |
public function getId(): int | |
{ | |
return (int) $this->id; | |
} | |
/** | |
* Set owner. | |
* | |
* @return Lead | |
*/ | |
public function setOwner(User $owner = null) | |
{ | |
$this->isChanged('owner', $owner); | |
$this->owner = $owner; | |
return $this; | |
} | |
/** | |
* @return User|null | |
*/ | |
public function getOwner() | |
{ | |
return $this->owner; | |
} | |
/** | |
* Returns the user to be used for permissions. | |
* | |
* @return User|int | |
*/ | |
public function getPermissionUser() | |
{ | |
return $this->getOwner() ?? $this->getCreatedBy(); | |
} | |
/** | |
* Add ipAddress. | |
* | |
* @return Lead | |
*/ | |
public function addIpAddress(IpAddress $ipAddress) | |
{ | |
if (!$ipAddress->isTrackable()) { | |
return $this; | |
} | |
$ip = $ipAddress->getIpAddress(); | |
if (!isset($this->ipAddresses[$ip])) { | |
$this->isChanged('ipAddresses', $ipAddress); | |
$this->ipAddresses[$ip] = $ipAddress; | |
} | |
return $this; | |
} | |
/** | |
* Remove ipAddress. | |
*/ | |
public function removeIpAddress(IpAddress $ipAddress): void | |
{ | |
$this->ipAddresses->removeElement($ipAddress); | |
} | |
/** | |
* Get ipAddresses. | |
* | |
* @return Collection | |
*/ | |
public function getIpAddresses() | |
{ | |
return $this->ipAddresses; | |
} | |
/** | |
* Get full name. | |
* | |
* @param bool $lastFirst | |
* | |
* @return string | |
*/ | |
public function getName($lastFirst = false) | |
{ | |
$firstName = $this->getFirstname(); | |
$lastName = $this->getLastname(); | |
$fullName = ''; | |
if ($lastFirst && $firstName && $lastName) { | |
$fullName = $lastName.', '.$firstName; | |
} elseif ($firstName && $lastName) { | |
$fullName = $firstName.' '.$lastName; | |
} elseif ($firstName) { | |
$fullName = $firstName; | |
} elseif ($lastName) { | |
$fullName = $lastName; | |
} | |
return $fullName; | |
} | |
/** | |
* Get preferred locale. | |
* | |
* @return string | |
*/ | |
public function getPreferredLocale() | |
{ | |
if (isset($this->updatedFields['preferred_locale'])) { | |
return $this->updatedFields['preferred_locale']; | |
} | |
if (!empty($this->fields['core']['preferred_locale']['value'])) { | |
return $this->fields['core']['preferred_locale']['value']; | |
} | |
return ''; | |
} | |
/** | |
* Get the primary identifier for the lead. | |
* | |
* @param bool $lastFirst | |
* | |
* @return string | |
*/ | |
public function getPrimaryIdentifier($lastFirst = false) | |
{ | |
if ($name = $this->getName($lastFirst)) { | |
return $name; | |
} elseif ($this->getCompany()) { | |
return $this->getCompany(); | |
} elseif ($this->getEmail()) { | |
return $this->getEmail(); | |
} elseif ($socialIdentity = $this->getFirstSocialIdentity()) { | |
return $socialIdentity; | |
} elseif (count($ips = $this->getIpAddresses())) { | |
return $ips->first()->getIpAddress(); | |
} else { | |
return 'mautic.lead.lead.anonymous'; | |
} | |
} | |
/** | |
* Get the secondary identifier for the lead; mainly company. | |
* | |
* @return string | |
*/ | |
public function getSecondaryIdentifier() | |
{ | |
if ($this->getCompany()) { | |
return $this->getCompany(); | |
} | |
return ''; | |
} | |
/** | |
* Get the location for the lead. | |
*/ | |
public function getLocation(): string | |
{ | |
$location = ''; | |
if ($this->getCity()) { | |
$location .= $this->getCity().', '; | |
} | |
if ($this->getState()) { | |
$location .= $this->getState().', '; | |
} | |
if ($this->getCountry()) { | |
$location .= $this->getCountry().', '; | |
} | |
return rtrim($location, ', '); | |
} | |
/** | |
* Point changes are tracked and will be persisted as a direct DB query to avoid PHP memory overwrites with concurrent requests | |
* The risk in this is that the $changes['points'] may not be accurate but at least no points are lost. | |
* | |
* @param int $points | |
* @param string $operator | |
* | |
* @return Lead | |
*/ | |
public function adjustPoints($points, $operator = self::POINTS_ADD) | |
{ | |
if (!$points = (int) $points) { | |
return $this; | |
} | |
// Use $updatedPoints in an attempt to keep track in the $changes log although this may not be accurate if the DB updates the points rather | |
// than PHP memory | |
if (null == $this->updatedPoints) { | |
$this->updatedPoints = $this->points; | |
} | |
$oldPoints = $this->updatedPoints; | |
switch ($operator) { | |
case self::POINTS_ADD: | |
$this->updatedPoints += $points; | |
$operator = '+'; | |
break; | |
case self::POINTS_SUBTRACT: | |
$this->updatedPoints -= $points; | |
$operator = '-'; | |
break; | |
case self::POINTS_MULTIPLY: | |
$this->updatedPoints *= $points; | |
$operator = '*'; | |
break; | |
case self::POINTS_DIVIDE: | |
$this->updatedPoints /= $points; | |
$operator = '/'; | |
break; | |
default: | |
throw new \UnexpectedValueException('Invalid operator'); | |
} | |
// Keep track of point changes to make a direct DB query | |
// Ignoring Aunt Sally here (PEMDAS) | |
if (!isset($this->pointChanges[$operator])) { | |
$this->pointChanges[$operator] = 0; | |
} | |
$this->pointChanges[$operator] += $points; | |
$this->isChanged('points', (int) $this->updatedPoints, (int) $oldPoints); | |
return $this; | |
} | |
/** | |
* @return array | |
*/ | |
public function getPointChanges() | |
{ | |
return $this->pointChanges; | |
} | |
/** | |
* Set points. | |
* | |
* @param int $points | |
* | |
* @return Lead | |
*/ | |
public function setPoints($points) | |
{ | |
$this->isChanged('points', $points); | |
$this->points = (int) $points; | |
// Something is setting points directly so reset points updated by database | |
$this->resetPointChanges(); | |
return $this; | |
} | |
/** | |
* Get points. | |
* | |
* @return int | |
*/ | |
public function getPoints() | |
{ | |
if (null !== $this->actualPoints) { | |
return $this->actualPoints; | |
} elseif (null !== $this->updatedPoints) { | |
return $this->updatedPoints; | |
} | |
return $this->points; | |
} | |
/** | |
* Set by the repository method when points are updated and requeried directly on the DB side. | |
*/ | |
public function setActualPoints($points): void | |
{ | |
$this->actualPoints = (int) $points; | |
$this->pointChanges = []; | |
} | |
/** | |
* Reset point changes. | |
* | |
* @return $this | |
*/ | |
public function resetPointChanges() | |
{ | |
$this->actualPoints = null; | |
$this->pointChanges = []; | |
$this->updatedPoints = null; | |
return $this; | |
} | |
/** | |
* Creates a points change entry. | |
*/ | |
public function addPointsChangeLogEntry(string $type, string $name, string $action, int $pointChanges, IpAddress $ip, Group $group = null): void | |
{ | |
if (0 === $pointChanges) { | |
// No need to record no change | |
return; | |
} | |
// Create a new points change event | |
$event = new PointsChangeLog(); | |
$event->setType($type); | |
$event->setEventName($name); | |
$event->setActionName($action); | |
$event->setDateAdded(new \DateTime()); | |
$event->setDelta($pointChanges); | |
$event->setIpAddress($ip); | |
$event->setLead($this); | |
if ($group) { | |
$event->setGroup($group); | |
} | |
$this->addPointsChangeLog($event); | |
} | |
/** | |
* Add pointsChangeLog. | |
* | |
* @return Lead | |
*/ | |
public function addPointsChangeLog(PointsChangeLog $pointsChangeLog) | |
{ | |
$this->pointsChangeLog[] = $pointsChangeLog; | |
return $this; | |
} | |
/** | |
* Creates a points change entry. | |
*/ | |
public function stageChangeLogEntry($stage, $name, $action): void | |
{ | |
// create a new points change event | |
$event = new StagesChangeLog(); | |
$event->setStage($stage); | |
$event->setEventName($name); | |
$event->setActionName($action); | |
$event->setDateAdded(new \DateTime()); | |
$event->setLead($this); | |
$this->stageChangeLog($event); | |
} | |
/** | |
* Add StagesChangeLog. | |
* | |
* @return Lead | |
*/ | |
public function stageChangeLog(StagesChangeLog $stageChangeLog) | |
{ | |
$this->stageChangeLog[] = $stageChangeLog; | |
return $this; | |
} | |
/** | |
* @return \Doctrine\Common\Collections\Collection<int, \Mautic\LeadBundle\Entity\StagesChangeLog> | |
*/ | |
public function getStageChangeLog() | |
{ | |
return $this->stageChangeLog; | |
} | |
/** | |
* Remove pointsChangeLog. | |
*/ | |
public function removePointsChangeLog(PointsChangeLog $pointsChangeLog): void | |
{ | |
$this->pointsChangeLog->removeElement($pointsChangeLog); | |
} | |
/** | |
* Get pointsChangeLog. | |
* | |
* @return Collection | |
*/ | |
public function getPointsChangeLog() | |
{ | |
return $this->pointsChangeLog; | |
} | |
public function addCompanyChangeLogEntry($type, $name, $action, $company = null): ?CompanyChangeLog | |
{ | |
if (!$company) { | |
// No need to record a null delta | |
return null; | |
} | |
// Create a new company change event | |
$event = new CompanyChangeLog(); | |
$event->setType($type); | |
$event->setEventName($name); | |
$event->setActionName($action); | |
$event->setDateAdded(new \DateTime()); | |
$event->setCompany($company); | |
$event->setLead($this); | |
$this->addCompanyChangeLog($event); | |
return $event; | |
} | |
/** | |
* Add Company ChangeLog. | |
* | |
* @return Lead | |
*/ | |
public function addCompanyChangeLog(CompanyChangeLog $companyChangeLog) | |
{ | |
$this->companyChangeLog[] = $companyChangeLog; | |
return $this; | |
} | |
/** | |
* @return \Doctrine\Common\Collections\Collection<int, \Mautic\LeadBundle\Entity\CompanyChangeLog> | |
*/ | |
public function getCompanyChangeLog() | |
{ | |
return $this->companyChangeLog; | |
} | |
/** | |
* @param bool $enabled | |
* @param bool $mobile | |
* | |
* @return $this | |
*/ | |
public function addPushIDEntry($identifier, $enabled = true, $mobile = false) | |
{ | |
$entity = new PushID(); | |
/** @var PushID $id */ | |
foreach ($this->pushIds as $id) { | |
if ($id->getPushID() === $identifier) { | |
if ($id->isEnabled() === $enabled) { | |
return $this; | |
} else { | |
$entity = $id; | |
$this->removePushID($id); | |
} | |
} | |
} | |
$entity->setPushID($identifier); | |
$entity->setLead($this); | |
$entity->setEnabled($enabled); | |
$entity->setMobile($mobile); | |
$this->addPushID($entity); | |
$this->isChanged('pushIds', $this->pushIds); | |
return $this; | |
} | |
/** | |
* @return $this | |
*/ | |
public function addPushID(PushID $pushID) | |
{ | |
$this->pushIds[] = $pushID; | |
return $this; | |
} | |
public function removePushID(PushID $pushID): void | |
{ | |
$this->pushIds->removeElement($pushID); | |
} | |
/** | |
* @return \Doctrine\Common\Collections\Collection<int, \Mautic\NotificationBundle\Entity\PushID> | |
*/ | |
public function getPushIDs() | |
{ | |
return $this->pushIds; | |
} | |
/** | |
* @return $this | |
*/ | |
public function addEventLog(LeadEventLog $log) | |
{ | |
$this->eventLog[] = $log; | |
$log->setLead($this); | |
return $this; | |
} | |
public function removeEventLog(LeadEventLog $eventLog): void | |
{ | |
$this->eventLog->removeElement($eventLog); | |
} | |
public function getLastEventLogByAction(string $action): ?LeadEventLog | |
{ | |
$criteria = Criteria::create() | |
->where(Criteria::expr()->eq('action', $action)) | |
->orderBy(['dateAdded' => Criteria::DESC]) | |
->setFirstResult(0) | |
->setMaxResults(1); | |
return $this->eventLog->matching($criteria)->first() ?: null; | |
} | |
/** | |
* @return $this | |
*/ | |
public function addDoNotContactEntry(DoNotContact $doNotContact) | |
{ | |
$this->changes['dnc_channel_status'][$doNotContact->getChannel()] = [ | |
'reason' => $doNotContact->getReason(), | |
'comments' => $doNotContact->getComments(), | |
]; | |
$this->doNotContact[$doNotContact->getChannel()] = $doNotContact; | |
return $this; | |
} | |
public function removeDoNotContactEntry(DoNotContact $doNotContact): void | |
{ | |
$this->changes['dnc_channel_status'][$doNotContact->getChannel()] = [ | |
'reason' => DoNotContact::IS_CONTACTABLE, | |
'old_reason' => $doNotContact->getReason(), | |
'comments' => $doNotContact->getComments(), | |
]; | |
$this->doNotContact->removeElement($doNotContact); | |
} | |
/** | |
* @return \Doctrine\Common\Collections\Collection<int, \Mautic\LeadBundle\Entity\DoNotContact> | |
*/ | |
public function getDoNotContact(): Collection | |
{ | |
return $this->doNotContact; | |
} | |
/** | |
* Set internal storage. | |
*/ | |
public function setInternal($internal): void | |
{ | |
$this->internal = $internal; | |
} | |
/** | |
* Get internal storage. | |
* | |
* @return mixed | |
*/ | |
public function getInternal() | |
{ | |
return $this->internal; | |
} | |
/** | |
* Set social cache. | |
*/ | |
public function setSocialCache($cache): void | |
{ | |
$this->socialCache = $cache; | |
} | |
/** | |
* Get social cache. | |
* | |
* @return mixed | |
*/ | |
public function getSocialCache() | |
{ | |
return $this->socialCache; | |
} | |
/** | |
* @return mixed | |
*/ | |
public function getColor() | |
{ | |
return $this->color; | |
} | |
/** | |
* @param mixed $color | |
*/ | |
public function setColor($color): void | |
{ | |
$this->color = $color; | |
} | |
public function isAnonymous(): bool | |
{ | |
return !($this->getName() | |
|| $this->getFirstname() | |
|| $this->getLastname() | |
|| $this->getCompany() | |
|| $this->getEmail() | |
|| $this->getFirstSocialIdentity() | |
); | |
} | |
public function wasAnonymous(): bool | |
{ | |
return null == $this->dateIdentified && false === $this->isAnonymous(); | |
} | |
/** | |
* @return bool | |
*/ | |
protected function getFirstSocialIdentity() | |
{ | |
if (isset($this->fields['social'])) { | |
foreach ($this->fields['social'] as $social) { | |
if (!empty($social['value'])) { | |
return $social['value']; | |
} | |
} | |
} elseif (!empty($this->updatedFields)) { | |
foreach ($this->availableSocialFields as $social) { | |
if (!empty($this->updatedFields[$social])) { | |
return $this->updatedFields[$social]; | |
} | |
} | |
} | |
return false; | |
} | |
/** | |
* @return self | |
*/ | |
public function setManipulator(LeadManipulator $manipulator = null) | |
{ | |
$this->manipulator = $manipulator; | |
return $this; | |
} | |
/** | |
* @return LeadManipulator|null | |
*/ | |
public function getManipulator() | |
{ | |
return $this->manipulator; | |
} | |
/** | |
* @return bool | |
*/ | |
public function isNewlyCreated() | |
{ | |
return $this->newlyCreated; | |
} | |
/** | |
* @param bool $newlyCreated Created | |
*/ | |
public function setNewlyCreated($newlyCreated): void | |
{ | |
$this->newlyCreated = $newlyCreated; | |
} | |
/** | |
* @return mixed | |
*/ | |
public function getNotes() | |
{ | |
return $this->notes; | |
} | |
/** | |
* @param string $source | |
*/ | |
public function setPreferredProfileImage($source): void | |
{ | |
$this->preferredProfileImage = $source; | |
} | |
/** | |
* @return string | |
*/ | |
public function getPreferredProfileImage() | |
{ | |
return $this->preferredProfileImage; | |
} | |
/** | |
* @return mixed | |
*/ | |
public function getDateIdentified() | |
{ | |
return $this->dateIdentified; | |
} | |
/** | |
* @param mixed $dateIdentified | |
*/ | |
public function setDateIdentified($dateIdentified): void | |
{ | |
$this->isChanged('dateIdentified', $dateIdentified); | |
$this->dateIdentified = $dateIdentified; | |
} | |
/** | |
* @return mixed | |
*/ | |
public function getLastActive() | |
{ | |
return $this->lastActive; | |
} | |
/** | |
* @param mixed $lastActive | |
*/ | |
public function setLastActive($lastActive): void | |
{ | |
$this->changes['dateLastActive'] = [$this->lastActive, $lastActive]; | |
$this->lastActive = $lastActive; | |
} | |
public function setAvailableSocialFields(array $availableSocialFields): void | |
{ | |
$this->availableSocialFields = $availableSocialFields; | |
} | |
/** | |
* Add tag. | |
* | |
* @return Lead | |
*/ | |
public function addTag(Tag $tag) | |
{ | |
$this->isChanged('tags', $tag); | |
$this->tags[$tag->getTag()] = $tag; | |
return $this; | |
} | |
/** | |
* Remove tag. | |
*/ | |
public function removeTag(Tag $tag): void | |
{ | |
$this->isChanged('tags', $tag->getTag()); | |
$this->tags->removeElement($tag); | |
} | |
/** | |
* Get tags. | |
* | |
* @return mixed | |
*/ | |
public function getTags() | |
{ | |
return $this->tags; | |
} | |
/** | |
* Set tags. | |
* | |
* @return $this | |
*/ | |
public function setTags($tags) | |
{ | |
$this->tags = $tags; | |
return $this; | |
} | |
/** | |
* Get utm tags. | |
* | |
* @return mixed | |
*/ | |
public function getUtmTags() | |
{ | |
return $this->utmtags; | |
} | |
/** | |
* Set utm tags. | |
* | |
* @return $this | |
*/ | |
public function setUtmTags($utmTags) | |
{ | |
$this->isChanged('utmtags', $utmTags); | |
$this->utmtags[] = $utmTags; | |
return $this; | |
} | |
public function removeUtmTagEntry(UtmTag $utmTag): void | |
{ | |
$this->changes['utmtags'] = ['removed', 'UtmTagID:'.$utmTag->getId()]; | |
$this->utmtags->removeElement($utmTag); | |
} | |
/** | |
* Set stage. | |
* | |
* @return Stage | |
*/ | |
public function setStage(Stage $stage = null) | |
{ | |
$this->isChanged('stage', $stage); | |
$this->stage = $stage; | |
return $this; | |
} | |
/** | |
* Get stage. | |
* | |
* @return Stage|null | |
*/ | |
public function getStage() | |
{ | |
return $this->stage; | |
} | |
/** | |
* Set frequency rules. | |
* | |
* @param FrequencyRule[] $frequencyRules | |
* | |
* @return Lead | |
*/ | |
public function setFrequencyRules($frequencyRules) | |
{ | |
$this->frequencyRules = new ArrayCollection($frequencyRules); | |
return $this; | |
} | |
/** | |
* Get frequency rules. | |
* | |
* @return \Doctrine\Common\Collections\Collection<int, \Mautic\LeadBundle\Entity\FrequencyRule> | |
*/ | |
public function getFrequencyRules() | |
{ | |
return $this->frequencyRules; | |
} | |
/** | |
* Remove frequencyRule. | |
*/ | |
public function removeFrequencyRule(FrequencyRule $frequencyRule): void | |
{ | |
$this->isChanged('frequencyRules', $frequencyRule->getId(), false); | |
$this->frequencyRules->removeElement($frequencyRule); | |
} | |
/** | |
* Add frequency rule. | |
*/ | |
public function addFrequencyRule(FrequencyRule $frequencyRule): void | |
{ | |
$this->isChanged('frequencyRules', $frequencyRule, false); | |
$this->frequencyRules[] = $frequencyRule; | |
} | |
/** | |
* Get attribution value. | |
*/ | |
public function getAttribution(): float | |
{ | |
return (float) $this->getFieldValue('attribution'); | |
} | |
/** | |
* If there is an attribution amount but no date, insert today's date. | |
*/ | |
public function checkAttributionDate(): void | |
{ | |
$attribution = $this->getFieldValue('attribution'); | |
$attributionDate = $this->getFieldValue('attribution_date'); | |
if (!empty($attribution) && empty($attributionDate)) { | |
$this->addUpdatedField('attribution_date', (new \DateTime())->format('Y-m-d')); | |
} elseif (empty($attribution) && !empty($attributionDate)) { | |
$this->addUpdatedField('attribution_date', null); | |
} | |
} | |
/** | |
* Set date identified. | |
*/ | |
public function checkDateIdentified(): void | |
{ | |
if ($this->wasAnonymous()) { | |
$this->dateIdentified = new \DateTime(); | |
$this->changes['dateIdentified'] = ['', $this->dateIdentified]; | |
} | |
} | |
/** | |
* Set date added if not already set. | |
*/ | |
public function checkDateAdded(): void | |
{ | |
if (null === $this->getDateAdded()) { | |
$this->setDateAdded(new \DateTime()); | |
} | |
} | |
/** | |
* @return mixed | |
*/ | |
public function getPrimaryCompany() | |
{ | |
return $this->primaryCompany; | |
} | |
/** | |
* @param mixed $primaryCompany | |
* | |
* @return Lead | |
*/ | |
public function setPrimaryCompany($primaryCompany) | |
{ | |
$this->primaryCompany = $primaryCompany; | |
return $this; | |
} | |
/** | |
* @return mixed | |
*/ | |
public function getTitle() | |
{ | |
return $this->title; | |
} | |
/** | |
* @param mixed $title | |
* | |
* @return Lead | |
*/ | |
public function setTitle($title) | |
{ | |
$this->isChanged('title', $title); | |
$this->title = $title; | |
return $this; | |
} | |
/** | |
* @return mixed | |
*/ | |
public function getFirstname() | |
{ | |
return $this->firstname; | |
} | |
/** | |
* @param mixed $firstname | |
* | |
* @return Lead | |
*/ | |
public function setFirstname($firstname) | |
{ | |
$this->isChanged('firstname', $firstname); | |
$this->firstname = $firstname; | |
return $this; | |
} | |
/** | |
* @return mixed | |
*/ | |
public function getLastname() | |
{ | |
return $this->lastname; | |
} | |
/** | |
* @param mixed $lastname | |
* | |
* @return Lead | |
*/ | |
public function setLastname($lastname) | |
{ | |
$this->isChanged('lastname', $lastname); | |
$this->lastname = $lastname; | |
return $this; | |
} | |
/** | |
* @return mixed | |
*/ | |
public function getPosition() | |
{ | |
return $this->position; | |
} | |
/** | |
* @param mixed $position | |
* | |
* @return Lead | |
*/ | |
public function setPosition($position) | |
{ | |
$this->isChanged('position', $position); | |
$this->position = $position; | |
return $this; | |
} | |
/** | |
* @return mixed | |
*/ | |
public function getPhone() | |
{ | |
return $this->phone; | |
} | |
/** | |
* @param mixed $phone | |
* | |
* @return Lead | |
*/ | |
public function setPhone($phone) | |
{ | |
$this->isChanged('phone', $phone); | |
$this->phone = $phone; | |
return $this; | |
} | |
/** | |
* @return mixed | |
*/ | |
public function getMobile() | |
{ | |
return $this->mobile; | |
} | |
/** | |
* @param mixed $mobile | |
* | |
* @return Lead | |
*/ | |
public function setMobile($mobile) | |
{ | |
$this->isChanged('mobile', $mobile); | |
$this->mobile = $mobile; | |
return $this; | |
} | |
/** | |
* @return string|null | |
*/ | |
public function getLeadPhoneNumber() | |
{ | |
return $this->getMobile() ?: $this->getPhone(); | |
} | |
/** | |
* @return mixed | |
*/ | |
public function getAddress1() | |
{ | |
return $this->address1; | |
} | |
/** | |
* @param mixed $address1 | |
* | |
* @return Lead | |
*/ | |
public function setAddress1($address1) | |
{ | |
$this->isChanged('address1', $address1); | |
$this->address1 = $address1; | |
return $this; | |
} | |
/** | |
* @return mixed | |
*/ | |
public function getAddress2() | |
{ | |
return $this->address2; | |
} | |
/** | |
* @param mixed $address2 | |
* | |
* @return Lead | |
*/ | |
public function setAddress2($address2) | |
{ | |
$this->isChanged('address2', $address2); | |
$this->address2 = $address2; | |
return $this; | |
} | |
/** | |
* @return mixed | |
*/ | |
public function getCity() | |
{ | |
return $this->city; | |
} | |
/** | |
* @param mixed $city | |
* | |
* @return Lead | |
*/ | |
public function setCity($city) | |
{ | |
$this->isChanged('city', $city); | |
$this->city = $city; | |
return $this; | |
} | |
/** | |
* @return mixed | |
*/ | |
public function getState() | |
{ | |
return $this->state; | |
} | |
/** | |
* @param mixed $state | |
* | |
* @return Lead | |
*/ | |
public function setState($state) | |
{ | |
$this->isChanged('state', $state); | |
$this->state = $state; | |
return $this; | |
} | |
/** | |
* @return mixed | |
*/ | |
public function getZipcode() | |
{ | |
return $this->zipcode; | |
} | |
/** | |
* @param mixed $zipcode | |
* | |
* @return Lead | |
*/ | |
public function setZipcode($zipcode) | |
{ | |
$this->isChanged('zipcode', $zipcode); | |
$this->zipcode = $zipcode; | |
return $this; | |
} | |
/** | |
* @return string | |
*/ | |
public function getTimezone() | |
{ | |
return $this->timezone; | |
} | |
/** | |
* @param string $timezone | |
* | |
* @return Lead | |
*/ | |
public function setTimezone($timezone) | |
{ | |
$this->isChanged('timezone', $timezone); | |
$this->timezone = $timezone; | |
return $this; | |
} | |
/** | |
* @return mixed | |
*/ | |
public function getCountry() | |
{ | |
return $this->country; | |
} | |
/** | |
* @param mixed $country | |
* | |
* @return Lead | |
*/ | |
public function setCountry($country) | |
{ | |
$this->isChanged('country', $country); | |
$this->country = $country; | |
return $this; | |
} | |
/** | |
* @return mixed | |
*/ | |
public function getCompany() | |
{ | |
return $this->company; | |
} | |
/** | |
* @param mixed $company | |
* | |
* @return Lead | |
*/ | |
public function setCompany($company) | |
{ | |
$this->isChanged('company', $company); | |
$this->company = $company; | |
return $this; | |
} | |
/** | |
* @return mixed | |
*/ | |
public function getEmail() | |
{ | |
return $this->email; | |
} | |
/** | |
* @param mixed $email | |
* | |
* @return Lead | |
*/ | |
public function setEmail($email) | |
{ | |
$this->isChanged('email', $email); | |
$this->email = $email; | |
return $this; | |
} | |
/** | |
* Returns array of rules with preferred channels first. | |
* | |
* @return mixed | |
*/ | |
public function getChannelRules() | |
{ | |
if (null === $this->channelRules) { | |
$frequencyRules = $this->getFrequencyRules()->toArray(); | |
$dnc = $this->getDoNotContact(); | |
$dncChannels = []; | |
/** @var DoNotContact $record */ | |
foreach ($dnc as $record) { | |
$dncChannels[$record->getChannel()] = $record->getReason(); | |
} | |
$this->channelRules = self::generateChannelRules($frequencyRules, $dncChannels); | |
} | |
return $this->channelRules; | |
} | |
/** | |
* @return $this | |
*/ | |
public function setChannelRules(array $rules) | |
{ | |
$this->channelRules = $rules; | |
return $this; | |
} | |
/** | |
* Used mostly when batching to generate preferred channels without hydrating associations one at a time. | |
* | |
* @return array<mixed, array<'dnc'|'frequency', mixed>> | |
*/ | |
public static function generateChannelRules(array $frequencyRules, array $dncRules): array | |
{ | |
$rules = []; | |
$dncFrequencyRules = []; | |
foreach ($frequencyRules as $rule) { | |
if ($rule instanceof FrequencyRule) { | |
$ruleArray = [ | |
'channel' => $rule->getChannel(), | |
'pause_from_date' => $rule->getPauseFromDate(), | |
'pause_to_date' => $rule->getPauseToDate(), | |
'preferred_channel' => $rule->getPreferredChannel(), | |
'frequency_time' => $rule->getFrequencyTime(), | |
'frequency_number' => $rule->getFrequencyNumber(), | |
]; | |
if (array_key_exists($rule->getChannel(), $dncRules)) { | |
$dncFrequencyRules[$rule->getChannel()] = $ruleArray; | |
} else { | |
$rules[$rule->getChannel()] = $ruleArray; | |
} | |
} else { | |
// Already an array | |
break; | |
} | |
} | |
if (count($rules)) { | |
$frequencyRules = $rules; | |
} | |
/* @var FrequencyRule $rule */ | |
usort( | |
$frequencyRules, | |
function ($a, $b): int { | |
if ($a['pause_from_date'] && $a['pause_to_date']) { | |
$now = new \DateTime(); | |
if ($now >= $a['pause_from_date'] && $now <= $a['pause_to_date']) { | |
// A is paused so give lower preference | |
return 1; | |
} | |
} | |
if ($a['preferred_channel'] === $b['preferred_channel']) { | |
if (!$a['frequency_time'] || !$b['frequency_time'] || !$a['frequency_number'] || !$b['frequency_number']) { | |
return 0; | |
} | |
// Order by which ever can be sent more frequent | |
if ($a['frequency_time'] === $b['frequency_time']) { | |
if ($a['frequency_number'] === $b['frequency_number']) { | |
return 0; | |
} | |
return ($a['frequency_number'] > $b['frequency_number']) ? -1 : 1; | |
} else { | |
$convertToMonth = fn ($number, $unit) => match ($unit) { | |
FrequencyRule::TIME_MONTH => (int) $number, | |
FrequencyRule::TIME_WEEK => $number * 4, | |
FrequencyRule::TIME_DAY => $number * 30, | |
default => $number, | |
}; | |
$aFrequency = $convertToMonth($a['frequency_number'], $a['frequency_time']); | |
$bFrequency = $convertToMonth($b['frequency_number'], $b['frequency_time']); | |
return $bFrequency <=> $aFrequency; | |
} | |
} | |
return ($a['preferred_channel'] > $b['preferred_channel']) ? -1 : 1; | |
} | |
); | |
$rules = []; | |
foreach ($frequencyRules as $rule) { | |
$rules[$rule['channel']] = | |
[ | |
'frequency' => $rule, | |
'dnc' => DoNotContact::IS_CONTACTABLE, | |
]; | |
} | |
if (count($dncRules)) { | |
foreach ($dncRules as $channel => $reason) { | |
$rules[$channel] = [ | |
'frequency' => $dncFrequencyRules[$channel] ?? null, | |
'dnc' => $reason, | |
]; | |
} | |
} | |
return $rules; | |
} | |
/** | |
* @return ArrayCollection<int,GroupContactScore> | |
*/ | |
public function getGroupScores(): Collection | |
{ | |
return $this->groupScores; | |
} | |
public function getGroupScore(Group $group): ?GroupContactScore | |
{ | |
foreach ($this->groupScores as $groupScore) { | |
if ($groupScore->getGroup() === $group) { | |
return $groupScore; | |
} | |
} | |
return null; | |
} | |
/** | |
* @param ArrayCollection<int,GroupContactScore> $groupScores | |
*/ | |
public function setGroupScores($groupScores): void | |
{ | |
$this->groupScores = $groupScores; | |
} | |
public function addGroupScore(GroupContactScore $groupContactScore): Lead | |
{ | |
$this->groupScores[] = $groupContactScore; | |
return $this; | |
} | |
public function removeGroupScore(GroupContactScore $groupContactScore): void | |
{ | |
$this->groupScores->removeElement($groupContactScore); | |
} | |
} | |