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

Mapper::uniqueFields()   B

Complexity

Conditions 8
Paths 6

Size

Total Lines 29
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 15
c 1
b 0
f 0
dl 0
loc 29
rs 8.4444
cc 8
nc 6
nop 2
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);
0 ignored issues
show
Bug introduced by
$importerKey of type string is incompatible with the type Dynamic\Salsify\Model\stirng expected by parameter $importerKey of Dynamic\Salsify\Model\Service::__construct(). ( Ignorable by Annotation )

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

71
        parent::__construct(/** @scrutinizer ignore-type */ $importerKey);
Loading history...
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
     * Generates the current hash for the mapping
85
     *
86
     * @return string
87
     */
88
    private function getMappingHash()
89
    {
90
        return md5(serialize($this->config()->get('mapping')));
91
    }
92
93
    /**
94
     * Updates the mapping hash for the mapper
95
     * @param DataObject|SalsifyIDExtension $object
96
     * @param bool $relations
97
     */
98
    private function updateMappingHash($object, $relations)
99
    {
100
        $filter = [
101
            'MapperService' => $this->importerKey,
102
            'ForRelations' => $relations,
103
        ];
104
        /** @var MapperHash $mapperHash */
105
        $mapperHash = $object->MapperHashes()->filter($filter)->first();
106
        if ($mapperHash == null) {
107
            $mapperHash = MapperHash::create();
108
            $mapperHash->MappedObjectID = $object->ID;
109
            $mapperHash->MappedObjectClass = $object->ClassName;
110
            $mapperHash->MapperService = $this->importerKey;
111
            $mapperHash->ForRelations = $relations;
112
        }
113
114
        $mapperHash->MapperHash = $this->getMappingHash();
115
        $mapperHash->write();
116
    }
117
118
    /**
119
     *
120
     */
121
    public function resetProductStream()
122
    {
123
        $this->productStream = JsonMachine::fromFile($this->file, '/4/products');
124
    }
125
126
    /**
127
     *
128
     */
129
    public function resetAssetStream()
130
    {
131
        $this->assetStream = JsonMachine::fromFile($this->file, '/3/digital_assets');
132
    }
133
134
    /**
135
     * Maps the data
136
     * @throws \Exception
137
     */
138
    public function map()
139
    {
140
        $this->extend('onBeforeMap', $this->file, Mapper::$MULTIPLE);
141
142
        foreach ($this->yieldKeyVal($this->productStream) as $name => $data) {
143
            foreach ($this->yieldKeyVal($this->config()->get('mapping')) as $class => $mappings) {
144
                $this->mapToObject($class, $mappings, $data);
0 ignored issues
show
Bug introduced by
It seems like $class can also be of type true; however, parameter $class of Dynamic\Salsify\Model\Mapper::mapToObject() does only seem to accept SilverStripe\ORM\DataObject|string, maybe add an additional type check? ( Ignorable by Annotation )

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

144
                $this->mapToObject(/** @scrutinizer ignore-type */ $class, $mappings, $data);
Loading history...
145
                $this->currentUniqueFields = [];
146
            }
147
        }
148
149
        if ($this->mappingHasSalsifyRelation()) {
150
            ImportTask::output("----------------");
151
            ImportTask::output("Setting up salsify relations");
152
            ImportTask::output("----------------");
153
            $this->resetProductStream();
154
155
            foreach ($this->yieldKeyVal($this->productStream) as $name => $data) {
156
                foreach ($this->yieldKeyVal($this->config()->get('mapping')) as $class => $mappings) {
157
                    $this->mapToObject($class, $mappings, $data, null, true);
158
                    $this->currentUniqueFields = [];
159
                }
160
            }
161
        }
162
163
        ImportTask::output("Imported and updated $this->importCount products.");
164
        $this->extend('onAfterMap', $this->file, Mapper::$MULTIPLE);
165
    }
166
167
    /**
168
     * @param string|DataObject $class
169
     * @param array $mappings The mapping for a specific class
170
     * @param array $data
171
     * @param DataObject|null $object
172
     * @param bool $salsifyRelations
173
     * @param bool $forceUpdate
174
     *
175
     * @return DataObject|null
176
     * @throws \Exception
177
     */
178
    public function mapToObject(
179
        $class,
180
        $mappings,
181
        $data,
182
        $object = null,
183
        $salsifyRelations = false,
184
        $forceUpdate = false
185
    ) {
186
        if ($salsifyRelations) {
187
            if (!$this->classConfigHasSalsifyRelation($mappings)) {
188
                return null;
189
            }
190
        }
191
192
        // if object was not passed
193
        if ($object === null) {
194
            $object = $this->findObjectByUnique($class, $mappings, $data);
195
196
            // if no existing object was found but a unique filter is valid (not empty)
197
            if (!$object) {
0 ignored issues
show
introduced by
$object is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
198
                // don't try to create related objects that don't exist
199
                if (!$salsifyRelations) {
200
                    return null;
201
                }
202
203
                if (!$this->hasValidUniqueFilter($class, $mappings, $data)) {
204
                    return null;
205
                }
206
                
207
                $object = $class::create();
208
            }
209
        }
210
211
        $wasPublished = $object->hasExtension(Versioned::class) ? $object->isPublished() : false;
212
        $wasWritten = $object->isInDB();
213
214
        $firstUniqueKey = array_keys($this->uniqueFields($class, $mappings))[0];
215
        if (array_key_exists($mappings[$firstUniqueKey]['salsifyField'], $data)) {
216
            $firstUniqueValue = $data[$mappings[$firstUniqueKey]['salsifyField']];
217
        } else {
218
            $firstUniqueValue = 'NULL';
219
        }
220
        ImportTask::output("Updating $class $firstUniqueKey $firstUniqueValue");
221
222
        if (
223
            !$forceUpdate &&
224
            $this->objectUpToDate($object, $data, $firstUniqueKey, $firstUniqueValue, $salsifyRelations)
225
        ) {
226
            return $object;
227
        }
228
229
        foreach ($this->yieldKeyVal($mappings) as $dbField => $salsifyField) {
230
            $field = $this->getField($salsifyField, $data);
231
            if ($field === false) {
232
                $this->clearValue($object, $dbField, $salsifyField);
233
                continue;
234
            }
235
236
            $type = $this->getFieldType($salsifyField);
237
            // skip all but salsify relations types if not doing relations
238
            if ($salsifyRelations && !$this->typeRequiresSalsifyObjects($type)) {
239
                continue;
240
            }
241
242
            // skip salsify relations types if not doing relations
243
            if (!$salsifyRelations && $this->typeRequiresSalsifyObjects($type)) {
244
                continue;
245
            }
246
247
            $value = null;
248
            $objectData = $data;
0 ignored issues
show
Unused Code introduced by
The assignment to $objectData is dead and can be removed.
Loading history...
249
250
            if ($this->handleShouldSkip($class, $dbField, $salsifyField, $data)) {
0 ignored issues
show
Bug introduced by
It seems like $dbField can also be of type true; however, parameter $dbField of Dynamic\Salsify\Model\Mapper::handleShouldSkip() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

250
            if ($this->handleShouldSkip($class, /** @scrutinizer ignore-type */ $dbField, $salsifyField, $data)) {
Loading history...
251
                if (!$this->skipSilently) {
252
                    ImportTask::output("Skipping $class $firstUniqueKey $firstUniqueValue");
253
                    $this->skipSilently = false;
254
                }
255
                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...
256
            };
257
258
            $objectData = $this->handleModification($type, $class, $dbField, $salsifyField, $data);
0 ignored issues
show
Bug introduced by
It seems like $dbField can also be of type true; however, parameter $dbField of Dynamic\Salsify\Model\Mapper::handleModification() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

258
            $objectData = $this->handleModification($type, $class, /** @scrutinizer ignore-type */ $dbField, $salsifyField, $data);
Loading history...
259
            $sortColumn = $this->getSortColumn($salsifyField);
260
261
            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...
262
                continue;
263
            }
264
265
            $value = $this->handleType($type, $class, $objectData, $field, $salsifyField, $dbField);
0 ignored issues
show
Bug introduced by
It seems like $dbField can also be of type true; however, parameter $dbField of Dynamic\Salsify\Model\Mapper::handleType() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

265
            $value = $this->handleType($type, $class, $objectData, $field, $salsifyField, /** @scrutinizer ignore-type */ $dbField);
Loading history...
266
            $this->writeValue($object, $type, $dbField, $value, $sortColumn);
0 ignored issues
show
Bug introduced by
It seems like $dbField can also be of type true; however, parameter $dbField of Dynamic\Salsify\Model\Mapper::writeValue() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

266
            $this->writeValue($object, $type, /** @scrutinizer ignore-type */ $dbField, $value, $sortColumn);
Loading history...
267
        }
268
269
        if (!$this->isMapperHashUpToDate($object, $salsifyRelations)) {
270
            $this->updateMappingHash($object, $salsifyRelations);
271
        }
272
273
        $this->extend('beforeObjectWrite', $object);
274
        if ($object->isChanged()) {
275
            $object->write();
276
            $this->importCount++;
277
            $this->extend('afterObjectWrite', $object, $wasWritten, $wasPublished);
278
        } else {
279
            ImportTask::output("$class $firstUniqueKey $firstUniqueValue was not changed.");
280
        }
281
        return $object;
282
    }
283
284
    /**
285
     * @param DataObject $object
286
     * @param array $data
287
     * @param string $firstUniqueKey
288
     * @param string $firstUniqueValue
289
     * @param bool $salsifyRelations
290
     * @return bool
291
     */
292
    private function objectUpToDate($object, $data, $firstUniqueKey, $firstUniqueValue, $salsifyRelations = false)
293
    {
294
        if ($this->config()->get('skipUpToDate') == false) {
295
            return false;
296
        }
297
298
        if (!$this->isMapperHashUpToDate($object, $salsifyRelations)) {
299
            ImportTask::output("Forcing update for $firstUniqueKey $firstUniqueValue. Mappings changed.");
300
            return false;
301
        }
302
303
        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...
304
            if ($this->objectDataUpToDate($object, $data, $firstUniqueKey, $firstUniqueValue)) {
305
                ImportTask::output("Skipping $firstUniqueKey $firstUniqueValue. It is up to Date.");
306
                return true;
307
            }
308
        } else {
309
            if ($this->objectRelationsUpToDate($object, $data, $firstUniqueKey, $firstUniqueValue)) {
310
                ImportTask::output("Skipping $firstUniqueKey $firstUniqueValue relations. It is up to Date.");
311
                return true;
312
            }
313
        }
314
315
        return false;
316
    }
317
318
    /**
319
     * @param DataObject $object
320
     * @param array $data
321
     * @param string $firstUniqueKey
322
     * @param string $firstUniqueValue
323
     * @return bool
324
     */
325
    public function objectDataUpToDate($object, $data, $firstUniqueKey, $firstUniqueValue)
326
    {
327
        // assume not up to date if field does not exist on object
328
        if (!$object->hasField('SalsifyUpdatedAt')) {
329
            return false;
330
        }
331
332
        if ($data['salsify:updated_at'] != $object->getField('SalsifyUpdatedAt')) {
333
            return false;
334
        }
335
336
        return true;
337
    }
338
339
    /**
340
     * @param DataObject $object
341
     * @param array $data
342
     * @param string $firstUniqueKey
343
     * @param string $firstUniqueValue
344
     * @return bool
345
     */
346
    public function objectRelationsUpToDate($object, $data, $firstUniqueKey, $firstUniqueValue)
347
    {
348
        // assume not up to date if field does not exist on object
349
        if (!$object->hasField('SalsifyRelationsUpdatedAt')) {
350
            return false;
351
        }
352
353
        // relations were never updated, so its up to date
354
        if (!isset($data['salsify:relations_updated_at'])) {
355
            return true;
356
        }
357
358
        if ($data['salsify:relations_updated_at'] != $object->getField('SalsifyRelationsUpdatedAt')) {
359
            return false;
360
        }
361
362
        return true;
363
    }
364
365
    /**
366
     * @param array $salsifyField
367
     * @param array $data
368
     *
369
     * @return string|false
370
     */
371
    private function getField($salsifyField, $data)
372
    {
373
        if (!is_array($salsifyField)) {
0 ignored issues
show
introduced by
The condition is_array($salsifyField) is always true.
Loading history...
374
            return array_key_exists($salsifyField, $data) ? $salsifyField : false;
375
        }
376
377
        $hasSalsifyField = array_key_exists('salsifyField', $salsifyField);
378
        $isLiteralField = (
379
            $this->getFieldType($salsifyField) === 'Literal' &&
0 ignored issues
show
introduced by
The condition $this->getFieldType($salsifyField) === 'Literal' is always false.
Loading history...
380
            array_key_exists('value', $salsifyField)
381
        );
382
        $isSalsifyRelationField = (
383
            $this->getFieldType($salsifyField) === 'SalsifyRelation' &&
0 ignored issues
show
introduced by
The condition $this->getFieldType($sal...) === 'SalsifyRelation' is always false.
Loading history...
384
            $hasSalsifyField
385
        );
386
387
        if ($isLiteralField) {
0 ignored issues
show
introduced by
The condition $isLiteralField is always false.
Loading history...
388
            return $salsifyField['value'];
389
        }
390
391
        if ($isSalsifyRelationField) {
0 ignored issues
show
introduced by
The condition $isSalsifyRelationField is always false.
Loading history...
392
            return $salsifyField['salsifyField'];
393
        }
394
395
        if (!$hasSalsifyField) {
396
            return false;
397
        }
398
399
        if (array_key_exists($salsifyField['salsifyField'], $data)) {
400
            return $salsifyField['salsifyField'];
401
        } elseif (array_key_exists('fallback', $salsifyField)) {
402
            // make fallback an array
403
            if (!is_array($salsifyField['fallback'])) {
404
                $salsifyField['fallback'] = [$salsifyField['fallback']];
405
            }
406
407
            foreach ($this->yieldSingle($salsifyField['fallback']) as $fallback) {
408
                if (array_key_exists($fallback, $data)) {
409
                    return $fallback;
410
                }
411
            }
412
        } elseif (array_key_exists('modification', $salsifyField)) {
413
            return $salsifyField['salsifyField'];
414
        }
415
416
        return false;
417
    }
418
419
    /**
420
     * @param string $class
421
     * @param array $mappings
422
     * @param array $data
423
     *
424
     * @return bool
425
     */
426
    private function hasValidUniqueFilter($class, $mappings, $data)
427
    {
428
        return (bool) count(array_filter($this->getUniqueFilter($class, $mappings, $data)));
429
    }
430
431
    /**
432
     * @param string $class
433
     * @param array $mappings
434
     * @param array $data
435
     *
436
     * @return array
437
     */
438
    private function getUniqueFilter($class, $mappings, $data)
439
    {
440
        $uniqueFields = $this->uniqueFields($class, $mappings);
441
        // creates a filter
442
        $filter = [];
443
        foreach ($this->yieldKeyVal($uniqueFields) as $dbField => $salsifyField) {
444
            $modifiedData = $data;
445
            $fieldMapping = $mappings[$dbField];
446
            $fieldType = $this->getFieldType($salsifyField);
447
            $modifiedData = $this->handleModification($fieldType, $class, $dbField, $fieldMapping, $modifiedData);
0 ignored issues
show
Bug introduced by
It seems like $dbField can also be of type true; however, parameter $dbField of Dynamic\Salsify\Model\Mapper::handleModification() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

447
            $modifiedData = $this->handleModification($fieldType, $class, /** @scrutinizer ignore-type */ $dbField, $fieldMapping, $modifiedData);
Loading history...
448
449
            // adds unique fields to filter
450
            if (array_key_exists($salsifyField, $modifiedData)) {
451
                $filter[$dbField] = $modifiedData[$salsifyField];
452
            }
453
        }
454
455
        return $filter;
456
    }
457
458
    /**
459
     * @param string $class
460
     * @param array $mappings
461
     * @param array $data
462
     *
463
     * @return \SilverStripe\ORM\DataObject
464
     */
465
    private function findObjectByUnique($class, $mappings, $data)
466
    {
467
        if ($obj = $this->findBySalsifyID($class, $mappings, $data)) {
468
            return $obj;
469
        }
470
471
        if (!$this->hasValidUniqueFilter($class, $mappings, $data)) {
472
            return null;
473
        }
474
475
        $filter = $this->getUniqueFilter($class, $mappings, $data);
476
        return DataObject::get($class)->filter($filter)->first();
477
    }
478
479
    /**
480
     * @param string $class
481
     * @param array $mappings
482
     * @param array $data
483
     *
484
     * @return \SilverStripe\ORM\DataObject|bool
485
     */
486
    private function findBySalsifyID($class, $mappings, $data)
487
    {
488
        /** @var DataObject $genericObject */
489
        $genericObject = Injector::inst()->get($class);
490
        if (
491
            !$genericObject->hasExtension(SalsifyIDExtension::class) &&
492
            !$genericObject->hasField('SalsifyID')
493
        ) {
494
            return false;
495
        }
496
497
        $obj = DataObject::get($class)->filter([
498
            'SalsifyID' => $data['salsify:id'],
499
        ])->first();
500
        if ($obj) {
501
            return $obj;
502
        }
503
504
        return false;
505
    }
506
507
    /**
508
     * Gets a list of all the unique field keys
509
     *
510
     * @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...
511
     * @param array $mappings
512
     * @return array
513
     */
514
    private function uniqueFields($class, $mappings)
515
    {
516
        // cached after first map
517
        if (array_key_exists($class, $this->currentUniqueFields) && !empty($this->currentUniqueFields[$class])) {
518
            return $this->currentUniqueFields[$class];
519
        }
520
521
        $uniqueFields = [];
522
        foreach ($this->yieldKeyVal($mappings) as $dbField => $salsifyField) {
523
            if (!is_array($salsifyField)) {
524
                continue;
525
            }
526
527
            if (
528
                !array_key_exists('unique', $salsifyField) ||
529
                !array_key_exists('salsifyField', $salsifyField)
530
            ) {
531
                continue;
532
            }
533
534
            if ($salsifyField['unique'] !== true) {
535
                continue;
536
            }
537
538
            $uniqueFields[$dbField] = $salsifyField['salsifyField'];
539
        }
540
541
        $this->currentUniqueFields[$class] = $uniqueFields;
542
        return $uniqueFields;
543
    }
544
545
    /**
546
     * @param array|string $salsifyField
547
     * @return bool|mixed
548
     */
549
    private function getSortColumn($salsifyField)
550
    {
551
        if (!is_array($salsifyField)) {
552
            return false;
553
        }
554
555
        if (array_key_exists('sortColumn', $salsifyField)) {
556
            return $salsifyField['sortColumn'];
557
        }
558
559
        return false;
560
    }
561
562
    /**
563
     * @return bool
564
     */
565
    private function mappingHasSalsifyRelation()
566
    {
567
        foreach ($this->yieldKeyVal($this->config()->get('mapping')) as $class => $mappings) {
568
            if ($this->classConfigHasSalsifyRelation($mappings)) {
569
                return true;
570
            }
571
        }
572
        return false;
573
    }
574
575
    /**
576
     * @param array $classConfig
577
     * @return bool
578
     */
579
    private function classConfigHasSalsifyRelation($classConfig)
580
    {
581
        foreach ($this->yieldKeyVal($classConfig) as $field => $config) {
582
            if (!is_array($config)) {
583
                continue;
584
            }
585
586
            if (!array_key_exists('salsifyField', $config)) {
587
                continue;
588
            }
589
590
            if (!array_key_exists('type', $config)) {
591
                continue;
592
            }
593
594
            if (in_array($config['type'], $this->getFieldsRequiringSalsifyObjects())) {
595
                return true;
596
            }
597
        }
598
        return false;
599
    }
600
601
    /**
602
     * @return array
603
     */
604
    private function getFieldsRequiringSalsifyObjects()
605
    {
606
        $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...
607
        $types = [];
608
        foreach ($this->yieldKeyVal($this->config()->get('field_types')) as $field => $config) {
609
            $type = [
610
                'type' => $field,
611
                'config' => $config,
612
            ];
613
            if ($this->typeRequiresSalsifyObjects($type)) {
614
                $types[] = $field;
615
            }
616
        }
617
618
        return $types;
619
    }
620
621
    /**
622
     * @param array $type
623
     * @return bool
624
     */
625
    private function typeRequiresWrite($type)
626
    {
627
        $config = $type['config'];
628
629
        if (array_key_exists('requiresWrite', $config)) {
630
            return $config['requiresWrite'];
631
        }
632
633
        return false;
634
    }
635
636
    /**
637
     * @param array $type
638
     * @return bool
639
     */
640
    private function typeRequiresSalsifyObjects($type)
641
    {
642
        $config = $type['config'];
643
644
        if (array_key_exists('requiresSalsifyObjects', $config)) {
645
            return $config['requiresSalsifyObjects'];
646
        }
647
648
        return false;
649
    }
650
651
    /**
652
     * @param array $type
653
     * @return string|bool
654
     */
655
    private function typeFallback($type)
656
    {
657
        $config = $type['config'];
658
659
        if (array_key_exists('fallback', $config)) {
660
            return $config['fallback'];
661
        }
662
663
        return false;
664
    }
665
666
    /**
667
     * @param array $type
668
     * @return bool
669
     */
670
    private function canModifyType($type)
671
    {
672
        $config = $type['config'];
673
674
        if (array_key_exists('allowsModification', $config)) {
675
            return $config['allowsModification'];
676
        }
677
678
        return true;
679
    }
680
681
    /**
682
     * @param array $type
683
     * @param string $class
684
     * @param string $dbField
685
     * @param array $config
686
     * @param array $data
687
     * @return array
688
     */
689
    private function handleModification($type, $class, $dbField, $config, $data)
690
    {
691
        if (!is_array($config)) {
0 ignored issues
show
introduced by
The condition is_array($config) is always true.
Loading history...
692
            return $data;
693
        }
694
695
        if (!$this->canModifyType($type)) {
696
            return $data;
697
        }
698
699
        if (array_key_exists('modification', $config)) {
700
            $mod = $config['modification'];
701
            if ($this->hasMethod($mod)) {
702
                return $this->{$mod}($class, $dbField, $config, $data);
703
            }
704
            ImportTask::output("{$mod} is not a valid field modifier. skipping modification for field {$dbField}.");
705
        }
706
        return $data;
707
    }
708
709
    /**
710
     * @param string $class
711
     * @param string $dbField
712
     * @param array $config
713
     * @param array $data
714
     * @return boolean
715
     */
716
    public function handleShouldSkip($class, $dbField, $config, $data)
717
    {
718
        if (!is_array($config)) {
0 ignored issues
show
introduced by
The condition is_array($config) is always true.
Loading history...
719
            return false;
720
        }
721
722
        if (array_key_exists('shouldSkip', $config)) {
723
            $skipMethod = $config['shouldSkip'];
724
            if ($this->hasMethod($skipMethod)) {
725
                return $this->{$skipMethod}($class, $dbField, $config, $data);
726
            }
727
            ImportTask::output(
728
                "{$skipMethod} is not a valid skip test method. Skipping skip test for field {$dbField}."
729
            );
730
        }
731
        return false;
732
    }
733
734
    /**
735
     * @param string|array $field
736
     * @return array
737
     */
738
    public function getFieldType($field)
739
    {
740
        $fieldTypes = $this->config()->get('field_types');
741
        if (is_array($field) && array_key_exists('type', $field)) {
742
            if (array_key_exists($field['type'], $fieldTypes)) {
743
                return [
744
                    'type' => $field['type'],
745
                    'config' => $fieldTypes[$field['type']],
746
                ];
747
            }
748
        }
749
        // default to raw
750
        return [
751
            'type' => 'Raw',
752
            'config' => $fieldTypes['Raw'],
753
        ];
754
    }
755
756
    /**
757
     * @param array $type
758
     * @param string|DataObject $class
759
     * @param array $salsifyData
760
     * @param string $salsifyField
761
     * @param array $dbFieldConfig
762
     * @param string $dbField
763
     *
764
     * @return mixed
765
     */
766
    private function handleType($type, $class, $salsifyData, $salsifyField, $dbFieldConfig, $dbField)
767
    {
768
        $typeName = $type['type'];
769
        $typeConfig = $type['config'];
770
        if ($this->hasMethod("handle{$typeName}Type")) {
771
            return $this->{"handle{$typeName}Type"}($class, $salsifyData, $salsifyField, $dbFieldConfig, $dbField);
772
        }
773
774
        if (array_key_exists('fallback', $typeConfig)) {
775
            $fallback = $typeConfig['fallback'];
776
            if ($this->hasMethod("handle{$fallback}Type")) {
777
                return $this->{"handle{$fallback}Type"}($class, $salsifyData, $salsifyField, $dbFieldConfig, $dbField);
778
            }
779
        }
780
781
        ImportTask::output("{$typeName} is not a valid type. skipping field {$dbField}.");
782
        return '';
783
    }
784
785
    /**
786
     * @param DataObject $object
787
     * @param array $type
788
     * @param string $dbField
789
     * @param mixed $value
790
     * @param string|bool $sortColumn
791
     *
792
     * @throws \Exception
793
     */
794
    private function writeValue($object, $type, $dbField, $value, $sortColumn)
795
    {
796
        $isManyRelation = array_key_exists($dbField, $object->config()->get('has_many')) ||
797
            array_key_exists($dbField, $object->config()->get('many_many')) ||
798
            array_key_exists($dbField, $object->config()->get('belongs_many_many'));
799
800
        $isSingleRelation = array_key_exists(rtrim($dbField, 'ID'), $object->config()->get('has_one'));
801
802
        // write the object so relations can be written
803
        if ($this->typeRequiresWrite($type) && !$object->exists()) {
804
            $this->extend('beforeObjectWrite', $object);
805
            $object->write();
806
        }
807
808
        if (!$isManyRelation) {
809
            if (!$isSingleRelation || ($isSingleRelation && $value !== false)) {
810
                $object->$dbField = $value;
811
            }
812
            return;
813
        }
814
815
        // change to an array and filter out empty values
816
        if (!is_array($value)) {
817
            $value = [$value];
818
        }
819
        $value = array_filter($value);
820
821
        // don't try to write an empty set
822
        if (!count($value)) {
823
            return;
824
        }
825
826
        $this->removeUnrelated($object, $dbField, $value);
827
        $this->writeManyRelation($object, $dbField, $value, $sortColumn);
828
    }
829
830
    /**
831
     * @param DataObject $object
832
     * @param string $dbField
833
     * @param array $value
834
     * @param string|bool $sortColumn
835
     *
836
     * @throws \Exception
837
     */
838
    private function writeManyRelation($object, $dbField, $value, $sortColumn)
839
    {
840
        /** @var DataList|HasManyList|ManyManyList $relation */
841
        $relation = $object->{$dbField}();
842
843
        if ($sortColumn && $relation instanceof ManyManyList) {
844
            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...
845
                $relation->add($value[$i], [$sortColumn => $i]);
846
            }
847
            return;
848
        }
849
850
        // HasManyList, so it exists on the value
851
        if ($sortColumn) {
852
            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...
853
                $value[$i]->{$sortColumn} = $i;
854
                $relation->add($value[$i]);
855
            }
856
            return;
857
        }
858
859
        $relation->addMany($value);
860
    }
861
862
    /**
863
     * Removes unrelated objects in the relation that were previously related
864
     * @param DataObject $object
865
     * @param string $dbField
866
     * @param array $value
867
     */
868
    private function removeUnrelated($object, $dbField, $value)
869
    {
870
        $ids = [];
871
        foreach ($value as $v) {
872
            $ids[] = $v->ID;
873
        }
874
875
        /** @var DataList $relation */
876
        $relation = $object->{$dbField}();
877
        // remove all unrelated - removeAll had an odd side effect (relations only got added back half the time)
878
        if (!empty($ids)) {
879
            $relation->removeMany(
880
                $relation->exclude([
881
                    'ID' => $ids,
882
                ])->column('ID')
883
            );
884
        }
885
    }
886
887
    /**
888
     * @param string|array $salsifyField
889
     * @throws Exception
890
     */
891
    private function clearValue($object, $dbField, $salsifyField)
892
    {
893
        if (
894
            is_array($salsifyField) &&
895
            array_key_exists('keepExistingValue', $salsifyField) &&
896
            $salsifyField['keepExistingValue']
897
        ) {
898
            return;
899
        }
900
901
        $type = [
902
            'type' => 'null',
903
            'config' => [],
904
        ];
905
906
        // clear any existing value
907
        $this->writeValue($object, $type, $dbField, null, null);
908
    }
909
910
    /**
911
     * @return JsonMachine
912
     */
913
    public function getProductStream()
914
    {
915
        return $this->productStream;
916
    }
917
918
    /**
919
     * @return JsonMachine
920
     */
921
    public function getAssetStream()
922
    {
923
        return $this->assetStream;
924
    }
925
926
    /**
927
     * @return bool
928
     */
929
    public function hasFile()
930
    {
931
        return $this->file !== null;
932
    }
933
934
    /**
935
     * @param DataObject|SalsifyIDExtension $object
936
     * @return bool
937
     */
938
    private function isMapperHashUpToDate($object, $relations)
939
    {
940
        if (!$object->hasMethod('MapperHashes')) {
0 ignored issues
show
Bug introduced by
The method hasMethod() does not exist on Dynamic\Salsify\ORM\SalsifyIDExtension. ( Ignorable by Annotation )

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

940
        if (!$object->/** @scrutinizer ignore-call */ hasMethod('MapperHashes')) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
941
            return true;
942
        }
943
944
        $filter = [
945
            'MapperService' => $this->importerKey,
946
            'ForRelations' => $relations,
947
        ];
948
        /** @var MapperHash $hash */
949
        if ($hash = $object->MapperHashes()->filter($filter)->first()) {
950
            if ($hash->MapperHash != $this->getMappingHash()) {
951
                return false;
952
            }
953
        } else {
954
            return false;
955
        }
956
957
        return true;
958
    }
959
}
960