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.
Passed
Push — filters-dev ( c59b06...8ccd21 )
by Ivan
14:40 queued 04:15
created

Product::afterSave()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 7
rs 9.4285
cc 1
eloc 3
nc 1
nop 2
1
<?php
2
3
namespace app\modules\shop\models;
4
5
use app\behaviors\CleanRelations;
6
use app\behaviors\Tree;
7
use app\components\Helper;
8
use app\modules\image\models\Image;
9
use app\models\Object;
10
use app\modules\data\components\ImportableInterface;
11
use app\modules\data\components\ExportableInterface;
12
use app\modules\shop\traits\HasAddonTrait;
13
use app\modules\shop\ShopModule;
14
use app\properties\HasProperties;
15
use app\properties\PropertiesHelper;
16
use app\traits\GetImages;
17
use devgroup\TagDependencyHelper\ActiveRecordHelper;
18
use Yii;
19
use yii\base\Exception;
20
use yii\behaviors\TimestampBehavior;
21
use yii\caching\TagDependency;
22
use yii\data\ActiveDataProvider;
23
use yii\db\ActiveQuery;
24
use yii\db\ActiveRecord;
25
use yii\db\Expression;
26
use yii\db\Query;
27
use yii\helpers\ArrayHelper;
28
use yii\data\Pagination;
29
use yii\web\ServerErrorHttpException;
30
31
/**
32
 * This is the model class for table "product".
33
 *
34
 * @property integer $id
35
 * @property integer $main_category_id
36
 * @property string $name
37
 * @property string $title
38
 * @property string $h1
39
 * @property string $meta_description
40
 * @property string $breadcrumbs_label
41
 * @property string $slug
42
 * @property string $slug_compiled
43
 * @property integer $slug_absolute
44
 * @property string $content
45
 * @property string $announce
46
 * @property integer $sort_order
47
 * @property integer $active
48
 * @property double $price
49
 * @property double $old_price
50
 * @property integer $parent_id
51
 * @property integer $currency_id
52
 * @property integer $measure_id
53
 * @property Product[] $relatedProducts
54
 * @property Measure $measure
55
 * @property string $sku
56
 * @property boolean $unlimited_count
57
 * @property string $date_added
58
 * @property string $date_modified
59
 * Relations:
60
 * @property Category $category
61
 * @property Product[] $children
62
 */
63
64
class Product extends ActiveRecord implements ImportableInterface, ExportableInterface, \JsonSerializable
65
{
66
    use GetImages;
67
    use HasAddonTrait;
68
69
    private static $identity_map = [];
70
    private static $slug_to_id = [];
71
    private $category_ids = null;
72
73
    public $relatedProductsArray = [];
74
75
    /**
76
     * @var null|WarehouseProduct[] Stores warehouses state of product. Use Product::getWarehousesState() to retrieve
77
     */
78
    private $activeWarehousesState = null;
79
80
    /**
81
     * @inheritdoc
82
     */
83
    public static function tableName()
84
    {
85
        return '{{%product}}';
86
    }
87
88
    /**
89
     * @inheritdoc
90
     */
91
    public function rules()
92
    {
93
        return [
94
            [['sku'], 'filter', 'filter' => ['\app\backend\components\Helper', 'toString']],
95
            [['main_category_id', 'name', 'slug'], 'required'],
96
            [
97
                [
98
                    'main_category_id',
99
                    'slug_absolute',
100
                    'sort_order',
101
                    'parent_id',
102
                    'currency_id',
103
                ],
104
                'integer'
105
            ],
106
            [
107
                [
108
                    'name',
109
                    'title',
110
                    'h1',
111
                    'meta_description',
112
                    'breadcrumbs_label',
113
                    'content',
114
                    'announce',
115
                    'option_generate',
116
                    'sku'
117
                ],
118
                'string'
119
            ],
120
            [
121
                [
122
                    'unlimited_count',
123
                    'active',
124
                    'slug_absolute',
125
                ],
126
                'boolean',
127
            ],
128
            [['price', 'old_price'], 'number'],
129
            [['slug'], 'string', 'max' => 80],
130
            [['slug_compiled'], 'string', 'max' => 180],
131
            [['old_price', 'price',], 'default', 'value' => 0,],
132
            [['active', 'unlimited_count'], 'default', 'value' => true],
133
            [['parent_id', 'slug_absolute', 'sort_order'], 'default', 'value' => 0],
134
            [['sku', 'name'], 'default', 'value' => ''],
135
            [['unlimited_count', 'currency_id', 'measure_id'], 'default', 'value' => 1],
136
            [['relatedProductsArray'], 'safe'],
137
            [['slug'], 'unique', 'targetAttribute' => ['slug', 'main_category_id']],
138
            [['date_added', 'date_modified'], 'safe'],
139
140
        ];
141
    }
142
    /**
143
     * @inheritdoc
144
     */
145
    public function attributeLabels()
146
    {
147
        return [
148
            'id' => Yii::t('app', 'ID'),
149
            'main_category_id' => Yii::t('app', 'Main Category ID'),
150
            'parent_id' => Yii::t('app', 'Parent ID'),
151
            'name' => Yii::t('app', 'Name'),
152
            'title' => Yii::t('app', 'Title'),
153
            'h1' => Yii::t('app', 'H1'),
154
            'meta_description' => Yii::t('app', 'Meta Description'),
155
            'breadcrumbs_label' => Yii::t('app', 'Breadcrumbs Label'),
156
            'slug' => Yii::t('app', 'Slug'),
157
            'slug_compiled' => Yii::t('app', 'Slug Compiled'),
158
            'slug_absolute' => Yii::t('app', 'Slug Absolute'),
159
            'content' => Yii::t('app', 'Content'),
160
            'announce' => Yii::t('app', 'Announce'),
161
            'sort_order' => Yii::t('app', 'Sort Order'),
162
            'active' => Yii::t('app', 'Active'),
163
            'price' => Yii::t('app', 'Price'),
164
            'old_price' => Yii::t('app', 'Old Price'),
165
            'option_generate' => Yii::t('app', 'Option Generate'),
166
            'in_warehouse' => Yii::t('app', 'Items in warehouse'),
167
            'sku' => Yii::t('app', 'SKU'),
168
            'unlimited_count' => Yii::t('app', 'Unlimited items(don\'t count in warehouse)'),
169
            'reserved_count' => Yii::t('app', 'Items reserved'),
170
            'relatedProductsArray' => Yii::t('app', 'Related products'),
171
            'currency_id' => Yii::t('app', 'Currency'),
172
            'measure_id' => Yii::t('app', 'Measure'),
173
            'date_added' => Yii::t('app', 'Date Added'),
174
            'date_modified' => Yii::t('app', 'Date Modified'),
175
        ];
176
    }
177
178
    /**
179
     * @inheritdoc
180
     */
181 View Code Duplication
    public function behaviors()
182
    {
183
        return [
184
            [
185
                'class' => Tree::className(),
186
                'activeAttribute' => 'active',
187
                'sortOrder' => [
188
                    'sort_order' => SORT_ASC,
189
                    'id' => SORT_ASC
190
                ],
191
            ],
192
            [
193
                'class' => HasProperties::className(),
194
            ],
195
            [
196
                'class' => ActiveRecordHelper::className(),
197
            ],
198
            [
199
                'class' => CleanRelations::className(),
200
            ],
201
            [
202
                'class' => TimestampBehavior::className(),
203
                'createdAtAttribute' => 'date_added',
204
                'updatedAtAttribute' => 'date_modified',
205
                'value' => new Expression('NOW()'),
206
                'attributes' => [
207
                    ActiveRecord::EVENT_BEFORE_INSERT => ['date_added'],
208
                    ActiveRecord::EVENT_BEFORE_UPDATE => ['date_modified'],
209
                ],
210
            ],
211
        ];
212
    }
213
214
    /**
215
     * Search products
216
     * @param $params
217
     * @return ActiveDataProvider
218
     */
219
    public function search($params)
220
    {
221
        /* @var $query \yii\db\ActiveQuery */
222
        $query = self::find()->where(['parent_id' => 0])->with('images');
223
        $dataProvider = new ActiveDataProvider(
224
            [
225
                'query' => $query,
226
                'pagination' => [
227
                    'pageSize' => 10,
228
                ],
229
            ]
230
        );
231
        if (!($this->load($params))) {
232
            return $dataProvider;
233
        }
234
        $query->andFilterWhere(['id' => $this->id]);
235
        $query->andFilterWhere(['like', 'name', $this->name]);
236
        $query->andFilterWhere(['like', 'slug', $this->slug]);
237
        $query->andFilterWhere(['active' => $this->active]);
238
        $query->andFilterWhere(['price' => $this->price]);
239
        $query->andFilterWhere(['old_price' => $this->old_price]);
240
        $query->andFilterWhere(['like', 'sku', $this->sku]);
241
        return $dataProvider;
242
    }
243
244
    /**
245
     * Returns model instance by ID using per-request Identity Map and cache
246
     * @param $id
247
     * @param int $isActive Return only active
248
     * @return Product
249
     */
250
    public static function findById($id, $isActive = 1)
251
    {
252
        if (!is_numeric($id)) {
253
            return null;
254
        }
255
        if (!isset(static::$identity_map[$id])) {
256
            $cacheKey = static::tableName() . ":$id:$isActive";
257
            if (false === $model = Yii::$app->cache->get($cacheKey)) {
258
                $model = static::find()->where(['id' => $id])->with('images');
259
                if (null !== $isActive) {
260
                    $model->andWhere(['active' => $isActive]);
261
                }
262
                if (null !== $model = $model->one()) {
263
                    /**
264
                     * @var self $model
265
                     */
266
                    static::$slug_to_id[$model->slug] = $id;
267
                    Yii::$app->cache->set(
268
                        $cacheKey,
269
                        $model,
270
                        86400,
271
                        new TagDependency(
272
                            [
273
                                'tags' => [
274
                                    ActiveRecordHelper::getCommonTag(static::className())
275
                                ]
276
                            ]
277
                        )
278
                    );
279
                }
280
            }
281
            static::$identity_map[$id] = $model;
282
        }
283
        return static::$identity_map[$id];
284
    }
285
286
    /**
287
     * Find a product by slug
288
     * @param string $slug
289
     * @param int $inCategoryId
0 ignored issues
show
Documentation introduced by
Should the type for parameter $inCategoryId not be integer|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
290
     * @param int $isActive
291
     * @return Product
292
     */
293
    public static function findBySlug($slug, $inCategoryId = null, $isActive = 1)
294
    {
295
        if (!isset(static::$slug_to_id[$slug])) {
296
            $cacheKey = static::tableName() . "$slug:$inCategoryId";
297
            if (false === $model = Yii::$app->cache->get($cacheKey)) {
298
                $tags = [];
299
                /** @var ActiveQuery $model */
300
                $query = static::find()->where(
301
                    [
302
                        'slug' => $slug,
303
                        'active' => $isActive,
304
                    ]
305
                )->with('images', 'relatedProducts');
306
                if (!is_null($inCategoryId)) {
307
                    $query->andWhere(['main_category_id' => $inCategoryId]);
308
                    $tags[] = ActiveRecordHelper::getObjectTag(Category::className(), $inCategoryId);
309
                }
310
                $model = $query->one();
311
                /**
312
                 * @var self|null $model
313
                 */
314
                if (is_null($model)) {
315
                    $tags[] = ActiveRecordHelper::getCommonTag(Product::className());
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
316
                } else {
317
                    $tags[] = ActiveRecordHelper::getObjectTag(Product::className(), $model->id);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
318
                }
319
                Yii::$app->cache->set(
320
                    $cacheKey,
321
                    $model,
322
                    86400,
323
                    new TagDependency([
324
                        'tags' => $tags,
325
                    ])
326
                );
327
            }
328
            if (is_object($model)) {
329
                static::$identity_map[$model->id] = $model;
330
                static::$slug_to_id[$slug] = $model->id;
331
                return $model;
332
            }
333
            return null;
334
        } else {
335
            if (isset(static::$identity_map[static::$slug_to_id[$slug]])) {
336
                return static::$identity_map[static::$slug_to_id[$slug]];
337
            }
338
            return static::findById(static::$slug_to_id[$slug]);
339
        }
340
    }
341
342
    /**
343
     * @return Category|null
344
     */
345
    public function getCategory()
346
    {
347
        return $this->hasOne(Category::className(), ['id' => 'main_category_id']);
348
    }
349
350
    public function getOptions()
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
351
    {
352
        return $this->hasMany(static::className(), ['parent_id' => 'id']);
353
    }
354
355
    public function getCurrency()
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
356
    {
357
        return $this->hasOne(Currency::className(), ['id' => 'currency_id']);
358
    }
359
360
    /**
361
     * @return ActiveQuery
362
     */
363
    public function getRelatedProducts()
364
    {
365
        return $this->hasMany(Product::className(), ['id' => 'related_product_id'])
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
366
            ->viaTable(
367
                RelatedProduct::tableName(),
368
                ['product_id' => 'id'],
369
                function($relation) {
370
                    /** @var \yii\db\ActiveQuery $relation */
371
                    return $relation->orderBy('sort_order ASC');
372
                }
373
            )
374
            ->innerJoin('{{%related_product}}','related_product.product_id=:id AND related_product.related_product_id =product.id',[':id'=>$this->id])
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 150 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
375
            ->orderBy('related_product.sort_order ASC')
376
            ->andWhere(["active" => 1]);
377
    }
378
379
    public function getImage()
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
380
    {
381
        $result = $this->hasOne(Image::className(), ['object_model_id' => 'id']);
382
        $object = Object::getForClass($this->className());
383
        return $result->andWhere(['object_id' => $object->id]);
384
    }
385
386
387
    /**
388
     * Returns remains of this product in all active warehouses.
389
     * Note that if warehouse was added after product edit - it will not be shown here.
390
     * @return WarehouseProduct[]
391
     */
392
    public function getWarehousesState()
393
    {
394
        if ($this->activeWarehousesState === null) {
395
            $this->activeWarehousesState = WarehouseProduct::getDb()->cache(
396
                function($db) {
0 ignored issues
show
Unused Code introduced by
The parameter $db is not used and could be removed.

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

Loading history...
397
                    return WarehouseProduct::find()
398
                        ->where(['in', 'warehouse_id', Warehouse::activeWarehousesIds()])
399
                        ->andWhere('product_id=:product_id', [':product_id'=>$this->id])
400
                        ->with('warehouse')
401
                        ->all();
402
                },
403
                86400,
404
                new TagDependency(
405
                    [
406
                        'tags' => [
407
                            ActiveRecordHelper::getObjectTag($this->className(), $this->id),
408
                        ]
409
                    ]
410
                )
411
            );
412
        }
413
414
        return $this->activeWarehousesState;
415
    }
416
417
    public function beforeSave($insert)
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
418
    {
419
        if (empty($this->breadcrumbs_label)) {
420
            $this->breadcrumbs_label = $this->name;
421
        }
422
423
        if (empty($this->h1)) {
424
            $this->h1 = $this->name;
425
        }
426
427
        if (empty($this->title)) {
428
            $this->title = $this->name;
429
        }
430
        $object = Object::getForClass(static::className());
431
432
        TagDependency::invalidate(
433
            Yii::$app->cache,
434
            [
435
                'Images:' . $object->id . ':' . $this->id
436
            ]
437
        );
438
439
        return parent::beforeSave($insert);
440
    }
441
442
    /**
443
     * @inheritdoc
444
     */
445
    public function afterSave($insert, $changedAttributes)
446
    {
447
        parent::afterSave($insert, $changedAttributes);
448
449
        static::$identity_map[$this->id] = $this;
450
451
    }
452
453
    /**
454
     * Preparation to delete product.
455
     * Deleting all inserted products.
456
     * @return bool
457
     */
458 View Code Duplication
    public function beforeDelete()
459
    {
460
        if (!parent::beforeDelete()) {
461
            return false;
462
        }
463
        foreach ($this->children as $child) {
464
            $child->delete();
465
        }
466
        return true;
467
    }
468
469
    public function saveCategoriesBindings(array $categories_ids)
470
    {
471
        $object = Object::getForClass(static::className());
472
        $catIds = $this->getCategoryIds();
473
474
        $remove = [];
475
        $add = [];
476
477
        foreach ($catIds as $catsKey => $value) {
478
            $key = array_search($value, $categories_ids);
479
            if ($key === false) {
480
                $remove[] = $value;
481
                unset($this->category_ids[$catsKey]);
482
            } else {
483
                unset($categories_ids[$key]);
484
            }
485
        }
486
        foreach ($categories_ids as $value) {
487
            $add[] = [
488
                $value,
489
                $this->id
490
            ];
491
            $this->category_ids[] = $value;
492
        }
493
494
        Yii::$app->db->createCommand()->delete(
495
            $object->categories_table_name,
496
            ['and', 'object_model_id = :id', ['in', 'category_id', $remove]],
497
            [':id' => $this->id]
498
        )->execute();
499 View Code Duplication
        if (!empty($add)) {
500
            Yii::$app->db->createCommand()->batchInsert(
501
                $object->categories_table_name,
502
                ['category_id', 'object_model_id'],
503
                $add
504
            )->execute();
505
        }
506
507
508
    }
509
510
    /**
511
     * @return array
512
     */
513
    public function getCategoryIds()
514
    {
515
        if ($this->category_ids === null) {
516
            $object = Object::getForClass(static::className());
517
            $this->category_ids = (new Query())->select('category_id')->from([$object->categories_table_name])->where(
518
                'object_model_id = :id',
519
                [':id' => $this->id]
520
            )->orderBy(['sort_order' => SORT_ASC, 'id' => SORT_ASC])->column();
521
        }
522
        return $this->category_ids;
523
    }
524
525
    /**
526
     * Process fields before the actual model is saved(inserted or updated)
527
     * @param array $fields
528
     * @param $multipleValuesDelimiter
529
     * @param array $additionalFields
530
     */
531
    public function processImportBeforeSave(array $fields, $multipleValuesDelimiter, array $additionalFields)
532
    {
533
        $_attributes = $this->attributes();
534
        foreach ($fields as $key => $value) {
535
            if (in_array($key, $_attributes)) {
536
                $this->$key = $value;
537
            }
538
        }
539
540
        $categories = $this->unpackCategories($fields, $multipleValuesDelimiter, $additionalFields);
541
        if ($categories !== false && $this->main_category_id < 1) {
542
            if (count($categories) == 0) {
543
                $categories = [1];
544
            }
545
            $this->main_category_id = $categories[0];
546
        }
547
548
        if (empty($this->slug)) {
549
            $this->slug = Helper::createSlug($this->name);
550
        } elseif (mb_strlen($this->slug) > 80) {
551
            $this->slug = mb_substr($this->slug, 0, 80);
552
        }
553
554
        if (empty($this->name)) {
555
            $this->name = 'unnamed-product';
556
        }
557
558
        if (!is_numeric($this->price)) {
559
            $this->price = 0;
560
        }
561
        if (!is_numeric($this->old_price)) {
562
            $this->old_price = 0;
563
        }
564
    }
565
566
    /**
567
     * Process fields after the actual model is saved(inserted or updated)
568
     * @param array $fields
569
     * @param $multipleValuesDelimiter
570
     * @param array $additionalFields
571
     */
572
    public function processImportAfterSave(array $fields, $multipleValuesDelimiter, array $additionalFields)
573
    {
574
        $categories = $this->unpackCategories($fields, $multipleValuesDelimiter, $additionalFields);
575
576
        if ($categories === false) {
577
            $categories = [$this->main_category_id];
578
        }
579
        $this->saveCategoriesBindings($categories);
580
581
582
        $images = isset($fields['images']) ? $fields['images'] : (isset($fields['image']) ? $fields['image'] : false);
583
        if ($images !== false) {
584 View Code Duplication
            if (strpos($images, $multipleValuesDelimiter) > 0) {
585
                $images = explode($multipleValuesDelimiter, $images);
586
            } elseif (strpos($multipleValuesDelimiter, '/') === 0) {
587
                $images = preg_split($multipleValuesDelimiter, $images);
588
            } else {
589
                $images = [$images];
590
            }
591
            $input_array = [];
592
            foreach ($images as $image_src) {
593
                $input_array[] = [
594
                    'filename' => $image_src,
595
                ];
596
            }
597
            if (count($input_array) > 0) {
598
                Image::replaceForModel($this, $input_array);
599
            }
600
        }
601
602
        if (isset($additionalFields['relatedProducts'])) {
603
            if (isset($additionalFields['relatedProducts']['processValuesAs'],$fields['relatedProducts'])) {
604
605
                $ids = explode($multipleValuesDelimiter, $fields['relatedProducts']);
606
                $this->relatedProductsArray=$ids;
607
                $this->saveRelatedProducts();
608
//                $this->unlinkAll('relatedProducts', true);
0 ignored issues
show
Unused Code Comprehensibility introduced by
60% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
609
//
610
//                foreach ($ids as $index => $id) {
611
//                    $product = Product::findById($id);
612
//                    $this->link('relatedProduct', $product, ['sort_order'=>$index]);
613
//                }
614
615
616
617
            }
618
        }
619
    }
620
621
    /**
622
     * Makes an array of category ids from string
623
     *
624
     * @param array $fields
625
     * @param $multipleValuesDelimiter
626
     * @param array $additionalFields
627
     * @return array|bool
628
     */
629
    private function unpackCategories(array $fields, $multipleValuesDelimiter, array $additionalFields)
630
    {
631
        $categories = isset($fields['categories']) ? $fields['categories'] : (isset($fields['category']) ? $fields['category'] : false);
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 136 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
632
        if ($categories === false || empty($fields['categories'])) {
633
            return $this->getCategoryIds();
634
635
        }
636
        if ($categories !== false) {
637 View Code Duplication
            if (strpos($categories, $multipleValuesDelimiter) > 0) {
638
                $categories = explode($multipleValuesDelimiter, $categories);
639
            } elseif (strpos($multipleValuesDelimiter, '/') === 0) {
640
                $categories = preg_split($multipleValuesDelimiter, $categories);
641
            } else {
642
                $categories = [$categories];
643
            }
644
            $typecast = 'id';
645
646
            if (isset($additionalFields['categories'])) {
647
                if (isset($additionalFields['categories']['processValuesAs'])) {
648
                    $typecast = $additionalFields['categories']['processValuesAs'];
649
                }
650
            }
651
            if ($typecast === 'id') {
652
                $categories = array_map('intval', $categories);
653
            } elseif ($typecast === 'slug') {
654
                $categories = array_map('trim', $categories);
655
                $categoryIds = [];
656 View Code Duplication
                foreach ($categories as $part) {
657
                    $cat = Category::findBySlug($part, 1, -1);
658
                    if (is_object($cat)) {
659
                        $categoryIds[] = $cat->id;
660
                    }
661
                    unset($cat);
662
                }
663
                $categories = array_map('intval', $categoryIds);
664
            } elseif ($typecast === 'name') {
665
                $categories = array_map('trim', $categories);
666
                $categoryIds = [];
667 View Code Duplication
                foreach ($categories as $part) {
668
                    $cat = Category::findByName($part, 1, -1);
669
                    if (is_object($cat)) {
670
                        $categoryIds[] = $cat->id;
671
                    }
672
                    unset($cat);
673
                }
674
                $categories = array_map('intval', $categoryIds);
675
            } else {
676
                // that's unusual behavior
677
                $categories = false;
678
            }
679
680
            // post-process categories
681
            // find & add parent category
682
            // if we need to show products of child categories in products list
683
            /** @var ShopModule $module */
684
            $module = Yii::$app->getModule('shop');
685
            if (is_array($categories) && $module->showProductsOfChildCategories) {
686
                do {
687
                    $repeat = false;
688
                    foreach ($categories as $cat) {
689
                        $model = Category::findById($cat, null, null);
690
                        if ($model === null) {
691
                            echo "\n\nUnknown category with id ".intval($cat) ." for model with id:".$this->id."\n\n";
692
                            continue;
693
                        }
694
                        if (intval($model->parent_id) > 0 && in_array($model->parent_id, $categories) === false) {
695
                            $categories[] = $model->parent_id;
696
                            $repeat = true;
697
                        }
698
699
                        unset($model);
700
                    }
701
                } while ($repeat === true);
702
            }
703
704
        }
705
        return $categories;
706
    }
707
708
    /**
709
     * Additional fields with labels.
710
     * Translation should be implemented internally in this function.
711
     * For now will be rendered as checkbox list with label.
712
     * Note: properties should not be in the result - they are served other way.
713
     * Format:
714
     * [
715
     *      'field_key' => 'Your awesome translated field title',
716
     *      'another' => 'Another field label',
717
     * ]
718
     * @return array
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array<string,array>.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
719
     */
720
    public static function exportableAdditionalFields()
721
    {
722
        return [
723
            'categories' => [
724
                'label' => Yii::t('app', 'Categories'),
725
                'processValueAs' => [
726
                    'id' => Yii::t('app', 'ID'),
727
                    'name' => Yii::t('app', 'Name'),
728
                    'slug' => Yii::t('app', 'Slug'),
729
                ]
730
            ],
731
            'images' => [
732
                'label' => Yii::t('app', 'Images'),
733
                'processValueAs' => [
734
                    'filename' => Yii::t('app', 'Filename'),
735
                    'id' => Yii::t('app', 'ID'),
736
                ]
737
            ],
738
            'relatedProducts' => [
739
                'label' => Yii::t('app', 'Related products'),
740
                'processValueAs' => [
741
                    'id' => Yii::t('app', 'ID'),
742
                ],
743
            ],
744
        ];
745
    }
746
747
    /**
748
     * Returns additional fields data by field key.
749
     * If value of field is array it will be converted to string
750
     * using multipleValuesDelimiter specified in ImportModel
751
     * @param array $configuration
752
     * @return array
753
     */
754
    public function getAdditionalFields(array $configuration)
755
    {
756
        $result = [];
757
758
        if (isset($configuration['categories'], $configuration['categories']['processValuesAs'])
759
            && $configuration['categories']['enabled']
760
        ) {
761
            if ($configuration['categories']['processValuesAs'] === 'id') {
762
                $result['categories'] = $this->getCategoryIds();
763
            } else {
764
                $ids = $this->getCategoryIds();
765
                $result['categories'] = [];
766
767
                foreach ($ids as $id) {
768
                    $category = Category::findById($id, null, null);
769
                    if ($category) {
770
                        $result['categories'][] = $category->getAttribute(
771
                            $configuration['categories']['processValuesAs']
772
                        );
773
                    }
774
                    unset($category);
775
                }
776
            }
777
        }
778
        if (isset($configuration['images'], $configuration['images']['processValuesAs'])
779
            && $configuration['images']['enabled']
780
        ) {
781
            $object = Object::getForClass($this->className());
782
            $images = Image::getForModel($object->id, $this->id);
783
            $result['images'] = ArrayHelper::getColumn($images, $configuration['images']['processValuesAs']);
784
        }
785
786
        if (isset($configuration['relatedProducts'], $configuration['relatedProducts']['processValuesAs'])
787
            && $configuration['relatedProducts']['enabled']
788
        ) {
789
            $result['relatedProducts'] = ArrayHelper::getColumn($this->relatedProducts, $configuration['relatedProducts']['processValuesAs']);
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 142 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
790
        }
791
792
793
        return $result;
794
    }
795
796
    /**
797
     * Returns products for special filtration query
798
     * Used in ProductsWidget and ProductController
799
     *
800
     * @param $category_group_id
801
     * @param array $values_by_property_id
802
     * @param null|integer|string $selected_category_id
803
     * @param bool|string $force_sorting If false - use UserPreferences, if string - use supplied orderBy condition
804
     * @param null|integer $limit limit query results
805
     * @param bool $apply_filterquery Should we apply filter query(filters based on query params ie. price_min/max)
806
     * @param bool $force_limit False to use Pagination, true to use $limit and ignore pagination
807
     * @param array $additional_filters Array of callables that will apply additional filters to query
808
     * @return array
809
     * @throws ServerErrorHttpException
810
     * @throws \Exception
811
     */
812
    public static function filteredProducts(
813
        $category_group_id,
814
        array $values_by_property_id = [],
815
        $selected_category_id = null,
816
        $force_sorting = false,
817
        $limit = null,
818
        $apply_filterquery = true,
819
        $force_limit = false,
820
        array $additional_filters = []
821
    ) {
822
        Yii::beginProfile("FilteredProducts");
823
        /** @var \app\models\Object $object */
824
        if (null === $object = Object::getForClass(static::className())) {
825
            throw new ServerErrorHttpException('Object not found.');
826
        }
827
828
        /** @var \app\modules\shop\ShopModule $module */
829
        $module = Yii::$app->getModule('shop');
830
831
        $onlyParents = $module->filterOnlyByParentProduct;
832
        $query = static::find()->with('images');
833
        if (true === $onlyParents) {
834
            $query->andWhere([static::tableName() . '.parent_id' => 0, static::tableName() . '.active' => 1]);
835
        } else {
836
            $query->andWhere(['!=', static::tableName() . '.parent_id', 0]);
837
            $query->andWhere([static::tableName() . '.active' => 1]);
838
        }
839
840
        if (null !== $selected_category_id) {
841
            $query->innerJoin(
842
                $object->categories_table_name . ' ocats',
843
                'ocats.category_id = :catid AND ocats.object_model_id = ' . static::tableName() . '.id',
844
                [':catid' => $selected_category_id]
845
            );
846
        } else {
847
            $query->innerJoin(
848
                $object->categories_table_name . ' ocats',
849
                'ocats.object_model_id = ' . static::tableName() . '.id'
850
            );
851
        }
852
853
        $query->innerJoin(
854
            Category::tableName() . ' ocatt',
855
            'ocatt.id = ocats.category_id AND ocatt.category_group_id = :gcatid AND ocatt.active = 1',
856
            [':gcatid' => $category_group_id]
857
        );
858
        $query->addGroupBy(static::tableName() . ".id");
859
860
861
        $userSelectedSortingId = UserPreferences::preferences()->getAttributes()['productListingSortId'];
862
        $allSorts = [];
863
        if ($force_sorting === false) {
864
            $allSorts = ProductListingSort::enabledSorts();
865
            if (isset($allSorts[$userSelectedSortingId])) {
866
                $query->addOrderBy(
867
                    $allSorts[$userSelectedSortingId]['sort_field'] . ' '
868
                    . $allSorts[$userSelectedSortingId]['asc_desc']
869
                );
870
            } else {
871
                $query->addOrderBy(static::tableName() . '.sort_order');
872
            }
873
        } elseif (empty($force_sorting) === false || is_array($force_sorting) === true) {
874
            $query->addOrderBy($force_sorting);
875
        }
876
877
        $productsPerPage = $limit === null ? UserPreferences::preferences()->getAttributes(
878
        )['productsPerPage'] : $limit;
879
880
        PropertiesHelper::appendPropertiesFilters(
881
            $object,
882
            $query,
883
            $values_by_property_id,
884
            Yii::$app->request->get('p', [])
885
        );
886
887
888
        // apply additional filters
889
        $cacheKeyAppend = "";
890
        if ($apply_filterquery) {
891
            $query = Yii::$app->filterquery->filter($query, $cacheKeyAppend);
892
        }
893
894
        foreach ($additional_filters as $filter) {
895
            if (is_callable($filter)) {
896
                call_user_func_array(
897
                    $filter,
898
                    [
899
                        &$query,
900
                        &$cacheKeyAppend
901
                    ]
902
                );
903
            }
904
        }
905
906
        $cacheKey = 'ProductsCount:' . implode(
907
                '_',
908
                [
909
                    md5($query->createCommand()->getRawSql()),
910
                    $limit ? '1' : '0',
911
                    $force_limit ? '1' : '0',
912
                    $productsPerPage
913
                ]
914
            ) . $cacheKeyAppend;
915
916
917
        $pages = null;
918
919
        if ($force_limit === true) {
920
            $query->limit($limit);
921
        } else {
922
            $products_query = clone $query;
923
            $products_query->limit(null);
924
925
            if (false === $pages = Yii::$app->cache->get($cacheKey)) {
926
                $pages = new Pagination(
927
                    [
928
                        'defaultPageSize' => !is_null($query->limit) ? $query->limit : $productsPerPage,
929
                        'pageSizeLimit' => [],
930
                        'forcePageParam' => false,
931
                        'totalCount' => $products_query->count(),
932
                    ]
933
                );
934
935
                Yii::$app->cache->set(
936
                    $cacheKey,
937
                    $pages,
938
                    86400,
939
                    new TagDependency(
940
                        [
941
                            'tags' => [
942
                                ActiveRecordHelper::getCommonTag(Category::className()),
943
                                ActiveRecordHelper::getCommonTag(static::className()),
944
                                ActiveRecordHelper::getCommonTag($module->className()),
945
                            ]
946
                        ]
947
                    )
948
                );
949
            }
950
951
            $query->offset($pages->offset)->limit($pages->limit);
952
        }
953
954
        $cacheKey .= '-' . Yii::$app->request->get('page', 1);
955
956
        if (false === $products = Yii::$app->cache->get($cacheKey)) {
957
            $products = $query->all();
958
            Yii::$app->cache->set(
959
                $cacheKey,
960
                $products,
961
                86400,
962
                new TagDependency(
963
                    [
964
                        'tags' => [
965
                            ActiveRecordHelper::getCommonTag(Category::className()),
966
                            ActiveRecordHelper::getCommonTag(static::className()),
967
                            ActiveRecordHelper::getCommonTag($module->className()),
968
                        ]
969
                    ]
970
                )
971
            );
972
        }
973
974
        Yii::endProfile("FilteredProducts");
975
        return [
976
            'products' => $products,
977
            'pages' => $pages,
978
            'allSorts' => $allSorts,
979
        ];
980
981
    }
982
983
    /**
984
     * Returns product main category model instance using per-request Identity Map
985
     * @return Category|null
986
     */
987
    public function getMainCategory()
988
    {
989
        return Category::findById($this->main_category_id, null);
990
    }
991
992
    public function loadRelatedProductsArray()
993
    {
994
        $this->relatedProductsArray = [];
995
        foreach ($this->relatedProducts as $product) {
996
            $this->relatedProductsArray[] = $product->id;
997
        }
998
    }
999
1000
    public function saveRelatedProducts()
1001
    {
1002
        if (!is_array($this->relatedProductsArray)) {
1003
            $this->relatedProductsArray = explode(',', $this->relatedProductsArray);
1004
        }
1005
1006
        RelatedProduct::deleteAll(
1007
            [
1008
                'product_id' => $this->id,
1009
            ]
1010
        );
1011
1012
        foreach ($this->relatedProductsArray as $index => $relatedProductId) {
1013
            $relation = new RelatedProduct;
1014
            $relation->attributes = [
1015
                'product_id' => $this->id,
1016
                'related_product_id' => $relatedProductId,
1017
                'sort_order' => $index,
1018
            ];
1019
            $relation->save();
1020
        }
1021
    }
1022
1023
    /**
1024
     * Returns product price(old old_price) converted to $currency. If currency is not set(null) will be converted to main currency
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 131 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
1025
     * @param null|Currency $currency Currency to use in conversion, null for main shop currency
1026
     * @param bool $oldPrice True if you want to return Old Price instead of price
1027
     * @return float
1028
     */
1029
    public function convertedPrice($currency = null, $oldPrice = false)
1030
    {
1031
        if ($currency === null) {
1032
            $currency = Currency::getMainCurrency();
1033
        }
1034
        $price = $oldPrice === true ? $this->old_price : $this->price;
1035
1036
        if ($this->currency_id !== $currency->id) {
1037
            // we need to convert!
1038
            $foreignCurrency = Currency::findById($this->currency_id);
1039
            return round($price / $foreignCurrency->convert_nominal * $foreignCurrency->convert_rate, 2);
1040
        }
1041
        return $price;
1042
    }
1043
1044
    /**
1045
     * Formats price to needed currency
1046
     * @param null|Currency $currency Currency to use in conversion, null for main shop currency
1047
     * @param boolean $oldPrice True if you want to return Old Price instead of price
1048
     * @param boolean $schemaOrg Return schema.org http://schema.org/Offer markup with span's
1049
     * @return string
1050
     */
1051
    public function formattedPrice($currency = null, $oldPrice = false, $schemaOrg = true)
1052
    {
1053
        if ($currency === null) {
1054
            $currency = Currency::getMainCurrency();
1055
        }
1056
1057
        $price = $this->convertedPrice($currency, $oldPrice);
1058
1059
1060
        $formatted_string = $currency->format($price);
1061
        if ($schemaOrg == true) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
1062
            return strtr(
1063
                '
1064
                <span itemtype="http://schema.org/Offer" itemprop="offers" itemscope>
1065
                    <meta itemprop="priceCurrency" content="%iso_code%">
1066
                    <span itemprop="price" content="%price%">
1067
                        %formatted_string%
1068
                    </span>
1069
                </span>
1070
                ',
1071
                [
1072
                    '%iso_code%' => $currency->iso_code,
1073
                    '%price%' => $price,
1074
                    '%formatted_string%' => $formatted_string,
1075
                ]
1076
            );
1077
        } else {
1078
            return $formatted_string;
1079
        }
1080
1081
    }
1082
1083
    /**
1084
     * Formats price in product's currency
1085
     * @param bool $oldPrice
1086
     * @param bool $schemaOrg
1087
     * @return string
1088
     */
1089
    public function nativeCurrencyPrice($oldPrice = false, $schemaOrg = true)
1090
    {
1091
        $currency = Currency::findById($this->currency_id);
1092
        return $this->formattedPrice($currency, $oldPrice, $schemaOrg);
1093
    }
1094
1095
    public function getMeasure()
1096
    {
1097
        $measure = Measure::findById($this->measure_id);
1098
        if (is_null($measure)) {
1099
            $measure = Measure::findById(Yii::$app->getModule('shop')->defaultMeasureId);
1100
        }
1101
        if (is_null($measure)) {
1102
            throw new Exception('Measure not found');
1103
        }
1104
        return $measure;
1105
    }
1106
1107
    /**
1108
     * @param int|Category|null $category
1109
     * @param bool $asMainCategory
1110
     * @return bool
1111
     * @throws \yii\db\Exception
1112
     */
1113
    public function linkToCategory($category = null, $asMainCategory = false)
1114
    {
1115
        if ($category instanceof Category) {
1116
            $category = $category->id;
1117
        } elseif (is_int($category) || is_string($category)) {
1118
            $category = intval($category);
1119
        } else {
1120
            return false;
1121
        }
1122
1123
        if ($asMainCategory) {
1124
            $this->main_category_id = $category;
1125
            return $this->save();
1126
        } else {
1127
            $tableName = $this->getObject()->categories_table_name;
1128
            $query = new Query();
1129
            $query = $query->select(['id'])
1130
                ->from($tableName)
1131
                ->where(['category_id' => $category, 'object_model_id' => $this->id])
1132
                ->one();
1133
            if (false === $query) {
1134
                try {
1135
                    $result = Yii::$app->db->createCommand()->insert($tableName, [
1136
                        'category_id' => $category,
1137
                        'object_model_id' => $this->id
1138
                    ])->execute();
1139
                } catch (\yii\db\Exception $e) {
0 ignored issues
show
Bug introduced by
The class yii\db\Exception 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...
1140
                    $result = false;
1141
                }
1142
                return boolval($result);
1143
            }
1144
        }
1145
1146
        return false;
1147
    }
1148
1149
    public function getCacheTags()
1150
    {
1151
1152
        $tags = [
1153
            ActiveRecordHelper::getObjectTag(self::className(), $this->id),
1154
        ];
1155
        $category = $this->getMainCategory();
1156
        $tags [] = ActiveRecordHelper::getObjectTag(Category::className(), $category->id);
1157
        $categoryParentsIds = $category->getParentIds();
1158
        foreach ($categoryParentsIds as $id) {
1159
            $tags [] = ActiveRecordHelper::getObjectTag(Category::className(), $id);
1160
        };
1161
1162
1163
        return $tags;
1164
    }
1165
1166
    /**
1167
     * @return string
1168
     */
1169
    public function __toString()
1170
    {
1171
        return ($this->className() . ':' . $this->id);
1172
    }
1173
1174
    /**
1175
     * @return string
1176
     */
1177
    public function jsonSerialize()
1178
    {
1179
        return ($this->className() . ':' . $this->id);
1180
    }
1181
}
1182