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

Mapper   F

Complexity

Total Complexity 60

Size/Duplication

Total Lines 377
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 60
eloc 135
c 2
b 0
f 0
dl 0
loc 377
rs 3.6

15 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 11 3
B mapToObject() 0 48 9
A map() 0 9 3
A resetAssetStream() 0 3 1
A hasFile() 0 3 1
A getAssetStream() 0 3 1
B getField() 0 36 11
A handleShouldSkip() 0 12 3
A findObjectByUnique() 0 16 2
A handleType() 0 7 2
B uniqueFields() 0 29 7
A objectUpToDate() 0 11 4
A handleModification() 0 10 3
A writeValue() 0 21 6
A getFieldType() 0 10 4

How to fix   Complexity   

Complex Class

Complex classes like Mapper often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Mapper, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Dynamic\Salsify\Model;
4
5
use Dynamic\Salsify\Task\ImportTask;
6
use Exception;
7
use JsonMachine\JsonMachine;
8
use SilverStripe\ORM\DataObject;
9
10
/**
11
 * Class Mapper
12
 * @package Dynamic\Salsify\Model
13
 */
14
class Mapper extends Service
15
{
16
17
    /**
18
     * @var
19
     */
20
    private $file = null;
21
22
    /**
23
     * @var JsonMachine
24
     */
25
    private $productStream;
26
27
    /**
28
     * @var JsonMachine
29
     */
30
    private $assetStream;
31
32
    /**
33
     * @var array
34
     */
35
    private $currentUniqueFields;
36
37
    /**
38
     * @var int
39
     */
40
    private $importCount = 0;
41
42
    /**
43
     * @var bool
44
     */
45
    public $skipSiliently = false;
46
47
    /**
48
     * Mapper constructor.
49
     * @param string $importerKey
50
     * @param $file
51
     * @throws \Exception
52
     */
53
    public function __construct($importerKey, $file = null)
54
    {
55
        parent::__construct($importerKey);
56
        if (!$this->config()->get('mapping')) {
57
            throw  new Exception('A Mapper needs a mapping');
58
        }
59
60
        if ($file !== null) {
61
            $this->file = $file;
62
            $this->productStream = JsonMachine::fromFile($file, '/4/products');
63
            $this->resetAssetStream();
64
        }
65
    }
66
67
    /**
68
     *
69
     */
70
    public function resetAssetStream()
71
    {
72
        $this->assetStream = JsonMachine::fromFile($this->file, '/3/digital_assets');
73
    }
74
75
    /**
76
     * Maps the data
77
     * @throws \Exception
78
     */
79
    public function map()
80
    {
81
        foreach ($this->productStream as $name => $data) {
82
            foreach ($this->config()->get('mapping') as $class => $mappings) {
83
                $this->mapToObject($class, $mappings, $data);
84
                $this->currentUniqueFields = [];
85
            }
86
        }
87
        ImportTask::output("Imported and updated $this->importCount products.");
88
    }
89
90
    /**
91
     * @param string|DataObject $class
92
     * @param array $mappings The mapping for a specific class
93
     * @param array $data
94
     *
95
     * @return DataObject
96
     * @throws \Exception
97
     */
98
    public function mapToObject($class, $mappings, $data)
99
    {
100
        $object = $this->findObjectByUnique($class, $mappings, $data);
101
        if (!$object) {
0 ignored issues
show
introduced by
$object is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
102
            $object = $class::create();
103
        }
104
105
        $firstUniqueKey = array_keys($this->uniqueFields($mappings))[0];
106
        $firstUniqueValue = $data[$mappings[$firstUniqueKey]['salsifyField']];
107
        ImportTask::output("Updating $firstUniqueKey $firstUniqueValue");
108
109
        if ($this->objectUpToDate($object, $data, $firstUniqueKey, $firstUniqueValue)) {
110
            return $object;
111
        }
112
113
        foreach ($mappings as $dbField => $salsifyField) {
114
            $field = $this->getField($salsifyField, $data);
115
            if ($field === false) {
116
                continue;
117
            }
118
            
119
            $value = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $value is dead and can be removed.
Loading history...
120
            $type = $this->getFieldType($salsifyField);
121
            $objectData = $data;
122
123
            if (is_array($salsifyField)) {
124
                if ($this->handleShouldSkip($dbField, $salsifyField, $data)) {
125
                    if (!$this->skipSiliently) {
126
                        ImportTask::output("Skipping $firstUniqueKey $firstUniqueValue");
127
                        $this->skipSiliently = false;
128
                    }
129
                    return null;
130
                };
131
132
                $objectData = $this->handleModification($dbField, $salsifyField, $data);
133
            }
134
135
            $value = $this->handleType($type, $objectData, $field, $salsifyField, $dbField, $class);
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

135
            $value = $this->handleType(/** @scrutinizer ignore-type */ $type, $objectData, $field, $salsifyField, $dbField, $class);
Loading history...
136
            $this->writeValue($object, $dbField, $value);
137
        }
138
139
        if ($object->isChanged()) {
140
            $object->write();
141
            $this->importCount++;
142
        } else {
143
            ImportTask::output("$firstUniqueKey $firstUniqueValue was not changed.");
144
        }
145
        return $object;
146
    }
147
148
    /**
149
     * @param DataObject $object
150
     * @param array $data
151
     * @param string $firstUniqueKey
152
     * @param string $firstUniqueValue
153
     * @return bool
154
     */
155
    private function objectUpToDate($object, $data, $firstUniqueKey, $firstUniqueValue)
156
    {
157
        if (
158
            $this->config()->get('skipUpToDate') == true &&
159
            $object->hasField('SalsifyUpdatedAt') &&
160
            $data['salsify:updated_at'] == $object->getField('SalsifyUpdatedAt')
161
        ) {
162
            ImportTask::output("Skipping $firstUniqueKey $firstUniqueValue. It is up to Date.");
163
            return true;
164
        }
165
        return false;
166
    }
167
168
    /**
169
     * @param array $salsifyField
170
     * @param array $data
171
     *
172
     * @return string|false
173
     */
174
    private function getField($salsifyField, $data)
175
    {
176
        if (!is_array($salsifyField)) {
0 ignored issues
show
introduced by
The condition is_array($salsifyField) is always true.
Loading history...
177
            return array_key_exists($salsifyField, $data) ? $salsifyField : false;
178
        }
179
180
        $hasSalsifyField = array_key_exists('salsifyField', $salsifyField);
181
        $isLiteralField = (
182
            $this->getFieldType($salsifyField) === 'Literal' &&
183
            array_key_exists('value', $salsifyField)
184
        );
185
186
        if ($isLiteralField) {
187
            return $salsifyField['value'];
188
        }
189
190
        if (!$hasSalsifyField) {
191
            return false;
192
        }
193
194
        if (array_key_exists($salsifyField['salsifyField'], $data)) {
195
            return $salsifyField['salsifyField'];
196
        } elseif (array_key_exists('fallback', $salsifyField)) {
197
            // make fallback an array
198
            if (!is_array($salsifyField['fallback'])) {
199
                $salsifyField['fallback'] = [$salsifyField['fallback']];
200
            }
201
202
            foreach ($salsifyField['fallback'] as $fallback) {
203
                if (array_key_exists($fallback, $data)) {
204
                    return $fallback;
205
                }
206
            }
207
        }
208
209
        return false;
210
    }
211
212
    /**
213
     * @param string $class
214
     * @param array $mappings
215
     * @param array $data
216
     *
217
     * @return \SilverStripe\ORM\DataObject
218
     */
219
    private function findObjectByUnique($class, $mappings, $data)
220
    {
221
        $uniqueFields = $this->uniqueFields($mappings);
222
        // creates a filter
223
        $filter = [];
224
        foreach ($uniqueFields as $dbField => $salsifyField) {
225
            $modifiedData = $data;
226
            $fieldMapping = $mappings[$dbField];
227
228
            $modifiedData = $this->handleModification($dbField, $fieldMapping, $modifiedData);
229
230
            // adds unique fields to filter
231
            $filter[$dbField] = $modifiedData[$salsifyField];
232
        }
233
234
        return DataObject::get($class)->filter($filter)->first();
235
    }
236
237
    /**
238
     * Gets a list of all the unique field keys
239
     *
240
     * @param array $mappings
241
     * @return array
242
     */
243
    private function uniqueFields($mappings)
244
    {
245
        // cached after first map
246
        if (!empty($this->currentUniqueFields)) {
247
            return $this->currentUniqueFields;
248
        }
249
250
        $uniqueFields = [];
251
        foreach ($mappings as $dbField => $salsifyField) {
252
            if (!is_array($salsifyField)) {
253
                continue;
254
            }
255
256
            if (
257
                !array_key_exists('unique', $salsifyField) ||
258
                !array_key_exists('salsifyField', $salsifyField)
259
            ) {
260
                continue;
261
            }
262
263
            if ($salsifyField['unique'] !== true) {
264
                continue;
265
            }
266
267
            $uniqueFields[$dbField] = $salsifyField['salsifyField'];
268
        }
269
270
        $this->currentUniqueFields = $uniqueFields;
271
        return $uniqueFields;
272
    }
273
274
    /**
275
     * @param string $dbField
276
     * @param array $config
277
     * @param array $data
278
     * @return array
279
     */
280
    private function handleModification($dbField, $config, $data)
281
    {
282
        if (array_key_exists('modification', $config)) {
283
            $mod = $config['modification'];
284
            if ($this->hasMethod($mod)) {
285
                return $this->{$mod}($dbField, $config, $data);
286
            }
287
            ImportTask::output("{$mod} is not a valid field modifier. skipping modification for field {$dbField}.");
288
        }
289
        return $data;
290
    }
291
292
    /**
293
     * @param string $dbField
294
     * @param array $config
295
     * @param array $data
296
     * @return boolean
297
     */
298
    private function handleShouldSkip($dbField, $config, $data)
299
    {
300
        if (array_key_exists('shouldSkip', $config)) {
301
            $skipMethod = $config['shouldSkip'];
302
            if ($this->hasMethod($skipMethod)) {
303
                return $this->{$skipMethod}($dbField, $config, $data);
304
            }
305
            ImportTask::output(
306
                "{$skipMethod} is not a valid skip test method. Skipping skip test for field {$dbField}."
307
            );
308
        }
309
        return false;
310
    }
311
312
    /**
313
     * @param string|array $field
314
     * @return string
315
     */
316
    public function getFieldType($field)
317
    {
318
        $fieldTypes = $this->config()->get('field_types');
319
        if (is_array($field) && array_key_exists('type', $field)) {
320
            if (in_array($field['type'], $fieldTypes)) {
321
                return $field['type'];
322
            }
323
        }
324
        // default to raw
325
        return 'Raw';
326
    }
327
328
    /**
329
     * @param int $type
330
     * @param array $salsifyData
331
     * @param string $salsifyField
332
     * @param array $dbFieldConfig
333
     * @param string $dbField
334
     * @param string $class
335
     *
336
     * @return mixed
337
     */
338
    private function handleType($type, $salsifyData, $salsifyField, $dbFieldConfig, $dbField, $class)
339
    {
340
        if ($this->hasMethod("handle{$type}Type")) {
341
            return $this->{"handle{$type}Type"}($salsifyData, $salsifyField, $dbFieldConfig, $dbField, $class);
342
        }
343
        ImportTask::output("{$type} is not a valid type. skipping field {$dbField}.");
344
        return '';
345
    }
346
347
    /**
348
     * @param DataObject $object
349
     * @param string $dbField
350
     * @param mixed $value
351
     *
352
     * @throws \Exception
353
     */
354
    private function writeValue($object, $dbField, $value)
355
    {
356
        $isManyRelation = array_key_exists($dbField, $object->config()->get('has_many')) ||
357
            array_key_exists($dbField, $object->config()->get('many_many')) ||
358
            array_key_exists($dbField, $object->config()->get('belongs_many_many'));
359
360
        if (!$isManyRelation) {
361
            $object->$dbField = $value;
362
            return;
363
        }
364
365
        if (!$object->exists()) {
366
            $object->write();
367
        }
368
369
        if (is_array($value)) {
370
            $object->{$dbField}()->addMany($value);
371
            return;
372
        }
373
374
        $object->{$dbField}()->add($value);
375
    }
376
377
    /**
378
     * @return \JsonMachine\JsonMachine
379
     */
380
    public function getAssetStream()
381
    {
382
        return $this->assetStream;
383
    }
384
385
    /**
386
     * @return bool
387
     */
388
    public function hasFile()
389
    {
390
        return $this->file !== null;
391
    }
392
}
393