Passed
Pull Request — master (#59)
by Matthew
08:06
created

Mapper::writeManyRelation()   A

Complexity

Conditions 6
Paths 5

Size

Total Lines 22
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 11
c 0
b 0
f 0
dl 0
loc 22
rs 9.2222
cc 6
nc 5
nop 4
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
    {
152
        if ($salsifyRelations) {
153
            if (!$this->classConfigHasSalsifyRelation($mappings)) {
154
                return null;
155
            }
156
        }
157
158
        // if object was not passed
159
        if ($object === null) {
160
            $object = $this->findObjectByUnique($class, $mappings, $data);
161
162
            $filter = $this->getUniqueFilter($class, $mappings, $data);
163
            if (count(array_filter($filter)) == 0) {
164
                return null;
165
            }
166
167
            // if no existing object was found but a unique filter is valid (not empty)
168
            if (!$object) {
0 ignored issues
show
introduced by
$object is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
169
                $object = $class::create();
170
            }
171
        }
172
173
        $wasPublished = $object->hasExtension(Versioned::class) ? $object->isPublished() : false;
174
        $wasWritten = $object->isInDB();
175
176
        $firstUniqueKey = array_keys($this->uniqueFields($class, $mappings))[0];
177
        if (array_key_exists($mappings[$firstUniqueKey]['salsifyField'], $data)) {
178
            $firstUniqueValue = $data[$mappings[$firstUniqueKey]['salsifyField']];
179
        } else {
180
            $firstUniqueValue = 'NULL';
181
        }
182
        ImportTask::output("Updating $class $firstUniqueKey $firstUniqueValue");
183
184
        if (
185
            !$forceUpdate &&
186
            $this->objectUpToDate($object, $data, $firstUniqueKey, $firstUniqueValue, $salsifyRelations)
187
        ) {
188
            return $object;
189
        }
190
191
        foreach ($this->yieldKeyVal($mappings) as $dbField => $salsifyField) {
192
            $field = $this->getField($salsifyField, $data);
193
            if ($field === false) {
194
                $this->clearValue($object, $dbField, $salsifyField);
195
                continue;
196
            }
197
198
            $type = $this->getFieldType($salsifyField);
199
            // skip all but salsify relations types if not doing relations
200
            if ($salsifyRelations && !$this->typeRequiresSalsifyObjects($type)) {
201
                continue;
202
            }
203
204
            // skip salsify relations types if not doing relations
205
            if (!$salsifyRelations && $this->typeRequiresSalsifyObjects($type)) {
206
                continue;
207
            }
208
209
            $value = null;
210
            $objectData = $data;
0 ignored issues
show
Unused Code introduced by
The assignment to $objectData is dead and can be removed.
Loading history...
211
212
            if ($this->handleShouldSkip($class, $dbField, $salsifyField, $data)) {
213
                if (!$this->skipSilently) {
214
                    ImportTask::output("Skipping $class $firstUniqueKey $firstUniqueValue");
215
                    $this->skipSilently = false;
216
                }
217
                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...
218
            };
219
220
            $objectData = $this->handleModification($type, $class, $dbField, $salsifyField, $data);
221
            $sortColumn = $this->getSortColumn($salsifyField);
222
223
            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...
224
                continue;
225
            }
226
227
            $value = $this->handleType($type, $class, $objectData, $field, $salsifyField, $dbField);
228
            $this->writeValue($object, $type, $dbField, $value, $sortColumn);
229
        }
230
231
        if ($object->isChanged()) {
232
            $object->write();
233
            $this->importCount++;
234
            $this->extend('afterObjectWrite', $object, $wasWritten, $wasPublished);
235
        } else {
236
            ImportTask::output("$class $firstUniqueKey $firstUniqueValue was not changed.");
237
        }
238
        return $object;
239
    }
240
241
    /**
242
     * @param DataObject $object
243
     * @param array $data
244
     * @param string $firstUniqueKey
245
     * @param string $firstUniqueValue
246
     * @param bool $salsifyRelations
247
     * @return bool
248
     */
249
    private function objectUpToDate($object, $data, $firstUniqueKey, $firstUniqueValue, $salsifyRelations = false)
250
    {
251
        if ($this->config()->get('skipUpToDate') == false) {
252
            return false;
253
        }
254
255
        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...
256
            if ($this->objectDataUpToDate($object, $data, $firstUniqueKey, $firstUniqueValue)) {
257
                ImportTask::output("Skipping $firstUniqueKey $firstUniqueValue. It is up to Date.");
258
                return true;
259
            }
260
261
        } else {
262
            if ($this->objectRelationsUpToDate($object, $data, $firstUniqueKey, $firstUniqueValue)) {
263
                ImportTask::output("Skipping $firstUniqueKey $firstUniqueValue relations. It is up to Date.");
264
                return true;
265
            }
266
        }
267
268
        return false;
269
    }
270
271
    /**
272
     * @param DataObject $object
273
     * @param array $data
274
     * @param string $firstUniqueKey
275
     * @param string $firstUniqueValue
276
     * @return bool
277
     */
278
    public function objectDataUpToDate($object, $data, $firstUniqueKey, $firstUniqueValue)
279
    {
280
        // assume not up to date if field does not exist on object
281
        if (!$object->hasField('SalsifyUpdatedAt')) {
282
            return false;
283
        }
284
285
        if ($data['salsify:updated_at'] != $object->getField('SalsifyUpdatedAt')) {
286
            return false;
287
        }
288
289
        return true;
290
    }
291
292
    /**
293
     * @param DataObject $object
294
     * @param array $data
295
     * @param string $firstUniqueKey
296
     * @param string $firstUniqueValue
297
     * @return bool
298
     */
299
    public function objectRelationsUpToDate($object, $data, $firstUniqueKey, $firstUniqueValue)
300
    {
301
        // assume not up to date if field does not exist on object
302
        if (!$object->hasField('SalsifyRelationsUpdatedAt')) {
303
            return false;
304
        }
305
306
        // relations were never updated, so its up to date
307
        if (!isset($data['salsify:relations_updated_at'])) {
308
            return true;
309
        }
310
311
        if ($data['salsify:relations_updated_at'] != $object->getField('SalsifyRelationsUpdatedAt')) {
312
            return false;
313
        }
314
315
        return true;
316
    }
317
318
    /**
319
     * @param array $salsifyField
320
     * @param array $data
321
     *
322
     * @return string|false
323
     */
324
    private function getField($salsifyField, $data)
325
    {
326
        if (!is_array($salsifyField)) {
0 ignored issues
show
introduced by
The condition is_array($salsifyField) is always true.
Loading history...
327
            return array_key_exists($salsifyField, $data) ? $salsifyField : false;
328
        }
329
330
        $hasSalsifyField = array_key_exists('salsifyField', $salsifyField);
331
        $isLiteralField = (
332
            $this->getFieldType($salsifyField) === 'Literal' &&
0 ignored issues
show
introduced by
The condition $this->getFieldType($salsifyField) === 'Literal' is always false.
Loading history...
333
            array_key_exists('value', $salsifyField)
334
        );
335
        $isSalsifyRelationField = (
336
            $this->getFieldType($salsifyField) === 'SalsifyRelation' &&
0 ignored issues
show
introduced by
The condition $this->getFieldType($sal...) === 'SalsifyRelation' is always false.
Loading history...
337
            $hasSalsifyField
338
        );
339
340
        if ($isLiteralField) {
0 ignored issues
show
introduced by
The condition $isLiteralField is always false.
Loading history...
341
            return $salsifyField['value'];
342
        }
343
344
        if ($isSalsifyRelationField) {
0 ignored issues
show
introduced by
The condition $isSalsifyRelationField is always false.
Loading history...
345
            return $salsifyField['salsifyField'];
346
        }
347
348
        if (!$hasSalsifyField) {
349
            return false;
350
        }
351
352
        if (array_key_exists($salsifyField['salsifyField'], $data)) {
353
            return $salsifyField['salsifyField'];
354
        } elseif (array_key_exists('fallback', $salsifyField)) {
355
            // make fallback an array
356
            if (!is_array($salsifyField['fallback'])) {
357
                $salsifyField['fallback'] = [$salsifyField['fallback']];
358
            }
359
360
            foreach ($this->yieldSingle($salsifyField['fallback']) as $fallback) {
361
                if (array_key_exists($fallback, $data)) {
362
                    return $fallback;
363
                }
364
            }
365
        } elseif (array_key_exists('modification', $salsifyField)) {
366
            return $salsifyField['salsifyField'];
367
        }
368
369
        return false;
370
    }
371
372
    /**
373
     * @param string $class
374
     * @param array $mappings
375
     * @param array $data
376
     *
377
     * @return array
378
     */
379
    private function getUniqueFilter($class, $mappings, $data)
380
    {
381
        $uniqueFields = $this->uniqueFields($class, $mappings);
382
        // creates a filter
383
        $filter = [];
384
        foreach ($this->yieldKeyVal($uniqueFields) as $dbField => $salsifyField) {
385
            $modifiedData = $data;
386
            $fieldMapping = $mappings[$dbField];
387
            $fieldType = $this->getFieldType($salsifyField);
388
            $modifiedData = $this->handleModification($fieldType, $class, $dbField, $fieldMapping, $modifiedData);
389
390
            // adds unique fields to filter
391
            if (array_key_exists($salsifyField, $modifiedData)) {
392
                $filter[$dbField] = $modifiedData[$salsifyField];
393
            }
394
        }
395
396
        return $filter;
397
    }
398
399
    /**
400
     * @param string $class
401
     * @param array $mappings
402
     * @param array $data
403
     *
404
     * @return \SilverStripe\ORM\DataObject
405
     */
406
    private function findObjectByUnique($class, $mappings, $data)
407
    {
408
        if ($obj = $this->findBySalsifyID($class, $mappings, $data)) {
409
            return $obj;
410
        }
411
412
        $filter = $this->getUniqueFilter($class, $mappings, $data);
413
        return DataObject::get($class)->filter($filter)->first();
414
    }
415
416
    /**
417
     * @param string $class
418
     * @param array $mappings
419
     * @param array $data
420
     *
421
     * @return \SilverStripe\ORM\DataObject|bool
422
     */
423
    private function findBySalsifyID($class, $mappings, $data)
424
    {
425
        /** @var DataObject $genericObject */
426
        $genericObject = Injector::inst()->get($class);
427
        if (
428
            !$genericObject->hasExtension(SalsifyIDExtension::class) &&
429
            !$genericObject->hasField('SalsifyID')
430
        ) {
431
            return false;
432
        }
433
434
        $obj = DataObject::get($class)->filter([
435
            'SalsifyID' => $data['salsify:id'],
436
        ])->first();
437
        if ($obj) {
0 ignored issues
show
introduced by
$obj is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
438
            return $obj;
439
        }
440
441
        return false;
442
    }
443
444
    /**
445
     * Gets a list of all the unique field keys
446
     *
447
     * @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...
448
     * @param array $mappings
449
     * @return array
450
     */
451
    private function uniqueFields($class, $mappings)
452
    {
453
        // cached after first map
454
        if (array_key_exists($class, $this->currentUniqueFields) && !empty($this->currentUniqueFields[$class])) {
455
            return $this->currentUniqueFields[$class];
456
        }
457
458
        $uniqueFields = [];
459
        foreach ($this->yieldKeyVal($mappings) as $dbField => $salsifyField) {
460
            if (!is_array($salsifyField)) {
461
                continue;
462
            }
463
464
            if (
465
                !array_key_exists('unique', $salsifyField) ||
466
                !array_key_exists('salsifyField', $salsifyField)
467
            ) {
468
                continue;
469
            }
470
471
            if ($salsifyField['unique'] !== true) {
472
                continue;
473
            }
474
475
            $uniqueFields[$dbField] = $salsifyField['salsifyField'];
476
        }
477
478
        $this->currentUniqueFields[$class] = $uniqueFields;
479
        return $uniqueFields;
480
    }
481
482
    /**
483
     * @param array|string $salsifyField
484
     * @return bool|mixed
485
     */
486
    private function getSortColumn($salsifyField)
487
    {
488
        if (!is_array($salsifyField)) {
489
            return false;
490
        }
491
492
        if (array_key_exists('sortColumn', $salsifyField)) {
493
            return $salsifyField['sortColumn'];
494
        }
495
496
        return false;
497
    }
498
499
    /**
500
     * @return bool
501
     */
502
    private function mappingHasSalsifyRelation()
503
    {
504
        foreach ($this->yieldKeyVal($this->config()->get('mapping')) as $class => $mappings) {
505
            if ($this->classConfigHasSalsifyRelation($mappings)) {
506
                return true;
507
            }
508
        }
509
        return false;
510
    }
511
512
    /**
513
     * @param array $classConfig
514
     * @return bool
515
     */
516
    private function classConfigHasSalsifyRelation($classConfig)
517
    {
518
        foreach ($this->yieldKeyVal($classConfig) as $field => $config) {
519
            if (!is_array($config)) {
520
                continue;
521
            }
522
523
            if (!array_key_exists('salsifyField', $config)) {
524
                continue;
525
            }
526
527
            if (!array_key_exists('type', $config)) {
528
                continue;
529
            }
530
531
            if (in_array($config['type'], $this->getFieldsRequiringSalsifyObjects())) {
532
                return true;
533
            }
534
        }
535
        return false;
536
    }
537
538
    /**
539
     * @return array
540
     */
541
    private function getFieldsRequiringSalsifyObjects()
542
    {
543
        $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...
544
        $types = [];
545
        foreach ($this->yieldKeyVal($this->config()->get('field_types')) as $field => $config) {
546
            if ($config['requiresSalsifyObjects']) {
547
                $types[] = $field;
548
            }
549
        }
550
551
        return $types;
552
    }
553
554
    /**
555
     * @param array $type
556
     * @return bool
557
     */
558
    private function typeRequiresWrite($type)
559
    {
560
        $config = $type['config'];
561
562
        if (array_key_exists('requiresWrite', $config)) {
563
            return $config['requiresWrite'];
564
        }
565
566
        return false;
567
    }
568
569
    /**
570
     * @param array $type
571
     * @return bool
572
     */
573
    private function typeRequiresSalsifyObjects($type)
574
    {
575
        $config = $type['config'];
576
577
        if (array_key_exists('requiresSalsifyObjects', $config)) {
578
            return $config['requiresSalsifyObjects'];
579
        }
580
581
        return false;
582
    }
583
584
    /**
585
     * @param array $type
586
     * @return string|bool
587
     */
588
    private function typeFallback($type)
589
    {
590
        $config = $type['config'];
591
592
        if (array_key_exists('fallback', $config)) {
593
            return $config['fallback'];
594
        }
595
596
        return false;
597
    }
598
599
    /**
600
     * @param array $type
601
     * @return bool
602
     */
603
    private function canModifyType($type)
604
    {
605
        $config = $type['config'];
606
607
        if (array_key_exists('allowsModification', $config)) {
608
            return $config['allowsModification'];
609
        }
610
611
        return true;
612
    }
613
614
    /**
615
     * @param array $type
616
     * @param string $class
617
     * @param string $dbField
618
     * @param array $config
619
     * @param array $data
620
     * @return array
621
     */
622
    private function handleModification($type, $class, $dbField, $config, $data)
623
    {
624
        if (!is_array($config)) {
0 ignored issues
show
introduced by
The condition is_array($config) is always true.
Loading history...
625
            return $data;
626
        }
627
628
        if (!$this->canModifyType($type)) {
629
            return $data;
630
        }
631
632
        if (array_key_exists('modification', $config)) {
633
            $mod = $config['modification'];
634
            if ($this->hasMethod($mod)) {
635
                return $this->{$mod}($class, $dbField, $config, $data);
636
            }
637
            ImportTask::output("{$mod} is not a valid field modifier. skipping modification for field {$dbField}.");
638
        }
639
        return $data;
640
    }
641
642
    /**
643
     * @param string $class
644
     * @param string $dbField
645
     * @param array $config
646
     * @param array $data
647
     * @return boolean
648
     */
649
    public function handleShouldSkip($class, $dbField, $config, $data)
650
    {
651
        if (!is_array($config)) {
0 ignored issues
show
introduced by
The condition is_array($config) is always true.
Loading history...
652
            return false;
653
        }
654
655
        if (array_key_exists('shouldSkip', $config)) {
656
            $skipMethod = $config['shouldSkip'];
657
            if ($this->hasMethod($skipMethod)) {
658
                return $this->{$skipMethod}($class, $dbField, $config, $data);
659
            }
660
            ImportTask::output(
661
                "{$skipMethod} is not a valid skip test method. Skipping skip test for field {$dbField}."
662
            );
663
        }
664
        return false;
665
    }
666
667
    /**
668
     * @param string|array $field
669
     * @return array
670
     */
671
    public function getFieldType($field)
672
    {
673
        $fieldTypes = $this->config()->get('field_types');
674
        if (is_array($field) && array_key_exists('type', $field)) {
675
            if (array_key_exists($field['type'], $fieldTypes)) {
676
677
                return [
678
                    'type' => $field['type'],
679
                    'config' => $fieldTypes[$field['type']],
680
                ];
681
            }
682
        }
683
        // default to raw
684
        return [
685
            'type' => 'Raw',
686
            'config' => $fieldTypes['Raw'],
687
        ];
688
    }
689
690
    /**
691
     * @param array $type
692
     * @param string|DataObject $class
693
     * @param array $salsifyData
694
     * @param string $salsifyField
695
     * @param array $dbFieldConfig
696
     * @param string $dbField
697
     *
698
     * @return mixed
699
     */
700
    private function handleType($type, $class, $salsifyData, $salsifyField, $dbFieldConfig, $dbField)
701
    {
702
        $typeName = $type['type'];
703
        $typeConfig = $type['config'];
704
        if ($this->hasMethod("handle{$typeName}Type")) {
705
            return $this->{"handle{$typeName}Type"}($class, $salsifyData, $salsifyField, $dbFieldConfig, $dbField);
706
        }
707
708
        if (array_key_exists( 'fallback', $typeConfig)) {
709
            $fallback = $typeConfig['fallback'];
710
            if ($this->hasMethod("handle{$fallback}Type")) {
711
                return $this->{"handle{$fallback}Type"}($class, $salsifyData, $salsifyField, $dbFieldConfig, $dbField);
712
            }
713
        }
714
715
        ImportTask::output("{$typeName} is not a valid type. skipping field {$dbField}.");
716
        return '';
717
    }
718
719
    /**
720
     * @param DataObject $object
721
     * @param array $type
722
     * @param string $dbField
723
     * @param mixed $value
724
     * @param string|bool $sortColumn
725
     *
726
     * @throws \Exception
727
     */
728
    private function writeValue($object, $type, $dbField, $value, $sortColumn)
729
    {
730
        $isManyRelation = array_key_exists($dbField, $object->config()->get('has_many')) ||
731
            array_key_exists($dbField, $object->config()->get('many_many')) ||
732
            array_key_exists($dbField, $object->config()->get('belongs_many_many'));
733
734
        $isSingleRelation = array_key_exists(rtrim($dbField, 'ID'), $object->config()->get('has_one'));
735
736
        // write the object so relations can be written
737
        if ($this->typeRequiresWrite($type) && !$object->exists()) {
738
            $object->write();
739
        }
740
741
        if (!$isManyRelation) {
742
            if (!$isSingleRelation || ($isSingleRelation && $value !== false)) {
743
                $object->$dbField = $value;
744
            }
745
            return;
746
        }
747
748
        // change to an array and filter out empty values
749
        if (!is_array($value)) {
750
            $value = [$value];
751
        }
752
        $value = array_filter($value);
753
754
        // don't try to write an empty set
755
        if (!count($value)) {
756
            return;
757
        }
758
759
        $this->removeUnrelated($object, $dbField, $value);
760
        $this->writeManyRelation($object, $dbField, $value, $sortColumn);
761
    }
762
763
    /**
764
     * @param DataObject $object
765
     * @param string $dbField
766
     * @param array $value
767
     * @param string|bool $sortColumn
768
     *
769
     * @throws \Exception
770
     */
771
    private function writeManyRelation($object, $dbField, $value, $sortColumn)
772
    {
773
        /** @var DataList|HasManyList|ManyManyList $relation */
774
        $relation = $object->{$dbField}();
775
776
        if ($sortColumn && $relation instanceof ManyManyList) {
777
            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...
778
                $relation->add($value[$i], [$sortColumn => $i]);
779
            }
780
            return;
781
        }
782
783
        // HasManyList, so it exists on the value
784
        if ($sortColumn) {
785
            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...
786
                $value[$i]->{$sortColumn} = $i;
787
                $relation->add($value[$i]);
788
            }
789
            return;
790
        }
791
792
        $relation->addMany($value);
793
    }
794
795
    /**
796
     * Removes unrelated objects in the relation that were previously related
797
     * @param DataObject $object
798
     * @param string $dbField
799
     * @param array $value
800
     */
801
    private function removeUnrelated($object, $dbField, $value)
802
    {
803
        $ids = [];
804
        foreach ($value as $v) {
805
            $ids[] = $v->ID;
806
        }
807
808
        /** @var DataList $relation */
809
        $relation = $object->{$dbField}();
810
        // remove all unrelated - removeAll had an odd side effect (relations only got added back half the time)
811
        if (!empty($ids)) {
812
            $relation->removeMany(
813
                $relation->exclude([
814
                    'ID' => $ids,
815
                ])->column('ID')
816
            );
817
        }
818
    }
819
820
    /**
821
     * @param string|array $salsifyField
822
     * @throws Exception
823
     */
824
    private function clearValue($object, $dbField, $salsifyField)
825
    {
826
        if (
827
            is_array($salsifyField) &&
828
            array_key_exists('keepExistingValue', $salsifyField) &&
829
            $salsifyField['keepExistingValue']
830
        ) {
831
            return;
832
        }
833
834
        $type = [
835
            'type' => 'null',
836
            'config' => [],
837
        ];
838
839
        // clear any existing value
840
        $this->writeValue($object, $type, $dbField, null, null);
841
    }
842
843
    /**
844
     * @return \JsonMachine\JsonMachine
845
     */
846
    public function getAssetStream()
847
    {
848
        return $this->assetStream;
849
    }
850
851
    /**
852
     * @return bool
853
     */
854
    public function hasFile()
855
    {
856
        return $this->file !== null;
857
    }
858
}
859