Passed
Push — master ( 3195fd...ce0453 )
by Matthew
13:01
created

Mapper::typeRequiresWrite()   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
            $type = [
545
                'type' => $field,
546
                'config' => $config,
547
            ];
548
            if ($this->typeRequiresSalsifyObjects($type)) {
549
                $types[] = $field;
550
            }
551
        }
552
553
        return $types;
554
    }
555
556
    /**
557
     * @param array $type
558
     * @return bool
559
     */
560
    private function typeRequiresWrite($type)
561
    {
562
        $config = $type['config'];
563
564
        if (array_key_exists('requiresWrite', $config)) {
565
            return $config['requiresWrite'];
566
        }
567
568
        return false;
569
    }
570
571
    /**
572
     * @param array $type
573
     * @return bool
574
     */
575
    private function typeRequiresSalsifyObjects($type)
576
    {
577
        $config = $type['config'];
578
579
        if (array_key_exists('requiresSalsifyObjects', $config)) {
580
            return $config['requiresSalsifyObjects'];
581
        }
582
583
        return false;
584
    }
585
586
    /**
587
     * @param array $type
588
     * @return string|bool
589
     */
590
    private function typeFallback($type)
591
    {
592
        $config = $type['config'];
593
594
        if (array_key_exists('fallback', $config)) {
595
            return $config['fallback'];
596
        }
597
598
        return false;
599
    }
600
601
    /**
602
     * @param array $type
603
     * @return bool
604
     */
605
    private function canModifyType($type)
606
    {
607
        $config = $type['config'];
608
609
        if (array_key_exists('allowsModification', $config)) {
610
            return $config['allowsModification'];
611
        }
612
613
        return true;
614
    }
615
616
    /**
617
     * @param array $type
618
     * @param string $class
619
     * @param string $dbField
620
     * @param array $config
621
     * @param array $data
622
     * @return array
623
     */
624
    private function handleModification($type, $class, $dbField, $config, $data)
625
    {
626
        if (!is_array($config)) {
0 ignored issues
show
introduced by
The condition is_array($config) is always true.
Loading history...
627
            return $data;
628
        }
629
630
        if (!$this->canModifyType($type)) {
631
            return $data;
632
        }
633
634
        if (array_key_exists('modification', $config)) {
635
            $mod = $config['modification'];
636
            if ($this->hasMethod($mod)) {
637
                return $this->{$mod}($class, $dbField, $config, $data);
638
            }
639
            ImportTask::output("{$mod} is not a valid field modifier. skipping modification for field {$dbField}.");
640
        }
641
        return $data;
642
    }
643
644
    /**
645
     * @param string $class
646
     * @param string $dbField
647
     * @param array $config
648
     * @param array $data
649
     * @return boolean
650
     */
651
    public function handleShouldSkip($class, $dbField, $config, $data)
652
    {
653
        if (!is_array($config)) {
0 ignored issues
show
introduced by
The condition is_array($config) is always true.
Loading history...
654
            return false;
655
        }
656
657
        if (array_key_exists('shouldSkip', $config)) {
658
            $skipMethod = $config['shouldSkip'];
659
            if ($this->hasMethod($skipMethod)) {
660
                return $this->{$skipMethod}($class, $dbField, $config, $data);
661
            }
662
            ImportTask::output(
663
                "{$skipMethod} is not a valid skip test method. Skipping skip test for field {$dbField}."
664
            );
665
        }
666
        return false;
667
    }
668
669
    /**
670
     * @param string|array $field
671
     * @return array
672
     */
673
    public function getFieldType($field)
674
    {
675
        $fieldTypes = $this->config()->get('field_types');
676
        if (is_array($field) && array_key_exists('type', $field)) {
677
            if (array_key_exists($field['type'], $fieldTypes)) {
678
                return [
679
                    'type' => $field['type'],
680
                    'config' => $fieldTypes[$field['type']],
681
                ];
682
            }
683
        }
684
        // default to raw
685
        return [
686
            'type' => 'Raw',
687
            'config' => $fieldTypes['Raw'],
688
        ];
689
    }
690
691
    /**
692
     * @param array $type
693
     * @param string|DataObject $class
694
     * @param array $salsifyData
695
     * @param string $salsifyField
696
     * @param array $dbFieldConfig
697
     * @param string $dbField
698
     *
699
     * @return mixed
700
     */
701
    private function handleType($type, $class, $salsifyData, $salsifyField, $dbFieldConfig, $dbField)
702
    {
703
        $typeName = $type['type'];
704
        $typeConfig = $type['config'];
705
        if ($this->hasMethod("handle{$typeName}Type")) {
706
            return $this->{"handle{$typeName}Type"}($class, $salsifyData, $salsifyField, $dbFieldConfig, $dbField);
707
        }
708
709
        if (array_key_exists('fallback', $typeConfig)) {
710
            $fallback = $typeConfig['fallback'];
711
            if ($this->hasMethod("handle{$fallback}Type")) {
712
                return $this->{"handle{$fallback}Type"}($class, $salsifyData, $salsifyField, $dbFieldConfig, $dbField);
713
            }
714
        }
715
716
        ImportTask::output("{$typeName} is not a valid type. skipping field {$dbField}.");
717
        return '';
718
    }
719
720
    /**
721
     * @param DataObject $object
722
     * @param array $type
723
     * @param string $dbField
724
     * @param mixed $value
725
     * @param string|bool $sortColumn
726
     *
727
     * @throws \Exception
728
     */
729
    private function writeValue($object, $type, $dbField, $value, $sortColumn)
730
    {
731
        $isManyRelation = array_key_exists($dbField, $object->config()->get('has_many')) ||
732
            array_key_exists($dbField, $object->config()->get('many_many')) ||
733
            array_key_exists($dbField, $object->config()->get('belongs_many_many'));
734
735
        $isSingleRelation = array_key_exists(rtrim($dbField, 'ID'), $object->config()->get('has_one'));
736
737
        // write the object so relations can be written
738
        if ($this->typeRequiresWrite($type) && !$object->exists()) {
739
            $object->write();
740
        }
741
742
        if (!$isManyRelation) {
743
            if (!$isSingleRelation || ($isSingleRelation && $value !== false)) {
744
                $object->$dbField = $value;
745
            }
746
            return;
747
        }
748
749
        // change to an array and filter out empty values
750
        if (!is_array($value)) {
751
            $value = [$value];
752
        }
753
        $value = array_filter($value);
754
755
        // don't try to write an empty set
756
        if (!count($value)) {
757
            return;
758
        }
759
760
        $this->removeUnrelated($object, $dbField, $value);
761
        $this->writeManyRelation($object, $dbField, $value, $sortColumn);
762
    }
763
764
    /**
765
     * @param DataObject $object
766
     * @param string $dbField
767
     * @param array $value
768
     * @param string|bool $sortColumn
769
     *
770
     * @throws \Exception
771
     */
772
    private function writeManyRelation($object, $dbField, $value, $sortColumn)
773
    {
774
        /** @var DataList|HasManyList|ManyManyList $relation */
775
        $relation = $object->{$dbField}();
776
777
        if ($sortColumn && $relation instanceof ManyManyList) {
778
            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...
779
                $relation->add($value[$i], [$sortColumn => $i]);
780
            }
781
            return;
782
        }
783
784
        // HasManyList, so it exists on the value
785
        if ($sortColumn) {
786
            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...
787
                $value[$i]->{$sortColumn} = $i;
788
                $relation->add($value[$i]);
789
            }
790
            return;
791
        }
792
793
        $relation->addMany($value);
794
    }
795
796
    /**
797
     * Removes unrelated objects in the relation that were previously related
798
     * @param DataObject $object
799
     * @param string $dbField
800
     * @param array $value
801
     */
802
    private function removeUnrelated($object, $dbField, $value)
803
    {
804
        $ids = [];
805
        foreach ($value as $v) {
806
            $ids[] = $v->ID;
807
        }
808
809
        /** @var DataList $relation */
810
        $relation = $object->{$dbField}();
811
        // remove all unrelated - removeAll had an odd side effect (relations only got added back half the time)
812
        if (!empty($ids)) {
813
            $relation->removeMany(
814
                $relation->exclude([
815
                    'ID' => $ids,
816
                ])->column('ID')
817
            );
818
        }
819
    }
820
821
    /**
822
     * @param string|array $salsifyField
823
     * @throws Exception
824
     */
825
    private function clearValue($object, $dbField, $salsifyField)
826
    {
827
        if (
828
            is_array($salsifyField) &&
829
            array_key_exists('keepExistingValue', $salsifyField) &&
830
            $salsifyField['keepExistingValue']
831
        ) {
832
            return;
833
        }
834
835
        $type = [
836
            'type' => 'null',
837
            'config' => [],
838
        ];
839
840
        // clear any existing value
841
        $this->writeValue($object, $type, $dbField, null, null);
842
    }
843
844
    /**
845
     * @return \JsonMachine\JsonMachine
846
     */
847
    public function getAssetStream()
848
    {
849
        return $this->assetStream;
850
    }
851
852
    /**
853
     * @return bool
854
     */
855
    public function hasFile()
856
    {
857
        return $this->file !== null;
858
    }
859
}
860