Completed
Push — master ( e1d59a...95b59d )
by Nate
07:06
created

Objects::findObjectIdFromElementId()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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