Passed
Push — master ( c6e689...0729c5 )
by eXeCUT
07:29
created

ModelsExtractor   F

Complexity

Total Complexity 98

Size/Duplication

Total Lines 416
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 98
eloc 240
c 3
b 0
f 0
dl 0
loc 416
rs 2
ccs 0
cts 226
cp 0

10 Methods

Rating   Name   Duplication   Size   Complexity  
A getAttributesNamesForFind() 0 9 3
A triggerOperation() 0 2 1
F getModels() 0 279 70
C saveModel() 0 43 12
A logError() 0 6 2
A isBadRow() 0 2 1
A startOperation() 0 3 1
A reset() 0 2 1
A deleteOldRecords() 0 13 6
A endOperation() 0 3 1

How to fix   Complexity   

Complex Class

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

1
<?php
2
/**
3
 * Created by PhpStorm.
4
 * User: execut
5
 * Date: 12/22/17
6
 * Time: 12:57 PM
7
 */
8
9
namespace execut\import\components;
10
11
12
use execut\import\ModelInterface;
13
use execut\import\Query;
14
use yii\base\Component;
15
use yii\base\Exception;
16
use yii\db\ActiveQuery;
17
use yii\db\ActiveRecord;
18
use yii\helpers\ArrayHelper;
19
20
class ModelsExtractor extends Component
21
{
22
    public $id = '';
23
24
    /**
25
     * @var ActiveQuery $query
26
     */
27
    public $query;
28
    public $scopes = null;
29
    public $isImport = false;
30
    public $attributes = [];
31
    public $scenario = null;
32
    /**
33
     * @var Importer
34
     */
35
    public $importer = null;
36
    protected $modelsByUniqueKey = [];
37
    public $isNoCreate = false;
38
    public $isNoUpdate = false;
39
    public $isDelete = false;
40
    public $deletedIds = [];
41
    public $uniqueKeys = null;
42
43
    public function reset() {
44
        $this->modelsByUniqueKey = [];
45
    }
46
47
    public function deleteOldRecords() {
48
        if ($this->isDelete) {
49
            $ids = $this->deletedIds;
50
            if (!empty($ids)) {
51
                if (count($ids) > 500000) {
52
                    throw new Exception('Many than 500 000 records to delete. Dangerous situation.');
53
                }
54
55
                $modelClass = $this->query->modelClass;
56
                while ($idsPart = array_splice($ids, 0, 65534)) {
57
                    if (count($idsPart) > 0) {
58
                        $modelClass::deleteAll([
59
                            'id' => $idsPart
60
                        ]);
61
                    }
62
                }
63
            }
64
        }
65
    }
66
67
    public function getModels($isSave = true, $isMarkBad = true) {
68
        /**
69
         * @var ActiveRecord $model
70
         */
71
        $this->startOperation('extract');
72
        $whereValues = [];
73
        $relationsModels = [];
74
        $this->startOperation('construct where');
75
        foreach ($this->attributes as $attribute => $attributeParams) {
76
            if (empty($attributeParams['extractor'])) {
77
                $extractorId = $attribute;
78
            } else {
79
                $extractorId = $attributeParams['extractor'];
80
            }
81
82
            if (empty($attributeParams['value']) && ($extractor = $this->importer->getExtractor($extractorId))) {
83
                $models = $extractor->getModels(false, $isMarkBad && !empty($attributeParams['isFind']));
84
                foreach ($this->importer->rows as $rowNbr => $row) {
0 ignored issues
show
Bug Best Practice introduced by
The property rows does not exist on execut\import\components\Importer. Since you implemented __get, consider adding a @property annotation.
Loading history...
85
                    if ($this->isBadRow($rowNbr)) {
86
                        continue;
87
                    }
88
89
                    if (empty($models[$rowNbr])) {
90
                        if ($isMarkBad && !empty($attributeParams['isFind'])) {
91
                            $this->importer->setIsBadRow($rowNbr);
92
                        }
93
94
                        continue;
95
                    }
96
97
                    $model = $models[$rowNbr];
98
99
                    if (empty($relationsModels[$rowNbr])) {
100
                        $relationsModels[$rowNbr] = [];
101
                    }
102
103
                    if (empty($extractor->isNoCreate) || !$model->isNewRecord) {
104
                        $relationsModels[$rowNbr][$attribute] = $model;
105
                    }
106
107
                    if (!empty($attributeParams['isFind'])) {
108
                        if (!$model->isNewRecord && !empty($model->dirtyAttributes)) {
109
                            if (empty($extractor->isNoUpdate)) {
110
                                if (!$this->saveModel($model, $rowNbr)) {
111
                                    unset($whereValues[$rowNbr]);
112
                                    continue;
113
                                }
114
                            }
115
                        } else if ($model->isNewRecord) {
116
                            if (empty($extractor->isNoCreate)) {
117
                                // Поиск не нужен, модель новая
118
                                unset($whereValues[$rowNbr]);
119
                                $this->saveModel($model, $rowNbr);
120
                                continue;
121
                            }
122
                        }
123
124
                        if ($model->isNewRecord && !empty($extractor->isNoCreate)) {
125
                            $this->logError('Related record is not founded with attributes ' . serialize(array_filter($model->attributes)), $rowNbr, $model, null, $isMarkBad);
126
                            unset($whereValues[$rowNbr]);
127
                            continue;
128
                        }
129
130
                        if (empty($whereValues[$rowNbr])) {
131
                            $whereValues[$rowNbr] = [];
132
                        }
133
134
                        $whereValues[$rowNbr][$attribute] = (int)$model->id;
135
                    } else {
136
                        if ($model->isNewRecord && empty($extractor->isNoCreate)) {
137
                            if (!$this->saveModel($model, $rowNbr, false)) {
138
                                unset($relationsModels[$rowNbr][$attribute]);
139
                            }
140
                        }
141
                    }
142
                }
143
            } else {
144
                if (!empty($attributeParams['isFind'])) {
145
                    foreach ($this->importer->rows as $rowNbr => $row) {
146
                        if ($this->isBadRow($rowNbr)) {
147
                            continue;
148
                        }
149
150
                        if (!empty($attributeParams['value'])) {
151
                            $whereValues[$rowNbr][$attribute] = $attributeParams['value'];
152
                            continue;
153
                        }
154
155
                        if (empty($attributeParams['column'])) {
156
                            return [];
157
                            throw new Exception('Column key is required for attribute params. Extractor: ' . $extractorId . '. Attribute params: ' . var_export($attributeParams, true));
0 ignored issues
show
Unused Code introduced by
ThrowNode is not reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
158
                        }
159
160
                        if (empty($row[$attributeParams['column'] - 1])) {
161
                            $this->logError($attribute . ' is required for record find. Extractor: ' . $this->id, $rowNbr, null, $attributeParams['column'], $isMarkBad);
162
                            unset($whereValues[$rowNbr]);
163
                            continue;
164
                        }
165
166
                        if (empty($whereValues[$rowNbr])) {
167
                            $whereValues[$rowNbr] = [];
168
                        }
169
170
                        $modelClass = $this->query->modelClass;
171
                        $value = $row[$attributeParams['column'] - 1];
172
                        if (method_exists($modelClass, 'filtrateAttribute')) {
173
                            $value = $modelClass::filtrateAttribute($attribute, $value);
174
                        }
175
176
                        $whereValues[$rowNbr][$attribute] = $value;
177
                    }
178
                }
179
            }
180
        }
181
182
        $result = [];
183
        foreach ($whereValues as $rowNbr => $whereValue) {
184
            if ($this->importer->isBadRow($rowNbr)) {
185
                continue;
186
            }
187
188
            $uniqueKey = serialize($whereValue);
189
            if (!isset($this->modelsByUniqueKey[$uniqueKey])) {
190
                $result[$uniqueKey] = $whereValue;
191
            }
192
        }
193
194
        $whereValues = $result;
195
196
        $this->endOperation('construct where');
197
        if (count($whereValues)) {
198
            $attributesNames = $this->getAttributesNamesForFind();
199
            $query = clone $this->query;
200
            $query->indexBy(function ($row) use ($attributesNames) {
201
                $searchedAttributes = [];
202
                foreach ($attributesNames as $attributesName) {
203
                    $modelClass = $this->query->modelClass;
204
                    $value = $row[$attributesName];
205
                    if (method_exists($modelClass, 'filtrateAttribute')) {
206
                        $value = $modelClass::filtrateAttribute($attributesName, $value);
207
                    }
208
209
                    $searchedAttributes[$attributesName] = $value;
210
                }
211
                return serialize($searchedAttributes);
212
            });
213
214
            if ($this->scopes !== null) {
215
                foreach ($this->scopes as $scope) {
216
                    $scope($query, [
217
                        'IN',
218
                        $attributesNames,
219
                        $whereValues,
220
                    ]);
221
                }
222
            } else {
223
                $query->andWhere([
224
                    'IN',
225
                    $attributesNames,
226
                    $whereValues,
227
                ]);
228
            }
229
230
            $this->startOperation('find');
231
            $models = $query->all();
232
            $this->endOperation('find');
233
            $this->startOperation('keys collect');
234
            foreach ($models as $uniqueKey => $model) {
235
                if ($this->uniqueKeys !== null) {
236
                    $callback = $this->uniqueKeys;
237
                    $uniqueKeys = $callback($model, $attributesNames, $whereValues);
238
                } else {
239
                    $uniqueKeys = [$uniqueKey];
240
                }
241
242
                foreach ($uniqueKeys as $uniqueKey) {
0 ignored issues
show
Comprehensibility Bug introduced by
$uniqueKey is overwriting a variable from outer foreach loop.
Loading history...
243
                    $this->modelsByUniqueKey[$uniqueKey] = $model;
244
                }
245
            }
246
247
            $this->endOperation('keys collect');
248
        }
249
250
        $models = [];
251
        $this->startOperation('models collect');
252
253
        foreach ($this->importer->rows as $rowNbr => $row) {
254
            if ($this->isBadRow($rowNbr)) {
255
                continue;
256
            }
257
258
            $attributes = [];
259
            foreach ($this->attributes as $attribute => $attributeParams) {
260
                if (empty($attributeParams['extractor'])) {
261
                    $extractorId = $attribute;
262
                } else {
263
                    $extractorId = $attributeParams['extractor'];
264
                }
265
266
                if (empty($attributeParams['value']) && $this->importer->hasExtractor($extractorId)) {
267
                    if (empty($relationsModels[$rowNbr]) || empty($relationsModels[$rowNbr][$attribute])) {
268
//                        $attributes[$attribute] = null;
269
                    } else {
270
                        $p = $relationsModels[$rowNbr][$attribute]->id;
271
                        $attributes[$attribute] = (int)$p;
272
                    }
273
                } else {
274
                    if (!empty($attributeParams['value'])) {
275
                        $attributes[$attribute] = $attributeParams['value'];
276
                    } else {
277
                        if (empty($attributeParams['column'])) {
278
                            throw new Exception('Not setted column for attribute ' . $attribute . ' for extractor ' . $this->id);
279
                        }
280
281
                        if (empty($row[$attributeParams['column'] - 1])) {
282
                            continue;
283
                        } else {
284
                            $attributes[$attribute] = $row[$attributeParams['column'] - 1];
285
                        }
286
                    }
287
                }
288
            }
289
290
            $attributesNames = $this->getAttributesNamesForFind();
291
            $searchedAttributes = [];
292
            foreach ($attributesNames as $attributesName) {
293
                $modelClass = $this->query->modelClass;
294
                if (empty($attributes[$attributesName])) {
295
                    $value = null;
296
                } else {
297
                    $value = $attributes[$attributesName];
298
                }
299
300
                if (method_exists($modelClass, 'filtrateAttribute')) {
301
                    $value = $modelClass::filtrateAttribute($attributesName, $value);
302
                    $attributes[$attributesName] = $value;
303
                }
304
305
                $searchedAttributes[$attributesName] = $value;
306
            }
307
308
            $uniqueKey = serialize($searchedAttributes);
309
            if (isset($this->modelsByUniqueKey[$uniqueKey])) {
310
                $model = $this->modelsByUniqueKey[$uniqueKey];
311
            } else {
312
                $model = new $this->query->modelClass;
313
                $this->modelsByUniqueKey[$uniqueKey] = $model;
314
            }
315
316
            if ($this->isDelete && !$model->isNewRecord) {
317
                unset($this->deletedIds[$model->id]);
318
            }
319
320
            $modelAttributes = $model->getAttributes(array_keys($attributes));
321
            if (!$isSave || $model->isNewRecord || array_diff($attributes, $modelAttributes)) {
322
                if ($this->scenario) {
323
                    $model->scenario = $this->scenario;
324
                }
325
326
                $model->attributes = $attributes;
327
328
                $models[$rowNbr] = $model;
329
            }
330
        }
331
332
333
334
        $this->endOperation('models collect');
335
336
        $this->endOperation('extract');
337
        if ($isSave) {
338
            foreach ($models as $rowNbr => $model) {
339
                if (!$this->isBadRow($rowNbr)) {
340
                    $this->saveModel($model, $rowNbr);
341
                }
342
            }
343
        }
344
345
        return $models;
346
    }
347
348
    protected $times = [];
349
    protected function startOperation($name) {
350
        echo 'start ' . $name . ' ' . $this->id . "\n";
351
        $this->times[$name] = microtime(true);
352
    }
353
354
    protected function endOperation($name) {
355
        $time = microtime(true) - $this->times[$name];
356
        echo 'end ' . $name . ' ' . $this->id . ' after ' . $time . ' seconds' . "\n";
357
    }
358
359
    protected function triggerOperation($name) {
360
        echo $name . ' ' . $this->id . "\n";
361
    }
362
363
    protected function isBadRow($rowNbr) {
364
        return $this->importer->isBadRow($rowNbr);
365
    }
366
367
    protected function logError($message, $rowNbr, $model, $columnNbr = null, $isMarkBad = true) {
368
        if ($isMarkBad) {
369
            $this->importer->setIsBadRow($rowNbr);
370
        }
371
372
        $this->importer->logError($message, $rowNbr, $model, $columnNbr);
373
    }
374
375
    /**
376
     * @return array
377
     */
378
    public function getAttributesNamesForFind(): array
379
    {
380
        $attributesNames = [];
381
        foreach ($this->attributes as $attribute => $attributeParams) {
382
            if (!empty($attributeParams['isFind'])) {
383
                $attributesNames[] = $attribute;
384
            }
385
        }
386
        return $attributesNames;
387
    }
388
389
    /**
390
     * @param $model
391
     * @return mixed
392
     */
393
    protected function saveModel($model, $rowNbr, $isMarkBad = true)
394
    {
395
        $currentRowNbr = $this->importer->getCurrentStackRowNbr() + $rowNbr;
396
        $modelString = $model->tableName() . ' #' . $model->id . ' ' . serialize(array_filter($model->attributes));
397
        echo 'Row #' . $currentRowNbr . ': ';
398
        if ($this->scenario) {
399
            $model->scenario = $this->scenario;
400
        }
401
        if (!$model->validate()) {
402
            $message = 'Error validate ' . $model->tableName() . ' #' . $model->id . ' ' . serialize(array_filter($model->attributes)) . ' ' . serialize($model->errors);
403
            $this->logError($message, $rowNbr, $model, null, $isMarkBad);
404
            return false;
405
        }
406
407
        $isNewRecord = ($model->isNewRecord && !$this->isNoCreate);
408
        $isUpdatedRecord = (!$model->isNewRecord && !$this->isNoUpdate && !empty($model->dirtyAttributes));
409
        if ($isNewRecord || $isUpdatedRecord) {
410
            if ($isNewRecord) {
411
                $reason = 'created';
412
            } else {
413
                $reason = 'updated';
414
            }
415
416
            echo 'Saving ' . $modelString . ' because they is ' . $reason . "\n";
417
            if ($isUpdatedRecord) {
418
                echo 'Changed attributes ' . serialize(array_keys($model->dirtyAttributes)) . "\n";
419
                $oldValues = [];
420
                foreach ($model->dirtyAttributes as $attribute => $value) {
421
                    $oldValues[$attribute] = $model->oldAttributes[$attribute];
422
                }
423
424
                echo 'Old values ' . serialize($oldValues) . "\n";
425
                echo 'New values ' . serialize($model->dirtyAttributes) . "\n";
426
            }
427
428
            if (!$model->save()) {
429
                $this->logError('Error while saving ' . $modelString, $rowNbr, $model);
430
            }
431
        } else {
432
            echo $modelString . ' Is skipped because is not changed' . "\n";
433
        }
434
435
        return true;
436
    }
437
}