Issues (3627)

Integration/DynamicsIntegration.php (19 issues)

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 Joomla\Http\Response;
15
use Mautic\LeadBundle\Entity\Company;
16
use Mautic\LeadBundle\Entity\Lead;
17
use Mautic\LeadBundle\Helper\IdentifyCompanyHelper;
18
use Mautic\PluginBundle\Entity\IntegrationEntity;
19
use Mautic\PluginBundle\Entity\IntegrationEntityRepository;
20
use Mautic\PluginBundle\Exception\ApiErrorException;
21
use Symfony\Component\Console\Helper\ProgressBar;
22
use Symfony\Component\Console\Output\ConsoleOutput;
23
use Symfony\Component\Console\Output\OutputInterface;
24
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
25
use Symfony\Component\Form\FormBuilder;
26
27
class DynamicsIntegration extends CrmAbstractIntegration
28
{
29
    public function getName()
30
    {
31
        return 'Dynamics';
32
    }
33
34
    public function getDisplayName()
35
    {
36
        return 'Dynamics CRM';
37
    }
38
39
    /**
40
     * @return array
41
     */
42
    public function getSupportedFeatures()
43
    {
44
        return ['push_lead', 'get_leads', 'push_leads'];
45
    }
46
47
    /**
48
     * Return's authentication method such as oauth2, oauth1a, key, etc.
49
     *
50
     * @return string
51
     */
52
    public function getAuthenticationType()
53
    {
54
        return 'oauth2';
55
    }
56
57
    /**
58
     * Return array of key => label elements that will be converted to inputs to
59
     * obtain from the user.
60
     *
61
     * @return array
62
     */
63
    public function getRequiredKeyFields()
64
    {
65
        return [
66
            'resource'      => 'mautic.integration.dynamics.resource',
67
            'client_id'     => 'mautic.integration.dynamics.client_id',
68
            'client_secret' => 'mautic.integration.dynamics.client_secret',
69
        ];
70
    }
71
72
    /**
73
     * @param FormBuilder $builder
74
     * @param array       $data
75
     * @param string      $formArea
76
     */
77
    public function appendToForm(&$builder, $data, $formArea)
78
    {
79
        $builder->add(
80
            'updateBlanks',
81
            ChoiceType::class,
82
            [
83
                'choices' => [
84
                    'mautic.integrations.blanks' => 'updateBlanks',
85
                ],
86
                'expanded'          => true,
87
                'multiple'          => true,
88
                'label'             => 'mautic.integrations.form.blanks',
89
                'label_attr'        => ['class' => 'control-label'],
90
                'placeholder'       => false,
91
                'required'          => false,
92
            ]
93
        );
94
        if ('features' === $formArea) {
95
            $builder->add(
96
                'objects',
97
                ChoiceType::class,
98
                [
99
                    'choices' => [
100
                        'mautic.dynamics.object.contact'  => 'contacts',
101
                        'mautic.dynamics.object.company'  => 'company',
102
                    ],
103
                    'expanded'          => true,
104
                    'multiple'          => true,
105
                    'label'             => 'mautic.dynamics.form.objects_to_pull_from',
106
                    'label_attr'        => ['class' => ''],
107
                    'placeholder'       => false,
108
                    'required'          => false,
109
                ]
110
            );
111
        }
112
    }
113
114
    /**
115
     * {@inheritdoc}
116
     */
117
    public function sortFieldsAlphabetically()
118
    {
119
        return false;
120
    }
121
122
    /**
123
     * Get the array key for the auth token.
124
     *
125
     * @return string
126
     */
127
    public function getAuthTokenKey()
128
    {
129
        return 'access_token';
130
    }
131
132
    /**
133
     * Get the keys for the refresh token and expiry.
134
     *
135
     * @return array
136
     */
137
    public function getRefreshTokenKeys()
138
    {
139
        return ['refresh_token', 'expires_on'];
140
    }
141
142
    /**
143
     * @return string
144
     */
145
    public function getApiUrl()
146
    {
147
        return $this->keys['resource'];
148
    }
149
150
    /**
151
     * {@inheritdoc}
152
     *
153
     * @return string
154
     */
155
    public function getAccessTokenUrl()
156
    {
157
        return 'https://login.microsoftonline.com/common/oauth2/token';
158
    }
159
160
    /**
161
     * {@inheritdoc}
162
     *
163
     * @return string
164
     */
165
    public function getAuthenticationUrl()
166
    {
167
        return 'https://login.microsoftonline.com/common/oauth2/authorize';
168
    }
169
170
    /**
171
     * {@inheritdoc}
172
     */
173
    public function getAuthLoginUrl()
174
    {
175
        $url = parent::getAuthLoginUrl();
176
177
        return $url.('&resource='.urlencode($this->keys['resource']));
178
    }
179
180
    /**
181
     * {@inheritdoc}
182
     *
183
     * @param bool $inAuthorization
184
     */
185
    public function getBearerToken($inAuthorization = false)
186
    {
187
        if (!$inAuthorization && isset($this->keys[$this->getAuthTokenKey()])) {
188
            return $this->keys[$this->getAuthTokenKey()];
189
        }
190
191
        return false;
192
    }
193
194
    /**
195
     * {@inheritdoc}
196
     *
197
     * @return bool
198
     */
199
    public function getDataPriority()
200
    {
201
        return true;
202
    }
203
204
    /**
205
     * {@inheritdoc}
206
     *
207
     * @param $section
208
     *
209
     * @return string|array
210
     */
211
    public function getFormNotes($section)
212
    {
213
        if ('custom' === $section) {
214
            return [
215
                'template'   => 'MauticCrmBundle:Integration:dynamics.html.php',
216
                'parameters' => [
217
                ],
218
            ];
219
        }
220
221
        return parent::getFormNotes($section);
222
    }
223
224
    /**
225
     * @param $lead
226
     * @param $config
227
     *
228
     * @return array
229
     */
230
    public function populateLeadData($lead, $config = [], $object = 'Contacts')
231
    {
232
        if ('company' === $object) {
233
            $object = 'accounts';
234
        }
235
        $config['object'] = $object;
236
237
        return parent::populateLeadData($lead, $config);
238
    }
239
240
    /**
241
     * Get available company fields for choices in the config UI.
242
     *
243
     * @param array $settings
244
     *
245
     * @return array
246
     */
247
    public function getFormCompanyFields($settings = [])
248
    {
249
        return $this->getFormFieldsByObject('accounts', $settings);
250
    }
251
252
    /**
253
     * @param array $settings
254
     *
255
     * @return array|mixed
256
     */
257
    public function getFormLeadFields($settings = [])
258
    {
259
        return  $this->getFormFieldsByObject('contacts', $settings);
260
    }
261
262
    /**
263
     * @param array $settings
264
     *
265
     * @return array|bool
266
     *
267
     * @throws ApiErrorException
268
     */
269
    public function getAvailableLeadFields($settings = [])
270
    {
271
        $dynamicsFields    = [];
272
        $silenceExceptions = isset($settings['silence_exceptions']) ? $settings['silence_exceptions'] : true;
273
        if (isset($settings['feature_settings']['objects'])) {
274
            $dynamicsObjects = $settings['feature_settings']['objects'];
275
        } else {
276
            $settings        = $this->settings->getFeatureSettings();
277
            $dynamicsObjects = isset($settings['objects']) ? $settings['objects'] : ['contacts'];
278
        }
279
        try {
280
            if ($this->isAuthorized()) {
281
                if (!empty($dynamicsObjects) && is_array($dynamicsObjects)) {
282
                    foreach ($dynamicsObjects as $dynamicsObject) {
283
                        // Check the cache first
284
                        $settings['cache_suffix'] = $cacheSuffix = '.'.$dynamicsObject;
285
                        if ($fields = parent::getAvailableLeadFields($settings)) {
286
                            $dynamicsFields[$dynamicsObject] = $fields;
287
                            continue;
288
                        }
289
                        $leadObject = $this->getApiHelper()->getLeadFields($dynamicsObject);
0 ignored issues
show
The method getLeadFields() does not exist on MauticPlugin\MauticCrmBundle\Api\CrmApi. It seems like you code against a sub-type of said class. However, the method does not exist in MauticPlugin\MauticCrmBundle\Api\PipedriveApi or MauticPlugin\MauticCrmBundle\Api\ConnectwiseApi. Are you sure you never get one of those? ( Ignorable by Annotation )

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

289
                        $leadObject = $this->getApiHelper()->/** @scrutinizer ignore-call */ getLeadFields($dynamicsObject);
Loading history...
290
                        if (null === $leadObject || !array_key_exists('value', $leadObject)) {
291
                            return [];
292
                        }
293
                        /** @var array $opts */
294
                        $fields = $leadObject['value'];
295
                        foreach ($fields as $field) {
296
                            $type      = 'string';
297
                            $fieldType = $field['AttributeTypeName']['Value'];
298
                            if (in_array($fieldType, [
299
                                 'LookupType',
300
                                 'OwnerType',
301
                                 'PicklistType',
302
                                 'StateType',
303
                                 'StatusType',
304
                                 'UniqueidentifierType',
305
                            ], true)) {
306
                                continue;
307
                            } elseif (in_array($fieldType, [
308
                                'DoubleType',
309
                                 'IntegerType',
310
                                 'MoneyType',
311
                            ], true)) {
312
                                $type = 'int';
313
                            } elseif ('Boolean' === $fieldType) {
314
                                $type = 'boolean';
315
                            } elseif ('DateTimeType' === $fieldType) {
316
                                $type = 'datetime';
317
                            }
318
                            $dynamicsFields[$dynamicsObject][$field['LogicalName']] = [
319
                                'type'     => $type,
320
                                'label'    => $field['DisplayName']['UserLocalizedLabel']['Label'],
321
                                'dv'       => $field['LogicalName'],
322
                                'required' => 'ApplicationRequired' === $field['RequiredLevel']['Value'],
323
                            ];
324
                        }
325
                        $this->cache->set('leadFields'.$cacheSuffix, $dynamicsFields[$dynamicsObject]);
326
                    }
327
                }
328
            }
329
        } catch (ApiErrorException $exception) {
330
            $this->logIntegrationError($exception);
331
            if (!$silenceExceptions) {
332
                throw $exception;
333
            }
334
335
            return false;
336
        }
337
338
        return $dynamicsFields;
339
    }
340
341
    /**
342
     * @param Lead  $lead
343
     * @param array $config
344
     *
345
     * @return array|bool
346
     */
347
    public function pushLead($lead, $config = [])
348
    {
349
        $config = $this->mergeConfigToFeatureSettings($config);
350
351
        if (empty($config['leadFields'])) {
352
            return [];
353
        }
354
355
        $mappedData = $this->populateLeadData($lead, $config, 'contacts');
356
357
        $this->amendLeadDataBeforePush($mappedData);
358
359
        if (empty($mappedData)) {
360
            return false;
361
        }
362
363
        try {
364
            if ($this->isAuthorized()) {
365
                $object = 'contacts';
366
                /** @var IntegrationEntityRepository $integrationEntityRepo */
367
                $integrationEntityRepo = $this->em->getRepository('MauticPluginBundle:IntegrationEntity');
368
                $integrationId         = $integrationEntityRepo->getIntegrationsEntityId('Dynamics', $object, 'lead', $lead->getId());
369
                if (!empty($integrationId)) {
370
                    $integrationEntityId = $integrationId[0]['integration_entity_id'];
371
                    $this->getApiHelper()->updateLead($mappedData, $integrationEntityId);
0 ignored issues
show
The method updateLead() does not exist on MauticPlugin\MauticCrmBundle\Api\CrmApi. It seems like you code against a sub-type of MauticPlugin\MauticCrmBundle\Api\CrmApi such as MauticPlugin\MauticCrmBundle\Api\DynamicsApi or MauticPlugin\MauticCrmBundle\Api\PipedriveApi or MauticPlugin\MauticCrmBundle\Api\ZohoApi. ( Ignorable by Annotation )

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

371
                    $this->getApiHelper()->/** @scrutinizer ignore-call */ updateLead($mappedData, $integrationEntityId);
Loading history...
372
373
                    return $integrationEntityId;
374
                }
375
                /** @var Response $response */
376
                $response = $this->getApiHelper()->createLead($mappedData, $lead);
0 ignored issues
show
The call to MauticPlugin\MauticCrmBu...pi\CrmApi::createLead() has too many arguments starting with $mappedData. ( Ignorable by Annotation )

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

376
                $response = $this->getApiHelper()->/** @scrutinizer ignore-call */ createLead($mappedData, $lead);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
377
                // OData-EntityId: https://clientname.crm.dynamics.com/api/data/v8.2/contacts(9844333b-c955-e711-80f1-c4346bad526c)
378
                $header = $response->headers['OData-EntityId'];
379
                if (preg_match('/contacts\((.+)\)/', $header, $out)) {
380
                    $id = $out[1];
381
                    if (empty($integrationId)) {
382
                        $integrationEntity = new IntegrationEntity();
383
                        $integrationEntity->setDateAdded(new \DateTime());
384
                        $integrationEntity->setIntegration('Dynamics');
385
                        $integrationEntity->setIntegrationEntity($object);
386
                        $integrationEntity->setIntegrationEntityId($id);
387
                        $integrationEntity->setInternalEntity('lead');
388
                        $integrationEntity->setInternalEntityId($lead->getId());
389
                    } else {
390
                        $integrationEntity = $integrationEntityRepo->getEntity($integrationId[0]['id']);
391
                    }
392
                    $integrationEntity->setLastSyncDate(new \DateTime());
393
                    $this->em->persist($integrationEntity);
394
                    $this->em->flush($integrationEntity);
395
396
                    return $id;
397
                }
398
399
                return true;
400
            }
401
        } catch (\Exception $e) {
402
            $this->logIntegrationError($e);
403
        }
404
405
        return false;
406
    }
407
408
    /**
409
     * @param array      $params
410
     * @param array|null $query
411
     *
412
     * @return int|null
413
     */
414
    public function getLeads($params = [], $query = null, &$executed = null, $result = [], $object = 'contacts')
415
    {
416
        if ('Contact' === $object) {
417
            $object = 'contacts';
418
        }
419
        $executed    = 0;
420
        $MAX_RECORDS = 200; // Default max records is 5000
421
        try {
422
            if ($this->isAuthorized()) {
423
                $config           = $this->mergeConfigToFeatureSettings();
424
                $fields           = $config['leadFields'];
425
                $config['object'] = $object;
426
                $aFields          = $this->getAvailableLeadFields($config);
427
                $mappedData       = [];
428
                foreach (array_keys($fields) as $k) {
429
                    if (isset($aFields[$object][$k])) {
430
                        $mappedData[] = $aFields[$object][$k]['dv'];
431
                    }
432
                }
433
                $oparams['request_settings']['headers']['Prefer'] = 'odata.maxpagesize='.$MAX_RECORDS;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$oparams was never initialized. Although not strictly required by PHP, it is generally a good practice to add $oparams = array(); before regardless.
Loading history...
434
                $oparams['$select']                               = implode(',', $mappedData);
435
                if (isset($params['fetchAll'], $params['start']) && !$params['fetchAll']) {
436
                    $oparams['$filter'] = sprintf('modifiedon ge %sZ', substr($params['start'], 0, '-6'));
437
                }
438
439
                if (isset($params['output']) && $params['output']->getVerbosity() < OutputInterface::VERBOSITY_VERBOSE) {
440
                    $progress = new ProgressBar($params['output']);
441
                    $progress->start();
442
                }
443
444
                while (true) {
445
                    $data = $this->getApiHelper()->getLeads($oparams);
0 ignored issues
show
The method getLeads() does not exist on MauticPlugin\MauticCrmBundle\Api\CrmApi. It seems like you code against a sub-type of MauticPlugin\MauticCrmBundle\Api\CrmApi such as MauticPlugin\MauticCrmBundle\Api\DynamicsApi or MauticPlugin\MauticCrmBundle\Api\SalesforceApi or MauticPlugin\MauticCrmBundle\Api\ZohoApi or MauticPlugin\MauticCrmBundle\Api\SugarcrmApi. ( Ignorable by Annotation )

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

445
                    $data = $this->getApiHelper()->/** @scrutinizer ignore-call */ getLeads($oparams);
Loading history...
446
447
                    if (!isset($data['value'])) {
448
                        break; // no more data, exit loop
449
                    }
450
451
                    $result = $this->amendLeadDataBeforeMauticPopulate($data, $object);
452
                    $executed += array_key_exists('value', $data) ? count($data['value']) : count($result);
453
454
                    if (isset($params['output'])) {
455
                        if ($params['output']->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) {
456
                            $params['output']->writeln($result);
457
                        } else {
458
                            $progress->advance(count($result));
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $progress does not seem to be defined for all execution paths leading up to this point.
Loading history...
459
                        }
460
                    }
461
462
                    if (!isset($data['@odata.nextLink'])) {
463
                        break; // default exit
464
                    }
465
466
                    // prepare next loop
467
                    $nextLink              = $data['@odata.nextLink'];
468
                    $oparams['$skiptoken'] = urldecode(substr($nextLink, strpos($nextLink, '$skiptoken=') + 11));
469
                }
470
471
                if (isset($params['output']) && $params['output']->getVerbosity() < OutputInterface::VERBOSITY_VERBOSE) {
472
                    $progress->finish();
473
                }
474
            }
475
        } catch (\Exception $e) {
476
            $this->logIntegrationError($e);
477
        }
478
479
        return $executed;
480
    }
481
482
    /**
483
     * @param array $params
484
     *
485
     * @return int|null
486
     */
487
    public function getCompanies($params = [])
488
    {
489
        $executed    = 0;
490
        $MAX_RECORDS = 200; // Default max records is 5000
491
        $object      = 'company';
492
        try {
493
            if ($this->isAuthorized()) {
494
                $config           = $this->mergeConfigToFeatureSettings();
495
                $fields           = $config['companyFields'];
496
                $config['object'] = $object;
497
                $aFields          = $this->getAvailableLeadFields($config);
498
                $mappedData       = [];
499
                if (isset($aFields['company'])) {
500
                    $aFields = $aFields['company'];
501
                }
502
                foreach (array_keys($fields) as $k) {
503
                    $mappedData[] = $aFields[$k]['dv'];
504
                }
505
                $oparams['request_settings']['headers']['Prefer'] = 'odata.maxpagesize='.$MAX_RECORDS;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$oparams was never initialized. Although not strictly required by PHP, it is generally a good practice to add $oparams = array(); before regardless.
Loading history...
506
                $oparams['$select']                               = implode(',', $mappedData);
507
                if (isset($params['fetchAll'], $params['start']) && !$params['fetchAll']) {
508
                    $oparams['$filter'] = sprintf('modifiedon ge %sZ', substr($params['start'], 0, '-6'));
509
                }
510
511
                if (isset($params['output']) && $params['output']->getVerbosity() < OutputInterface::VERBOSITY_VERBOSE) {
512
                    $progress = new ProgressBar($params['output']);
513
                    $progress->start();
514
                }
515
516
                while (true) {
517
                    $data = $this->getApiHelper()->getCompanies($oparams);
0 ignored issues
show
The method getCompanies() does not exist on MauticPlugin\MauticCrmBundle\Api\CrmApi. It seems like you code against a sub-type of MauticPlugin\MauticCrmBundle\Api\CrmApi such as MauticPlugin\MauticCrmBundle\Api\DynamicsApi or MauticPlugin\MauticCrmBundle\Api\HubspotApi or MauticPlugin\MauticCrmBundle\Api\ConnectwiseApi or MauticPlugin\MauticCrmBundle\Api\ZohoApi. ( Ignorable by Annotation )

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

517
                    $data = $this->getApiHelper()->/** @scrutinizer ignore-call */ getCompanies($oparams);
Loading history...
518
                    if (!isset($data['value'])) {
519
                        break; // no more data, exit loop
520
                    }
521
522
                    $result = $this->amendLeadDataBeforeMauticPopulate($data, $object);
523
                    $executed += count($result);
524
525
                    if (isset($params['output'])) {
526
                        if ($params['output']->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) {
527
                            $params['output']->writeln($result);
528
                        } else {
529
                            $progress->advance(count($result));
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $progress does not seem to be defined for all execution paths leading up to this point.
Loading history...
530
                        }
531
                    }
532
533
                    if (!isset($data['@odata.nextLink'])) {
534
                        break; // default exit
535
                    }
536
537
                    // prepare next loop
538
                    $nextLink              = $data['@odata.nextLink'];
539
                    $oparams['$skiptoken'] = urldecode(substr($nextLink, strpos($nextLink, '$skiptoken=') + 11));
540
                }
541
542
                if (isset($params['output']) && $params['output']->getVerbosity() < OutputInterface::VERBOSITY_VERBOSE) {
543
                    $progress->finish();
544
                }
545
            }
546
        } catch (\Exception $e) {
547
            $this->logIntegrationError($e);
548
        }
549
550
        return $executed;
551
    }
552
553
    /**
554
     * Amend mapped lead data before creating to Mautic.
555
     *
556
     * @param array  $data
557
     * @param string $object
558
     *
559
     * @return array
560
     */
561
    public function amendLeadDataBeforeMauticPopulate($data, $object = null)
562
    {
563
        if ('company' === $object) {
564
            $object = 'accounts';
565
        } elseif ('Lead' === $object || 'Contact' === $object) {
566
            $object = 'contacts';
567
        }
568
569
        $config = $this->mergeConfigToFeatureSettings([]);
570
571
        $result = [];
572
        if (isset($data['value'])) {
573
            $this->em->getConnection()->getConfiguration()->setSQLLogger(null);
574
            $entity = null;
575
            /** @var IntegrationEntityRepository $integrationEntityRepo */
576
            $integrationEntityRepo = $this->em->getRepository('MauticPluginBundle:IntegrationEntity');
577
            $objects               = $data['value'];
578
            $integrationEntities   = [];
579
            /** @var array $objects */
580
            foreach ($objects as $entityData) {
581
                $isModified = false;
582
                if ('accounts' === $object) {
583
                    $recordId = $entityData['accountid'];
584
                    // first try to find integration entity
585
                    $integrationId = $integrationEntityRepo->getIntegrationsEntityId('Dynamics', $object, 'company',
586
                        null, null, null, false, 0, 0, "'".$recordId."'");
587
                    if (count($integrationId)) { // company exists, then update local fields
588
                        /** @var Company $entity */
589
                        $entity        = $this->companyModel->getEntity($integrationId[0]['internal_entity_id']);
590
                        $matchedFields = $this->populateMauticLeadData($entityData, $config, 'company');
591
592
                        // Match that data with mapped lead fields
593
                        $fieldsToUpdateInMautic = $this->getPriorityFieldsForMautic($config, $object, 'mautic_company');
594
                        if (!empty($fieldsToUpdateInMautic)) {
595
                            $fieldsToUpdateInMautic = array_intersect_key($config['companyFields'], array_flip($fieldsToUpdateInMautic));
596
                            $newMatchedFields       = array_intersect_key($matchedFields, array_flip($fieldsToUpdateInMautic));
597
                        } else {
598
                            $newMatchedFields = $matchedFields;
599
                        }
600
                        if (!isset($newMatchedFields['companyname'])) {
601
                            if (isset($newMatchedFields['companywebsite'])) {
602
                                $newMatchedFields['companyname'] = $newMatchedFields['companywebsite'];
603
                            }
604
                        }
605
606
                        // update values if already empty
607
                        foreach ($matchedFields as $field => $value) {
608
                            if (empty($entity->getFieldValue($field))) {
609
                                $newMatchedFields[$field] = $value;
610
                            }
611
                        }
612
613
                        // remove unchanged fields
614
                        foreach ($newMatchedFields as $k => $v) {
615
                            if ($entity->getFieldValue($k) === $v) {
616
                                unset($newMatchedFields[$k]);
617
                            }
618
                        }
619
620
                        if (count($newMatchedFields)) {
621
                            $this->companyModel->setFieldValues($entity, $newMatchedFields, false, false);
0 ignored issues
show
The call to Mautic\LeadBundle\Model\...Model::setFieldValues() has too many arguments starting with false. ( Ignorable by Annotation )

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

621
                            $this->companyModel->/** @scrutinizer ignore-call */ 
622
                                                 setFieldValues($entity, $newMatchedFields, false, false);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
622
                            $this->companyModel->saveEntity($entity, false);
623
                            $isModified = true;
624
                        }
625
                    } else {
626
                        $entity = $this->getMauticCompany($entityData, 'company');
627
                    }
628
                    if ($entity) {
629
                        $result[] = $entity->getName();
630
                    }
631
                    $mauticObjectReference = 'company';
632
                } elseif ('contacts' === $object) {
633
                    $recordId = $entityData['contactid'];
634
                    // first try to find integration entity
635
                    $integrationId = $integrationEntityRepo->getIntegrationsEntityId('Dynamics', $object, 'lead',
636
                        null, null, null, false, 0, 0, "'".$recordId."'");
637
                    if (count($integrationId)) { // lead exists, then update
638
                        /** @var Lead $entity */
639
                        $entity        = $this->leadModel->getEntity($integrationId[0]['internal_entity_id']);
640
                        $matchedFields = $this->populateMauticLeadData($entityData, $config);
641
642
                        // Match that data with mapped lead fields
643
                        $fieldsToUpdateInMautic = $this->getPriorityFieldsForMautic($config, $object, 'mautic');
644
                        if (!empty($fieldsToUpdateInMautic)) {
645
                            $fieldsToUpdateInMautic = array_intersect_key($config['leadFields'], array_flip($fieldsToUpdateInMautic));
646
                            $newMatchedFields       = array_intersect_key($matchedFields, array_flip($fieldsToUpdateInMautic));
647
                        } else {
648
                            $newMatchedFields = $matchedFields;
649
                        }
650
651
                        // update values if already empty
652
                        foreach ($matchedFields as $field => $value) {
653
                            if (empty($entity->getFieldValue($field))) {
654
                                $newMatchedFields[$field] = $value;
655
                            }
656
                        }
657
658
                        // remove unchanged fields
659
                        foreach ($newMatchedFields as $k => $v) {
660
                            if ($entity->getFieldValue($k) === $v) {
661
                                unset($newMatchedFields[$k]);
662
                            }
663
                        }
664
665
                        if (count($newMatchedFields)) {
666
                            $this->leadModel->setFieldValues($entity, $newMatchedFields, false, false);
667
                            $this->leadModel->saveEntity($entity, false);
668
                            $isModified = true;
669
                        }
670
                    } else {
671
                        /** @var Lead $entity */
672
                        $entity = $this->getMauticLead($entityData);
673
                    }
674
675
                    if ($entity) {
676
                        $result[] = $entity->getEmail();
677
                    }
678
679
                    // Associate lead company
680
                    if (!empty($entityData['parentcustomerid']) // company
681
                        && $entityData['parentcustomerid'] !== $this->translator->trans(
682
                            'mautic.integration.form.lead.unknown'
683
                        )
684
                    ) {
685
                        $company = IdentifyCompanyHelper::identifyLeadsCompany(
686
                            ['company' => $entityData['parentcustomerid']],
687
                            null,
688
                            $this->companyModel
689
                        );
690
691
                        if (!empty($company[2])) {
692
                            $syncLead = $this->companyModel->addLeadToCompany($company[2], $entity);
0 ignored issues
show
The assignment to $syncLead is dead and can be removed.
Loading history...
693
                            $this->em->detach($company[2]);
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

693
                            /** @scrutinizer ignore-deprecated */ $this->em->detach($company[2]);

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...
694
                        }
695
                    }
696
697
                    $mauticObjectReference = 'lead';
698
                } else {
699
                    $this->logIntegrationError(
700
                        new \Exception(
701
                            sprintf('Received an unexpected object "%s"', $object)
702
                        )
703
                    );
704
                    continue;
705
                }
706
707
                if ($entity) {
708
                    $integrationId = $integrationEntityRepo->getIntegrationsEntityId(
709
                        'Dynamics',
710
                        $object,
711
                        $mauticObjectReference,
712
                        $entity->getId()
713
                    );
714
715
                    if (0 === count($integrationId)) {
716
                        $integrationEntity = new IntegrationEntity();
717
                        $integrationEntity->setDateAdded(new \DateTime());
718
                        $integrationEntity->setIntegration('Dynamics');
719
                        $integrationEntity->setIntegrationEntity($object);
720
                        $integrationEntity->setIntegrationEntityId($recordId);
721
                        $integrationEntity->setInternalEntity($mauticObjectReference);
722
                        $integrationEntity->setInternalEntityId($entity->getId());
723
                        $integrationEntities[] = $integrationEntity;
724
                    } else {
725
                        $integrationEntity = $integrationEntityRepo->getEntity($integrationId[0]['id']);
726
                        if ($isModified) {
727
                            $integrationEntity->setLastSyncDate(new \DateTime());
728
                            $integrationEntities[] = $integrationEntity;
729
                        }
730
                    }
731
                    $this->em->detach($entity);
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

731
                    /** @scrutinizer ignore-deprecated */ $this->em->detach($entity);

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...
732
                    unset($entity);
733
                }
734
            }
735
736
            $integrationEntityRepo->saveEntities($integrationEntities);
737
            $this->em->clear('Mautic\PluginBundle\Entity\IntegrationEntity');
738
            $this->em->clear();
739
740
            unset($integrationEntityRepo, $integrationEntities);
741
        }
742
743
        return $result;
744
    }
745
746
    /**
747
     * @param array $params
748
     *
749
     * @return mixed
750
     */
751
    public function pushLeads($params = [])
752
    {
753
        $MAX_RECORDS = (isset($params['limit']) && $params['limit'] < 100) ? $params['limit'] : 100;
754
        if (isset($params['fetchAll']) && $params['fetchAll']) {
755
            $params['start'] = null;
756
            $params['end']   = null;
757
        }
758
        $object                = 'contacts';
759
        $config                = $this->mergeConfigToFeatureSettings();
760
        $integrationEntityRepo = $this->em->getRepository('MauticPluginBundle:IntegrationEntity');
761
        $fieldsToUpdateInCrm   = isset($config['update_mautic']) ? array_keys($config['update_mautic'], 0) : [];
762
        $leadFields            = array_unique(array_values($config['leadFields']));
763
        $totalUpdated          = $totalCreated          = $totalErrors          = 0;
764
765
        if ($key = array_search('mauticContactTimelineLink', $leadFields)) {
766
            unset($leadFields[$key]);
767
        }
768
        if ($key = array_search('mauticContactIsContactableByEmail', $leadFields)) {
769
            unset($leadFields[$key]);
770
        }
771
772
        if (empty($leadFields)) {
773
            return [0, 0, 0];
774
        }
775
776
        $fields = implode(', l.', $leadFields);
777
        $fields = 'l.'.$fields;
778
779
        $availableFields         = $this->getAvailableLeadFields(['feature_settings' => ['objects' => [$object]]]);
780
        $fieldsToUpdate[$object] = array_values(array_intersect(array_keys($availableFields[$object]), $fieldsToUpdateInCrm));
0 ignored issues
show
Comprehensibility Best Practice introduced by
$fieldsToUpdate was never initialized. Although not strictly required by PHP, it is generally a good practice to add $fieldsToUpdate = array(); before regardless.
Loading history...
781
        $fieldsToUpdate[$object] = array_intersect_key($config['leadFields'], array_flip($fieldsToUpdate[$object]));
782
783
        $progress      = false;
784
        $totalToUpdate = array_sum($integrationEntityRepo->findLeadsToUpdate('Dynamics', 'lead', $fields, false, $params['start'], $params['end'], [$object]));
785
        $totalToCreate = $integrationEntityRepo->findLeadsToCreate('Dynamics', $fields, false, $params['start'], $params['end']);
786
        $totalCount    = $totalToCreate + $totalToUpdate;
787
788
        if (defined('IN_MAUTIC_CONSOLE')) {
789
            // start with update
790
            if ($totalToUpdate + $totalToCreate) {
791
                $output = new ConsoleOutput();
792
                $output->writeln("About $totalToUpdate to update and about $totalToCreate to create/update");
793
                $output->writeln('<info>This could take some time. Please wait until the process is completed</info>');
794
                $progress = new ProgressBar($output, $totalCount);
795
            }
796
        }
797
798
        // Start with contacts so we know who is a contact when we go to process converted leads
799
        $leadsToCreateInD    = [];
800
        $leadsToUpdateInD    = [];
801
        $integrationEntities = [];
802
803
        $toUpdate = $integrationEntityRepo->findLeadsToUpdate('Dynamics', 'lead', $fields, $totalToUpdate, $params['start'], $params['end'], $object, [])[$object];
804
805
        if (is_array($toUpdate)) {
806
            $totalUpdated += count($toUpdate);
807
            foreach ($toUpdate as $lead) {
808
                if (isset($lead['email']) && !empty($lead['email'])) {
809
                    $key                        = mb_strtolower($this->cleanPushData($lead['email']));
0 ignored issues
show
It seems like $this->cleanPushData($lead['email']) can also be of type boolean; however, parameter $string of mb_strtolower() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

809
                    $key                        = mb_strtolower(/** @scrutinizer ignore-type */ $this->cleanPushData($lead['email']));
Loading history...
810
                    $lead                       = $this->getCompoundMauticFields($lead);
811
                    $lead['integration_entity'] = $object;
812
                    $leadsToUpdateInD[$key]     = $lead;
813
                    $integrationEntity          = $this->em->getReference('MauticPluginBundle:IntegrationEntity', $lead['id']);
814
                    $integrationEntities[]      = $integrationEntity->setLastSyncDate(new \DateTime());
815
                }
816
            }
817
        }
818
        unset($toUpdate);
819
820
        //create lead records, including deleted on D side (last_sync = null)
821
        /** @var array $leadsToCreate */
822
        $leadsToCreate = $integrationEntityRepo->findLeadsToCreate('Dynamics', $fields, $totalToCreate, $params['start'], $params['end']);
823
        if (is_array($leadsToCreate)) {
0 ignored issues
show
The condition is_array($leadsToCreate) is always true.
Loading history...
824
            $totalCreated += count($leadsToCreate);
825
            foreach ($leadsToCreate as $lead) {
826
                if (isset($lead['email']) && !empty($lead['email'])) {
827
                    $key                        = mb_strtolower($this->cleanPushData($lead['email']));
828
                    $lead                       = $this->getCompoundMauticFields($lead);
829
                    $lead['integration_entity'] = $object;
830
                    $leadsToCreateInD[$key]     = $lead;
831
                }
832
            }
833
        }
834
        unset($leadsToCreate);
835
836
        if (count($integrationEntities)) {
837
            // Persist updated entities if applicable
838
            $integrationEntityRepo->saveEntities($integrationEntities);
839
            $this->em->clear(IntegrationEntity::class);
840
        }
841
842
        // update contacts
843
        $leadData = [];
844
        $rowNum   = 0;
845
        foreach ($leadsToUpdateInD as $lead) {
846
            $mappedData = [];
847
            if (defined('IN_MAUTIC_CONSOLE') && $progress) {
848
                $progress->advance();
849
            }
850
            $existingPerson = $this->getExistingRecord('emailaddress1', $lead['email'], $object);
851
852
            $objectFields            = $this->prepareFieldsForPush($availableFields[$object]);
853
            $fieldsToUpdate[$object] = $this->getBlankFieldsToUpdate($fieldsToUpdate[$object], $existingPerson, $objectFields, $config);
854
855
            // Match that data with mapped lead fields
856
            foreach ($fieldsToUpdate[$object] as $k => $v) {
857
                foreach ($lead as $dk => $dv) {
858
                    if ($v === $dk) {
859
                        if ($dv) {
860
                            if (isset($availableFields[$object][$k])) {
861
                                $mappedData[$availableFields[$object][$k]['dv']] = $dv;
862
                            }
863
                        }
864
                    }
865
                }
866
            }
867
            $leadData[$lead['integration_entity_id']] = $mappedData;
868
869
            ++$rowNum;
870
            // SEND 100 RECORDS AT A TIME
871
            if ($MAX_RECORDS === $rowNum) {
872
                $this->getApiHelper()->updateLeads($leadData, $object);
0 ignored issues
show
The method updateLeads() does not exist on MauticPlugin\MauticCrmBundle\Api\CrmApi. It seems like you code against a sub-type of MauticPlugin\MauticCrmBundle\Api\CrmApi such as MauticPlugin\MauticCrmBundle\Api\DynamicsApi. ( Ignorable by Annotation )

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

872
                $this->getApiHelper()->/** @scrutinizer ignore-call */ updateLeads($leadData, $object);
Loading history...
873
                $leadData = [];
874
                $rowNum   = 0;
875
            }
876
        }
877
        $this->getApiHelper()->updateLeads($leadData, $object);
878
879
        // create  contacts
880
        $leadData = [];
881
        $rowNum   = 0;
882
        foreach ($leadsToCreateInD as $lead) {
883
            $mappedData = [];
884
            if (defined('IN_MAUTIC_CONSOLE') && $progress) {
885
                $progress->advance();
886
            }
887
            // Match that data with mapped lead fields
888
            foreach ($config['leadFields'] as $k => $v) {
889
                foreach ($lead as $dk => $dv) {
890
                    if ($v === $dk) {
891
                        if ($dv) {
892
                            if (isset($availableFields[$object][$k])) {
893
                                $mappedData[$availableFields[$object][$k]['dv']] = $dv;
894
                            }
895
                        }
896
                    }
897
                }
898
            }
899
            $leadData[$lead['internal_entity_id']] = $mappedData;
900
901
            ++$rowNum;
902
            // SEND 100 RECORDS AT A TIME
903
            if ($MAX_RECORDS === $rowNum) {
904
                $ids = $this->getApiHelper()->createLeads($leadData, $object);
0 ignored issues
show
The method createLeads() does not exist on MauticPlugin\MauticCrmBundle\Api\CrmApi. Did you maybe mean createLead()? ( Ignorable by Annotation )

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

904
                $ids = $this->getApiHelper()->/** @scrutinizer ignore-call */ createLeads($leadData, $object);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
905
                $this->createIntegrationEntities($ids, $object, $integrationEntityRepo);
906
                $leadData = [];
907
                $rowNum   = 0;
908
            }
909
        }
910
        $ids = $this->getApiHelper()->createLeads($leadData, $object);
911
        $this->createIntegrationEntities($ids, $object, $integrationEntityRepo);
912
913
        if ($progress) {
914
            $progress->finish();
915
            $output->writeln('');
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $output does not seem to be defined for all execution paths leading up to this point.
Loading history...
916
        }
917
918
        return [$totalUpdated, $totalCreated, $totalErrors];
919
    }
920
921
    /**
922
     * @param array $ids
923
     * @param $object
924
     * @param IntegrationEntityRepository $integrationEntityRepo
925
     */
926
    private function createIntegrationEntities($ids, $object, $integrationEntityRepo)
927
    {
928
        foreach ($ids as $oid => $leadId) {
929
            $this->logger->debug('CREATE INTEGRATION ENTITY: '.$oid);
930
            $integrationId = $integrationEntityRepo->getIntegrationsEntityId('Dynamics', $object,
931
                'lead', null, null, null, false, 0, 0,
932
                "'".$oid."'"
933
            );
934
935
            if (0 === count($integrationId)) {
936
                $this->createIntegrationEntity($object, $oid, 'lead', $leadId);
937
            }
938
        }
939
    }
940
941
    private function getExistingRecord($seachColumn, $searchValue, $object = 'contacts')
942
    {
943
        $availableFields    = $this->getAvailableLeadFields();
944
        $oparams['$select'] = implode(',', array_keys($availableFields[$object]));
945
        $oparams['$filter'] = $seachColumn.' eq \''.$searchValue.'\'';
946
        $data               = $this->getApiHelper()->getLeads($oparams);
947
948
        return (isset($data['value'][0]) && !empty($data['value'][0])) ? $data['value'][0] : [];
949
    }
950
}
951