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.

ImportProductWriter::saveProductParameter()   A
last analyzed

Complexity

Conditions 4
Paths 6

Size

Total Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
nc 6
nop 4
dl 0
loc 28
rs 9.472
c 0
b 0
f 0
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
  protected $successWriteProductsAmount = 0;
53
54
  protected $parameterNamesCache = array();
55
56
  protected $parameterGroupCache = array();
57
58
  private $allProductsAmount = 0;
59
60
  private $skipProductsAmount = 0;
61
62
  private $modelsCache = array();
63
64
  private $parameterVariantsCache = array();
65
66
  public function beforeProcessNewFile()
67
  {
68
    parent::beforeProcessNewFile();
69
70
    $this->allProductsAmount = 0;
71
72
    $this->successWriteProductsAmount = 0;
73
74
    $this->skipProductsAmount = 0;
75
76
    $this->parameterNamesCache = array();
77
78
    $this->parameterGroupCache = array();
79
80
    $this->modelsCache = array();
81
82
    $this->parameterVariantsCache = array();
83
84
    ImportHelper::clearUrlCache(null);
85
  }
86
87 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...
88
  {
89
    if( empty($data) )
90
      return;
91
92
    $itemsAmount = count($data);
93
    $this->allProductsAmount = $itemsAmount;
94
95
    $progress = new ConsoleProgressBar($itemsAmount);
96
    $this->logger->log('Начало записи в БД');
97
    $progress->start();
98
    foreach($data as $item)
99
    {
100
      $this->safeWriteItem($item);
101
      $progress->setValueMap('memory', Yii::app()->format->formatSize(memory_get_usage()));
102
      $progress->advance();
103
    }
104
    $progress->finish();
105
  }
106
107
  public function writePartial(array $data)
108
  {
109
    if( empty($data) )
110
      return;
111
112
    foreach($data as $item)
113
    {
114
      $this->allProductsAmount++;
115
      $this->safeWriteItem($item);
116
    }
117
  }
118
119
  public function showStatistics()
120
  {
121
    $this->logger->log('Записано '.$this->successWriteProductsAmount.' продуктов из '.$this->allProductsAmount.' (пропущено '.$this->skipProductsAmount.')');
122
    $this->logger->log('Записи в БД завершена');
123
  }
124
125
  protected function safeWriteItem($item)
126
  {
127
    try
128
    {
129
      $this->write($item);
130
    }
131
    catch(WarningException $e)
132
    {
133
      $itemId = '';
134
135
      if( !empty($item['uniqueIndex']) && !empty($item['uniqueAttribute']) )
136
        $itemId = ' '.$item['uniqueAttribute'].'='.$item['uniqueIndex'];
137
138
      $this->logger->warning($e->getMessage().$itemId);
139
    }
140
  }
141
142
  protected function write(array $item)
143
  {
144
    /**
145
     * @var BProduct $newProductModel
146
     * @var BProduct $product
147
     */
148
149
    $newProductModel = ImportHelper::getModelWithoutBehaviors('BProduct', $this->importScenario);
150
151
    if( $product = $newProductModel->findByAttributes(array($item['uniqueAttribute'] => $item['uniqueIndex'])) )
152
    {
153
      $this->updateProduct($product, $item);
154
    }
155
    else
156
    {
157
      $this->saveNewProduct($newProductModel, $item);
158
    }
159
  }
160
161
  protected function saveNewProduct(BProduct $newProductModel, array $item)
162
  {
163
    $newProductModel->setAttributes($item['product'], false);
164
    $newProductModel->url = ImportHelper::createUniqueUrl($newProductModel->tableName(), $newProductModel->url, true);
165
166
    $sectionModel = $this->prepareAssignmentAndGetSectionModel($newProductModel, $item);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $sectionModel is correct as $this->prepareAssignment...newProductModel, $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...
167
168
    if( !$newProductModel->save() )
169
      throw new ImportModelValidateException($newProductModel, 'Не удалось создать продукт (строка '.$item['rowIndex'].' файл '.$item['file'].')');
170
171
    $this->saveModifications($newProductModel, $item['modification'], $sectionModel);
172
173
    if( !empty($this->assignment) )
174
    {
175
      BProductAssignment::model()->saveAssignments($newProductModel, Arr::extract($newProductModel, $this->assignment, array()));
176
    }
177
178
    $this->saveParameters($newProductModel, $item['parameter'], $sectionModel);
179
180
    if( !empty($item['basketParameter']) )
181
      $this->saveParameters($newProductModel, $item['basketParameter'], $sectionModel);
182
183
    $this->successWriteProductsAmount++;
184
  }
185
186
  protected function updateProduct(BProduct $product, array $item)
187
  {
188
    $product->scenario = !empty($product->parent) ? $this->importModificationScenario : $this->importScenario;
189
    $product->detachBehaviors();
190
191
    $this->skipProductsAmount++;
192
    //$product->save();
193
  }
194
195
  protected function prepareAssignmentAndGetSectionModel(&$product, array $item)
196
  {
197
    $sectionModel = null;
198
    foreach($item['assignment'] as $attribute => $assignment)
199
    {
200
      $modelName = BProductStructure::getModelName($attribute);
201
      $setAttributes = isset($this->assignmentTree[$attribute]) ? array('parent_id' => $product->{$this->assignmentTree[$attribute]}) : array();
202
203
      $values = !is_array($assignment) ? array($assignment) : $assignment;
204
205
      $associationModels = array();
206
      foreach($values as $value)
207
      {
208
        if( $associationModel = $this->getProductAssignmentModel($modelName, $value, $setAttributes) )
209
        {
210
          $associationModels[$value] = $associationModel;
211
          if( $associationModel instanceof BProductSection )
212
            $sectionModel = $associationModel;
213
        }
214
      }
215
216
      if( count($associationModels) == 0 )
217
        $attributeValue = null;
218
      else if( count($associationModels) == 1 )
219
        $attributeValue = reset($associationModels)->id;
220
      else
221
      {
222
        $attributeValue = array_keys(CHtml::listData($associationModels, 'id', 'id'));
223
      }
224
225
      $this->setAttribute($product, $attribute, $attributeValue);
226
    }
227
228
    return $sectionModel;
229
  }
230
231
  protected function saveModifications($parentProduct, $modifications = array(), $sectionModel)
232
  {
233
    foreach($modifications as $item)
234
    {
235
      /**
236
       * @var BProduct $product
237
       */
238
      $product = ImportHelper::getModelWithoutBehaviors('BProduct', $this->importModificationScenario);
239
      $product->setAttributes($item['product'], false);
240
      $product->url = ImportHelper::createUniqueUrl($product->tableName(), $product->url, true);
241
      $product->parent = $parentProduct->id;
242
243
      if( !$product->save() )
244
        throw new ImportModelValidateException($product, 'Не удалось создать модификацию product_id='.$parentProduct->id);
245
246
      $this->saveParameters($product, $item['parameter'], $sectionModel);
247
    }
248
  }
249
250
  protected function saveParameters(BProduct $product, $data, $sectionModel = null)
251
  {
252
    foreach($data as $attributes)
253
    {
254
      if( empty($attributes['name']) || empty($attributes['value']) )
255
        continue;
256
257
      try
258
      {
259
        $parameterGroupId = $this->getParameterGroupId($sectionModel, Arr::cut($attributes, 'common'));
260
        $parameterName = $this->getParameterName(Arr::cut($attributes, 'name'), Arr::cut($attributes, 'type'), $parameterGroupId, Arr::cut($attributes, 'key'));
261
      }
262
      catch(WarningException $e)
263
      {
264
        throw new WarningException($e->getMessage().' product_id = '.$product->id);
265
      }
266
267
      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...
268
      {
269
        try
270
        {
271
          $this->saveProductParameter($variant, $attributes, $parameterName, $product);
272
        }
273
        catch(WarningException $e)
274
        {
275
          $this->logger->warning($e->getMessage().' product_id = '.$product->id);
276
        }
277
      }
278
    }
279
  }
280
281
  /**
282
   * @param $name
283
   * @param string $type
284
   * @param integer $parameterGroupId
285
   * @param string $key
286
   *
287
   * @return mixed
288
   * @throws WarningException
289
   */
290
  protected function getParameterName($name, $type = 'checkbox', $parameterGroupId, $key = '')
291
  {
292
    $cashKey = mb_strtolower($name).$parameterGroupId;
293
    $parameterName = Arr::get($this->parameterNamesCache, $cashKey);
294
    if( !$parameterName )
295
    {
296
      $criteria = new CDbCriteria();
297
      $criteria->compare('name', $name);
298
      $criteria->compare('parent', '>'.BProductParamName::ROOT_ID);
299
      $criteria->compare('parent', $parameterGroupId);
300
      $parameterName = BProductParamName::model()->find($criteria);
301
    }
302
303
    if( !$parameterName )
304
    {
305
      $parameterName = new BProductParamName();
306
      $parameterName->parent = $parameterGroupId;
307
      $parameterName->name = $name;
308
      $parameterName->type = $type;
309
      $parameterName->key = $key;
310
311
      if( !$parameterName->save() )
312
      {
313
        throw new WarningException('Ошибка при создании параметра '.$name);
314
      }
315
    }
316
317
    $this->parameterNamesCache[$cashKey] = $parameterName;
318
319
    return $this->parameterNamesCache[$cashKey];
320
  }
321
322
  /**
323
   * @param BProductSection|null $sectionModel
324
   * @param bool $common
325
   *
326
   * @return BProductParamName
327
   * @throws WarningException
328
   */
329
  protected function getParameterGroupId($sectionModel = null, $common = false)
330
  {
331
    if( $common )
332
      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...
333
334
    if( !($sectionModel instanceof BProductSection) )
335
      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...
336
337
    if( !isset($this->parameterGroupCache[$sectionModel->id]) )
338
    {
339
      $parameterName = BProductParamName::model()->findByAttributes(array('parent' => BProductParamName::ROOT_ID, 'name' => $sectionModel->name));
340
341
      if( !$parameterName )
342
      {
343
        $parameterName = new BProductParamName();
344
        $parameterName->parent = BProductParamName::ROOT_ID;
345
        $parameterName->name = $sectionModel->name;
346
347
        if( !$parameterName->save() )
348
        {
349
          throw new WarningException('Ошибка при создании группы параметров '.$parameterName->name);
350
        }
351
        $this->saveParameterAssignment($parameterName, array('section_id' => $sectionModel->id));
352
      }
353
354
      $this->parameterGroupCache[$sectionModel->id] = $parameterName;
355
    }
356
357
    return $this->parameterGroupCache[$sectionModel->id]->id;
358
  }
359
360
  /**
361
   * @param string $modelName
362
   * @param string $name
363
   * @param array $attributes
364
   *
365
   * @return BActiveRecord
366
   * @throws ImportModelValidateException
367
   */
368
  private function getProductAssignmentModel($modelName, $name, array $attributes = array())
369
  {
370
    if( empty($name) )
371
      return null;
372
373
    $cacheKey = $modelName.$name.serialize($attributes);
374
375
    /**
376
     * @var BActiveRecord $model
377
     */
378
    if( !($model = Arr::get($this->modelsCache, $cacheKey, $this->tryFindProductAssignmentModel($modelName, $name, $attributes))) )
379
    {
380
      $model = new $modelName;
381
      $model->name = $name;
382
383
      $this->setAttribute($model, 'url', ImportHelper::createUniqueUrl($model->tableName(), Utils::translite($name)));
384
      $this->setAttribute($model, 'visible', 1);
385
386
      foreach($attributes as $attribute => $value)
387
      {
388
        $this->setAttribute($model, $attribute, $value);
389
      }
390
391
      if( !$model->save() )
392
      {
393
        throw new ImportModelValidateException($model, 'Ошибка при создании модели '.$modelName.' - '.$name);
394
      }
395
396
      $this->modelsCache[$cacheKey] = $model;
397
    }
398
399
    return $model;
400
  }
401
402
  /**
403
   * @param $modelName
404
   * @param $name
405
   * @param array $attributes
406
   *
407
   * @return BProductStructure|BTreeAssignmentBehavior|bool
408
   */
409
  private function tryFindProductAssignmentModel($modelName, $name, array $attributes = array())
410
  {
411
    /**
412
     * @var BProductStructure|BTreeAssignmentBehavior $model
413
     */
414
    $model = new $modelName;
415
416
    if( $model->asa('tree') && isset($attributes['parent_id']) )
417
    {
418
      $model->parent_id = $attributes['parent_id'];
419
    }
420
421
    $model->visible = null;
422
423
    $criteria = new CDbCriteria();
424
    $criteria->compare('t.name', $name);
425
    $data = $model->search($criteria)->getData();
0 ignored issues
show
Bug introduced by
The method search does only exist in BProductStructure, but not in BTreeAssignmentBehavior.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
426
427
    return Arr::reset($data);
428
  }
429
430
  private function setAttribute(BActiveRecord $model, $attribute, $value)
431
  {
432
    try
433
    {
434
      $model->$attribute = $value;
435
    }
436
    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...
437
    {
438
    }
439
  }
440
441
  private function prepareVariantValues($data)
442
  {
443
    if( $data == '' || (is_array($data) && empty($data)) )
444
      return array();
445
446
    if( !is_array($data) )
447
    {
448
      $values = array($data);
449
    }
450
    else
451
    {
452
      $values = $data;
453
    }
454
455
    return Arr::trim($values);
456
  }
457
458
  private function saveProductParameter($variantName, $attributes, BProductParamName $parameterName, BProduct $product)
459
  {
460
    $parameter = new BProductParam();
461
    $parameter->setAttributes($attributes, false);
462
    $parameter->param_id = $parameterName->id;
463
    $parameter->product_id = $product->id;
464
465
    if( $parameterName->type === 'checkbox' )
466
    {
467
      $parameter->variant_id = $this->getVariantId($parameterName->id, $variantName);
468
    }
469
    else
470
    {
471
      $parameter->value = $variantName;
472
    }
473
474
    try
475
    {
476
      if( !$parameter->save() )
477
      {
478
        throw new ImportModelValidateException($parameter, 'Ошибка при создании параметра продукта');
479
      }
480
    }
481
    catch(CDbException $e)
482
    {
483
      throw new WarningException('Ошибка при создании параметра parameter_id = '.$parameterName->id.' '.$e->getMessage(), $e->getCode());
484
    }
485
  }
486
487
  private function saveParameterAssignment(BProductParamName $parameterName, array $attributes)
488
  {
489
    if( empty($attributes['section_id']) )
490
      return;
491
492
    $parameterAssignment = new BProductParamAssignment();
493
    $parameterAssignment->setAttributes($attributes);
494
    $parameterAssignment->param_id = $parameterName->id;
495
496
    if( !$parameterAssignment->save() )
497
    {
498
      throw new WarningException('Ошибка при создании ParameterAssignment для парамера '.$parameterName->name);
499
    }
500
  }
501
502
  private function getVariantId($paramId, $name)
503
  {
504
    if( !$variant = Arr::get($this->parameterVariantsCache, $paramId.$name, BProductParamVariant::model()->findByAttributes(array('param_id' => $paramId, 'name' => $name))) )
505
    {
506
      $variant = new BProductParamVariant();
507
      $variant->param_id = $paramId;
508
      $variant->name = $name;
509
510
      try
511
      {
512
        if( !$variant->save() )
513
          throw new ImportModelValidateException($variant, 'Ошибка при создании варианта для параметра '.$name.' id = '.$paramId);
514
      }
515
      catch(CDbException $e)
516
      {
517
        if( strpos($e->getMessage(), 'CDbCommand failed to execute the SQL statement: SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry') !== false )
518
        {
519
          throw new WarningException('Дублирование варианта "'.$name.'"');
520
        }
521
        else
522
        {
523
          throw $e;
524
        }
525
      }
526
527
      $this->parameterVariantsCache[$paramId.$name] = $variant;
528
    }
529
530
    return $variant->id;
531
  }
532
}