Passed
Pull Request — master (#59)
by Matthew
02:07
created

Mapper::typeRequiresSalsifyObjects()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 4
c 0
b 0
f 0
dl 0
loc 9
rs 10
cc 2
nc 2
nop 1
1
<?php
2
3
namespace Dynamic\Salsify\Model;
4
5
use Dynamic\Salsify\ORM\SalsifyIDExtension;
6
use Dynamic\Salsify\Task\ImportTask;
7
use Exception;
8
use JsonMachine\JsonMachine;
9
use SilverStripe\Core\Injector\Injector;
10
use SilverStripe\ORM\DataList;
11
use SilverStripe\ORM\DataObject;
12
use SilverStripe\ORM\HasManyList;
13
use SilverStripe\ORM\ManyManyList;
14
use SilverStripe\Versioned\Versioned;
0 ignored issues
show
Bug introduced by
The type SilverStripe\Versioned\Versioned was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
15
16
/**
17
 * Class Mapper
18
 * @package Dynamic\Salsify\Model
19
 */
20
class Mapper extends Service
21
{
22
23
    /**
24
     * @var bool
25
     */
26
    public static $SINGLE = false;
27
28
    /**
29
     * @var bool
30
     */
31
    public static $MULTIPLE = true;
32
33
    /**
34
     * @var
35
     */
36
    private $file = null;
37
38
    /**
39
     * @var JsonMachine
40
     */
41
    private $productStream;
42
43
    /**
44
     * @var JsonMachine
45
     */
46
    private $assetStream;
47
48
    /**
49
     * @var array
50
     */
51
    private $currentUniqueFields = [];
52
53
    /**
54
     * @var int
55
     */
56
    private $importCount = 0;
57
58
    /**
59
     * @var bool
60
     */
61
    public $skipSilently = false;
62
63
    /**
64
     * Mapper constructor.
65
     * @param string $importerKey
66
     * @param $file
67
     * @throws \Exception
68
     */
69
    public function __construct($importerKey, $file = null)
70
    {
71
        parent::__construct($importerKey);
72
        if (!$this->config()->get('mapping')) {
73
            throw  new Exception('A Mapper needs a mapping');
74
        }
75
76
        if ($file !== null) {
77
            $this->file = $file;
78
            $this->resetProductStream();
79
            $this->resetAssetStream();
80
        }
81
    }
82
83
    /**
84
     *
85
     */
86
    public function resetProductStream()
87
    {
88
        $this->productStream = JsonMachine::fromFile($this->file, '/4/products');
89
    }
90
91
    /**
92
     *
93
     */
94
    public function resetAssetStream()
95
    {
96
        $this->assetStream = JsonMachine::fromFile($this->file, '/3/digital_assets');
97
    }
98
99
    /**
100
     * Maps the data
101
     * @throws \Exception
102
     */
103
    public function map()
104
    {
105
        $this->extend('onBeforeMap', $this->file, Mapper::$MULTIPLE);
106
107
        foreach ($this->yieldKeyVal($this->productStream) as $name => $data) {
108
            foreach ($this->yieldKeyVal($this->config()->get('mapping')) as $class => $mappings) {
109
                $this->mapToObject($class, $mappings, $data);
110
                $this->currentUniqueFields = [];
111
            }
112
        }
113
114
        if ($this->mappingHasSalsifyRelation()) {
115
            ImportTask::output("----------------");
116
            ImportTask::output("Setting up salsify relations");
117
            ImportTask::output("----------------");
118
            $this->resetProductStream();
119
120
            foreach ($this->yieldKeyVal($this->productStream) as $name => $data) {
121
                foreach ($this->yieldKeyVal($this->config()->get('mapping')) as $class => $mappings) {
122
                    $this->mapToObject($class, $mappings, $data, null, true);
123
                    $this->currentUniqueFields = [];
124
                }
125
            }
126
        }
127
128
        ImportTask::output("Imported and updated $this->importCount products.");
129
        $this->extend('onAfterMap', $this->file, Mapper::$MULTIPLE);
130
    }
131
132
    /**
133
     * @param string|DataObject $class
134
     * @param array $mappings The mapping for a specific class
135
     * @param array $data
136
     * @param DataObject|null $object
137
     * @param bool $salsifyRelations
138
     * @param bool $forceUpdate
139
     *
140
     * @return DataObject|null
141
     * @throws \Exception
142
     */
143
    public function mapToObject(
144
        $class,
145
        $mappings,
146
        $data,
147
        $object = null,
148
        $salsifyRelations = false,
149
        $forceUpdate = false
150
    ) {
151
        if ($salsifyRelations) {
152
            if (!$this->classConfigHasSalsifyRelation($mappings)) {
153
                return null;
154
            }
155
        }
156
157
        // if object was not passed
158
        if ($object === null) {
159
            $object = $this->findObjectByUnique($class, $mappings, $data);
160
161
            $filter = $this->getUniqueFilter($class, $mappings, $data);
162
            if (count(array_filter($filter)) == 0) {
163
                return null;
164
            }
165
166
            // if no existing object was found but a unique filter is valid (not empty)
167
            if (!$object) {
0 ignored issues
show
introduced by
$object is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
168
                $object = $class::create();
169
            }
170
        }
171
172
        $wasPublished = $object->hasExtension(Versioned::class) ? $object->isPublished() : false;
173
        $wasWritten = $object->isInDB();
174
175
        $firstUniqueKey = array_keys($this->uniqueFields($class, $mappings))[0];
176
        if (array_key_exists($mappings[$firstUniqueKey]['salsifyField'], $data)) {
177
            $firstUniqueValue = $data[$mappings[$firstUniqueKey]['salsifyField']];
178
        } else {
179
            $firstUniqueValue = 'NULL';
180
        }
181
        ImportTask::output("Updating $class $firstUniqueKey $firstUniqueValue");
182
183
        if (
184
            !$forceUpdate &&
185
            $this->objectUpToDate($object, $data, $firstUniqueKey, $firstUniqueValue, $salsifyRelations)
186
        ) {
187
            return $object;
188
        }
189
190
        foreach ($this->yieldKeyVal($mappings) as $dbField => $salsifyField) {
191
            $field = $this->getField($salsifyField, $data);
192
            if ($field === false) {
193
                $this->clearValue($object, $dbField, $salsifyField);
194
                continue;
195
            }
196
197
            $type = $this->getFieldType($salsifyField);
198
            // skip all but salsify relations types if not doing relations
199
            if ($salsifyRelations && !$this->typeRequiresSalsifyObjects($type)) {
200
                continue;
201
            }
202
203
            // skip salsify relations types if not doing relations
204
            if (!$salsifyRelations && $this->typeRequiresSalsifyObjects($type)) {
205
                continue;
206
            }
207
208
            $value = null;
209
            $objectData = $data;
0 ignored issues
show
Unused Code introduced by
The assignment to $objectData is dead and can be removed.
Loading history...
210
211
            if ($this->handleShouldSkip($class, $dbField, $salsifyField, $data)) {
212
                if (!$this->skipSilently) {
213
                    ImportTask::output("Skipping $class $firstUniqueKey $firstUniqueValue");
214
                    $this->skipSilently = false;
215
                }
216
                return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type SilverStripe\ORM\DataObject|null.
Loading history...
217
            };
218
219
            $objectData = $this->handleModification($type, $class, $dbField, $salsifyField, $data);
220
            $sortColumn = $this->getSortColumn($salsifyField);
221
222
            if ($salsifyRelations == false && !array_key_exists($field, $objectData)) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
223
                continue;
224
            }
225
226
            $value = $this->handleType($type, $class, $objectData, $field, $salsifyField, $dbField);
227
            $this->writeValue($object, $type, $dbField, $value, $sortColumn);
228
        }
229
230
        if ($object->isChanged()) {
231
            $object->write();
232
            $this->importCount++;
233
            $this->extend('afterObjectWrite', $object, $wasWritten, $wasPublished);
234
        } else {
235
            ImportTask::output("$class $firstUniqueKey $firstUniqueValue was not changed.");
236
        }
237
        return $object;
238
    }
239
240
    /**
241
     * @param DataObject $object
242
     * @param array $data
243
     * @param string $firstUniqueKey
244
     * @param string $firstUniqueValue
245
     * @param bool $salsifyRelations
246
     * @return bool
247
     */
248
    private function objectUpToDate($object, $data, $firstUniqueKey, $firstUniqueValue, $salsifyRelations = false)
249
    {
250
        if ($this->config()->get('skipUpToDate') == false) {
251
            return false;
252
        }
253
254
        if ($salsifyRelations == false) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
255
            if ($this->objectDataUpToDate($object, $data, $firstUniqueKey, $firstUniqueValue)) {
256
                ImportTask::output("Skipping $firstUniqueKey $firstUniqueValue. It is up to Date.");
257
                return true;
258
            }
259
        } else {
260
            if ($this->objectRelationsUpToDate($object, $data, $firstUniqueKey, $firstUniqueValue)) {
261
                ImportTask::output("Skipping $firstUniqueKey $firstUniqueValue relations. It is up to Date.");
262
                return true;
263
            }
264
        }
265
266
        return false;
267
    }
268
269
    /**
270
     * @param DataObject $object
271
     * @param array $data
272
     * @param string $firstUniqueKey
273
     * @param string $firstUniqueValue
274
     * @return bool
275
     */
276
    public function objectDataUpToDate($object, $data, $firstUniqueKey, $firstUniqueValue)
277
    {
278
        // assume not up to date if field does not exist on object
279
        if (!$object->hasField('SalsifyUpdatedAt')) {
280
            return false;
281
        }
282
283
        if ($data['salsify:updated_at'] != $object->getField('SalsifyUpdatedAt')) {
284
            return false;
285
        }
286
287
        return true;
288
    }
289
290
    /**
291
     * @param DataObject $object
292
     * @param array $data
293
     * @param string $firstUniqueKey
294
     * @param string $firstUniqueValue
295
     * @return bool
296
     */
297
    public function objectRelationsUpToDate($object, $data, $firstUniqueKey, $firstUniqueValue)
298
    {
299
        // assume not up to date if field does not exist on object
300
        if (!$object->hasField('SalsifyRelationsUpdatedAt')) {
301
            return false;
302
        }
303
304
        // relations were never updated, so its up to date
305
        if (!isset($data['salsify:relations_updated_at'])) {
306
            return true;
307
        }
308
309
        if ($data['salsify:relations_updated_at'] != $object->getField('SalsifyRelationsUpdatedAt')) {
310
            return false;
311
        }
312
313
        return true;
314
    }
315
316
    /**
317
     * @param array $salsifyField
318
     * @param array $data
319
     *
320
     * @return string|false
321
     */
322
    private function getField($salsifyField, $data)
323
    {
324
        if (!is_array($salsifyField)) {
0 ignored issues
show
introduced by
The condition is_array($salsifyField) is always true.
Loading history...
325
            return array_key_exists($salsifyField, $data) ? $salsifyField : false;
326
        }
327
328
        $hasSalsifyField = array_key_exists('salsifyField', $salsifyField);
329
        $isLiteralField = (
330
            $this->getFieldType($salsifyField) === 'Literal' &&
0 ignored issues
show
introduced by
The condition $this->getFieldType($salsifyField) === 'Literal' is always false.
Loading history...
331
            array_key_exists('value', $salsifyField)
332
        );
333
        $isSalsifyRelationField = (
334
            $this->getFieldType($salsifyField) === 'SalsifyRelation' &&
0 ignored issues
show
introduced by
The condition $this->getFieldType($sal...) === 'SalsifyRelation' is always false.
Loading history...
335
            $hasSalsifyField
336
        );
337
338
        if ($isLiteralField) {
0 ignored issues
show
introduced by
The condition $isLiteralField is always false.
Loading history...
339
            return $salsifyField['value'];
340
        }
341
342
        if ($isSalsifyRelationField) {
0 ignored issues
show
introduced by
The condition $isSalsifyRelationField is always false.
Loading history...
343
            return $salsifyField['salsifyField'];
344
        }
345
346
        if (!$hasSalsifyField) {
347
            return false;
348
        }
349
350
        if (array_key_exists($salsifyField['salsifyField'], $data)) {
351
            return $salsifyField['salsifyField'];
352
        } elseif (array_key_exists('fallback', $salsifyField)) {
353
            // make fallback an array
354
            if (!is_array($salsifyField['fallback'])) {
355
                $salsifyField['fallback'] = [$salsifyField['fallback']];
356
            }
357
358
            foreach ($this->yieldSingle($salsifyField['fallback']) as $fallback) {
359
                if (array_key_exists($fallback, $data)) {
360
                    return $fallback;
361
                }
362
            }
363
        } elseif (array_key_exists('modification', $salsifyField)) {
364
            return $salsifyField['salsifyField'];
365
        }
366
367
        return false;
368
    }
369
370
    /**
371
     * @param string $class
372
     * @param array $mappings
373
     * @param array $data
374
     *
375
     * @return array
376
     */
377
    private function getUniqueFilter($class, $mappings, $data)
378
    {
379
        $uniqueFields = $this->uniqueFields($class, $mappings);
380
        // creates a filter
381
        $filter = [];
382
        foreach ($this->yieldKeyVal($uniqueFields) as $dbField => $salsifyField) {
383
            $modifiedData = $data;
384
            $fieldMapping = $mappings[$dbField];
385
            $fieldType = $this->getFieldType($salsifyField);
386
            $modifiedData = $this->handleModification($fieldType, $class, $dbField, $fieldMapping, $modifiedData);
387
388
            // adds unique fields to filter
389
            if (array_key_exists($salsifyField, $modifiedData)) {
390
                $filter[$dbField] = $modifiedData[$salsifyField];
391
            }
392
        }
393
394
        return $filter;
395
    }
396
397
    /**
398
     * @param string $class
399
     * @param array $mappings
400
     * @param array $data
401
     *
402
     * @return \SilverStripe\ORM\DataObject
403
     */
404
    private function findObjectByUnique($class, $mappings, $data)
405
    {
406
        if ($obj = $this->findBySalsifyID($class, $mappings, $data)) {
407
            return $obj;
408
        }
409
410
        $filter = $this->getUniqueFilter($class, $mappings, $data);
411
        return DataObject::get($class)->filter($filter)->first();
412
    }
413
414
    /**
415
     * @param string $class
416
     * @param array $mappings
417
     * @param array $data
418
     *
419
     * @return \SilverStripe\ORM\DataObject|bool
420
     */
421
    private function findBySalsifyID($class, $mappings, $data)
422
    {
423
        /** @var DataObject $genericObject */
424
        $genericObject = Injector::inst()->get($class);
425
        if (
426
            !$genericObject->hasExtension(SalsifyIDExtension::class) &&
427
            !$genericObject->hasField('SalsifyID')
428
        ) {
429
            return false;
430
        }
431
432
        $obj = DataObject::get($class)->filter([
433
            'SalsifyID' => $data['salsify:id'],
434
        ])->first();
435
        if ($obj) {
0 ignored issues
show
introduced by
$obj is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
436
            return $obj;
437
        }
438
439
        return false;
440
    }
441
442
    /**
443
     * Gets a list of all the unique field keys
444
     *
445
     * @param string class
0 ignored issues
show
Bug introduced by
The type Dynamic\Salsify\Model\class was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
446
     * @param array $mappings
447
     * @return array
448
     */
449
    private function uniqueFields($class, $mappings)
450
    {
451
        // cached after first map
452
        if (array_key_exists($class, $this->currentUniqueFields) && !empty($this->currentUniqueFields[$class])) {
453
            return $this->currentUniqueFields[$class];
454
        }
455
456
        $uniqueFields = [];
457
        foreach ($this->yieldKeyVal($mappings) as $dbField => $salsifyField) {
458
            if (!is_array($salsifyField)) {
459
                continue;
460
            }
461
462
            if (
463
                !array_key_exists('unique', $salsifyField) ||
464
                !array_key_exists('salsifyField', $salsifyField)
465
            ) {
466
                continue;
467
            }
468
469
            if ($salsifyField['unique'] !== true) {
470
                continue;
471
            }
472
473
            $uniqueFields[$dbField] = $salsifyField['salsifyField'];
474
        }
475
476
        $this->currentUniqueFields[$class] = $uniqueFields;
477
        return $uniqueFields;
478
    }
479
480
    /**
481
     * @param array|string $salsifyField
482
     * @return bool|mixed
483
     */
484
    private function getSortColumn($salsifyField)
485
    {
486
        if (!is_array($salsifyField)) {
487
            return false;
488
        }
489
490
        if (array_key_exists('sortColumn', $salsifyField)) {
491
            return $salsifyField['sortColumn'];
492
        }
493
494
        return false;
495
    }
496
497
    /**
498
     * @return bool
499
     */
500
    private function mappingHasSalsifyRelation()
501
    {
502
        foreach ($this->yieldKeyVal($this->config()->get('mapping')) as $class => $mappings) {
503
            if ($this->classConfigHasSalsifyRelation($mappings)) {
504
                return true;
505
            }
506
        }
507
        return false;
508
    }
509
510
    /**
511
     * @param array $classConfig
512
     * @return bool
513
     */
514
    private function classConfigHasSalsifyRelation($classConfig)
515
    {
516
        foreach ($this->yieldKeyVal($classConfig) as $field => $config) {
517
            if (!is_array($config)) {
518
                continue;
519
            }
520
521
            if (!array_key_exists('salsifyField', $config)) {
522
                continue;
523
            }
524
525
            if (!array_key_exists('type', $config)) {
526
                continue;
527
            }
528
529
            if (in_array($config['type'], $this->getFieldsRequiringSalsifyObjects())) {
530
                return true;
531
            }
532
        }
533
        return false;
534
    }
535
536
    /**
537
     * @return array
538
     */
539
    private function getFieldsRequiringSalsifyObjects()
540
    {
541
        $fieldTypes = $this->config()->get('field_types');
0 ignored issues
show
Unused Code introduced by
The assignment to $fieldTypes is dead and can be removed.
Loading history...
542
        $types = [];
543
        foreach ($this->yieldKeyVal($this->config()->get('field_types')) as $field => $config) {
544
            if ($config['requiresSalsifyObjects']) {
545
                $types[] = $field;
546
            }
547
        }
548
549
        return $types;
550
    }
551
552
    /**
553
     * @param array $type
554
     * @return bool
555
     */
556
    private function typeRequiresWrite($type)
557
    {
558
        $config = $type['config'];
559
560
        if (array_key_exists('requiresWrite', $config)) {
561
            return $config['requiresWrite'];
562
        }
563
564
        return false;
565
    }
566
567
    /**
568
     * @param array $type
569
     * @return bool
570
     */
571
    private function typeRequiresSalsifyObjects($type)
572
    {
573
        $config = $type['config'];
574
575
        if (array_key_exists('requiresSalsifyObjects', $config)) {
576
            return $config['requiresSalsifyObjects'];
577
        }
578
579
        return false;
580
    }
581
582
    /**
583
     * @param array $type
584
     * @return string|bool
585
     */
586
    private function typeFallback($type)
587
    {
588
        $config = $type['config'];
589
590
        if (array_key_exists('fallback', $config)) {
591
            return $config['fallback'];
592
        }
593
594
        return false;
595
    }
596
597
    /**
598
     * @param array $type
599
     * @return bool
600
     */
601
    private function canModifyType($type)
602
    {
603
        $config = $type['config'];
604
605
        if (array_key_exists('allowsModification', $config)) {
606
            return $config['allowsModification'];
607
        }
608
609
        return true;
610
    }
611
612
    /**
613
     * @param array $type
614
     * @param string $class
615
     * @param string $dbField
616
     * @param array $config
617
     * @param array $data
618
     * @return array
619
     */
620
    private function handleModification($type, $class, $dbField, $config, $data)
621
    {
622
        if (!is_array($config)) {
0 ignored issues
show
introduced by
The condition is_array($config) is always true.
Loading history...
623
            return $data;
624
        }
625
626
        if (!$this->canModifyType($type)) {
627
            return $data;
628
        }
629
630
        if (array_key_exists('modification', $config)) {
631
            $mod = $config['modification'];
632
            if ($this->hasMethod($mod)) {
633
                return $this->{$mod}($class, $dbField, $config, $data);
634
            }
635
            ImportTask::output("{$mod} is not a valid field modifier. skipping modification for field {$dbField}.");
636
        }
637
        return $data;
638
    }
639
640
    /**
641
     * @param string $class
642
     * @param string $dbField
643
     * @param array $config
644
     * @param array $data
645
     * @return boolean
646
     */
647
    public function handleShouldSkip($class, $dbField, $config, $data)
648
    {
649
        if (!is_array($config)) {
0 ignored issues
show
introduced by
The condition is_array($config) is always true.
Loading history...
650
            return false;
651
        }
652
653
        if (array_key_exists('shouldSkip', $config)) {
654
            $skipMethod = $config['shouldSkip'];
655
            if ($this->hasMethod($skipMethod)) {
656
                return $this->{$skipMethod}($class, $dbField, $config, $data);
657
            }
658
            ImportTask::output(
659
                "{$skipMethod} is not a valid skip test method. Skipping skip test for field {$dbField}."
660
            );
661
        }
662
        return false;
663
    }
664
665
    /**
666
     * @param string|array $field
667
     * @return array
668
     */
669
    public function getFieldType($field)
670
    {
671
        $fieldTypes = $this->config()->get('field_types');
672
        if (is_array($field) && array_key_exists('type', $field)) {
673
            if (array_key_exists($field['type'], $fieldTypes)) {
674
                return [
675
                    'type' => $field['type'],
676
                    'config' => $fieldTypes[$field['type']],
677
                ];
678
            }
679
        }
680
        // default to raw
681
        return [
682
            'type' => 'Raw',
683
            'config' => $fieldTypes['Raw'],
684
        ];
685
    }
686
687
    /**
688
     * @param array $type
689
     * @param string|DataObject $class
690
     * @param array $salsifyData
691
     * @param string $salsifyField
692
     * @param array $dbFieldConfig
693
     * @param string $dbField
694
     *
695
     * @return mixed
696
     */
697
    private function handleType($type, $class, $salsifyData, $salsifyField, $dbFieldConfig, $dbField)
698
    {
699
        $typeName = $type['type'];
700
        $typeConfig = $type['config'];
701
        if ($this->hasMethod("handle{$typeName}Type")) {
702
            return $this->{"handle{$typeName}Type"}($class, $salsifyData, $salsifyField, $dbFieldConfig, $dbField);
703
        }
704
705
        if (array_key_exists('fallback', $typeConfig)) {
706
            $fallback = $typeConfig['fallback'];
707
            if ($this->hasMethod("handle{$fallback}Type")) {
708
                return $this->{"handle{$fallback}Type"}($class, $salsifyData, $salsifyField, $dbFieldConfig, $dbField);
709
            }
710
        }
711
712
        ImportTask::output("{$typeName} is not a valid type. skipping field {$dbField}.");
713
        return '';
714
    }
715
716
    /**
717
     * @param DataObject $object
718
     * @param array $type
719
     * @param string $dbField
720
     * @param mixed $value
721
     * @param string|bool $sortColumn
722
     *
723
     * @throws \Exception
724
     */
725
    private function writeValue($object, $type, $dbField, $value, $sortColumn)
726
    {
727
        $isManyRelation = array_key_exists($dbField, $object->config()->get('has_many')) ||
728
            array_key_exists($dbField, $object->config()->get('many_many')) ||
729
            array_key_exists($dbField, $object->config()->get('belongs_many_many'));
730
731
        $isSingleRelation = array_key_exists(rtrim($dbField, 'ID'), $object->config()->get('has_one'));
732
733
        // write the object so relations can be written
734
        if ($this->typeRequiresWrite($type) && !$object->exists()) {
735
            $object->write();
736
        }
737
738
        if (!$isManyRelation) {
739
            if (!$isSingleRelation || ($isSingleRelation && $value !== false)) {
740
                $object->$dbField = $value;
741
            }
742
            return;
743
        }
744
745
        // change to an array and filter out empty values
746
        if (!is_array($value)) {
747
            $value = [$value];
748
        }
749
        $value = array_filter($value);
750
751
        // don't try to write an empty set
752
        if (!count($value)) {
753
            return;
754
        }
755
756
        $this->removeUnrelated($object, $dbField, $value);
757
        $this->writeManyRelation($object, $dbField, $value, $sortColumn);
758
    }
759
760
    /**
761
     * @param DataObject $object
762
     * @param string $dbField
763
     * @param array $value
764
     * @param string|bool $sortColumn
765
     *
766
     * @throws \Exception
767
     */
768
    private function writeManyRelation($object, $dbField, $value, $sortColumn)
769
    {
770
        /** @var DataList|HasManyList|ManyManyList $relation */
771
        $relation = $object->{$dbField}();
772
773
        if ($sortColumn && $relation instanceof ManyManyList) {
774
            for ($i = 0; $i < count($value); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
775
                $relation->add($value[$i], [$sortColumn => $i]);
776
            }
777
            return;
778
        }
779
780
        // HasManyList, so it exists on the value
781
        if ($sortColumn) {
782
            for ($i = 0; $i < count($value); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
783
                $value[$i]->{$sortColumn} = $i;
784
                $relation->add($value[$i]);
785
            }
786
            return;
787
        }
788
789
        $relation->addMany($value);
790
    }
791
792
    /**
793
     * Removes unrelated objects in the relation that were previously related
794
     * @param DataObject $object
795
     * @param string $dbField
796
     * @param array $value
797
     */
798
    private function removeUnrelated($object, $dbField, $value)
799
    {
800
        $ids = [];
801
        foreach ($value as $v) {
802
            $ids[] = $v->ID;
803
        }
804
805
        /** @var DataList $relation */
806
        $relation = $object->{$dbField}();
807
        // remove all unrelated - removeAll had an odd side effect (relations only got added back half the time)
808
        if (!empty($ids)) {
809
            $relation->removeMany(
810
                $relation->exclude([
811
                    'ID' => $ids,
812
                ])->column('ID')
813
            );
814
        }
815
    }
816
817
    /**
818
     * @param string|array $salsifyField
819
     * @throws Exception
820
     */
821
    private function clearValue($object, $dbField, $salsifyField)
822
    {
823
        if (
824
            is_array($salsifyField) &&
825
            array_key_exists('keepExistingValue', $salsifyField) &&
826
            $salsifyField['keepExistingValue']
827
        ) {
828
            return;
829
        }
830
831
        $type = [
832
            'type' => 'null',
833
            'config' => [],
834
        ];
835
836
        // clear any existing value
837
        $this->writeValue($object, $type, $dbField, null, null);
838
    }
839
840
    /**
841
     * @return \JsonMachine\JsonMachine
842
     */
843
    public function getAssetStream()
844
    {
845
        return $this->assetStream;
846
    }
847
848
    /**
849
     * @return bool
850
     */
851
    public function hasFile()
852
    {
853
        return $this->file !== null;
854
    }
855
}
856