Spaces:
No application file
No application file
namespace MauticPlugin\MauticCrmBundle\Integration; | |
use Mautic\LeadBundle\DataObject\LeadManipulator; | |
use Mautic\LeadBundle\Entity\Company; | |
use Mautic\LeadBundle\Entity\Lead; | |
use Mautic\LeadBundle\Helper\IdentifyCompanyHelper; | |
use Mautic\PluginBundle\Entity\Integration; | |
use Mautic\PluginBundle\Integration\AbstractIntegration; | |
use MauticPlugin\MauticCrmBundle\Api\CrmApi; | |
abstract class CrmAbstractIntegration extends AbstractIntegration | |
{ | |
protected $auth; | |
protected $helper; | |
public function setIntegrationSettings(Integration $settings): void | |
{ | |
// make sure URL does not have ending / | |
$keys = $this->getDecryptedApiKeys($settings); | |
if (isset($keys['url']) && str_ends_with($keys['url'], '/')) { | |
$keys['url'] = substr($keys['url'], 0, -1); | |
$this->encryptAndSetApiKeys($keys, $settings); | |
} | |
parent::setIntegrationSettings($settings); | |
} | |
/** | |
* @return string | |
*/ | |
public function getAuthenticationType() | |
{ | |
return 'rest'; | |
} | |
/** | |
* @return array | |
*/ | |
public function getSupportedFeatures() | |
{ | |
return ['push_lead', 'get_leads']; | |
} | |
/** | |
* @param Lead|array $lead | |
* @param array $config | |
* | |
* @return array|bool | |
*/ | |
public function pushLead($lead, $config = []) | |
{ | |
$config = $this->mergeConfigToFeatureSettings($config); | |
if (empty($config['leadFields'])) { | |
return []; | |
} | |
$mappedData = $this->populateLeadData($lead, $config); | |
$this->amendLeadDataBeforePush($mappedData); | |
if (empty($mappedData)) { | |
return false; | |
} | |
try { | |
if ($this->isAuthorized()) { | |
$this->getApiHelper()->createLead($mappedData, $lead); | |
return true; | |
} | |
} catch (\Exception $e) { | |
$this->logIntegrationError($e); | |
} | |
return false; | |
} | |
/** | |
* @param array $params | |
*/ | |
public function getLeads($params, $query, &$executed, $result = [], $object = 'Lead') | |
{ | |
$executed = null; | |
$query = $this->getFetchQuery($params); | |
try { | |
if ($this->isAuthorized()) { | |
$result = $this->getApiHelper()->getLeads($query); | |
return $this->amendLeadDataBeforeMauticPopulate($result, $object); | |
} | |
} catch (\Exception $e) { | |
$this->logIntegrationError($e); | |
} | |
return $executed; | |
} | |
/** | |
* Amend mapped lead data before pushing to CRM. | |
*/ | |
public function amendLeadDataBeforePush(&$mappedData): void | |
{ | |
} | |
/** | |
* get query to fetch lead data. | |
*/ | |
public function getFetchQuery($config) | |
{ | |
return null; | |
} | |
/** | |
* Ammend mapped lead data before creating to Mautic. | |
*/ | |
public function amendLeadDataBeforeMauticPopulate($data, $object) | |
{ | |
return null; | |
} | |
/** | |
* @return string | |
*/ | |
public function getClientIdKey() | |
{ | |
return 'client_id'; | |
} | |
/** | |
* @return string | |
*/ | |
public function getClientSecretKey() | |
{ | |
return 'client_secret'; | |
} | |
public function sortFieldsAlphabetically(): bool | |
{ | |
return false; | |
} | |
/** | |
* Get the API helper. | |
* | |
* @return CrmApi | |
*/ | |
public function getApiHelper() | |
{ | |
if (empty($this->helper)) { | |
$class = '\\MauticPlugin\\MauticCrmBundle\\Api\\'.$this->getName().'Api'; | |
$this->helper = new $class($this); | |
} | |
return $this->helper; | |
} | |
/** | |
* @param array $params | |
*/ | |
public function pushLeadActivity($params = []) | |
{ | |
return null; | |
} | |
/** | |
* @return array | |
*/ | |
public function getLeadData(\DateTime $startDate = null, \DateTime $endDate = null, $leadId) | |
{ | |
$leadIds = (!is_array($leadId)) ? [$leadId] : $leadId; | |
$leadActivity = []; | |
$config = $this->mergeConfigToFeatureSettings(); | |
if (!isset($config['activityEvents'])) { | |
// BC for pre 2.11.0 | |
$config['activityEvents'] = ['point.gained', 'form.submitted', 'email.read']; | |
} elseif (empty($config['activityEvents'])) { | |
// Inclusive filter meaning we only send events if something is selected | |
return []; | |
} | |
$filters = [ | |
'search' => '', | |
'includeEvents' => $config['activityEvents'], | |
'excludeEvents' => [], | |
]; | |
if ($startDate) { | |
$filters['dateFrom'] = $startDate; | |
$filters['dateTo'] = $endDate; | |
} | |
foreach ($leadIds as $leadId) { | |
$i = 0; | |
$activity = []; | |
$lead = $this->em->getReference(Lead::class, $leadId); | |
$page = 1; | |
while (true) { | |
$engagements = $this->leadModel->getEngagements($lead, $filters, null, $page, 100, false); | |
$events = $engagements[0]['events']; | |
if (empty($events)) { | |
break; | |
} | |
// inject lead into events | |
foreach ($events as $event) { | |
$link = ''; | |
$label = $event['eventLabel'] ?? $event['eventType']; | |
if (is_array($label)) { | |
$link = $label['href']; | |
$label = $label['label']; | |
} | |
$activity[$i]['eventType'] = $event['eventType']; | |
$activity[$i]['name'] = $event['eventType'].' - '.$label; | |
$activity[$i]['description'] = $link; | |
$activity[$i]['dateAdded'] = $event['timestamp']; | |
$id = match ($event['eventType']) { | |
'point.gained' => str_replace($event['eventType'], 'pointChange', $event['eventId']), | |
'form.submitted' => str_replace($event['eventType'], 'formSubmission', $event['eventId']), | |
'email.read' => str_replace($event['eventType'], 'emailStat', $event['eventId']), | |
default => str_replace(' ', '', ucwords(str_replace('.', ' ', $event['eventId']))), | |
}; | |
$activity[$i]['id'] = $id; | |
++$i; | |
} | |
++$page; | |
// Lots of entities will be loaded into memory while compiling these events so let's prevent memory overload by clearing the EM | |
$entityToNotDetach = [Integration::class, \Mautic\PluginBundle\Entity\Plugin::class]; | |
$loadedEntities = $this->em->getUnitOfWork()->getIdentityMap(); | |
foreach ($loadedEntities as $name => $loadedEntitySet) { | |
if (!in_array($name, $entityToNotDetach, true)) { | |
continue; | |
} | |
foreach ($loadedEntitySet as $loadedEntity) { | |
$this->em->detach($loadedEntity); | |
} | |
} | |
} | |
$leadActivity[$leadId] = [ | |
'records' => $activity, | |
]; | |
unset($activity); | |
} | |
return $leadActivity; | |
} | |
/** | |
* @return Company|null | |
*/ | |
public function getMauticCompany($data, $object = null) | |
{ | |
if (is_object($data)) { | |
// Convert to array in all levels | |
$data = json_encode(json_decode($data, true)); | |
} elseif (is_string($data)) { | |
// Assume JSON | |
$data = json_decode($data, true); | |
} | |
$config = $this->mergeConfigToFeatureSettings([]); | |
$matchedFields = $this->populateMauticLeadData($data, $config, 'company'); | |
$companyFieldTypes = $this->fieldModel->getFieldListWithProperties('company'); | |
foreach ($matchedFields as $companyField => $value) { | |
if (isset($companyFieldTypes[$companyField]['type'])) { | |
switch ($companyFieldTypes[$companyField]['type']) { | |
case 'text': | |
$matchedFields[$companyField] = substr($value, 0, 255); | |
break; | |
case 'date': | |
$date = new \DateTime($value); | |
$matchedFields[$companyField] = $date->format('Y-m-d'); | |
break; | |
case 'datetime': | |
$date = new \DateTime($value); | |
$matchedFields[$companyField] = $date->format('Y-m-d H:i:s'); | |
break; | |
} | |
} | |
} | |
// Default to new company | |
$company = new Company(); | |
$existingCompany = IdentifyCompanyHelper::identifyLeadsCompany($matchedFields, null, $this->companyModel); | |
if (!empty($existingCompany[2])) { | |
$company = $existingCompany[2]; | |
} | |
if (!empty($existingCompany[2])) { | |
$fieldsToUpdate = $this->getPriorityFieldsForMautic($config, $object, 'mautic_company'); | |
$fieldsToUpdate = array_intersect_key($config['companyFields'], $fieldsToUpdate); | |
$matchedFields = array_intersect_key($matchedFields, array_flip($fieldsToUpdate)); | |
} else { | |
$matchedFields = $this->hydrateCompanyName($matchedFields); | |
// If we don't have an company name, don't create the company because it'll result in what looks like an "empty" company | |
if (empty($matchedFields['companyname'])) { | |
return null; | |
} | |
} | |
$this->companyModel->setFieldValues($company, $matchedFields, false); | |
$this->companyModel->saveEntity($company, false); | |
return $company; | |
} | |
/** | |
* Create or update existing Mautic lead from the integration's profile data. | |
* | |
* @param mixed $data Profile data from integration | |
* @param bool|true $persist Set to false to not persist lead to the database in this method | |
* @param array|null $socialCache | |
* @param mixed|null $identifiers | |
* @param string|null $object | |
* | |
* @return Lead | |
*/ | |
public function getMauticLead($data, $persist = true, $socialCache = null, $identifiers = null, $object = null) | |
{ | |
if (is_object($data)) { | |
// Convert to array in all levels | |
$data = json_encode(json_decode($data, true)); | |
} elseif (is_string($data)) { | |
// Assume JSON | |
$data = json_decode($data, true); | |
} | |
$config = $this->mergeConfigToFeatureSettings([]); | |
// Match that data with mapped lead fields | |
$matchedFields = $this->populateMauticLeadData($data, $config); | |
if (empty($matchedFields)) { | |
return; | |
} | |
// Find unique identifier fields used by the integration | |
/** @var \Mautic\LeadBundle\Model\LeadModel $leadModel */ | |
$leadModel = $this->leadModel; | |
$uniqueLeadFields = $this->fieldModel->getUniqueIdentifierFields(); | |
$uniqueLeadFieldData = []; | |
$leadFieldTypes = $this->fieldModel->getFieldListWithProperties(); | |
foreach ($matchedFields as $leadField => $value) { | |
if (array_key_exists($leadField, $uniqueLeadFields) && !empty($value)) { | |
$uniqueLeadFieldData[$leadField] = $value; | |
} | |
$fieldType = $leadFieldTypes[$leadField]['type'] ?? ''; | |
$matchedFields[$leadField] = $this->limitString($value, $fieldType); | |
} | |
if (count(array_diff_key($uniqueLeadFields, $matchedFields)) == count($uniqueLeadFields)) { | |
// return if uniqueIdentifiers have no data set to avoid duplicating leads. | |
$this->logger->debug('getMauticLead: No unique identifiers', [ | |
'uniqueLeadFields' => $uniqueLeadFields, | |
'matchedFields' => $matchedFields, | |
]); | |
return; | |
} | |
// Default to new lead | |
$lead = new Lead(); | |
$lead->setNewlyCreated(true); | |
if (count($uniqueLeadFieldData)) { | |
$existingLeads = $this->em->getRepository(Lead::class) | |
->getLeadsByUniqueFields($uniqueLeadFieldData); | |
if (!empty($existingLeads)) { | |
$lead = array_shift($existingLeads); | |
} | |
} | |
$leadFields = $this->cleanPriorityFields($config, $object); | |
if (!$lead->isNewlyCreated()) { | |
$params = $this->commandParameters; | |
$this->getLeadDoNotContactByDate('email', $matchedFields, $object, $lead, $data, $params); | |
// Use only prioirty fields if updating | |
$fieldsToUpdateInMautic = $this->getPriorityFieldsForMautic($config, $object, 'mautic'); | |
if (empty($fieldsToUpdateInMautic)) { | |
$this->logger->debug('getMauticLead: No fields to update in Mautic', ['config' => $config, 'object' => $object]); | |
return; | |
} | |
$fieldsToUpdateInMautic = array_intersect_key($leadFields, $fieldsToUpdateInMautic); | |
$matchedFields = array_intersect_key($matchedFields, array_flip($fieldsToUpdateInMautic)); | |
if (isset($config['updateBlanks']) && isset($config['updateBlanks'][0]) && 'updateBlanks' == $config['updateBlanks'][0]) { | |
$matchedFields = $this->getBlankFieldsToUpdateInMautic($matchedFields, $lead->getFields(true), $leadFields, $data, $object); | |
} | |
} | |
$leadModel->setFieldValues($lead, $matchedFields, false, false); | |
if (!empty($socialCache)) { | |
// Update the social cache | |
$leadSocialCache = $lead->getSocialCache(); | |
if (!isset($leadSocialCache[$this->getName()])) { | |
$leadSocialCache[$this->getName()] = []; | |
} | |
$leadSocialCache[$this->getName()] = array_merge($leadSocialCache[$this->getName()], $socialCache); | |
// Check for activity while here | |
if (null !== $identifiers && in_array('public_activity', $this->getSupportedFeatures())) { | |
$this->getPublicActivity($identifiers, $leadSocialCache[$this->getName()]); | |
} | |
$lead->setSocialCache($leadSocialCache); | |
} | |
// Update the internal info integration object that has updated the record | |
if (isset($data['internal'])) { | |
$internalInfo = $lead->getInternal(); | |
$internalInfo[$this->getName()] = $data['internal']; | |
$lead->setInternal($internalInfo); | |
} | |
// Update the owner if it matches (needs to be set by the integration) when fetching the data | |
if (isset($data['owner_email']) && isset($config['updateOwner']) && isset($config['updateOwner'][0]) | |
&& 'updateOwner' == $config['updateOwner'][0] | |
) { | |
if ($mauticUser = $this->em->getRepository(\Mautic\UserBundle\Entity\User::class)->findOneBy(['email' => $data['owner_email']])) { | |
$lead->setOwner($mauticUser); | |
} | |
} | |
if ($persist && !empty($lead->getChanges(true))) { | |
// Only persist if instructed to do so as it could be that calling code needs to manipulate the lead prior to executing event listeners | |
$lead->setManipulator(new LeadManipulator( | |
'plugin', | |
$this->getName(), | |
null, | |
$this->getDisplayName() | |
)); | |
$leadModel->saveEntity($lead, false); | |
} | |
return $lead; | |
} | |
/** | |
* @return array|mixed | |
*/ | |
protected function getFormFieldsByObject($object, $settings = []) | |
{ | |
$settings['feature_settings']['objects'] = [$object => $object]; | |
$fields = ($this->isAuthorized()) ? $this->getAvailableLeadFields($settings) : []; | |
return $fields[$object] ?? []; | |
} | |
/** | |
* @param string $priorityObject | |
* | |
* @return array | |
*/ | |
protected function getPriorityFieldsForMautic($config, $entityObject = null, $priorityObject = 'mautic') | |
{ | |
return $this->cleanPriorityFields( | |
$this->getFieldsByPriority($config, $priorityObject, 1), | |
$entityObject | |
); | |
} | |
/** | |
* @param string $priorityObject | |
* | |
* @return array | |
*/ | |
protected function getPriorityFieldsForIntegration($config, $entityObject = null, $priorityObject = 'mautic') | |
{ | |
return $this->cleanPriorityFields( | |
$this->getFieldsByPriority($config, $priorityObject, 0), | |
$entityObject | |
); | |
} | |
/** | |
* @param string $priorityObject | |
* | |
* @return array | |
*/ | |
protected function getFieldsByPriority(array $config, $priorityObject, $direction) | |
{ | |
return isset($config['update_'.$priorityObject]) ? array_keys($config['update_'.$priorityObject], $direction) : array_keys($config['leadFields'] ?? []); | |
} | |
/** | |
* @param array $objects | |
* | |
* @return array | |
*/ | |
protected function cleanPriorityFields($fieldsToUpdate, $objects = null) | |
{ | |
if (!isset($fieldsToUpdate['leadFields'])) { | |
return $fieldsToUpdate; | |
} | |
if (null === $objects || is_array($objects)) { | |
return $fieldsToUpdate['leadFields']; | |
} | |
return $fieldsToUpdate['leadFields'][$objects] ?? $fieldsToUpdate; | |
} | |
/** | |
* @return array | |
*/ | |
protected function getSyncTimeframeDates(array $params) | |
{ | |
$fromDate = (isset($params['start'])) ? \DateTime::createFromFormat(\DateTime::ISO8601, $params['start'])->format('Y-m-d H:i:s') | |
: null; | |
$toDate = (isset($params['end'])) ? \DateTime::createFromFormat(\DateTime::ISO8601, $params['end'])->format('Y-m-d H:i:s') | |
: null; | |
return [$fromDate, $toDate]; | |
} | |
public function getBlankFieldsToUpdateInMautic($matchedFields, $leadFieldValues, $objectFields, $integrationData, $object = 'Lead') | |
{ | |
foreach ($objectFields as $integrationField => $mauticField) { | |
if (isset($leadFieldValues[$mauticField]) && empty($leadFieldValues[$mauticField]['value']) && !empty($integrationData[$integrationField.'__'.$object]) && $this->translator->trans('mautic.integration.form.lead.unknown') !== $integrationData[$integrationField.'__'.$object]) { | |
$matchedFields[$mauticField] = $integrationData[$integrationField.'__'.$object]; | |
} | |
} | |
return $matchedFields; | |
} | |
public function getBlankFieldsToUpdate($fields, $sfRecord, $objectFields, $config) | |
{ | |
// check if update blank fields is selected | |
if (isset($config['updateBlanks']) && isset($config['updateBlanks'][0]) | |
&& 'updateBlanks' == $config['updateBlanks'][0] | |
&& !empty($sfRecord) | |
&& isset($objectFields['required']['fields']) | |
) { | |
foreach ($sfRecord as $fieldName => $sfField) { | |
if (array_key_exists($fieldName, $objectFields['required']['fields'])) { | |
continue; // this will be treated differently | |
} | |
if (empty($sfField) && array_key_exists($fieldName, $objectFields['create']) && !array_key_exists($fieldName, $fields)) { | |
// map to mautic field | |
$fields[$fieldName] = $objectFields['create'][$fieldName]; | |
} | |
} | |
} | |
return $fields; | |
} | |
/** | |
* @return array | |
*/ | |
protected function prepareFieldsForPush($fields) | |
{ | |
$fieldMappings = []; | |
$required = []; | |
$config = $this->mergeConfigToFeatureSettings(); | |
$leadFields = $config['leadFields']; | |
foreach ($fields as $key => $field) { | |
if ($field['required']) { | |
$required[$key] = $field; | |
} | |
} | |
$fieldMappings['required'] = [ | |
'fields' => $required, | |
]; | |
$fieldMappings['create'] = $leadFields; | |
return $fieldMappings; | |
} | |
/** | |
* @return array | |
*/ | |
private function hydrateCompanyName(array $matchedFields) | |
{ | |
if (!empty($matchedFields['companyname'])) { | |
return $matchedFields; | |
} | |
if (!empty($matchedFields['companywebsite'])) { | |
$matchedFields['companyname'] = $matchedFields['companywebsite']; | |
return $matchedFields; | |
} | |
// We need something as company name so save whatever we have | |
if ($firstMatchedField = reset($matchedFields)) { | |
$matchedFields['companyname'] = $firstMatchedField; | |
return $matchedFields; | |
} | |
return $matchedFields; | |
} | |
/** | |
* Limits the string. | |
* | |
* @param mixed $value | |
* @param string $fieldType | |
* | |
* @return mixed | |
*/ | |
protected function limitString($value, $fieldType = '') | |
{ | |
// We must not convert boolean values to string, otherwise "false" will be converted to an empty string. | |
// "False" has to be converted to 0 instead. | |
if (('text' == $fieldType) && !is_bool($value)) { | |
return substr($value, 0, 255); | |
} | |
return $value; | |
} | |
} | |