Passed
Push — master ( 97cc2f...0a9015 )
by Matthew
03:35 queued 01:34
created

Mapper::classConfigHasSalsifyRelation()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 10
dl 0
loc 20
rs 9.2222
c 0
b 0
f 0
cc 6
nc 6
nop 1
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->productStream = JsonMachine::fromFile($file, '/4/products');
69
            $this->resetAssetStream();
70
        }
71
    }
72
73
    /**
74
     *
75
     */
76
    public function resetAssetStream()
77
    {
78
        $this->assetStream = JsonMachine::fromFile($this->file, '/3/digital_assets');
79
    }
80
81
    /**
82
     * Maps the data
83
     * @throws \Exception
84
     */
85
    public function map()
86
    {
87
        foreach ($this->yieldKeyVal($this->productStream) as $name => $data) {
88
            foreach ($this->yieldKeyVal($this->config()->get('mapping')) as $class => $mappings) {
89
                $this->mapToObject($class, $mappings, $data);
90
                $this->currentUniqueFields = [];
91
            }
92
        }
93
94
        if ($this->mappingHasSalsifyRelation()) {
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, null, true);
98
                    $this->currentUniqueFields = [];
99
                }
100
            }
101
        }
102
        ImportTask::output("Imported and updated $this->importCount products.");
103
    }
104
105
    /**
106
     * @param string|DataObject $class
107
     * @param array $mappings The mapping for a specific class
108
     * @param array $data
109
     * @param DataObject|null $object
110
     * @param bool $salsifyRelations
111
     *
112
     * @return DataObject|null
113
     * @throws \Exception
114
     */
115
    public function mapToObject($class, $mappings, $data, $object = null, $salsifyRelations = false)
116
    {
117
        if ($salsifyRelations) {
118
            if (!$this->classConfigHasSalsifyRelation($mappings)) {
119
                return null;
120
            }
121
        }
122
123
        // if object was not passed
124
        if ($object === null) {
125
            $object = $this->findObjectByUnique($class, $mappings, $data);
126
            // if no existing object was found
127
            if (!$object) {
0 ignored issues
show
introduced by
$object is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
128
                $object = $class::create();
129
            }
130
        }
131
132
        $wasPublished = $object->hasExtension(Versioned::class) ? $object->isPublished() : false;
133
        $wasWritten = $object->isInDB();
134
135
        $firstUniqueKey = array_keys($this->uniqueFields($class, $mappings))[0];
136
        if (array_key_exists($mappings[$firstUniqueKey]['salsifyField'], $data)) {
137
            $firstUniqueValue = $data[$mappings[$firstUniqueKey]['salsifyField']];
138
        } else {
139
            $firstUniqueValue = 'NULL';
140
        }
141
        ImportTask::output("Updating $class $firstUniqueKey $firstUniqueValue");
142
143
        if ($this->objectUpToDate($object, $data, $firstUniqueKey, $firstUniqueValue, $salsifyRelations)) {
144
            return $object;
145
        }
146
147
        foreach ($this->yieldKeyVal($mappings) as $dbField => $salsifyField) {
148
            $field = $this->getField($salsifyField, $data);
149
            if ($field === false) {
150
                continue;
151
            }
152
153
            $type = $this->getFieldType($salsifyField);
154
            if ($salsifyRelations && $type != 'SalsifyRelation') {
155
                continue;
156
            }
157
158
            $value = null;
159
            $objectData = $data;
0 ignored issues
show
Unused Code introduced by
The assignment to $objectData is dead and can be removed.
Loading history...
160
161
            if ($this->handleShouldSkip($class, $dbField, $salsifyField, $data)) {
162
                if (!$this->skipSiliently) {
163
                    ImportTask::output("Skipping $class $firstUniqueKey $firstUniqueValue");
164
                    $this->skipSiliently = false;
165
                }
166
                return null;
167
            };
168
169
            $objectData = $this->handleModification($class, $dbField, $salsifyField, $data);
170
            $sortColumn = $this->getSortColumn($salsifyField);
171
172
            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...
173
                continue;
174
            }
175
176
            $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

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