Spaces:
No application file
No application file
namespace MauticPlugin\MauticCrmBundle\Integration; | |
use Doctrine\ORM\EntityManager; | |
use Mautic\CoreBundle\Helper\ArrayHelper; | |
use Mautic\CoreBundle\Helper\CacheStorageHelper; | |
use Mautic\CoreBundle\Helper\EncryptionHelper; | |
use Mautic\CoreBundle\Helper\PathsHelper; | |
use Mautic\CoreBundle\Helper\UserHelper; | |
use Mautic\CoreBundle\Model\NotificationModel; | |
use Mautic\LeadBundle\DataObject\LeadManipulator; | |
use Mautic\LeadBundle\Entity\Lead; | |
use Mautic\LeadBundle\Entity\StagesChangeLog; | |
use Mautic\LeadBundle\Model\CompanyModel; | |
use Mautic\LeadBundle\Model\DoNotContact; | |
use Mautic\LeadBundle\Model\FieldModel; | |
use Mautic\LeadBundle\Model\LeadModel; | |
use Mautic\PluginBundle\Entity\IntegrationEntityRepository; | |
use Mautic\PluginBundle\Model\IntegrationEntityModel; | |
use Mautic\StageBundle\Entity\Stage; | |
use MauticPlugin\MauticCrmBundle\Api\HubspotApi; | |
use Monolog\Logger; | |
use Symfony\Component\EventDispatcher\EventDispatcherInterface; | |
use Symfony\Component\Form\Extension\Core\Type\ChoiceType; | |
use Symfony\Component\Form\Extension\Core\Type\TextType; | |
use Symfony\Component\Form\FormBuilder; | |
use Symfony\Component\HttpFoundation\RequestStack; | |
use Symfony\Component\HttpFoundation\Session\Session; | |
use Symfony\Component\Routing\Router; | |
use Symfony\Contracts\Translation\TranslatorInterface; | |
/** | |
* @method HubspotApi getApiHelper() | |
*/ | |
class HubspotIntegration extends CrmAbstractIntegration | |
{ | |
public const ACCESS_KEY = 'accessKey'; | |
public function __construct( | |
EventDispatcherInterface $eventDispatcher, | |
CacheStorageHelper $cacheStorageHelper, | |
EntityManager $entityManager, | |
Session $session, | |
RequestStack $requestStack, | |
Router $router, | |
TranslatorInterface $translator, | |
Logger $logger, | |
EncryptionHelper $encryptionHelper, | |
LeadModel $leadModel, | |
CompanyModel $companyModel, | |
PathsHelper $pathsHelper, | |
NotificationModel $notificationModel, | |
FieldModel $fieldModel, | |
IntegrationEntityModel $integrationEntityModel, | |
DoNotContact $doNotContact, | |
protected UserHelper $userHelper | |
) { | |
parent::__construct( | |
$eventDispatcher, | |
$cacheStorageHelper, | |
$entityManager, | |
$session, | |
$requestStack, | |
$router, | |
$translator, | |
$logger, | |
$encryptionHelper, | |
$leadModel, | |
$companyModel, | |
$pathsHelper, | |
$notificationModel, | |
$fieldModel, | |
$integrationEntityModel, | |
$doNotContact | |
); | |
} | |
public function getName(): string | |
{ | |
return 'Hubspot'; | |
} | |
/** | |
* @return array<string, string> | |
*/ | |
public function getRequiredKeyFields(): array | |
{ | |
return []; | |
} | |
public function getApiKey(): string | |
{ | |
return 'hapikey'; | |
} | |
/** | |
* Get the array key for the auth token. | |
*/ | |
public function getAuthTokenKey(): string | |
{ | |
return 'hapikey'; | |
} | |
public function getSupportedFeatures(): array | |
{ | |
return ['push_lead', 'get_leads']; | |
} | |
/** | |
* @param bool $inAuthorization | |
* | |
* @return mixed|string|null | |
*/ | |
public function getBearerToken($inAuthorization = false) | |
{ | |
$tokenData = $this->getKeys(); | |
return $tokenData[self::ACCESS_KEY] ?? null; | |
} | |
/** | |
* @return array<string, bool> | |
*/ | |
public function getFormSettings(): array | |
{ | |
return [ | |
'requires_callback' => false, | |
'requires_authorization' => false, | |
]; | |
} | |
public function getAuthenticationType(): string | |
{ | |
return $this->getBearerToken() ? 'oauth2' : 'key'; | |
} | |
public function getApiUrl(): string | |
{ | |
return 'https://api.hubapi.com'; | |
} | |
/** | |
* Get if data priority is enabled in the integration or not default is false. | |
*/ | |
public function getDataPriority(): bool | |
{ | |
return true; | |
} | |
/** | |
* Get available company fields for choices in the config UI. | |
* | |
* @param array $settings | |
* | |
* @return array | |
*/ | |
public function getFormCompanyFields($settings = []) | |
{ | |
return $this->getFormFieldsByObject('company', $settings); | |
} | |
/** | |
* @param array $settings | |
* | |
* @return array|mixed | |
*/ | |
public function getFormLeadFields($settings = []) | |
{ | |
return $this->getFormFieldsByObject('contacts', $settings); | |
} | |
/** | |
* @return mixed[] | |
*/ | |
public function getAvailableLeadFields($settings = []): array | |
{ | |
if ($fields = parent::getAvailableLeadFields()) { | |
return $fields; | |
} | |
$hubsFields = []; | |
$silenceExceptions = $settings['silence_exceptions'] ?? true; | |
if (isset($settings['feature_settings']['objects'])) { | |
$hubspotObjects = $settings['feature_settings']['objects']; | |
} else { | |
$settings = $this->settings->getFeatureSettings(); | |
$hubspotObjects = $settings['objects'] ?? ['contacts']; | |
} | |
try { | |
if ($this->isAuthorized()) { | |
if (!empty($hubspotObjects) and is_array($hubspotObjects)) { | |
foreach ($hubspotObjects as $object) { | |
// Check the cache first | |
$settings['cache_suffix'] = $cacheSuffix = '.'.$object; | |
if ($fields = parent::getAvailableLeadFields($settings)) { | |
$hubsFields[$object] = $fields; | |
continue; | |
} | |
$leadFields = $this->getApiHelper()->getLeadFields($object); | |
if (isset($leadFields)) { | |
foreach ($leadFields as $fieldInfo) { | |
$hubsFields[$object][$fieldInfo['name']] = [ | |
'type' => 'string', | |
'label' => $fieldInfo['label'], | |
'required' => ('email' === $fieldInfo['name']), | |
]; | |
if (!empty($fieldInfo['readOnlyValue'])) { | |
$hubsFields[$object][$fieldInfo['name']]['update_mautic'] = 1; | |
$hubsFields[$object][$fieldInfo['name']]['readOnly'] = 1; | |
} | |
} | |
} | |
$this->cache->set('leadFields'.$cacheSuffix, $hubsFields[$object]); | |
} | |
} | |
} | |
} catch (\Exception $e) { | |
$this->logIntegrationError($e); | |
if (!$silenceExceptions) { | |
throw $e; | |
} | |
} | |
return $hubsFields; | |
} | |
/** | |
* @param array $objects | |
* | |
* @return array | |
*/ | |
protected function cleanPriorityFields($fieldsToUpdate, $objects = null) | |
{ | |
if (null === $objects) { | |
$objects = ['Leads', 'Contacts']; | |
} | |
if (isset($fieldsToUpdate['leadFields'])) { | |
// Pass in the whole config | |
$fields = $fieldsToUpdate['leadFields']; | |
} else { | |
$fields = array_flip($fieldsToUpdate); | |
} | |
return $this->prepareFieldsForSync($fields, $fieldsToUpdate, $objects); | |
} | |
/** | |
* Format the lead data to the structure that HubSpot requires for the createOrUpdate request. | |
* | |
* @param array $leadData All the lead fields mapped | |
*/ | |
public function formatLeadDataForCreateOrUpdate($leadData, $lead, $updateLink = false): array | |
{ | |
$formattedLeadData = []; | |
if (!$updateLink) { | |
foreach ($leadData as $field => $value) { | |
if ('lifecyclestage' == $field || 'associatedcompanyid' == $field) { | |
continue; | |
} | |
$formattedLeadData['properties'][] = [ | |
'property' => $field, | |
'value' => $value, | |
]; | |
} | |
} | |
return $formattedLeadData; | |
} | |
public function isAuthorized(): bool | |
{ | |
$keys = $this->getKeys(); | |
return isset($keys[$this->getAuthTokenKey()]) || isset($keys[self::ACCESS_KEY]); | |
} | |
/** | |
* @return mixed | |
*/ | |
public function getHubSpotApiKey() | |
{ | |
$tokenData = $this->getKeys(); | |
return $tokenData[$this->getAuthTokenKey()]; | |
} | |
/** | |
* @param FormBuilder $builder | |
* @param array $data | |
* @param string $formArea | |
*/ | |
public function appendToForm(&$builder, $data, $formArea): void | |
{ | |
if ('keys' === $formArea) { | |
$builder->add( | |
self::ACCESS_KEY, | |
TextType::class, | |
[ | |
'label' => 'mautic.hubspot.form.accessKey', | |
'label_attr' => ['class' => 'control-label'], | |
'attr' => [ | |
'class' => 'form-control', | |
], | |
'required' => false, | |
] | |
); | |
$builder->add( | |
$this->getApiKey(), | |
TextType::class, | |
[ | |
'label' => 'mautic.hubspot.form.apikey', | |
'label_attr' => ['class' => 'control-label'], | |
'attr' => [ | |
'class' => 'form-control', | |
'readonly' => true, | |
], | |
'required' => false, | |
] | |
); | |
} | |
if ('features' == $formArea) { | |
$builder->add( | |
'objects', | |
ChoiceType::class, | |
[ | |
'choices' => [ | |
'mautic.hubspot.object.contact' => 'contacts', | |
'mautic.hubspot.object.company' => 'company', | |
], | |
'expanded' => true, | |
'multiple' => true, | |
'label' => $this->getTranslator()->trans('mautic.crm.form.objects_to_pull_from', ['%crm%' => 'Hubspot']), | |
'label_attr' => ['class' => ''], | |
'placeholder' => false, | |
'required' => false, | |
] | |
); | |
} | |
} | |
/** | |
* @return array | |
*/ | |
public function amendLeadDataBeforeMauticPopulate($data, $object) | |
{ | |
if (!isset($data['properties'])) { | |
return []; | |
} | |
foreach ($data['properties'] as $key => $field) { | |
$value = str_replace(';', '|', $field['value']); | |
$fieldsValues[$key] = $value; | |
} | |
if ('Lead' == $object && !isset($fieldsValues['email'])) { | |
foreach ($data['identity-profiles'][0]['identities'] as $identifiedProfile) { | |
if ('EMAIL' == $identifiedProfile['type']) { | |
$fieldsValues['email'] = $identifiedProfile['value']; | |
} | |
} | |
} | |
return $fieldsValues; | |
} | |
/** | |
* @param array $params | |
* @param array $result | |
* @param string $object | |
* | |
* @return array|null | |
*/ | |
public function getLeads($params = [], $query = null, &$executed = null, $result = [], $object = 'Lead') | |
{ | |
if (!is_array($executed)) { | |
$executed = [ | |
0 => 0, | |
1 => 0, | |
]; | |
} | |
try { | |
if ($this->isAuthorized()) { | |
$config = $this->mergeConfigToFeatureSettings(); | |
$fields = implode('&property=', array_keys($config['leadFields'])); | |
$params['post_append_to_query'] = '&property='.$fields.'&property=lifecyclestage'; | |
$params['Count'] = 100; | |
$data = $this->getApiHelper()->getContacts($params); | |
if (isset($data['contacts'])) { | |
foreach ($data['contacts'] as $contact) { | |
if (is_array($contact)) { | |
$contactData = $this->amendLeadDataBeforeMauticPopulate($contact, 'Lead'); | |
$contact = $this->getMauticLead($contactData); | |
if ($contact && !$contact->isNewlyCreated()) { // updated | |
$executed[0] = $executed[0] + 1; | |
} elseif ($contact && $contact->isNewlyCreated()) { // newly created | |
$executed[1] = $executed[1] + 1; | |
} | |
if ($contact) { | |
$this->em->detach($contact); | |
} | |
} | |
} | |
if ($data['has-more']) { | |
$params['vidOffset'] = $data['vid-offset']; | |
$params['timeOffset'] = $data['time-offset']; | |
$this->getLeads($params, $query, $executed); | |
} | |
} | |
return $executed; | |
} | |
} catch (\Exception $e) { | |
$this->logIntegrationError($e); | |
} | |
return $executed; | |
} | |
/** | |
* @param array $params | |
* @param bool $id | |
*/ | |
public function getCompanies($params = [], $id = false, &$executed = null) | |
{ | |
$results = []; | |
try { | |
if ($this->isAuthorized()) { | |
$params['Count'] = 100; | |
$data = $this->getApiHelper()->getCompanies($params, $id); | |
if ($id) { | |
$results['results'][] = array_merge($results, $data); | |
} else { | |
$results['results'] = array_merge($results, $data['results']); | |
} | |
foreach ($results['results'] as $company) { | |
if (isset($company['properties'])) { | |
$companyData = $this->amendLeadDataBeforeMauticPopulate($company, null); | |
$company = $this->getMauticCompany($companyData); | |
if ($id) { | |
return $company; | |
} | |
if ($company) { | |
++$executed; | |
$this->em->detach($company); | |
} | |
} | |
} | |
if (isset($data['hasMore']) and $data['hasMore']) { | |
$params['offset'] = $data['offset']; | |
if ($params['offset'] < strtotime($params['start'])) { | |
$this->getCompanies($params, $id, $executed); | |
} | |
} | |
return $executed; | |
} | |
} catch (\Exception $e) { | |
$this->logIntegrationError($e); | |
} | |
return $executed; | |
} | |
/** | |
* 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); | |
} | |
if (isset($data['lifecyclestage'])) { | |
$stageName = $data['lifecyclestage']; | |
unset($data['lifecyclestage']); | |
} | |
if (isset($data['associatedcompanyid'])) { | |
$company = $this->getCompanies([], $data['associatedcompanyid']); | |
unset($data['associatedcompanyid']); | |
} | |
if ($lead = parent::getMauticLead($data, false, $socialCache, $identifiers, $object)) { | |
if (isset($stageName)) { | |
$stage = $this->em->getRepository(Stage::class)->getStageByName($stageName); | |
if (empty($stage)) { | |
$stage = new Stage(); | |
$stage->setName($stageName); | |
$stages[$stageName] = $stage; | |
} | |
if (!$lead->getStage() && $lead->getStage() != $stage) { | |
$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); | |
} | |
} | |
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 | |
try { | |
$lead->setManipulator(new LeadManipulator( | |
'plugin', | |
$this->getName(), | |
null, | |
$this->getDisplayName() | |
)); | |
$this->leadModel->saveEntity($lead, false); | |
if (isset($company)) { | |
$this->leadModel->addToCompany($lead, $company); | |
$this->em->detach($company); | |
} | |
} catch (\Exception $exception) { | |
$this->logger->warning($exception->getMessage()); | |
return; | |
} | |
} | |
} | |
return $lead; | |
} | |
/** | |
* @param Lead $lead | |
* @param array $config | |
* | |
* @return array|bool | |
*/ | |
public function pushLead($lead, $config = []) | |
{ | |
$config = $this->mergeConfigToFeatureSettings($config); | |
if (empty($config['leadFields'])) { | |
return []; | |
} | |
$object = 'contacts'; | |
$createFields = $config['leadFields']; | |
$readOnlyFields = $this->getReadOnlyFields($object); | |
$createFields = array_filter( | |
$createFields, | |
function ($createField, $key) use ($readOnlyFields) { | |
if (!isset($readOnlyFields[$key])) { | |
return $createField; | |
} | |
}, | |
ARRAY_FILTER_USE_BOTH | |
); | |
$mappedData = $this->populateLeadData( | |
$lead, | |
[ | |
'leadFields' => $createFields, | |
'object' => $object, | |
'feature_settings' => ['objects' => $config['objects']], | |
] | |
); | |
$this->amendLeadDataBeforePush($mappedData); | |
if (empty($mappedData)) { | |
return false; | |
} | |
if ($this->isAuthorized()) { | |
$leadData = $this->getApiHelper()->createLead($mappedData, $lead); | |
if (!empty($leadData['vid'])) { | |
/** @var IntegrationEntityRepository $integrationEntityRepo */ | |
$integrationEntityRepo = $this->em->getRepository(\Mautic\PluginBundle\Entity\IntegrationEntity::class); | |
$integrationId = $integrationEntityRepo->getIntegrationsEntityId($this->getName(), $object, 'lead', $lead->getId()); | |
$integrationEntity = (empty($integrationId)) ? | |
$this->createIntegrationEntity( | |
$object, | |
$leadData['vid'], | |
'lead', | |
$lead->getId(), | |
[], | |
false | |
) : $integrationEntityRepo->getEntity($integrationId[0]['id']); | |
$integrationEntity->setLastSyncDate($this->getLastSyncDate()); | |
$this->getIntegrationEntityRepository()->saveEntity($integrationEntity); | |
$this->em->detach($integrationEntity); | |
} | |
return true; | |
} | |
return false; | |
} | |
/** | |
* Amend mapped lead data before pushing to CRM. | |
*/ | |
public function amendLeadDataBeforePush(&$mappedData): void | |
{ | |
foreach ($mappedData as &$data) { | |
$data = str_replace('|', ';', $data); | |
} | |
} | |
/** | |
* @throws \Exception | |
*/ | |
private function getReadOnlyFields($object): ?array | |
{ | |
$fields = ArrayHelper::getValue($object, $this->getAvailableLeadFields(), []); | |
return array_filter( | |
$fields, | |
function ($field) { | |
if (!empty($field['readOnly'])) { | |
return $field; | |
} | |
} | |
); | |
} | |
} | |