Issues (3627)

MauticCrmBundle/Integration/HubspotIntegration.php (1 issue)

1
<?php
2
3
/*
4
 * @copyright   2014 Mautic Contributors. All rights reserved
5
 * @author      Mautic
6
 *
7
 * @link        http://mautic.org
8
 *
9
 * @license     GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html
10
 */
11
12
namespace MauticPlugin\MauticCrmBundle\Integration;
13
14
use Doctrine\ORM\EntityManager;
15
use Mautic\CoreBundle\Helper\ArrayHelper;
16
use Mautic\CoreBundle\Helper\CacheStorageHelper;
17
use Mautic\CoreBundle\Helper\EncryptionHelper;
18
use Mautic\CoreBundle\Helper\PathsHelper;
19
use Mautic\CoreBundle\Helper\UserHelper;
20
use Mautic\CoreBundle\Model\NotificationModel;
21
use Mautic\LeadBundle\DataObject\LeadManipulator;
22
use Mautic\LeadBundle\Entity\Lead;
23
use Mautic\LeadBundle\Entity\StagesChangeLog;
24
use Mautic\LeadBundle\Model\CompanyModel;
25
use Mautic\LeadBundle\Model\DoNotContact;
26
use Mautic\LeadBundle\Model\FieldModel;
27
use Mautic\LeadBundle\Model\LeadModel;
28
use Mautic\PluginBundle\Entity\IntegrationEntityRepository;
29
use Mautic\PluginBundle\Model\IntegrationEntityModel;
30
use Mautic\StageBundle\Entity\Stage;
31
use MauticPlugin\MauticCrmBundle\Api\HubspotApi;
32
use Monolog\Logger;
33
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
34
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
35
use Symfony\Component\HttpFoundation\RequestStack;
36
use Symfony\Component\HttpFoundation\Session\Session;
37
use Symfony\Component\Routing\Router;
38
use Symfony\Component\Translation\TranslatorInterface;
39
40
/**
41
 * @method HubspotApi getApiHelper
42
 */
43
class HubspotIntegration extends CrmAbstractIntegration
44
{
45
    /**
46
     * @var UserHelper
47
     */
48
    protected $userHelper;
49
50
    public function __construct(
51
        EventDispatcherInterface $eventDispatcher,
52
        CacheStorageHelper $cacheStorageHelper,
53
        EntityManager $entityManager,
54
        Session $session,
55
        RequestStack $requestStack,
56
        Router $router,
57
        TranslatorInterface $translator,
58
        Logger $logger,
59
        EncryptionHelper $encryptionHelper,
60
        LeadModel $leadModel,
61
        CompanyModel $companyModel,
62
        PathsHelper $pathsHelper,
63
        NotificationModel $notificationModel,
64
        FieldModel $fieldModel,
65
        IntegrationEntityModel $integrationEntityModel,
66
        DoNotContact $doNotContact,
67
        UserHelper $userHelper
68
    ) {
69
        $this->userHelper = $userHelper;
70
71
        parent::__construct(
72
            $eventDispatcher,
73
            $cacheStorageHelper,
74
            $entityManager,
75
            $session,
76
            $requestStack,
77
            $router,
78
            $translator,
79
            $logger,
80
            $encryptionHelper,
81
            $leadModel,
82
            $companyModel,
83
            $pathsHelper,
84
            $notificationModel,
85
            $fieldModel,
86
            $integrationEntityModel,
87
            $doNotContact
88
        );
89
    }
90
91
    /**
92
     * {@inheritdoc}
93
     *
94
     * @return string
95
     */
96
    public function getName()
97
    {
98
        return 'Hubspot';
99
    }
100
101
    /**
102
     * {@inheritdoc}
103
     *
104
     * @return array
105
     */
106
    public function getRequiredKeyFields()
107
    {
108
        return [
109
            $this->getApiKey() => 'mautic.hubspot.form.apikey',
110
        ];
111
    }
112
113
    /**
114
     * @return string
115
     */
116
    public function getApiKey()
117
    {
118
        return 'hapikey';
119
    }
120
121
    /**
122
     * Get the array key for the auth token.
123
     *
124
     * @return string
125
     */
126
    public function getAuthTokenKey()
127
    {
128
        return 'hapikey';
129
    }
130
131
    /**
132
     * @return array
133
     */
134
    public function getSupportedFeatures()
135
    {
136
        return ['push_lead', 'get_leads'];
137
    }
138
139
    /**
140
     * {@inheritdoc}
141
     *
142
     * @return string
143
     */
144
    public function getAuthenticationType()
145
    {
146
        return 'key';
147
    }
148
149
    /**
150
     * @return string
151
     */
152
    public function getApiUrl()
153
    {
154
        return 'https://api.hubapi.com';
155
    }
156
157
    /**
158
     * Get if data priority is enabled in the integration or not default is false.
159
     *
160
     * @return string
161
     */
162
    public function getDataPriority()
163
    {
164
        return true;
165
    }
166
167
    /**
168
     * Get available company fields for choices in the config UI.
169
     *
170
     * @param array $settings
171
     *
172
     * @return array
173
     */
174
    public function getFormCompanyFields($settings = [])
175
    {
176
        return $this->getFormFieldsByObject('company', $settings);
177
    }
178
179
    /**
180
     * @param array $settings
181
     *
182
     * @return array|mixed
183
     */
184
    public function getFormLeadFields($settings = [])
185
    {
186
        return $this->getFormFieldsByObject('contacts', $settings);
187
    }
188
189
    /**
190
     * @return array|mixed
191
     */
192
    public function getAvailableLeadFields($settings = [])
193
    {
194
        if ($fields = parent::getAvailableLeadFields()) {
195
            return $fields;
196
        }
197
198
        $hubsFields        = [];
199
        $silenceExceptions = (isset($settings['silence_exceptions'])) ? $settings['silence_exceptions'] : true;
200
201
        if (isset($settings['feature_settings']['objects'])) {
202
            $hubspotObjects = $settings['feature_settings']['objects'];
203
        } else {
204
            $settings       = $this->settings->getFeatureSettings();
205
            $hubspotObjects = isset($settings['objects']) ? $settings['objects'] : ['contacts'];
206
        }
207
208
        try {
209
            if ($this->isAuthorized()) {
210
                if (!empty($hubspotObjects) and is_array($hubspotObjects)) {
211
                    foreach ($hubspotObjects as $object) {
212
                        // Check the cache first
213
                        $settings['cache_suffix'] = $cacheSuffix = '.'.$object;
214
                        if ($fields = parent::getAvailableLeadFields($settings)) {
215
                            $hubsFields[$object] = $fields;
216
217
                            continue;
218
                        }
219
220
                        $leadFields = $this->getApiHelper()->getLeadFields($object);
221
                        if (isset($leadFields)) {
222
                            foreach ($leadFields as $fieldInfo) {
223
                                $hubsFields[$object][$fieldInfo['name']] = [
224
                                    'type'     => 'string',
225
                                    'label'    => $fieldInfo['label'],
226
                                    'required' => ('email' === $fieldInfo['name']),
227
                                ];
228
                                if (!empty($fieldInfo['readOnlyValue'])) {
229
                                    $hubsFields[$object][$fieldInfo['name']]['update_mautic'] = 1;
230
                                    $hubsFields[$object][$fieldInfo['name']]['readOnly']      = 1;
231
                                }
232
                            }
233
                        }
234
235
                        $this->cache->set('leadFields'.$cacheSuffix, $hubsFields[$object]);
236
                    }
237
                }
238
            }
239
        } catch (\Exception $e) {
240
            $this->logIntegrationError($e);
241
242
            if (!$silenceExceptions) {
243
                throw $e;
244
            }
245
        }
246
247
        return $hubsFields;
248
    }
249
250
    /**
251
     * @param       $fieldsToUpdate
252
     * @param array $objects
253
     *
254
     * @return array
255
     */
256
    protected function cleanPriorityFields($fieldsToUpdate, $objects = null)
257
    {
258
        if (null === $objects) {
259
            $objects = ['Leads', 'Contacts'];
260
        }
261
262
        if (isset($fieldsToUpdate['leadFields'])) {
263
            // Pass in the whole config
264
            $fields = $fieldsToUpdate['leadFields'];
265
        } else {
266
            $fields = array_flip($fieldsToUpdate);
267
        }
268
269
        return $this->prepareFieldsForSync($fields, $fieldsToUpdate, $objects);
270
    }
271
272
    /**
273
     * Format the lead data to the structure that HubSpot requires for the createOrUpdate request.
274
     *
275
     * @param array $leadData All the lead fields mapped
276
     *
277
     * @return array
278
     */
279
    public function formatLeadDataForCreateOrUpdate($leadData, $lead, $updateLink = false)
280
    {
281
        $formattedLeadData = [];
282
283
        if (!$updateLink) {
284
            foreach ($leadData as $field => $value) {
285
                if ('lifecyclestage' == $field || 'associatedcompanyid' == $field) {
286
                    continue;
287
                }
288
                $formattedLeadData['properties'][] = [
289
                    'property' => $field,
290
                    'value'    => $value,
291
                ];
292
            }
293
        }
294
295
        return $formattedLeadData;
296
    }
297
298
    /**
299
     * {@inheritdoc}
300
     *
301
     * @return bool
302
     */
303
    public function isAuthorized()
304
    {
305
        $keys = $this->getKeys();
306
307
        return isset($keys[$this->getAuthTokenKey()]);
308
    }
309
310
    /**
311
     * @return mixed
312
     */
313
    public function getHubSpotApiKey()
314
    {
315
        $tokenData = $this->getKeys();
316
317
        return $tokenData[$this->getAuthTokenKey()];
318
    }
319
320
    /**
321
     * @param \Mautic\PluginBundle\Integration\Form|FormBuilder $builder
322
     * @param array                                             $data
323
     * @param string                                            $formArea
324
     */
325
    public function appendToForm(&$builder, $data, $formArea)
326
    {
327
        if ('features' == $formArea) {
328
            $builder->add(
329
                'objects',
330
                ChoiceType::class,
331
                [
332
                    'choices' => [
333
                        'mautic.hubspot.object.contact' => 'contacts',
334
                        'mautic.hubspot.object.company' => 'company',
335
                    ],
336
                    'expanded'          => true,
337
                    'multiple'          => true,
338
                    'label'             => $this->getTranslator()->trans('mautic.crm.form.objects_to_pull_from', ['%crm%' => 'Hubspot']),
339
                    'label_attr'        => ['class' => ''],
340
                    'placeholder'       => false,
341
                    'required'          => false,
342
                ]
343
            );
344
        }
345
    }
346
347
    /**
348
     * @param $data
349
     * @param $object
350
     *
351
     * @return array
352
     */
353
    public function amendLeadDataBeforeMauticPopulate($data, $object)
354
    {
355
        if (!isset($data['properties'])) {
356
            return [];
357
        }
358
        foreach ($data['properties'] as $key => $field) {
359
            $value              = str_replace(';', '|', $field['value']);
360
            $fieldsValues[$key] = $value;
361
        }
362
        if ('Lead' == $object && !isset($fieldsValues['email'])) {
363
            foreach ($data['identity-profiles'][0]['identities'] as $identifiedProfile) {
364
                if ('EMAIL' == $identifiedProfile['type']) {
365
                    $fieldsValues['email'] = $identifiedProfile['value'];
366
                }
367
            }
368
        }
369
370
        return $fieldsValues;
371
    }
372
373
    /**
374
     * @param array  $params
375
     * @param null   $query
376
     * @param null   $executed
377
     * @param array  $result
378
     * @param string $object
379
     *
380
     * @return array|null
381
     */
382
    public function getLeads($params = [], $query = null, &$executed = null, $result = [], $object = 'Lead')
383
    {
384
        if (!is_array($executed)) {
385
            $executed = [
386
                0 => 0,
387
                1 => 0,
388
            ];
389
        }
390
        try {
391
            if ($this->isAuthorized()) {
392
                $config                         = $this->mergeConfigToFeatureSettings();
393
                $fields                         = implode('&property=', array_keys($config['leadFields']));
394
                $params['post_append_to_query'] = '&property='.$fields.'&property=lifecyclestage';
395
                $params['Count']                = 100;
396
397
                $data = $this->getApiHelper()->getContacts($params);
398
                if (isset($data['contacts'])) {
399
                    foreach ($data['contacts'] as $contact) {
400
                        if (is_array($contact)) {
401
                            $contactData = $this->amendLeadDataBeforeMauticPopulate($contact, 'Lead');
402
                            $contact     = $this->getMauticLead($contactData);
403
                            if ($contact && !$contact->isNewlyCreated()) { //updated
404
                                $executed[0] = $executed[0] + 1;
405
                            } elseif ($contact && $contact->isNewlyCreated()) { //newly created
406
                                $executed[1] = $executed[1] + 1;
407
                            }
408
409
                            if ($contact) {
410
                                $this->em->detach($contact);
411
                            }
412
                        }
413
                    }
414
                    if ($data['has-more']) {
415
                        $params['vidOffset']  = $data['vid-offset'];
416
                        $params['timeOffset'] = $data['time-offset'];
417
418
                        $this->getLeads($params, $query, $executed);
419
                    }
420
                }
421
422
                return $executed;
423
            }
424
        } catch (\Exception $e) {
425
            $this->logIntegrationError($e);
426
        }
427
428
        return $executed;
429
    }
430
431
    /**
432
     * @param array $params
433
     * @param bool  $id
434
     * @param null  $executed
435
     */
436
    public function getCompanies($params = [], $id = false, &$executed = null)
437
    {
438
        $results = [];
439
        try {
440
            if ($this->isAuthorized()) {
441
                $params['Count'] = 100;
442
                $data            = $this->getApiHelper()->getCompanies($params, $id);
443
                if ($id) {
444
                    $results['results'][] = array_merge($results, $data);
445
                } else {
446
                    $results['results'] = array_merge($results, $data['results']);
447
                }
448
                if (isset($results['results'])) {
449
                    foreach ($results['results'] as $company) {
450
                        if (isset($company['properties'])) {
451
                            $companyData = $this->amendLeadDataBeforeMauticPopulate($company, null);
452
                            $company     = $this->getMauticCompany($companyData);
453
                            if ($id) {
454
                                return $company;
455
                            }
456
                            if ($company) {
457
                                ++$executed;
458
                                $this->em->detach($company);
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\ORM\EntityManager::detach() has been deprecated: 2.7 This method is being removed from the ORM and won't have any replacement ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

458
                                /** @scrutinizer ignore-deprecated */ $this->em->detach($company);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
459
                            }
460
                        }
461
                    }
462
                    if (isset($data['hasMore']) and $data['hasMore']) {
463
                        $params['offset'] = $data['offset'];
464
                        if ($params['offset'] < strtotime($params['start'])) {
465
                            $this->getCompanies($params, $id, $executed);
466
                        }
467
                    }
468
                }
469
470
                return $executed;
471
            }
472
        } catch (\Exception $e) {
473
            $this->logIntegrationError($e);
474
        }
475
476
        return $executed;
477
    }
478
479
    /**
480
     * Create or update existing Mautic lead from the integration's profile data.
481
     *
482
     * @param mixed       $data        Profile data from integration
483
     * @param bool|true   $persist     Set to false to not persist lead to the database in this method
484
     * @param array|null  $socialCache
485
     * @param mixed||null $identifiers
486
     * @param string|null $object
487
     *
488
     * @return Lead
489
     */
490
    public function getMauticLead($data, $persist = true, $socialCache = null, $identifiers = null, $object = null)
491
    {
492
        if (is_object($data)) {
493
            // Convert to array in all levels
494
            $data = json_encode(json_decode($data), true);
495
        } elseif (is_string($data)) {
496
            // Assume JSON
497
            $data = json_decode($data, true);
498
        }
499
500
        if (isset($data['lifecyclestage'])) {
501
            $stageName = $data['lifecyclestage'];
502
            unset($data['lifecyclestage']);
503
        }
504
505
        if (isset($data['associatedcompanyid'])) {
506
            $company = $this->getCompanies([], $data['associatedcompanyid']);
507
            unset($data['associatedcompanyid']);
508
        }
509
510
        if ($lead = parent::getMauticLead($data, false, $socialCache, $identifiers, $object)) {
511
            if (isset($stageName)) {
512
                $stage = $this->em->getRepository('MauticStageBundle:Stage')->getStageByName($stageName);
513
514
                if (empty($stage)) {
515
                    $stage = new Stage();
516
                    $stage->setName($stageName);
517
                    $stages[$stageName] = $stage;
518
                }
519
                if (!$lead->getStage() && $lead->getStage() != $stage) {
520
                    $lead->setStage($stage);
521
522
                    //add a contact stage change log
523
                    $log = new StagesChangeLog();
524
                    $log->setStage($stage);
525
                    $log->setEventName($stage->getId().':'.$stage->getName());
526
                    $log->setLead($lead);
527
                    $log->setActionName(
528
                        $this->translator->trans(
529
                            'mautic.stage.import.action.name',
530
                            [
531
                                '%name%' => $this->userHelper->getUser()->getUsername(),
532
                            ]
533
                        )
534
                    );
535
                    $log->setDateAdded(new \DateTime());
536
                    $lead->stageChangeLog($log);
537
                }
538
            }
539
540
            if ($persist && !empty($lead->getChanges(true))) {
541
                // Only persist if instructed to do so as it could be that calling code needs to manipulate the lead prior to executing event listeners
542
                try {
543
                    $lead->setManipulator(new LeadManipulator(
544
                        'plugin',
545
                        $this->getName(),
546
                        null,
547
                        $this->getDisplayName()
548
                    ));
549
                    $this->leadModel->saveEntity($lead, false);
550
                    if (isset($company)) {
551
                        $this->leadModel->addToCompany($lead, $company);
552
                        $this->em->detach($company);
553
                    }
554
                } catch (\Exception $exception) {
555
                    $this->logger->addWarning($exception->getMessage());
556
557
                    return;
558
                }
559
            }
560
        }
561
562
        return $lead;
563
    }
564
565
    /**
566
     * @param Lead  $lead
567
     * @param array $config
568
     *
569
     * @return array|bool
570
     */
571
    public function pushLead($lead, $config = [])
572
    {
573
        $config = $this->mergeConfigToFeatureSettings($config);
574
575
        if (empty($config['leadFields'])) {
576
            return [];
577
        }
578
579
        $object         = 'contacts';
580
        $fieldsToUpdate = $this->getPriorityFieldsForIntegration($config);
581
        $createFields   = $config['leadFields'];
582
583
        //@todo Hubspot's createLead uses createOrUpdate endpoint which means we don't know before we send mapped data if the contact will be updated or created; so we have to send all mapped fields
584
        $updateFields = array_intersect_key(
585
            $createFields,
586
            $fieldsToUpdate
587
        );
588
589
        $readOnlyFields = $this->getReadOnlyFields($object);
590
591
        $createFields = array_filter(
592
            $createFields,
593
            function ($createField, $key) use ($readOnlyFields) {
594
                if (!isset($readOnlyFields[$key])) {
595
                    return $createField;
596
                }
597
            },
598
            ARRAY_FILTER_USE_BOTH
599
        );
600
601
        $mappedData = $this->populateLeadData(
602
            $lead,
603
            [
604
                'leadFields'       => $createFields,
605
                'object'           => $object,
606
                'feature_settings' => ['objects' => $config['objects']],
607
            ]
608
        );
609
        $this->amendLeadDataBeforePush($mappedData);
610
611
        if (empty($mappedData)) {
612
            return false;
613
        }
614
615
        if ($this->isAuthorized()) {
616
            $leadData = $this->getApiHelper()->createLead($mappedData, $lead);
617
618
            if (!empty($leadData['vid'])) {
619
                /** @var IntegrationEntityRepository $integrationEntityRepo */
620
                $integrationEntityRepo = $this->em->getRepository('MauticPluginBundle:IntegrationEntity');
621
                $integrationId         = $integrationEntityRepo->getIntegrationsEntityId($this->getName(), $object, 'lead', $lead->getId());
622
                $integrationEntity     = (empty($integrationId)) ?
623
                    $this->createIntegrationEntity(
624
                        $object,
625
                        $leadData['vid'],
626
                        'lead',
627
                        $lead->getId(),
628
                        [],
629
                        false
630
                    ) : $integrationEntityRepo->getEntity($integrationId[0]['id']);
631
632
                $integrationEntity->setLastSyncDate($this->getLastSyncDate());
633
                $this->getIntegrationEntityRepository()->saveEntity($integrationEntity);
634
                $this->em->detach($integrationEntity);
635
            }
636
637
            return true;
638
        }
639
640
        return false;
641
    }
642
643
    /**
644
     * Amend mapped lead data before pushing to CRM.
645
     *
646
     * @param $mappedData
647
     */
648
    public function amendLeadDataBeforePush(&$mappedData)
649
    {
650
        foreach ($mappedData as &$data) {
651
            $data = str_replace('|', ';', $data);
652
        }
653
    }
654
655
    /**
656
     * @param $object
657
     *
658
     * @return array
659
     *
660
     * @throws \Exception
661
     */
662
    private function getReadOnlyFields($object)
663
    {
664
        $fields = ArrayHelper::getValue($object, $this->getAvailableLeadFields(), []);
665
666
        return array_filter(
667
            $fields,
668
            function ($field) {
669
                if (!empty($field['readOnly'])) {
670
                    return $field;
671
                }
672
            }
673
        );
674
    }
675
}
676