Completed
Push — master ( 0a9015...badab9 )
by Nic
14s queued 12s
created

Mapper::mappingHasSalsifyRelation()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 8
rs 10
c 0
b 0
f 0
cc 3
nc 3
nop 0
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
25
     */
26
    private $file = null;
27
28
    /**
29
     * @var JsonMachine
30
     */
31
    private $productStream;
32
33
    /**
34
     * @var JsonMachine
35
     */
36
    private $assetStream;
37
38
    /**
39
     * @var array
40
     */
41
    private $currentUniqueFields = [];
42
43
    /**
44
     * @var int
45
     */
46
    private $importCount = 0;
47
48
    /**
49
     * @var bool
50
     */
51
    public $skipSiliently = false;
52
53
    /**
54
     * Mapper constructor.
55
     * @param string $importerKey
56
     * @param $file
57
     * @throws \Exception
58
     */
59
    public function __construct($importerKey, $file = null)
60
    {
61
        parent::__construct($importerKey);
62
        if (!$this->config()->get('mapping')) {
63
            throw  new Exception('A Mapper needs a mapping');
64
        }
65
66
        if ($file !== null) {
67
            $this->file = $file;
68
            $this->resetProductStream();
69
            $this->resetAssetStream();
70
        }
71
    }
72
73
    /**
74
     *
75
     */
76
    public function resetProductStream()
77
    {
78
        $this->productStream = JsonMachine::fromFile($this->file, '/4/products');
79
    }
80
81
    /**
82
     *
83
     */
84
    public function resetAssetStream()
85
    {
86
        $this->assetStream = JsonMachine::fromFile($this->file, '/3/digital_assets');
87
    }
88
89
    /**
90
     * Maps the data
91
     * @throws \Exception
92
     */
93
    public function map()
94
    {
95
        foreach ($this->yieldKeyVal($this->productStream) as $name => $data) {
96
            foreach ($this->yieldKeyVal($this->config()->get('mapping')) as $class => $mappings) {
97
                $this->mapToObject($class, $mappings, $data);
98
                $this->currentUniqueFields = [];
99
            }
100
        }
101
102
        if ($this->mappingHasSalsifyRelation()) {
103
            $this->resetProductStream();
104
105
            foreach ($this->yieldKeyVal($this->productStream) as $name => $data) {
106
                foreach ($this->yieldKeyVal($this->config()->get('mapping')) as $class => $mappings) {
107
                    $this->mapToObject($class, $mappings, $data, null, true);
108
                    $this->currentUniqueFields = [];
109
                }
110
            }
111
        }
112
        ImportTask::output("Imported and updated $this->importCount products.");
113
    }
114
115
    /**
116
     * @param string|DataObject $class
117
     * @param array $mappings The mapping for a specific class
118
     * @param array $data
119
     * @param DataObject|null $object
120
     * @param bool $salsifyRelations
121
     *
122
     * @return DataObject|null
123
     * @throws \Exception
124
     */
125
    public function mapToObject($class, $mappings, $data, $object = null, $salsifyRelations = false)
126
    {
127
        if ($salsifyRelations) {
128
            if (!$this->classConfigHasSalsifyRelation($mappings)) {
129
                return null;
130
            }
131
        }
132
133
        // if object was not passed
134
        if ($object === null) {
135
            $object = $this->findObjectByUnique($class, $mappings, $data);
136
            // if no existing object was found
137
            if (!$object) {
0 ignored issues
show
introduced by
$object is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
138
                $object = $class::create();
139
            }
140
        }
141
142
        $wasPublished = $object->hasExtension(Versioned::class) ? $object->isPublished() : false;
143
        $wasWritten = $object->isInDB();
144
145
        $firstUniqueKey = array_keys($this->uniqueFields($class, $mappings))[0];
146
        if (array_key_exists($mappings[$firstUniqueKey]['salsifyField'], $data)) {
147
            $firstUniqueValue = $data[$mappings[$firstUniqueKey]['salsifyField']];
148
        } else {
149
            $firstUniqueValue = 'NULL';
150
        }
151
        ImportTask::output("Updating $class $firstUniqueKey $firstUniqueValue");
152
153
        if ($this->objectUpToDate($object, $data, $firstUniqueKey, $firstUniqueValue, $salsifyRelations)) {
154
            return $object;
155
        }
156
157
        foreach ($this->yieldKeyVal($mappings) as $dbField => $salsifyField) {
158
            $field = $this->getField($salsifyField, $data);
159
            if ($field === false) {
160
                continue;
161
            }
162
163
            $type = $this->getFieldType($salsifyField);
164
            if ($salsifyRelations && $type != 'SalsifyRelation') {
165
                continue;
166
            }
167
168
            $value = null;
169
            $objectData = $data;
0 ignored issues
show
Unused Code introduced by
The assignment to $objectData is dead and can be removed.
Loading history...
170
171
            if ($this->handleShouldSkip($class, $dbField, $salsifyField, $data)) {
172
                if (!$this->skipSiliently) {
173
                    ImportTask::output("Skipping $class $firstUniqueKey $firstUniqueValue");
174
                    $this->skipSiliently = false;
175
                }
176
                return null;
177
            };
178
179
            $objectData = $this->handleModification($class, $dbField, $salsifyField, $data);
180
            $sortColumn = $this->getSortColumn($salsifyField);
181
182
            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...
183
                continue;
184
            }
185
186
            $value = $this->handleType($type, $class, $objectData, $field, $salsifyField, $dbField);
0 ignored issues
show
Bug introduced by
$type of type string is incompatible with the type integer expected by parameter $type of Dynamic\Salsify\Model\Mapper::handleType(). ( Ignorable by Annotation )

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

186
            $value = $this->handleType(/** @scrutinizer ignore-type */ $type, $class, $objectData, $field, $salsifyField, $dbField);
Loading history...
187
            $this->writeValue($object, $dbField, $value, $sortColumn);
188
        }
189
190
        if ($object->isChanged()) {
191
            $object->write();
192
            $this->importCount++;
193
            $this->extend('afterObjectWrite', $object, $wasWritten, $wasPublished);
194
        } else {
195
            ImportTask::output("$class $firstUniqueKey $firstUniqueValue was not changed.");
196
        }
197
        return $object;
198
    }
199
200
    /**
201
     * @param DataObject $object
202
     * @param array $data
203
     * @param string $firstUniqueKey
204
     * @param string $firstUniqueValue
205
     * @param bool $salsifyRelations
206
     * @return bool
207
     */
208
    private function objectUpToDate($object, $data, $firstUniqueKey, $firstUniqueValue, $salsifyRelations = false)
209
    {
210
        if ($this->config()->get('skipUpToDate') == true) {
211
            if (
212
                $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...
213
                $object->hasField('SalsifyUpdatedAt') &&
214
                $data['salsify:updated_at'] == $object->getField('SalsifyUpdatedAt')
215
            ) {
216
                ImportTask::output("Skipping $firstUniqueKey $firstUniqueValue. It is up to Date.");
217
                return true;
218
            }
219
220
            if (
221
                $salsifyRelations == true &&
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...
222
                $object->hasField('SalsifyRelationsUpdatedAt') &&
223
                isset($data['salsify:relations_updated_at']) &&
224
                $data['salsify:relations_updated_at'] == $object->getField('SalsifyRelationsUpdatedAt')
225
            ) {
226
                return true;
227
            }
228
        }
229
        return false;
230
    }
231
232
    /**
233
     * @param array $salsifyField
234
     * @param array $data
235
     *
236
     * @return string|false
237
     */
238
    private function getField($salsifyField, $data)
239
    {
240
        if (!is_array($salsifyField)) {
0 ignored issues
show
introduced by
The condition is_array($salsifyField) is always true.
Loading history...
241
            return array_key_exists($salsifyField, $data) ? $salsifyField : false;
242
        }
243
244
        $hasSalsifyField = array_key_exists('salsifyField', $salsifyField);
245
        $isLiteralField = (
246
            $this->getFieldType($salsifyField) === 'Literal' &&
247
            array_key_exists('value', $salsifyField)
248
        );
249
        $isSalsifyRelationField = (
250
            $this->getFieldType($salsifyField) === 'SalsifyRelation' &&
251
            $hasSalsifyField
252
        );
253
254
        if ($isLiteralField) {
255
            return $salsifyField['value'];
256
        }
257
258
        if ($isSalsifyRelationField) {
259
            return $salsifyField['salsifyField'];
260
        }
261
262
        if (!$hasSalsifyField) {
263
            return false;
264
        }
265
266
        if (array_key_exists($salsifyField['salsifyField'], $data)) {
267
            return $salsifyField['salsifyField'];
268
        } elseif (array_key_exists('fallback', $salsifyField)) {
269
            // make fallback an array
270
            if (!is_array($salsifyField['fallback'])) {
271
                $salsifyField['fallback'] = [$salsifyField['fallback']];
272
            }
273
274
            foreach ($this->yieldSingle($salsifyField['fallback']) as $fallback) {
275
                if (array_key_exists($fallback, $data)) {
276
                    return $fallback;
277
                }
278
            }
279
        } elseif (array_key_exists('modification', $salsifyField)) {
280
            return $salsifyField['salsifyField'];
281
        }
282
283
        return false;
284
    }
285
286
    /**
287
     * @param string $class
288
     * @param array $mappings
289
     * @param array $data
290
     *
291
     * @return \SilverStripe\ORM\DataObject
292
     */
293
    private function findObjectByUnique($class, $mappings, $data)
294
    {
295
        if ($obj = $this->findBySalsifyID($class, $mappings, $data)) {
296
            return $obj;
297
        }
298
299
        $uniqueFields = $this->uniqueFields($class, $mappings);
300
        // creates a filter
301
        $filter = [];
302
        foreach ($this->yieldKeyVal($uniqueFields) as $dbField => $salsifyField) {
303
            $modifiedData = $data;
304
            $fieldMapping = $mappings[$dbField];
305
306
            $modifiedData = $this->handleModification($class, $dbField, $fieldMapping, $modifiedData);
307
308
            // adds unique fields to filter
309
            if (array_key_exists($salsifyField, $modifiedData)) {
310
                $filter[$dbField] = $modifiedData[$salsifyField];
311
            }
312
        }
313
314
        return DataObject::get($class)->filter($filter)->first();
315
    }
316
317
    /**
318
     * @param string $class
319
     * @param array $mappings
320
     * @param array $data
321
     *
322
     * @return \SilverStripe\ORM\DataObject|bool
323
     */
324
    private function findBySalsifyID($class, $mappings, $data)
325
    {
326
        /** @var DataObject $genericObject */
327
        $genericObject = Injector::inst()->get($class);
328
        if (
329
            !$genericObject->hasExtension(SalsifyIDExtension::class) &&
330
            !$genericObject->hasField('SalsifyID')
331
        ) {
332
            return false;
333
        }
334
335
        $modifiedData = $data;
336
        if (array_key_exists('salsify:id', $mappings)) {
337
            $modifiedData = $this->handleModification($class, 'salsify:id', $mappings['salsify:id'], $modifiedData);
338
        }
339
        $obj = DataObject::get($class)->filter([
340
            'SalsifyID' => $modifiedData['salsify:id'],
341
        ])->first();
342
        if ($obj) {
0 ignored issues
show
introduced by
$obj is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
343
            return $obj;
344
        }
345
346
        return false;
347
    }
348
349
    /**
350
     * Gets a list of all the unique field keys
351
     *
352
     * @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...
353
     * @param array $mappings
354
     * @return array
355
     */
356
    private function uniqueFields($class, $mappings)
357
    {
358
        // cached after first map
359
        if (array_key_exists($class, $this->currentUniqueFields) && !empty($this->currentUniqueFields[$class])) {
360
            return $this->currentUniqueFields[$class];
361
        }
362
363
        $uniqueFields = [];
364
        foreach ($this->yieldKeyVal($mappings) as $dbField => $salsifyField) {
365
            if (!is_array($salsifyField)) {
366
                continue;
367
            }
368
369
            if (
370
                !array_key_exists('unique', $salsifyField) ||
371
                !array_key_exists('salsifyField', $salsifyField)
372
            ) {
373
                continue;
374
            }
375
376
            if ($salsifyField['unique'] !== true) {
377
                continue;
378
            }
379
380
            $uniqueFields[$dbField] = $salsifyField['salsifyField'];
381
        }
382
383
        $this->currentUniqueFields[$class] = $uniqueFields;
384
        return $uniqueFields;
385
    }
386
387
    /**
388
     * @return bool|mixed
389
     */
390
    private function getSortColumn($salsifyField)
391
    {
392
        if (!is_array($salsifyField)) {
393
            return false;
394
        }
395
396
        if (array_key_exists('sortColumn', $salsifyField)) {
397
            return $salsifyField['sortColumn'];
398
        }
399
400
        return false;
401
    }
402
403
    /**
404
     * @return bool
405
     */
406
    private function mappingHasSalsifyRelation()
407
    {
408
        foreach ($this->yieldKeyVal($this->config()->get('mapping')) as $class => $mappings) {
409
            if ($this->classConfigHasSalsifyRelation($mappings)) {
410
                return true;
411
            }
412
        }
413
        return false;
414
    }
415
416
    /**
417
     * @param array $classConfig
418
     * @return bool
419
     */
420
    private function classConfigHasSalsifyRelation($classConfig)
421
    {
422
        foreach ($this->yieldKeyVal($classConfig) as $field => $config) {
423
            if (!is_array($config)) {
424
                continue;
425
            }
426
427
            if (!array_key_exists('salsifyField', $config)) {
428
                continue;
429
            }
430
431
            if (!array_key_exists('type', $config)) {
432
                continue;
433
            }
434
435
            if ($config['type'] === 'SalsifyRelation') {
436
                return true;
437
            }
438
        }
439
        return false;
440
    }
441
442
    /**
443
     * @param string $class
444
     * @param string $dbField
445
     * @param array $config
446
     * @param array $data
447
     * @return array
448
     */
449
    private function handleModification($class, $dbField, $config, $data)
450
    {
451
        if (!is_array($config)) {
0 ignored issues
show
introduced by
The condition is_array($config) is always true.
Loading history...
452
            return $data;
453
        }
454
455
        if (array_key_exists('modification', $config)) {
456
            $mod = $config['modification'];
457
            if ($this->hasMethod($mod)) {
458
                return $this->{$mod}($class, $dbField, $config, $data);
459
            }
460
            ImportTask::output("{$mod} is not a valid field modifier. skipping modification for field {$dbField}.");
461
        }
462
        return $data;
463
    }
464
465
    /**
466
     * @param string $class
467
     * @param string $dbField
468
     * @param array $config
469
     * @param array $data
470
     * @return boolean
471
     */
472
    private function handleShouldSkip($class, $dbField, $config, $data)
473
    {
474
        if (!is_array($config)) {
0 ignored issues
show
introduced by
The condition is_array($config) is always true.
Loading history...
475
            return false;
476
        }
477
478
        if (array_key_exists('shouldSkip', $config)) {
479
            $skipMethod = $config['shouldSkip'];
480
            if ($this->hasMethod($skipMethod)) {
481
                return $this->{$skipMethod}($class, $dbField, $config, $data);
482
            }
483
            ImportTask::output(
484
                "{$skipMethod} is not a valid skip test method. Skipping skip test for field {$dbField}."
485
            );
486
        }
487
        return false;
488
    }
489
490
    /**
491
     * @param string|array $field
492
     * @return string
493
     */
494
    public function getFieldType($field)
495
    {
496
        $fieldTypes = $this->config()->get('field_types');
497
        if (is_array($field) && array_key_exists('type', $field)) {
498
            if (in_array($field['type'], $fieldTypes)) {
499
                return $field['type'];
500
            }
501
        }
502
        // default to raw
503
        return 'Raw';
504
    }
505
506
    /**
507
     * @param int $type
508
     * @param string|DataObject $class
509
     * @param array $salsifyData
510
     * @param string $salsifyField
511
     * @param array $dbFieldConfig
512
     * @param string $dbField
513
     *
514
     * @return mixed
515
     */
516
    private function handleType($type, $class, $salsifyData, $salsifyField, $dbFieldConfig, $dbField)
517
    {
518
        if ($this->hasMethod("handle{$type}Type")) {
519
            return $this->{"handle{$type}Type"}($class, $salsifyData, $salsifyField, $dbFieldConfig, $dbField);
520
        }
521
        ImportTask::output("{$type} is not a valid type. skipping field {$dbField}.");
522
        return '';
523
    }
524
525
    /**
526
     * @param DataObject $object
527
     * @param string $dbField
528
     * @param mixed $value
529
     * @param string|bool $sortColumn
530
     *
531
     * @throws \Exception
532
     */
533
    private function writeValue($object, $dbField, $value, $sortColumn)
534
    {
535
        $isManyRelation = array_key_exists($dbField, $object->config()->get('has_many')) ||
536
            array_key_exists($dbField, $object->config()->get('many_many')) ||
537
            array_key_exists($dbField, $object->config()->get('belongs_many_many'));
538
539
        if (!$isManyRelation) {
540
            $object->$dbField = $value;
541
            return;
542
        }
543
544
        // write the object so relations can be written
545
        if (!$object->exists()) {
546
            $object->write();
547
        }
548
549
        // change to an array and filter out empty values
550
        if (!is_array($value)) {
551
            $value = [$value];
552
        }
553
        $value = array_filter($value);
554
555
        $this->removeUnrelated($object, $dbField, $value);
556
        $this->writeManyRelation($object, $dbField, $value, $sortColumn);
557
    }
558
559
    /**
560
     * @param DataObject $object
561
     * @param string $dbField
562
     * @param array $value
563
     * @param string|bool $sortColumn
564
     *
565
     * @throws \Exception
566
     */
567
    private function writeManyRelation($object, $dbField, $value, $sortColumn)
568
    {
569
        /** @var DataList|HasManyList|ManyManyList $relation */
570
        $relation = $object->{$dbField}();
571
572
        if ($sortColumn && $relation instanceof ManyManyList) {
573
            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...
574
                $relation->add($value[$i], [$sortColumn => $i]);
575
            }
576
            return;
577
        }
578
579
        // HasManyList, so it exists on the value
580
        if ($sortColumn) {
581
            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...
582
                $value[$i]->{$sortColumn} = $i;
583
                $relation->add($value[$i]);
584
            }
585
            return;
586
        }
587
588
        $relation->addMany($value);
589
    }
590
591
    /**
592
     * Removes unrelated objects in the relation that were previously related
593
     * @param DataObject $object
594
     * @param string $dbField
595
     * @param array $value
596
     */
597
    private function removeUnrelated($object, $dbField, $value)
598
    {
599
        $ids = [];
600
        foreach ($value as $v) {
601
            $ids[] = $v->ID;
602
        }
603
604
        /** @var DataList $relation */
605
        $relation = $object->{$dbField}();
606
        // remove all unrelated - removeAll had an odd side effect (relations only got added back half the time)
607
        if (!empty($ids)) {
608
            $relation->removeMany(
609
                $relation->exclude([
610
                    'ID' => $ids,
611
                ])->column('ID')
612
            );
613
        }
614
    }
615
616
    /**
617
     * @return \JsonMachine\JsonMachine
618
     */
619
    public function getAssetStream()
620
    {
621
        return $this->assetStream;
622
    }
623
624
    /**
625
     * @return bool
626
     */
627
    public function hasFile()
628
    {
629
        return $this->file !== null;
630
    }
631
}
632