ProductBulkLoader   C
last analyzed

Complexity

Total Complexity 57

Size/Duplication

Total Lines 306
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 168
dl 0
loc 306
rs 5.04
c 0
b 0
f 0
wmc 57

13 Methods

Rating   Name   Duplication   Size   Complexity  
A processVariation3() 0 3 1
A processVariation2() 0 3 1
A processVariation1() 0 3 1
A processVariation4() 0 3 1
A processVariation5() 0 3 1
A processVariation6() 0 3 1
A processRecord() 0 6 4
C variationRow() 0 61 15
A setContent() 0 6 2
C processVariation() 0 30 12
B processAll() 0 31 8
A setParent() 0 22 5
A imageByFilename() 0 17 5

How to fix   Complexity   

Complex Class

Complex classes like ProductBulkLoader 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 ProductBulkLoader, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace SilverShop\Admin;
4
5
use SilverShop\Model\Variation\AttributeType;
6
use SilverShop\Model\Variation\Variation;
7
use SilverShop\Page\ProductCategory;
8
use SilverStripe\Assets\Image;
9
use SilverStripe\Core\Convert;
10
use SilverStripe\Dev\CsvBulkLoader;
11
use SilverStripe\ORM\ArrayList;
12
13
/**
14
 * ProductBulkLoader - allows loading products via CSV file.
15
 *
16
 * Images should be uploaded before import, where the Photo/Image field
17
 * corresponds to the filename of a file that was uploaded.
18
 *
19
 * Variations can be specified in a "Variation" column this format:
20
 * Type:value,value,value
21
 * eg: Color: red, green, blue , yellow
22
 * up to 6 other variation columns can be specified by adding a number to the end, eg Variation2,$Variation3
23
 *
24
 * @package    shop
25
 * @subpackage cms
26
 */
27
class ProductBulkLoader extends CsvBulkLoader
28
{
29
    /**
30
     * You can force every product to be in a certain category, as long as you know its ID.
31
     *
32
     * @config
33
     * @var    null
34
     */
35
    private static $parent_page_id = null;
36
37
    /**
38
     * Set this if you want categories to be created if they don't exist.
39
     *
40
     * @config
41
     * @var    bool
42
     */
43
    protected static $create_new_product_groups = false;
44
45
    protected $foundParentId = null;
46
47
48
    // NB do NOT use functional indirection on any fields where they
49
    // will be used in $duplicateChecks as well - they simply don't work.
50
    public $columnMap = [
51
        'Price' => 'BasePrice',
52
53
        'Category' => '->setParent',
54
        'ProductGroup' => '->setParent',
55
        'ProductCategory' => '->setParent',
56
57
        'Product ID' => 'InternalItemID',
58
        'ProductID' => 'InternalItemID',
59
        'SKU' => 'InternalItemID',
60
61
        'Description' => '->setContent',
62
        'Long Description' => '->setContent',
63
        'Short Description' => 'MetaDescription',
64
65
        'Short Title' => 'MenuTitle',
66
67
        'Title' => 'Title',
68
        'Page name' => 'Title',
69
        'Page Name' => 'Title',
70
71
        'Variation' => '->processVariation',
72
        'Variation1' => '->processVariation1',
73
        'Variation2' => '->processVariation2',
74
        'Variation3' => '->processVariation3',
75
        'Variation4' => '->processVariation4',
76
        'Variation5' => '->processVariation5',
77
        'Variation6' => '->processVariation6',
78
79
        'VariationID' => '->variationRow',
80
        'Variation ID' => '->variationRow',
81
        'SubID' => '->variationRow',
82
        'Sub ID' => '->variationRow',
83
    ];
84
85
    public $duplicateChecks = [
86
        'InternalItemID' => 'InternalItemID',
87
        'SKU' => 'InternalItemID',
88
        'Product ID' => 'InternalItemID',
89
        'ProductID' => 'InternalItemID',
90
        'Title' => 'Title',
91
        'Page Title' => 'Title',
92
        'PageTitle' => 'Title',
93
    ];
94
95
    public $relationCallbacks = [
96
        'Image' => [
97
            'relationname' => 'Image', // relation accessor name
98
            'callback' => 'imageByFilename',
99
        ],
100
        'Photo' => [
101
            'relationname' => 'Image', // relation accessor name
102
            'callback' => 'imageByFilename',
103
        ],
104
    ];
105
106
    protected function processAll($filepath, $preview = false)
107
    {
108
        $this->extend('updateColumnMap', $this->columnMap);
109
110
        $results = parent::processAll($filepath, $preview);
111
        //After results have been processed, publish all created & updated products
112
        $objects = ArrayList::create();
113
        $objects->merge($results->Created());
114
        $objects->merge($results->Updated());
115
        $parentPageID = $this->config()->parent_page_id;
116
        foreach ($objects as $object) {
117
            if (!$object->ParentID) {
118
                //set parent page
119
                if (is_numeric($parentPageID) && ProductCategory::get()->byID($parentPageID)) { //cached option
120
                    $object->ParentID = $parentPageID;
121
                } elseif ($parentPage = ProductCategory::get()->filter('Title', 'Products')->sort('Created', 'DESC')->first()) { //page called 'Products'
122
                    $object->ParentID = $parentPageID = $parentPage->ID;
123
                } elseif ($parentpage = ProductCategory::get()->filter('ParentID', 0)->sort('Created', 'DESC')->first()) { //root page
124
                    $object->ParentID = $parentPageID = $parentpage->ID;
125
                } elseif ($parentpage = ProductCategory::get()->sort('Created', 'DESC')->first()) { //any product page
126
                    $object->ParentID = $parentPageID = $parentpage->ID;
127
                } else {
128
                    $object->ParentID = $parentPageID = 0;
129
                }
130
            }
131
            $this->foundParentId = $parentPageID;
132
            $object->extend('updateImport'); //could be used for setting other attributes, such as stock level
133
            $object->writeToStage('Stage');
134
            $object->publishSingle();
135
        }
136
        return $results;
137
    }
138
139
    public function processRecord($record, $columnMap, &$results, $preview = false)
140
    {
141
        if (!$record || !isset($record['Title']) || $record['Title'] == '') { //TODO: make required fields customisable
142
            return null;
143
        }
144
        return parent::processRecord($record, $columnMap, $results, $preview);
145
    }
146
147
    // set image, based on filename
148
    public function imageByFilename(&$obj, $val)
149
    {
150
        $filename = trim(strtolower(Convert::raw2sql($val)));
0 ignored issues
show
Bug introduced by
It seems like SilverStripe\Core\Convert::raw2sql($val) can also be of type array and array; however, parameter $string of strtolower() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

150
        $filename = trim(strtolower(/** @scrutinizer ignore-type */ Convert::raw2sql($val)));
Loading history...
151
        $filenamedashes = str_replace(' ', '-', $filename);
152
        if ($filename
153
            && $image = Image::get()->whereAny(
154
                [
155
                    "LOWER(\"FileFilename\") LIKE '%$filename%'",
156
                    "LOWER(\"FileFilename\") LIKE '%$filenamedashes%'"
157
                ]
158
            )->first()
159
        ) { //ignore case
160
            if ($image instanceof Image && $image->exists()) {
161
                return $image;
162
            }
163
        }
164
        return null;
165
    }
166
167
    // find product group parent (ie Cateogry)
168
    public function setParent(&$obj, $val)
169
    {
170
        $title = strtolower(Convert::raw2sql($val));
0 ignored issues
show
Bug introduced by
It seems like SilverStripe\Core\Convert::raw2sql($val) can also be of type array and array; however, parameter $string of strtolower() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

170
        $title = strtolower(/** @scrutinizer ignore-type */ Convert::raw2sql($val));
Loading history...
171
        if ($title) {
172
            // find or create parent category, if provided
173
            if ($parentPage = ProductCategory::get()->where(['LOWER("Title") = ?' => $title])->sort('Created', 'DESC')->first()) {
174
                $obj->ParentID = $parentPage->ID;
175
                $obj->write();
176
                $obj->writeToStage('Stage');
177
                $obj->publishSingle();
178
                //TODO: otherwise assign it to the first product group found
179
            } elseif ($this->config()->create_new_product_groups) {
180
                //create parent product group
181
                $pg = ProductCategory::create();
182
                $pg->setTitle($title);
183
                $pg->ParentID = ($this->foundParentId) ? $this->foundParentId : 0;
184
                $pg->writeToStage('Stage');
185
                $pg->publishSingle();
186
                $obj->ParentID = $pg->ID;
187
                $obj->write();
188
                $obj->writeToStage('Stage');
189
                $obj->publishSingle();
190
            }
191
        }
192
    }
193
194
    /**
195
     * Adds paragraphs to content.
196
     */
197
    public function setContent(&$obj, $val, $record)
0 ignored issues
show
Unused Code introduced by
The parameter $record is not used and could be removed. ( Ignorable by Annotation )

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

197
    public function setContent(&$obj, $val, /** @scrutinizer ignore-unused */ $record)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
198
    {
199
        $val = trim($val);
200
        if ($val) {
201
            $paragraphs = explode("\n", $val);
202
            $obj->Content = '<p>' . implode('</p><p>', $paragraphs) . '</p>';
203
        }
204
    }
205
206
    public function processVariation(&$obj, $val, $record)
207
    {
208
        if (isset($record['->variationRow'])) {
209
            return;
210
        } //don't use this technique for variation rows
211
        $parts = explode(':', $val);
212
        if (count($parts) == 2) {
213
            $attributetype = trim($parts[0]);
214
            $attributevalues = explode(',', $parts[1]);
215
            //get rid of empty values
216
            foreach ($attributevalues as $key => $value) {
217
                if (!$value || trim($value) == '') {
218
                    unset($attributevalues[$key]);
219
                }
220
            }
221
            if (count($attributevalues) >= 1) {
222
                $attributetype = AttributeType::find_or_make($attributetype);
223
                foreach ($attributevalues as $key => $value) {
224
                    $val = trim($value);
225
                    if ($val != '' && $val != null) {
226
                        $attributevalues[$key] = $val; //remove outside spaces from values
227
                    }
228
                }
229
                $attributetype->addValues($attributevalues);
230
                $obj->VariationAttributeTypes()->add($attributetype);
231
                //only generate variations if none exist yet
232
                if (!$obj->Variations()->exists() || $obj->WeAreBuildingVariations) {
233
                    //either start new variations, or multiply existing ones by new variations
234
                    $obj->generateVariationsFromAttributes($attributetype, $attributevalues);
235
                    $obj->WeAreBuildingVariations = true;
236
                }
237
            }
238
        }
239
    }
240
241
    //work around until I can figure out how to allow calling processVariation multiple times
242
    public function processVariation1(&$obj, $val, $record)
243
    {
244
        $this->processVariation($obj, $val, $record);
245
    }
246
247
    public function processVariation2(&$obj, $val, $record)
248
    {
249
        $this->processVariation($obj, $val, $record);
250
    }
251
252
    public function processVariation3(&$obj, $val, $record)
253
    {
254
        $this->processVariation($obj, $val, $record);
255
    }
256
257
    public function processVariation4(&$obj, $val, $record)
258
    {
259
        $this->processVariation($obj, $val, $record);
260
    }
261
262
    public function processVariation5(&$obj, $val, $record)
263
    {
264
        $this->processVariation($obj, $val, $record);
265
    }
266
267
    public function processVariation6(&$obj, $val, $record)
268
    {
269
        $this->processVariation($obj, $val, $record);
270
    }
271
272
    public function variationRow(&$obj, $val, $record)
273
    {
274
275
        $obj->write(); //make sure product is in DB
276
        //TODO: or find existing variation
277
        $variation = Variation::get()->filter('InternalItemID', $val)->first();
278
        if (!$variation) {
279
            $variation = Variation::create();
280
            $variation->InternalItemID = $val;
281
            $variation->ProductID = $obj->ID; //link to product
282
            $variation->write();
283
        }
284
        $varcols = [
285
            '->processVariation',
286
            '->processVariation1',
287
            '->processVariation2',
288
            '->processVariation3',
289
            '->processVariation4',
290
            '->processVariation5',
291
            '->processVariation6',
292
        ];
293
        foreach ($varcols as $col) {
294
            if (isset($record[$col])) {
295
                $parts = explode(':', $record[$col]);
296
                if (count($parts) == 2) {
297
                    $attributetype = trim($parts[0]);
298
                    $attributevalues = explode(',', $parts[1]);
299
                    //get rid of empty values
300
                    foreach ($attributevalues as $key => $value) {
301
                        if (!$value || trim($value) == '') {
302
                            unset($attributevalues[$key]);
303
                        }
304
                    }
305
                    if (count($attributevalues) == 1) {
306
                        $attributetype = AttributeType::find_or_make($attributetype);
307
                        foreach ($attributevalues as $key => $value) {
308
                            $val = trim($value);
309
                            if ($val != '' && $val != null) {
310
                                $attributevalues[$key] = $val; //remove outside spaces from values
311
                            }
312
                        }
313
                        $attributetype->addValues($attributevalues); //create and add values to attribute type
314
                        $obj->VariationAttributeTypes()->add($attributetype); //add variation attribute type to product
315
                        //TODO: if existing variation, then remove current values
316
                        //record vairation attribute values (variation1, 2 etc)
317
                        foreach ($attributetype->convertArrayToValues($attributevalues) as $value) {
318
                            $variation->AttributeValues()->add($value);
319
                            break;
320
                        }
321
                    }
322
                }
323
            }
324
        }
325
        //copy db values into variation (InternalItemID, Price, Stock, etc) ...there will be unknowns from extensions.
326
        $dbfields = $variation->getSchema()->fieldSpecs(Variation::class);
327
        foreach ($record as $field => $value) {
328
            if (isset($dbfields[$field])) {
329
                $variation->$field = $value;
330
            }
331
        }
332
        $variation->write();
333
    }
334
}
335