GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Push — master ( 1ba1f4...ea41a5 )
by Alexey
05:37
created

ImportProductWriter::safeWriteItem()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 16
rs 9.2
cc 4
eloc 8
nc 3
nop 1
1
<?php
2
/**
3
 * @author Alexey Tatarinov <[email protected]>
4
 * @link https://github.com/shogodev/argilla/
5
 * @copyright Copyright &copy; 2003-2015 Shogo
6
 * @license http://argilla.ru/LICENSE
7
 */
8
Yii::import('frontend.share.*');
9
Yii::import('frontend.share.behaviors.*');
10
Yii::import('frontend.share.helpers.*');
11
Yii::import('frontend.share.validators.*');
12
Yii::import('backend.components.*');
13
Yii::import('backend.components.db.*');
14
Yii::import('backend.components.interfaces.*');
15
Yii::import('backend.models.behaviors.*');
16
Yii::import('backend.modules.product.models.*');
17
Yii::import('backend.modules.product.models.behaviors.*');
18
Yii::import('backend.modules.product.components.*');
19
Yii::import('frontend.extensions.upload.components.*');
20
21
Yii::import('backend.modules.product.modules.import.components.exceptions.*');
22
Yii::import('backend.modules.product.modules.import.components.abstracts.AbstractImportWriter');
23
24
class ImportProductWriter extends AbstractImportWriter
25
{
26
  public $assignmentTree = array();
27
28
  public $assignment = array();
29
30
  public $clearTables = array(
31
    '{{product}}',
32
    '{{product_assignment}}',
33
    '{{product_param}}',
34
    '{{product_param_variant}}',
35
36
    '{{product_tree_assignment}}',
37
    '{{product_section}}',
38
    '{{product_type}}',
39
    '{{product_category}}',
40
    '{{product_collection}}',
41
  );
42
43
  /**
44
   * @var int $defaultCommonParameterGroup - id группы общих параметров
45
   */
46
  public $defaultCommonParameterGroup = 2;
47
48
  public $importScenario = 'import';
49
50
  public $importModificationScenario = BModificationBehavior::SCENARIO_MODIFICATION;
51
52
  private $allProductsAmount = 0;
53
54
  private $successWriteProductsAmount = 0;
55
56
  private $skipProductsAmount = 0;
57
58
  private $modelsCache = array();
59
60
  private $parameterNamesCache = array();
61
62
  private $parameterVariantsCache = array();
63
64
  private $urlCache;
65
66
  private $parameterGroupCache = array();
67
68
  public function init()
69
  {
70
    parent::init();
71
72
    $this->allProductsAmount = 0;
73
74
    $this->successWriteProductsAmount = 0;
75
76
    $this->skipProductsAmount = 0;
77
  }
78
79 View Code Duplication
  public function writeAll(array $data)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
80
  {
81
    if( empty($data) )
82
      return;
83
84
    $itemsAmount = count($data);
85
    $this->allProductsAmount = $itemsAmount;
86
87
    $progress = new ConsoleProgressBar($itemsAmount);
88
    $this->logger->log('Начало записи в БД');
89
    $progress->start();
90
    foreach($data as $item)
91
    {
92
      $this->safeWriteItem($item);
93
      $progress->setValueMap('memory', Yii::app()->format->formatSize(memory_get_usage()));
94
      $progress->advance();
95
    }
96
    $progress->finish();
97
  }
98
99
  public function writePartial(array $data)
100
  {
101
    if( empty($data) )
102
      return;
103
104
    foreach($data as $item)
105
    {
106
      $this->allProductsAmount++;
107
      $this->safeWriteItem($item);
108
    }
109
  }
110
111
  public function showStatistics()
112
  {
113
    $this->logger->log('Записано '.$this->successWriteProductsAmount.' продуктов из '.$this->allProductsAmount.' (пропущено '.$this->skipProductsAmount.')');
114
    $this->logger->log('Записи в БД завершена');
115
  }
116
117
  protected function safeWriteItem($item)
118
  {
119
    try
120
    {
121
      $this->write($item);
122
    }
123
    catch(WarningException $e)
124
    {
125
      $itemId = '';
126
127
      if( !empty($item['uniqueIndex']) && !empty($item['uniqueAttribute']) )
128
        $itemId = ' '.$item['uniqueAttribute'].'='.$item['uniqueIndex'];
129
130
      $this->logger->warning($e->getMessage().$itemId);
131
    }
132
  }
133
134
  protected function write(array $item)
135
  {
136
    /**
137
     * @var BProduct $product
138
     */
139
    $product = ImportHelper::getModelWithoutBehaviors('BProduct', $this->importScenario);
140
141
    /**
142
     * @var BProduct $product
143
     */
144
    if( $foundProduct = $product->findByAttributes(array($item['uniqueAttribute'] => $item['uniqueIndex'])) )
145
    {
146
      $foundProduct->scenario = !empty($product->parent) ? $this->importModificationScenario : $this->importScenario;
147
      $foundProduct->detachBehaviors(); // детачим поведения подключенные в populateRecord
148
149
      $this->skipProductsAmount++;
150
151
      //$foundProduct->save();
152
      return;
153
    }
154
155
    $product->setAttributes($item['product'], false);
156
    $product->url = $this->createUniqueUrl($product->url);
157
    $product->visible = 1;
158
159
    $sectionModel = $this->prepareAssignmentAndGetSectionModel($product, $item);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $sectionModel is correct as $this->prepareAssignment...nModel($product, $item) (which targets ImportProductWriter::pre...entAndGetSectionModel()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
160
161
    if( !$product->save() )
162
      throw new ImportModelValidateException($product, 'Не удалось создать продукт (строка '.$item['rowIndex'].' файл '.$item['file'].')');
163
164
    $this->saveModifications($product, $item['modification'], $sectionModel);
165
166
    if( !empty($this->assignment) )
167
    {
168
      BProductAssignment::model()->saveAssignments($product, Arr::extract($product, $this->assignment, array()));
169
    }
170
171
    $this->saveParameters($product, $item['parameter'], $sectionModel);
172
173
    if( !empty($item['basketParameter']) )
174
      $this->saveParameters($product, $item['basketParameter'], $sectionModel);
175
176
    $this->successWriteProductsAmount++;
177
  }
178
179
  protected function prepareAssignmentAndGetSectionModel(&$product, array $item)
180
  {
181
    $sectionModel = null;
182
    foreach($item['assignment'] as $attribute => $assignment)
183
    {
184
      $modelName = BProductStructure::getModelName($attribute);
185
      $setAttributes = isset($this->assignmentTree[$attribute]) ? array('parent_id' => $product->{$this->assignmentTree[$attribute]}) : array();
186
187
      $values = !is_array($assignment) ? array($assignment) : $assignment;
188
189
      $associationModels = array();
190
      foreach($values as $value)
191
      {
192
        if( $associationModel = $this->getModel($modelName, $value, $setAttributes) )
193
        {
194
          $associationModels[$value] = $associationModel;
195
          if( $associationModel instanceof BProductSection )
196
            $sectionModel = $associationModel;
197
        }
198
      }
199
200
      if( count($associationModels) == 0 )
201
        $attributeValue = null;
202
      else if( count($associationModels) == 1 )
203
        $attributeValue = reset($associationModels)->id;
204
      else
205
      {
206
        $attributeValue = array_keys(CHtml::listData($associationModels, 'id', 'id'));
207
      }
208
209
      $this->setAttribute($product, $attribute, $attributeValue);
210
    }
211
212
    return $sectionModel;
213
  }
214
215
  protected function saveModifications($parentProduct, $modifications = array(), $sectionModel)
216
  {
217
    foreach($modifications as $item)
218
    {
219
      /**
220
       * @var BProduct $product
221
       */
222
      $product = ImportHelper::getModelWithoutBehaviors('BProduct', $this->importModificationScenario);
223
      $product->setAttributes($item['product'], false);
224
      $product->url = $this->createUniqueUrl($product->url);
225
      $product->parent = $parentProduct->id;
226
      $product->visible = 1;
227
228
      if( !$product->save() )
229
        throw new ImportModelValidateException($product, 'Не удалось создать модификацию product_id='.$parentProduct->id);
230
231
      $this->saveParameters($product, $item['parameter'], $sectionModel);
232
    }
233
  }
234
235
  /**
236
   * @param string $modelName
237
   * @param string $name
238
   * @param array $attributes
239
   *
240
   * @return BActiveRecord
241
   * @throws ImportModelValidateException
242
   */
243
  private function getModel($modelName, $name, array $attributes = array())
244
  {
245
    if( empty($name) )
246
      return null;
247
248
    /**
249
     * @var BActiveRecord $model
250
     */
251
    if( !($model = Arr::get($this->modelsCache, $modelName.$name, $modelName::model()->findByAttributes(array('name' => $name)))) )
252
    {
253
      $model = new $modelName;
254
      $model->name = $name;
255
256
      $this->setAttribute($model, 'url', Utils::translite($name));
257
      $this->setAttribute($model, 'visible', 1);
258
259
      foreach($attributes as $attribute => $value)
260
      {
261
        $this->setAttribute($model, $attribute, $value);
262
      }
263
264
      if( !$model->save() )
265
      {
266
        throw new ImportModelValidateException($model, 'Ошибка при создании модели '.$modelName.' - '.$name);
267
      }
268
269
      $this->modelsCache[$modelName.$name] = $model;
270
    }
271
272
    return $model;
273
  }
274
275
  private function setAttribute(BActiveRecord $model, $attribute, $value)
276
  {
277
    try
278
    {
279
      $model->$attribute = $value;
280
    }
281
    catch(CException $e)
0 ignored issues
show
Unused Code introduced by
catch (\CException $e) { } does not seem to be 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...
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
Bug introduced by
The class CException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
282
    {
283
    }
284
  }
285
286
  private function saveParameters(BProduct $product, $data, $sectionModel = null)
287
  {
288
    foreach($data as $attributes)
289
    {
290
      if( empty($attributes['name']) || empty($attributes['value']) )
291
        continue;
292
293
      try
294
      {
295
        $parameterGroupId = $this->getParameterGroupId($sectionModel, Arr::cut($attributes, 'common'));
296
        $parameterName = $this->getParameterName(Arr::cut($attributes, 'name'), Arr::cut($attributes, 'type'), $parameterGroupId, Arr::cut($attributes, 'key'));
297
      }
298
      catch(WarningException $e)
299
      {
300
        throw new WarningException($e->getMessage().' product_id = '.$product->id);
301
      }
302
303
      foreach($this->prepareVariantValues(Arr::cut($attributes, 'value')) as $variant)
0 ignored issues
show
Bug introduced by
The expression $this->prepareVariantVal...($attributes, 'value')) of type array|string is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
304
      {
305
        try
306
        {
307
          $this->saveProductParameter($variant, $attributes, $parameterName, $product);
308
        }
309
        catch(WarningException $e)
310
        {
311
          $this->logger->warning($e->getMessage().' product_id = '.$product->id);
312
        }
313
      }
314
    }
315
  }
316
317
  private function prepareVariantValues($data)
318
  {
319
    if( $data == '' || (is_array($data) && empty($data)) )
320
      return array();
321
322
    if( !is_array($data) )
323
    {
324
      $values = array($data);
325
    }
326
    else
327
    {
328
      $values = $data;
329
    }
330
331
    return Arr::trim($values);
332
  }
333
334
  private function saveProductParameter($variantName, $attributes, BProductParamName $parameterName, BProduct $product)
335
  {
336
    $parameter = new BProductParam();
337
    $parameter->setAttributes($attributes, false);
338
    $parameter->param_id = $parameterName->id;
339
    $parameter->product_id = $product->id;
340
341
    if( $parameterName->type === 'checkbox' )
342
    {
343
      $parameter->variant_id = $this->getVariantId($parameterName->id, $variantName);
344
    }
345
    else
346
    {
347
      $parameter->value = $variantName;
348
    }
349
350
    try
351
    {
352
      if( !$parameter->save() )
353
      {
354
        throw new ImportModelValidateException($parameter, 'Ошибка при создании параметра продукта');
355
      }
356
    }
357
    catch(CDbException $e)
358
    {
359
      throw new WarningException('Ошибка при создании параметра parameter_id = '.$parameterName->id.' '.$e->getMessage(), $e->getCode());
360
    }
361
  }
362
363
  /**
364
   * @param $name
365
   * @param string $type
366
   * @param integer $parameterGroupId
367
   * @param string $key
368
   *
369
   * @return mixed
370
   * @throws WarningException
371
   */
372
  private function getParameterName($name, $type = 'checkbox', $parameterGroupId, $key = '')
373
  {
374
    $cashKey = mb_strtolower($name).$parameterGroupId;
375
    $parameter = Arr::get($this->parameterNamesCache, $cashKey);
376
    if( !$parameter )
377
    {
378
      $criteria = new CDbCriteria();
379
      $criteria->compare('name', $name);
380
      $criteria->compare('parent', '>'.BProductParamName::ROOT_ID);
381
      $criteria->compare('parent', $parameterGroupId);
382
      $parameter = BProductParamName::model()->find($criteria);
383
    }
384
385
    if( !$parameter )
386
    {
387
      $parameterName = new BProductParamName();
388
      $parameterName->parent = $parameterGroupId;
389
      $parameterName->name = $name;
390
      $parameterName->type = $type;
391
      $parameterName->key = $key;
392
393
      if( !$parameterName->save() )
394
      {
395
        throw new WarningException('Ошибка при создании параметра '.$name);
396
      }
397
398
      $this->parameterNamesCache[$cashKey] = $parameterName;
399
    }
400
401
    return $this->parameterNamesCache[$cashKey];
402
  }
403
404
  /**
405
   * @param BProductSection|null $sectionModel
406
   * @param bool $common
407
   *
408
   * @return BProductParamName
409
   * @throws WarningException
410
   */
411
  private function getParameterGroupId($sectionModel = null, $common = false)
412
  {
413
    if( $common )
414
      return $this->defaultCommonParameterGroup;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->defaultCommonParameterGroup; (integer) is incompatible with the return type documented by ImportProductWriter::getParameterGroupId of type BProductParamName.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
415
416
    if(  !($sectionModel instanceof BProductSection) )
417
      return $this->defaultCommonParameterGroup;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->defaultCommonParameterGroup; (integer) is incompatible with the return type documented by ImportProductWriter::getParameterGroupId of type BProductParamName.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
418
419
    if( !isset($this->parameterGroupCache[$sectionModel->id]) )
420
    {
421
      $parameterName = new BProductParamName();
422
      $parameterName->parent = 1;
423
      $parameterName->name = $sectionModel->name;
424
425
      if( !$parameterName->save() )
426
      {
427
        throw new WarningException('Ошибка при создании группы параметров '.$parameterName->name);
428
      }
429
      $this->saveParameterAssignment($parameterName, array('section_id' => $sectionModel->id));
430
431
      $this->parameterGroupCache[$sectionModel->id] = $parameterName;
432
    }
433
434
    return $this->parameterGroupCache[$sectionModel->id]->id;
435
  }
436
437
  private function saveParameterAssignment(BProductParamName $parameterName, array $attributes)
438
  {
439
    if( empty($attributes['section_id']) )
440
      return;
441
442
    $parameterAssignment = new BProductParamAssignment();
443
    $parameterAssignment->setAttributes($attributes);
444
    $parameterAssignment->param_id = $parameterName->id;
445
446
    if( !$parameterAssignment->save() )
447
    {
448
      throw new WarningException('Ошибка при создании ParameterAssignment для парамера '.$parameterName->name);
449
    }
450
  }
451
452
  private function getVariantId($paramId, $name)
453
  {
454
    if( !$variant = Arr::get($this->parameterVariantsCache, $paramId.$name, BProductParamVariant::model()->findByAttributes(array('param_id' => $paramId, 'name' => $name))) )
455
    {
456
      $variant = new BProductParamVariant();
457
      $variant->param_id = $paramId;
458
      $variant->name = $name;
459
460
      try
461
      {
462
        if( !$variant->save() )
463
          throw new ImportModelValidateException($variant, 'Ошибка при создании варианта для параметра '.$name.' id = '.$paramId);
464
      }
465
      catch(CDbException $e)
466
      {
467
        if( strpos($e->getMessage(), 'CDbCommand failed to execute the SQL statement: SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry') !== false )
468
        {
469
          throw new WarningException('Дублирование варианта "'.$name.'"');
470
        }
471
        else
472
        {
473
          throw $e;
474
        }
475
      }
476
477
      $this->parameterVariantsCache[$paramId.$name] = $variant;
478
    }
479
480
    return $variant->id;
481
  }
482
483
  protected function createUniqueUrl($url)
484
  {
485
    if( is_null($this->urlCache) )
486
    {
487
      $this->urlCache = array();
488
489
      $criteria = new CDbCriteria();
490
      $criteria->select = 'url';
491
      $command = Yii::app()->db->schema->commandBuilder->createFindCommand('{{product}}', $criteria);
492
493
      foreach($command->queryColumn() as $itemUrl)
494
        $this->urlCache[$itemUrl] = $itemUrl;
495
    }
496
497
    $uniqueUrl = Utils::translite(trim($url));
498
    $suffix = 1;
499
    while(isset($this->urlCache[$uniqueUrl]))
500
    {
501
      $uniqueUrl = $url.'_'.$suffix++;
502
    }
503
504
    $this->urlCache[$uniqueUrl] = $uniqueUrl;
505
506
    return $uniqueUrl;
507
  }
508
}