Objects::pushToSalesforce()   A
last analyzed

Complexity

Conditions 4
Paths 8

Size

Total Lines 36

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
dl 0
loc 36
ccs 0
cts 26
cp 0
rs 9.344
c 0
b 0
f 0
cc 4
nc 8
nop 3
crap 20
1
<?php
2
3
/**
4
 * @copyright  Copyright (c) Flipbox Digital Limited
5
 * @license    https://flipboxfactory.com/software/force/license
6
 * @link       https://www.flipboxfactory.com/software/force/
7
 */
8
9
namespace flipbox\craft\salesforce\fields;
10
11
use Craft;
12
use craft\base\Element;
13
use craft\base\ElementInterface;
14
use craft\helpers\ArrayHelper;
15
use craft\helpers\Json;
16
use craft\helpers\StringHelper;
17
use flipbox\craft\ember\helpers\SiteHelper;
18
use flipbox\craft\integration\fields\Integrations;
19
use flipbox\craft\integration\queries\IntegrationAssociationQuery;
20
use flipbox\craft\salesforce\criteria\ObjectCriteria;
21
use flipbox\craft\salesforce\fields\actions\SyncItemFrom;
22
use flipbox\craft\salesforce\fields\actions\SyncItemTo;
23
use flipbox\craft\salesforce\fields\actions\SyncTo;
24
use flipbox\craft\salesforce\Force;
25
use flipbox\craft\salesforce\helpers\ObjectHelper;
26
use flipbox\craft\salesforce\helpers\TransformerHelper;
27
use flipbox\craft\salesforce\records\ObjectAssociation;
28
use flipbox\craft\salesforce\transformers\PopulateElementErrorsFromResponse;
29
use flipbox\craft\salesforce\transformers\PopulateElementErrorsFromUpsertResponse;
30
use Flipbox\Salesforce\Connections\ConnectionInterface;
31
use Psr\Http\Message\ResponseInterface;
32
use Psr\SimpleCache\CacheInterface;
33
34
/**
35
 * @author Flipbox Factory <[email protected]>
36
 * @since 1.0.0
37
 */
38
class Objects extends Integrations
39
{
40
    /**
41
     * The Plugin's translation category
42
     */
43
    const TRANSLATION_CATEGORY = 'salesforce';
44
45
    /**
46
     * @inheritdoc
47
     */
48
    const INPUT_TEMPLATE_PATH = 'salesforce/_components/fieldtypes/Objects/input';
49
50
    /**
51
     * @inheritdoc
52
     */
53
    const INPUT_ITEM_TEMPLATE_PATH = 'salesforce/_components/fieldtypes/Objects/_inputItem';
54
55
    /**
56
     * @inheritdoc
57
     */
58
    const SETTINGS_TEMPLATE_PATH = 'salesforce/_components/fieldtypes/Objects/settings';
59
60
    /**
61
     * @inheritdoc
62
     */
63
    const ACTION_PREFORM_ACTION_PATH = 'salesforce/cp/fields/perform-action';
64
65
    /**
66
     * @inheritdoc
67
     */
68
    const ACTION_CREATE_ITEM_PATH = 'salesforce/cp/fields/create-item';
69
70
    /**
71
     * @inheritdoc
72
     */
73
    const ACTION_ASSOCIATION_ITEM_PATH = 'salesforce/cp/objects/associate';
74
75
    /**
76
     * @inheritdoc
77
     */
78
    const ACTION_DISSOCIATION_ITEM_PATH = 'salesforce/cp/objects/dissociate';
79
80
    /**
81
     * @inheritdoc
82
     */
83
    const ACTION_PREFORM_ITEM_ACTION_PATH = 'salesforce/cp/fields/perform-item-action';
84
85
    /**
86
     * @var string
87
     */
88
    public $object;
89
90
    /**
91
     * Indicates whether the full sync operation should be preformed if a matching HubSpot Object was found but not
92
     * currently associated to the element.  For example, when attempting to Sync a Craft User to a HubSpot Contact, if
93
     * the HubSpot Contact already exists; true would override data in HubSpot while false would just perform
94
     * an association (note, a subsequent sync operation could be preformed)
95
     * @var bool
96
     *
97
     * @deprecated
98
     */
99
    public $syncOnMatch = false;
100
101
    /**
102
     * @inheritdoc
103
     */
104
    protected $defaultAvailableActions = [
105
        SyncTo::class
106
    ];
107
108
    /**
109
     * @inheritdoc
110
     */
111
    protected $defaultAvailableItemActions = [
112
        SyncItemFrom::class,
113
        SyncItemTo::class,
114
    ];
115
116
117
    /*******************************************
118
     * OBJECT
119
     *******************************************/
120
121
    /**
122
     * @return string
123
     */
124
    public function getObject(): string
125
    {
126
        return (string)$this->object ?: '';
127
    }
128
129
    /**
130
     * @return string
131
     */
132
    public function getObjectLabel(): string
133
    {
134
        return StringHelper::titleize($this->object);
135
    }
136
137
138
    /*******************************************
139
     * RULES
140
     *******************************************/
141
142
    /**
143
     * @inheritdoc
144
     */
145
    public function rules()
146
    {
147
        return array_merge(
148
            parent::rules(),
149
            [
150
                [
151
                    'object',
152
                    'required',
153
                    'message' => Force::t('Object cannot be empty.')
154
                ],
155
                [
156
                    [
157
                        'object',
158
                    ],
159
                    'safe',
160
                    'on' => [
161
                        self::SCENARIO_DEFAULT
162
                    ]
163
                ]
164
            ]
165
        );
166
    }
167
168
169
    /*******************************************
170
     * SETTINGS
171
     *******************************************/
172
173
    /**
174
     * @inheritdoc
175
     */
176
    public function settingsAttributes(): array
177
    {
178
        return array_merge(
179
            [
180
                'object'
181
            ],
182
            parent::settingsAttributes()
183
        );
184
    }
185
186
    /**
187
     * @inheritdoc
188
     */
189
    public static function recordClass(): string
190
    {
191
        return ObjectAssociation::class;
192
    }
193
194
    /**
195
     * @inheritdoc
196
     */
197
    public static function displayName(): string
198
    {
199
        return Force::t('Salesforce Objects');
200
    }
201
202
    /**
203
     * @inheritdoc
204
     */
205
    public static function defaultSelectionLabel(): string
206
    {
207
        return Force::t('Add a Salesforce Object');
208
    }
209
210
211
    /*******************************************
212
     * CONNECTION
213
     *******************************************/
214
215
    /**
216
     * @return ConnectionInterface
217
     * @throws \flipbox\craft\integration\exceptions\ConnectionNotFound
218
     */
219
    public function getConnection(): ConnectionInterface
220
    {
221
        return Force::getInstance()->getConnections()->get();
222
    }
223
224
225
    /*******************************************
226
     * CACHE
227
     *******************************************/
228
229
    /**
230
     * @return CacheInterface
231
     */
232
    public function getCache(): CacheInterface
233
    {
234
        return Force::getInstance()->getCache()->get();
235
    }
236
237
238
    /*******************************************
239
     * SALESFORCE
240
     *******************************************/
241
242
    /**
243
     * @param string $id
244
     * @return ResponseInterface
245
     */
246
    public function readFromSalesforce(
247
        string $id
248
    ): ResponseInterface {
249
250
        return (new ObjectCriteria([
251
            'connection' => $this->getConnection(),
252
            'cache' => $this->getCache(),
253
            'object' => $this->object,
254
            'id' => $id
255
        ]))->read();
256
    }
257
258
259
    /**
260
     * @param array $payload
261
     * @param string|null $id
262
     * @return ResponseInterface
263
     * @throws \flipbox\craft\integration\exceptions\ConnectionNotFound
264
     */
265
    protected function upsertToSalesforce(
266
        array $payload,
267
        string $id = null
268
    ) {
269
        return (new ObjectCriteria([
270
            'connection' => $this->getConnection(),
271
            'cache' => $this->getCache(),
272
            'object' => $this->object,
273
            'payload' => $payload,
274
            'id' => $id
275
        ]))->upsert();
276
    }
277
278
279
    /*******************************************
280
     * SYNC TO
281
     *******************************************/
282
283
    /**
284
     * @param ElementInterface $element
285
     * @param string|null $objectId
286
     * @param null $transformer
287
     * @return ResponseInterface|null
288
     * @throws \Throwable
289
     * @throws \flipbox\craft\integration\exceptions\ConnectionNotFound
290
     */
291
    public function pushToSalesforce(
292
        ElementInterface $element,
293
        string $objectId = null,
294
        $transformer = null
295
    ) {
296
        /** @var Element $element */
297
298
        $id = $objectId ?: $this->findObjectIdFromElementId($element->getId());
299
300
        // Get callable used to create payload
301
        if (null === ($transformer = TransformerHelper::resolveTransformer($transformer))) {
302
            $transformer = Force::getInstance()->getSettings()->getSyncUpsertPayloadTransformer();
303
        }
304
305
        // Create payload
306
        $payload = call_user_func_array(
307
            $transformer,
308
            [
309
                $element,
310
                $this,
311
                $id
312
            ]
313
        );
314
315
        $response = $this->upsertToSalesforce($payload, $id);
316
317
        if (!$this->handleSyncToSalesforceResponse(
318
            $response,
319
            $element,
320
            $id
321
        )) {
322
            return null;
323
        }
324
325
        return $response;
326
    }
327
328
    /**
329
     * @inheritdoc
330
     * @throws \Throwable
331
     */
332
    public function syncToSalesforce(
333
        ElementInterface $element,
334
        string $objectId = null,
335
        $transformer = null
336
    ): bool {
337
        return $this->pushToSalesforce($element, $objectId, $transformer) !== null;
338
    }
339
340
341
    /*******************************************
342
     * SYNC FROM
343
     *******************************************/
344
345
    /**
346
     * @param ElementInterface $element
347
     * @param string|null $objectId
348
     * @param null $transformer
349
     * @return ResponseInterface|null
350
     * @throws \Throwable
351
     * @throws \craft\errors\ElementNotFoundException
352
     * @throws \yii\base\Exception
353
     */
354
    public function pullFromSalesforce(
355
        ElementInterface $element,
356
        string $objectId = null,
357
        $transformer = null
358
    ) {
359
360
        $id = $objectId ?: $this->findObjectIdFromElementId($element->getId());
361
362
        if (null === $id) {
363
            return null;
364
        }
365
366
        $response = $this->readFromSalesforce($id);
367
368
        if (($response->getStatusCode() < 200 || $response->getStatusCode() >= 300)) {
369
            call_user_func_array(
370
                new PopulateElementErrorsFromResponse(),
371
                [
372
                    $response,
373
                    $element,
374
                    $this,
375
                    $id
376
                ]
377
            );
378
            return null;
379
        }
380
381
        // Get callable used to populate element
382
        if (null === ($transformer = TransformerHelper::resolveTransformer($transformer))) {
383
            $transformer = Force::getInstance()->getSettings()->getSyncPopulateElementTransformer();
384
        }
385
386
        // Populate element
387
        call_user_func_array(
388
            $transformer,
389
            [
390
                $response,
391
                $element,
392
                $this,
393
                &$id
394
            ]
395
        );
396
397
        if (!Craft::$app->getElements()->saveElement($element)) {
398
            return null;
399
        }
400
401
        if ($objectId !== null) {
402
            if (!ObjectHelper::isCaseSafeObjectId($id)) {
403
                $id = $this->getObjectIdFromResponse($response);
404
            }
405
406
            $this->addAssociation(
407
                $element,
408
                $id
409
            );
410
        }
411
412
        return $response;
413
    }
414
415
    /**
416
     * @@inheritdoc
417
     * @throws \Throwable
418
     * @throws \craft\errors\ElementNotFoundException
419
     * @throws \yii\base\Exception
420
     */
421
    public function syncFromSalesforce(
422
        ElementInterface $element,
423
        string $objectId = null,
424
        $transformer = null
425
    ): bool {
426
        return $this->pullFromSalesforce($element, $objectId, $transformer) !== null;
427
    }
428
429
    /*******************************************
430
     * ASSOCIATIONS
431
     *******************************************/
432
433
    /**
434
     * @param ElementInterface|Element $element
435
     * @param string $id
436
     * @return bool
437
     * @throws \Throwable
438
     */
439
    public function addAssociation(
440
        ElementInterface $element,
441
        string $id
442
    ) {
443
        /** @var IntegrationAssociationQuery $query */
444
        if (null === ($query = $element->getFieldValue($this->handle))) {
445
            Force::warning("Field is not available on element.");
446
            return false;
447
        };
448
449
        $associations = ArrayHelper::index($query->all(), 'objectId');
450
451
        if (!array_key_exists($id, $associations)) {
452
            $associations[$id] = $association = new ObjectAssociation([
453
                'element' => $element,
454
                'field' => $this,
455
                'siteId' => SiteHelper::ensureSiteId($element->siteId),
456
                'objectId' => $id
457
            ]);
458
459
            $query->setCachedResult(array_values($associations));
460
461
            return $association->save();
462
        }
463
464
        return true;
465
    }
466
467
    /**
468
     * @param ElementInterface|Element $element
469
     * @param string $id
470
     * @return bool
471
     * @throws \Throwable
472
     */
473
    public function removeAssociation(
474
        ElementInterface $element,
475
        string $id
476
    ) {
477
        /** @var IntegrationAssociationQuery $query */
478
        if (null === ($query = $element->getFieldValue($this->handle))) {
479
            Force::warning("Field is not available on element.");
480
            return false;
481
        };
482
483
        /** @var ObjectAssociation[] $association */
484
        $associations = ArrayHelper::index($query->all(), 'objectId');
485
486
        if ($association = ArrayHelper::remove($associations, $id)) {
487
            $query->clearCachedResult();
488
            return $association->delete();
489
        }
490
491
        return true;
492
    }
493
494
    /**
495
     * @param ResponseInterface|Element $response
496
     * @param ElementInterface $element
497
     * @param string|null $objectId
498
     * @return bool
499
     * @throws \Throwable
500
     */
501
    protected function handleSyncToSalesforceResponse(
502
        ResponseInterface $response,
503
        ElementInterface $element,
504
        string $objectId = null
505
    ): bool {
506
507
        if (!($response->getStatusCode() >= 200 && $response->getStatusCode() <= 299)) {
508
            call_user_func_array(
509
                new PopulateElementErrorsFromUpsertResponse(),
510
                [
511
                    $response,
512
                    $element,
513
                    $this,
514
                    $objectId
515
                ]
516
            );
517
            return false;
518
        }
519
520
        if (empty($objectId)) {
521
            if (null === ($objectId = $this->getObjectIdFromResponse($response))) {
522
                Force::error("Unable to determine object id from response");
523
                return false;
524
            };
525
526
            return $this->addAssociation($element, $objectId);
527
        }
528
529
        return true;
530
    }
531
532
    /**
533
     * @param ResponseInterface $response
534
     * @return string|null
535
     */
536
    protected function getObjectIdFromResponse(ResponseInterface $response)
537
    {
538
        $response->getBody()->rewind();
539
540
        $data = Json::decodeIfJson(
541
            $response->getBody()->getContents()
542
        );
543
544
        $id = $data['Id'] ?? ($data['id'] ?? null);
545
546
        return $id ? (string)$id : null;
547
    }
548
549
    /**
550
     * @param int $elementId
551
     * @param int|null $siteId
552
     * @return bool|false|string|null
553
     */
554
    public function findObjectIdFromElementId(
555
        int $elementId = null,
556
        int $siteId = null
557
    ) {
558
        if ($elementId === null) {
559
            return null;
560
        }
561
562
        if (!$objectId = ObjectAssociation::find()
563
            ->select(['objectId'])
564
            ->elementId($elementId)
565
            ->fieldId($this->id)
566
            ->siteId(SiteHelper::ensureSiteId($siteId))
567
            ->scalar()
568
        ) {
569
            Force::warning(sprintf(
570
                "Salesforce Object Id association was not found for element '%s'",
571
                $elementId
572
            ));
573
574
            return null;
575
        }
576
577
        Force::info(sprintf(
578
            "Salesforce Object Id '%s' was found for element '%s'",
579
            $objectId,
580
            $elementId
581
        ));
582
583
        return $objectId;
584
    }
585
586
    /**
587
     * @param string $objectId
588
     * @param int|null $siteId
589
     * @return bool|false|string|null
590
     */
591
    public function findElementIdFromObjectId(
592
        string $objectId = null,
593
        int $siteId = null
594
    ) {
595
        if ($objectId === null) {
596
            return null;
597
        }
598
599
        if (!$elementId = ObjectAssociation::find()
600
            ->select(['elementId'])
601
            ->objectId($objectId)
602
            ->fieldId($this->id)
603
            ->siteId(SiteHelper::ensureSiteId($siteId))
604
            ->scalar()
605
        ) {
606
            Force::warning(sprintf(
607
                "Element Id association was not found for Salesforce object '%s'",
608
                $objectId
609
            ));
610
611
            return null;
612
        }
613
614
        Force::info(sprintf(
615
            "Element Id '%s' was found for Salesforce object '%s'",
616
            $elementId,
617
            $objectId
618
        ));
619
620
        return $elementId;
621
    }
622
}
623