Passed
Pull Request — master (#82)
by Matthew
01:57
created

Mapper::hasValidUniqueFilter()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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

936
        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...
937
            return true;
938
        }
939
940
        $filter = [
941
            'MapperService' => $this->importerKey,
942
            'ForRelations' => $relations,
943
        ];
944
        /** @var MapperHash $hash */
945
        if ($hash = $object->MapperHashes()->filter($filter)->first()) {
946
            if ($hash->MapperHash != $this->getMappingHash()) {
947
                return false;
948
            }
949
        } else {
950
            return false;
951
        }
952
953
        return true;
954
    }
955
}
956