Passed
Pull Request — master (#40)
by Matthew
02:25
created

Mapper::resetAssetStream()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Dynamic\Salsify\Model;
4
5
use Dyanmic\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
        ImportTask::output("Imported and updated $this->importCount products.");
94
    }
95
96
    /**
97
     * @param string|DataObject $class
98
     * @param array $mappings The mapping for a specific class
99
     * @param array $data
100
     * @param DataObject|null $object
101
     *
102
     * @return DataObject
103
     * @throws \Exception
104
     */
105
    public function mapToObject($class, $mappings, $data, $object = null)
106
    {
107
        // if object was not passed
108
        if ($object === null) {
109
            $object = $this->findObjectByUnique($class, $mappings, $data);
110
            // if no existing object was found
111
            if (!$object) {
0 ignored issues
show
introduced by
$object is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
112
                $object = $class::create();
113
            }
114
        }
115
116
        $wasPublished = $object->hasExtension(Versioned::class) ? $object->isPublished() : false;
117
        $wasWritten = $object->isInDB();
118
119
        $firstUniqueKey = array_keys($this->uniqueFields($class, $mappings))[0];
120
        if (array_key_exists($mappings[$firstUniqueKey]['salsifyField'], $data)) {
121
            $firstUniqueValue = $data[$mappings[$firstUniqueKey]['salsifyField']];
122
        } else {
123
            $firstUniqueValue = 'NULL';
124
        }
125
        ImportTask::output("Updating $class $firstUniqueKey $firstUniqueValue");
126
127
        if ($this->objectUpToDate($object, $data, $firstUniqueKey, $firstUniqueValue)) {
128
            return $object;
129
        }
130
131
        foreach ($this->yieldKeyVal($mappings) as $dbField => $salsifyField) {
132
            $field = $this->getField($salsifyField, $data);
133
            if ($field === false) {
134
                continue;
135
            }
136
137
            $value = null;
138
            $type = $this->getFieldType($salsifyField);
139
            $objectData = $data;
0 ignored issues
show
Unused Code introduced by
The assignment to $objectData is dead and can be removed.
Loading history...
140
141
            if ($this->handleShouldSkip($class, $dbField, $salsifyField, $data)) {
142
                if (!$this->skipSiliently) {
143
                    ImportTask::output("Skipping $class $firstUniqueKey $firstUniqueValue");
144
                    $this->skipSiliently = false;
145
                }
146
                return null;
147
            };
148
149
            $objectData = $this->handleModification($class, $dbField, $salsifyField, $data);
150
            $sortColumn = $this->getSortColumn($salsifyField);
151
152
            if (!array_key_exists($field, $objectData)) {
153
                continue;
154
            }
155
156
            $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

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