Passed
Push — master ( a17713...3195fd )
by Matthew
02:07
created

Mapper   F

Complexity

Total Complexity 133

Size/Duplication

Total Lines 747
Duplicated Lines 0 %

Importance

Changes 8
Bugs 1 Features 0
Metric Value
wmc 133
eloc 272
c 8
b 1
f 0
dl 0
loc 747
rs 2

26 Methods

Rating   Name   Duplication   Size   Complexity  
A resetProductStream() 0 3 1
A __construct() 0 11 3
A map() 0 27 6
A resetAssetStream() 0 3 1
A clearValue() 0 12 4
A classConfigHasSalsifyRelation() 0 20 6
F mapToObject() 0 96 23
A mappingHasSalsifyRelation() 0 8 3
A writeManyRelation() 0 22 6
A objectDataUpToDate() 0 12 3
A objectRelationsUpToDate() 0 17 4
A removeUnrelated() 0 15 3
A getUniqueFilter() 0 18 3
A hasFile() 0 3 1
A getAssetStream() 0 3 1
C getField() 0 46 14
A handleShouldSkip() 0 16 4
A findObjectByUnique() 0 8 2
A handleType() 0 17 6
B uniqueFields() 0 29 8
A objectUpToDate() 0 20 5
A getSortColumn() 0 11 3
A handleModification() 0 14 4
B writeValue() 0 33 10
A getFieldType() 0 10 4
A findBySalsifyID() 0 23 5

How to fix   Complexity   

Complex Class

Complex classes like Mapper often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Mapper, and based on these observations, apply Extract Interface, too.

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 && ($type != 'SalsifyRelation' && $type != 'SalsifyRelationTimeStamp')) {
201
                continue;
202
            }
203
204
            // skip salsify relations types if not doing relations
205
            if (!$salsifyRelations && ($type == 'SalsifyRelation' || $type == 'SalsifyRelationTimeStamp')) {
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($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);
0 ignored issues
show
Bug introduced by
$type of type string is incompatible with the type integer expected by parameter $type of Dynamic\Salsify\Model\Mapper::handleType(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

227
            $value = $this->handleType(/** @scrutinizer ignore-type */ $type, $class, $objectData, $field, $salsifyField, $dbField);
Loading history...
228
            $this->writeValue($object, $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' &&
333
            array_key_exists('value', $salsifyField)
334
        );
335
        $isSalsifyRelationField = (
336
            $this->getFieldType($salsifyField) === 'SalsifyRelation' &&
337
            $hasSalsifyField
338
        );
339
340
        if ($isLiteralField) {
341
            return $salsifyField['value'];
342
        }
343
344
        if ($isSalsifyRelationField) {
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
388
            $modifiedData = $this->handleModification($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
        $modifiedData = $data;
435
        if (array_key_exists('salsify:id', $mappings)) {
436
            $modifiedData = $this->handleModification($class, 'salsify:id', $mappings['salsify:id'], $modifiedData);
437
        }
438
        $obj = DataObject::get($class)->filter([
439
            'SalsifyID' => $modifiedData['salsify:id'],
440
        ])->first();
441
        if ($obj) {
0 ignored issues
show
introduced by
$obj is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
442
            return $obj;
443
        }
444
445
        return false;
446
    }
447
448
    /**
449
     * Gets a list of all the unique field keys
450
     *
451
     * @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...
452
     * @param array $mappings
453
     * @return array
454
     */
455
    private function uniqueFields($class, $mappings)
456
    {
457
        // cached after first map
458
        if (array_key_exists($class, $this->currentUniqueFields) && !empty($this->currentUniqueFields[$class])) {
459
            return $this->currentUniqueFields[$class];
460
        }
461
462
        $uniqueFields = [];
463
        foreach ($this->yieldKeyVal($mappings) as $dbField => $salsifyField) {
464
            if (!is_array($salsifyField)) {
465
                continue;
466
            }
467
468
            if (
469
                !array_key_exists('unique', $salsifyField) ||
470
                !array_key_exists('salsifyField', $salsifyField)
471
            ) {
472
                continue;
473
            }
474
475
            if ($salsifyField['unique'] !== true) {
476
                continue;
477
            }
478
479
            $uniqueFields[$dbField] = $salsifyField['salsifyField'];
480
        }
481
482
        $this->currentUniqueFields[$class] = $uniqueFields;
483
        return $uniqueFields;
484
    }
485
486
    /**
487
     * @param array|string $salsifyField
488
     * @return bool|mixed
489
     */
490
    private function getSortColumn($salsifyField)
491
    {
492
        if (!is_array($salsifyField)) {
493
            return false;
494
        }
495
496
        if (array_key_exists('sortColumn', $salsifyField)) {
497
            return $salsifyField['sortColumn'];
498
        }
499
500
        return false;
501
    }
502
503
    /**
504
     * @return bool
505
     */
506
    private function mappingHasSalsifyRelation()
507
    {
508
        foreach ($this->yieldKeyVal($this->config()->get('mapping')) as $class => $mappings) {
509
            if ($this->classConfigHasSalsifyRelation($mappings)) {
510
                return true;
511
            }
512
        }
513
        return false;
514
    }
515
516
    /**
517
     * @param array $classConfig
518
     * @return bool
519
     */
520
    private function classConfigHasSalsifyRelation($classConfig)
521
    {
522
        foreach ($this->yieldKeyVal($classConfig) as $field => $config) {
523
            if (!is_array($config)) {
524
                continue;
525
            }
526
527
            if (!array_key_exists('salsifyField', $config)) {
528
                continue;
529
            }
530
531
            if (!array_key_exists('type', $config)) {
532
                continue;
533
            }
534
535
            if ($config['type'] === 'SalsifyRelation') {
536
                return true;
537
            }
538
        }
539
        return false;
540
    }
541
542
    /**
543
     * @param string $class
544
     * @param string $dbField
545
     * @param array $config
546
     * @param array $data
547
     * @return array
548
     */
549
    private function handleModification($class, $dbField, $config, $data)
550
    {
551
        if (!is_array($config)) {
0 ignored issues
show
introduced by
The condition is_array($config) is always true.
Loading history...
552
            return $data;
553
        }
554
555
        if (array_key_exists('modification', $config)) {
556
            $mod = $config['modification'];
557
            if ($this->hasMethod($mod)) {
558
                return $this->{$mod}($class, $dbField, $config, $data);
559
            }
560
            ImportTask::output("{$mod} is not a valid field modifier. skipping modification for field {$dbField}.");
561
        }
562
        return $data;
563
    }
564
565
    /**
566
     * @param string $class
567
     * @param string $dbField
568
     * @param array $config
569
     * @param array $data
570
     * @return boolean
571
     */
572
    public function handleShouldSkip($class, $dbField, $config, $data)
573
    {
574
        if (!is_array($config)) {
0 ignored issues
show
introduced by
The condition is_array($config) is always true.
Loading history...
575
            return false;
576
        }
577
578
        if (array_key_exists('shouldSkip', $config)) {
579
            $skipMethod = $config['shouldSkip'];
580
            if ($this->hasMethod($skipMethod)) {
581
                return $this->{$skipMethod}($class, $dbField, $config, $data);
582
            }
583
            ImportTask::output(
584
                "{$skipMethod} is not a valid skip test method. Skipping skip test for field {$dbField}."
585
            );
586
        }
587
        return false;
588
    }
589
590
    /**
591
     * @param string|array $field
592
     * @return string
593
     */
594
    public function getFieldType($field)
595
    {
596
        $fieldTypes = $this->config()->get('field_types');
597
        if (is_array($field) && array_key_exists('type', $field)) {
598
            if (in_array($field['type'], $fieldTypes)) {
599
                return $field['type'];
600
            }
601
        }
602
        // default to raw
603
        return 'Raw';
604
    }
605
606
    /**
607
     * @param int $type
608
     * @param string|DataObject $class
609
     * @param array $salsifyData
610
     * @param string $salsifyField
611
     * @param array $dbFieldConfig
612
     * @param string $dbField
613
     *
614
     * @return mixed
615
     */
616
    private function handleType($type, $class, $salsifyData, $salsifyField, $dbFieldConfig, $dbField)
617
    {
618
        if ($this->hasMethod("handle{$type}Type")) {
619
            return $this->{"handle{$type}Type"}($class, $salsifyData, $salsifyField, $dbFieldConfig, $dbField);
620
        }
621
622
        if ($fallbacks = $this->config()->get('typeFallbacks')) {
623
            foreach ($fallbacks as $original => $fallback) {
624
                if ($type == $original) {
625
                    if ($this->hasMethod("handle{$fallback}Type")) {
626
                        return $this->{"handle{$fallback}Type"}($class, $salsifyData, $salsifyField, $dbFieldConfig, $dbField);
627
                    }
628
                }
629
            }
630
        }
631
        ImportTask::output("{$type} is not a valid type. skipping field {$dbField}.");
632
        return '';
633
    }
634
635
    /**
636
     * @param DataObject $object
637
     * @param string $dbField
638
     * @param mixed $value
639
     * @param string|bool $sortColumn
640
     *
641
     * @throws \Exception
642
     */
643
    private function writeValue($object, $dbField, $value, $sortColumn)
644
    {
645
        $isManyRelation = array_key_exists($dbField, $object->config()->get('has_many')) ||
646
            array_key_exists($dbField, $object->config()->get('many_many')) ||
647
            array_key_exists($dbField, $object->config()->get('belongs_many_many'));
648
649
        $isSingleRelation = array_key_exists(rtrim($dbField, 'ID'), $object->config()->get('has_one'));
650
651
        if (!$isManyRelation) {
652
            if (!$isSingleRelation || ($isSingleRelation && $value !== false)) {
653
                $object->$dbField = $value;
654
            }
655
            return;
656
        }
657
658
        // change to an array and filter out empty values
659
        if (!is_array($value)) {
660
            $value = [$value];
661
        }
662
        $value = array_filter($value);
663
664
        // don't try to write an empty set
665
        if (!count($value)) {
666
            return;
667
        }
668
669
        // write the object so relations can be written
670
        if (!$object->exists()) {
671
            $object->write();
672
        }
673
674
        $this->removeUnrelated($object, $dbField, $value);
675
        $this->writeManyRelation($object, $dbField, $value, $sortColumn);
676
    }
677
678
    /**
679
     * @param DataObject $object
680
     * @param string $dbField
681
     * @param array $value
682
     * @param string|bool $sortColumn
683
     *
684
     * @throws \Exception
685
     */
686
    private function writeManyRelation($object, $dbField, $value, $sortColumn)
687
    {
688
        /** @var DataList|HasManyList|ManyManyList $relation */
689
        $relation = $object->{$dbField}();
690
691
        if ($sortColumn && $relation instanceof ManyManyList) {
692
            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...
693
                $relation->add($value[$i], [$sortColumn => $i]);
694
            }
695
            return;
696
        }
697
698
        // HasManyList, so it exists on the value
699
        if ($sortColumn) {
700
            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...
701
                $value[$i]->{$sortColumn} = $i;
702
                $relation->add($value[$i]);
703
            }
704
            return;
705
        }
706
707
        $relation->addMany($value);
708
    }
709
710
    /**
711
     * Removes unrelated objects in the relation that were previously related
712
     * @param DataObject $object
713
     * @param string $dbField
714
     * @param array $value
715
     */
716
    private function removeUnrelated($object, $dbField, $value)
717
    {
718
        $ids = [];
719
        foreach ($value as $v) {
720
            $ids[] = $v->ID;
721
        }
722
723
        /** @var DataList $relation */
724
        $relation = $object->{$dbField}();
725
        // remove all unrelated - removeAll had an odd side effect (relations only got added back half the time)
726
        if (!empty($ids)) {
727
            $relation->removeMany(
728
                $relation->exclude([
729
                    'ID' => $ids,
730
                ])->column('ID')
731
            );
732
        }
733
    }
734
735
    /**
736
     * @param string|array $salsifyField
737
     * @throws Exception
738
     */
739
    private function clearValue($object, $dbField, $salsifyField)
740
    {
741
        if (
742
            is_array($salsifyField) &&
743
            array_key_exists('keepExistingValue', $salsifyField) &&
744
            $salsifyField['keepExistingValue']
745
        ) {
746
            return;
747
        }
748
749
        // clear any existing value
750
        $this->writeValue($object, $dbField, null, null);
751
    }
752
753
    /**
754
     * @return \JsonMachine\JsonMachine
755
     */
756
    public function getAssetStream()
757
    {
758
        return $this->assetStream;
759
    }
760
761
    /**
762
     * @return bool
763
     */
764
    public function hasFile()
765
    {
766
        return $this->file !== null;
767
    }
768
}
769