Passed
Pull Request — master (#39)
by Matthew
01:58
created

Mapper::handleType()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 4
c 0
b 0
f 0
dl 0
loc 7
rs 10
cc 2
nc 2
nop 6
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\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...
13
14
/**
15
 * Class Mapper
16
 * @package Dynamic\Salsify\Model
17
 */
18
class Mapper extends Service
19
{
20
21
    public const STOP_GENERATOR = 'stop';
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->resetProductSteam();
69
            $this->resetAssetStream();
70
        }
71
    }
72
73
    /**
74
     *
75
     */
76
    protected function resetAssetStream()
77
    {
78
        $this->assetStream = JsonMachine::fromFile($this->file, '/3/digital_assets');
79
    }
80
81
    /**
82
     *
83
     */
84
    protected function resetProductSteam()
85
    {
86
        $this->productStream = JsonMachine::fromFile($this->file, '/4/products');
87
    }
88
89
    /**
90
     * @return \Generator|void
91
     * @throws Exception
92
     */
93
    public function getAssets()
94
    {
95
        foreach ($this->assetStream as $name => $data) {
96
            $injected = (yield $name => $data);
97
            if ($injected === static::STOP_GENERATOR) break;
98
        }
99
        $this->resetAssetStream();
100
    }
101
102
    /**
103
     * @return \Generator|void
104
     * @throws Exception
105
     */
106
    public function getProducts()
107
    {
108
        foreach ($this->productStream as $name => $data) {
109
            $injected = yield $name => $data;
110
            if ($injected === static::STOP_GENERATOR) break;
111
        }
112
        $this->resetProductSteam();
113
    }
114
115
    /**
116
     * @return \Generator
117
     */
118
    protected function getMappings()
119
    {
120
        foreach ($this->config()->get('mapping') as $class => $mappings) {
121
            yield $class => $mappings;
122
        }
123
    }
124
125
    /**
126
     * Maps the data
127
     * @throws \Exception
128
     */
129
    public function map()
130
    {
131
        foreach ($this->yieldKeyVal($this->productStream) as $name => $data) {
132
            foreach ($this->yieldKeyVal($this->config()->get('mapping')) as $class => $mappings) {
133
                $this->mapToObject($class, $mappings, $data);
134
                $this->currentUniqueFields = [];
135
            }
136
        }
137
        ImportTask::output("Imported and updated $this->importCount products.");
138
    }
139
140
    /**
141
     * @param $mappings
142
     * @return \Generator
143
     */
144
    protected function getFieldMappings($mappings)
145
    {
146
        foreach ($mappings as $dbField => $salsifyField) {
147
            yield $dbField => $salsifyField;
148
        }
149
    }
150
151
    /**
152
     * @param string|DataObject $class
153
     * @param array $mappings The mapping for a specific class
154
     * @param array $data
155
     * @param DataObject|null $object
156
     *
157
     * @return DataObject
158
     * @throws \Exception
159
     */
160
    public function mapToObject($class, $mappings, $data, $object = null)
161
    {
162
        // if object was not passed
163
        if ($object === null) {
164
            $object = $this->findObjectByUnique($class, $mappings, $data);
165
            // if no existing object was found
166
            if (!$object) {
0 ignored issues
show
introduced by
$object is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
167
                $object = $class::create();
168
            }
169
        }
170
171
        $wasPublished = $object->hasExtension(Versioned::class) ? $object->isPublished() : false;
172
        $wasWritten = $object->isInDB();
173
174
        $firstUniqueKey = array_keys($this->uniqueFields($class, $mappings))[0];
175
        if (array_key_exists($mappings[$firstUniqueKey]['salsifyField'], $data)) {
176
            $firstUniqueValue = $data[$mappings[$firstUniqueKey]['salsifyField']];
177
        } else {
178
            $firstUniqueValue = 'NULL';
179
        }
180
        ImportTask::output("Updating $class $firstUniqueKey $firstUniqueValue");
181
182
        if ($this->objectUpToDate($object, $data, $firstUniqueKey, $firstUniqueValue)) {
183
            return $object;
184
        }
185
186
187
        foreach ($this->yieldKeyVal($mappings) as $dbField => $salsifyField) {
188
            $field = $this->getField($salsifyField, $data);
189
            if ($field === false) {
190
                continue;
191
            }
192
193
            $value = null;
194
            $type = $this->getFieldType($salsifyField);
195
            $objectData = $data;
196
197
            if (is_array($salsifyField)) {
198
                if ($this->handleShouldSkip($class, $dbField, $salsifyField, $data)) {
199
                    if (!$this->skipSiliently) {
200
                        ImportTask::output("Skipping $class $firstUniqueKey $firstUniqueValue");
201
                        $this->skipSiliently = false;
202
                    }
203
                    return null;
204
                };
205
206
                $objectData = $this->handleModification($class, $dbField, $salsifyField, $data);
207
            }
208
209
            if (!array_key_exists($field, $objectData)) {
210
                continue;
211
            }
212
213
            $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

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