Mapper::handleModification()   A
last analyzed

Complexity

Conditions 5
Paths 5

Size

Total Lines 18
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 10
c 0
b 0
f 0
dl 0
loc 18
rs 9.6111
cc 5
nc 5
nop 5
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
        if ($mapperHash->isChanged() && $object->ID) {
116
            $mapperHash->write();
117
        }
118
    }
119
120
    /**
121
     *
122
     */
123
    public function resetProductStream()
124
    {
125
        $this->productStream = JsonMachine::fromFile($this->file, '/4/products');
126
    }
127
128
    /**
129
     *
130
     */
131
    public function resetAssetStream()
132
    {
133
        $this->assetStream = JsonMachine::fromFile($this->file, '/3/digital_assets');
134
    }
135
136
    /**
137
     * Maps the data
138
     * @throws \Exception
139
     */
140
    public function map()
141
    {
142
        $this->extend('onBeforeMap', $this->file, Mapper::$MULTIPLE);
143
144
        foreach ($this->yieldKeyVal($this->productStream) as $name => $data) {
145
            foreach ($this->yieldKeyVal($this->config()->get('mapping')) as $class => $mappings) {
146
                $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

146
                $this->mapToObject(/** @scrutinizer ignore-type */ $class, $mappings, $data);
Loading history...
147
                $this->currentUniqueFields = [];
148
            }
149
        }
150
151
        if ($this->mappingHasSalsifyRelation()) {
152
            ImportTask::output("----------------");
153
            ImportTask::output("Setting up salsify relations");
154
            ImportTask::output("----------------");
155
            $this->resetProductStream();
156
157
            foreach ($this->yieldKeyVal($this->productStream) as $name => $data) {
158
                foreach ($this->yieldKeyVal($this->config()->get('mapping')) as $class => $mappings) {
159
                    $this->mapToObject($class, $mappings, $data, null, true);
160
                    $this->currentUniqueFields = [];
161
                }
162
            }
163
        }
164
165
        ImportTask::output("Imported and updated $this->importCount products.");
166
        $this->extend('onAfterMap', $this->file, Mapper::$MULTIPLE);
167
    }
168
169
    /**
170
     * @param string|DataObject $class
171
     * @param array $mappings The mapping for a specific class
172
     * @param array $data
173
     * @param DataObject|null $object
174
     * @param bool $salsifyRelations
175
     * @param bool $forceUpdate
176
     *
177
     * @return DataObject|null
178
     * @throws \Exception
179
     */
180
    public function mapToObject(
181
        $class,
182
        $mappings,
183
        $data,
184
        $object = null,
185
        $salsifyRelations = false,
186
        $forceUpdate = false
187
    ) {
188
        if ($salsifyRelations) {
189
            if (!$this->classConfigHasSalsifyRelation($mappings)) {
190
                return null;
191
            }
192
        }
193
194
        // if object was not passed
195
        if ($object === null) {
196
            $object = $this->findObjectByUnique($class, $mappings, $data);
197
198
            // if no existing object was found but a unique filter is valid (not empty)
199
            if (!$object) {
0 ignored issues
show
introduced by
$object is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
200
                // don't try to create related objects that don't exist
201
                if ($salsifyRelations) {
202
                    return null;
203
                }
204
205
                if (!$this->hasValidUniqueFilter($class, $mappings, $data)) {
206
                    return null;
207
                }
208
209
                $object = $class::create();
210
            }
211
        }
212
213
        $wasPublished = $object->hasExtension(Versioned::class) ? $object->isPublished() : false;
214
        $wasWritten = $object->isInDB();
215
216
        $firstUniqueKey = array_keys($this->uniqueFields($class, $mappings))[0];
217
        if (array_key_exists($mappings[$firstUniqueKey]['salsifyField'], $data)) {
218
            $firstUniqueValue = $data[$mappings[$firstUniqueKey]['salsifyField']];
219
        } else {
220
            $firstUniqueValue = 'NULL';
221
        }
222
223
        if ($this->hasMethod('shouldSkipForObject')) {
224
            if ($this->shouldSkipForObject($object, $data)) {
0 ignored issues
show
Bug introduced by
The method shouldSkipForObject() does not exist on Dynamic\Salsify\Model\Mapper. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

224
            if ($this->/** @scrutinizer ignore-call */ shouldSkipForObject($object, $data)) {
Loading history...
225
                ImportTask::output("Skipping $class $firstUniqueKey $firstUniqueValue. Not the right object class");
226
                return;
227
            }
228
        }
229
230
        ImportTask::output("Updating $class $firstUniqueKey $firstUniqueValue");
231
232
        if (
233
            !$forceUpdate &&
234
            $this->objectUpToDate($object, $data, $firstUniqueKey, $firstUniqueValue, $salsifyRelations)
235
        ) {
236
            return $object;
237
        }
238
239
        if (array_key_exists('salsify:parent_id', $data) && $this->hasFile()) {
240
            $products = JsonMachine::fromFile($this->file, '/4/products');
241
            foreach ($this->yieldSingle($products) as $product) {
242
                if ($product['salsify:id'] === $data['salsify:parent_id']) {
243
                    $data = array_merge($product, $data);
244
                    break;
245
                }
246
            }
247
        }
248
249
        foreach ($this->yieldKeyVal($mappings) as $dbField => $salsifyField) {
250
            $field = $this->getField($salsifyField, $data);
251
            if ($field === false) {
252
                $this->clearValue($object, $dbField, $salsifyField);
253
                continue;
254
            }
255
256
            $type = $this->getFieldType($salsifyField);
257
            // skip all but salsify relations types if not doing relations
258
            if ($salsifyRelations && !$this->typeRequiresSalsifyObjects($type)) {
259
                continue;
260
            }
261
262
            // skip salsify relations types if not doing relations
263
            if (!$salsifyRelations && $this->typeRequiresSalsifyObjects($type)) {
264
                continue;
265
            }
266
267
            $value = null;
268
            $objectData = $data;
0 ignored issues
show
Unused Code introduced by
The assignment to $objectData is dead and can be removed.
Loading history...
269
270
            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

270
            if ($this->handleShouldSkip($class, /** @scrutinizer ignore-type */ $dbField, $salsifyField, $data)) {
Loading history...
271
                if (!$this->skipSilently) {
272
                    ImportTask::output("Skipping $class $firstUniqueKey $firstUniqueValue");
273
                }
274
                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...
275
            };
276
277
            $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

277
            $objectData = $this->handleModification($type, $class, /** @scrutinizer ignore-type */ $dbField, $salsifyField, $data);
Loading history...
278
            $sortColumn = $this->getSortColumn($salsifyField);
279
280
            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...
281
                continue;
282
            }
283
284
            $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

284
            $value = $this->handleType($type, $class, $objectData, $field, $salsifyField, /** @scrutinizer ignore-type */ $dbField);
Loading history...
285
            $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

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

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

958
        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...
959
            return true;
960
        }
961
962
        $filter = [
963
            'MapperService' => $this->importerKey,
964
            'ForRelations' => $relations,
965
        ];
966
        /** @var MapperHash $hash */
967
        if ($hash = $object->MapperHashes()->filter($filter)->first()) {
968
            if ($hash->MapperHash != $this->getMappingHash()) {
969
                return false;
970
            }
971
        } else {
972
            return false;
973
        }
974
975
        return true;
976
    }
977
}
978