protected/modules/store/models/Product.php

Summary

Maintainability
F
5 days
Test Coverage
<?php
use yupe\widgets\YPurifier;

Yii::import('zii.behaviors.CTimestampBehavior');
Yii::import('application.modules.comment.components.ICommentable');

/**
 * @property string $id
 * @property integer $type_id
 * @property integer $producer_id
 * @property integer $category_id
 * @property string $sku
 * @property string $name
 * @property string $slug
 * @property double $price
 * @property double $discount_price
 * @property double $discount
 * @property string $short_description
 * @property string $description
 * @property string $data
 * @property integer $is_special
 * @property double $length
 * @property double $height
 * @property double $width
 * @property double $weight
 * @property integer $quantity
 * @property integer $in_stock
 * @property integer $status
 * @property datetime $create_time
 * @property datetime $update_time
 * @property string $meta_title
 * @property string $meta_description
 * @property string $meta_keywords
 * @property string $image
 * @property double $average_price
 * @property double $purchase_price
 * @property double $recommended_price
 * @property integer $position
 * @property string $external_id
 * @property string $title
 * @property string $meta_canonical
 * @property string $image_alt
 * @property string $image_title
 * @property string $view
 *
 * @method getImageUrl
 *
 * The followings are the available model relations:
 * @property Type $type
 * @property Producer $producer
 * @property StoreCategory $category
 * @property ProductImage $mainImage
 * @property ProductImage[] $images
 * @property ProductVariant[] $variants
 * @property Comment[] $comments
 * @property StoreCategory[] $categories
 *
 */
class Product extends yupe\models\YModel implements ICommentable
{
    /**
     *
     */
    const SPECIAL_NOT_ACTIVE = 0;
    /**
     *
     */
    const SPECIAL_ACTIVE = 1;

    /**
     *
     */
    const STATUS_NOT_ACTIVE = 0;
    /**
     *
     */
    const STATUS_ACTIVE = 1;

    /**
     *
     */
    const STATUS_NOT_IN_STOCK = 0;
    /**
     *
     */
    const STATUS_IN_STOCK = 1;

    /**
     * @var array
     */
    public $selectedVariants = [];

    /**
     * @var
     */
    protected $_typeAttributes;

    /**
     * @var array кеш getVariantsOptions
     */
    protected $_variantsOptions = false;

    /**
     * @var array
     */
    protected $_attributesValues;


    /**
     * Returns the static model of the specified AR class.
     * @param string $className active record class name.
     * @return Product Static model class
     */
    public static function model($className = __CLASS__)
    {
        return parent::model($className);
    }

    /**
     * @return string the associated database table name
     */
    public function tableName()
    {
        return '{{store_product}}';
    }

    /**
     * @return array validation rules for model attributes.
     */
    public function rules()
    {
        return [
            [
                'name, title, description, short_description, slug, price, discount_price, discount, data, status, is_special',
                'filter',
                'filter' => 'trim',
            ],
            [
                'name, title, description, short_description, slug, price, discount_price, discount, data, status, is_special',
                'filter',
                'filter' => [$obj = new YPurifier(), 'purify'],
            ],
            ['name, slug', 'required'],
            [
                'status, is_special, producer_id, type_id, in_stock, category_id',
                'numerical',
                'integerOnly' => true,
            ],
            ['quantity', 'numerical', 'integerOnly' => true, 'min' => 0],
            [
                'price, average_price, purchase_price, recommended_price, discount_price, discount, length, height, width, weight',
                'store\components\validators\NumberValidator',
            ],
            [
                'name, title, meta_keywords, meta_title, meta_description, meta_canonical, image, image_alt, image_title',
                'length',
                'max' => 250,
            ],
            ['discount_price, discount, average_price, purchase_price, recommended_price', 'default', 'value' => null],
            ['sku, view', 'length', 'max' => 100],
            ['slug', 'length', 'max' => 150],
            ['external_id', 'length', 'max' => 100],
            [
                'slug',
                'yupe\components\validators\YSLugValidator',
                'message' => Yii::t('StoreModule.store', 'Illegal characters in {attribute}'),
            ],
            ['slug', 'unique'],
            ['status', 'in', 'range' => array_keys($this->getStatusList())],
            ['is_special', 'boolean'],
            ['length, height, width, weight', 'default', 'setOnEmpty' => true, 'value' => null],
            ['meta_canonical', 'url'],
            [
                'id, type_id, producer_id, sku, name, slug, price, discount_price, discount, short_description, description, data, is_special, length, height, width, weight, quantity, in_stock, status, create_time, update_time, meta_title, meta_description, meta_keywords, category_id',
                'safe',
                'on' => 'search',
            ],
        ];
    }

    /**
     * @return array relational rules.
     */
    public function relations()
    {
        return [
            'type' => [self::BELONGS_TO, 'Type', 'type_id'],
            'producer' => [self::BELONGS_TO, 'Producer', 'producer_id'],
            'categoryRelation' => [self::HAS_MANY, 'ProductCategory', 'product_id'],
            'categories' => [self::HAS_MANY, 'StoreCategory', ['category_id' => 'id'], 'through' => 'categoryRelation'],
            'category' => [self::BELONGS_TO, 'StoreCategory', ['category_id' => 'id']],
            'images' => [self::HAS_MANY, 'ProductImage', 'product_id'],
            'variants' => [
                self::HAS_MANY,
                'ProductVariant',
                ['product_id'],
                'with' => ['attribute'],
                'order' => 'variants.position ASC',
            ],
            'comments' => [
                self::HAS_MANY,
                'Comment',
                'model_id',
                'on' => 'model = :model AND comments.status = :status',
                'params' => [
                    ':model' => __CLASS__,
                    ':status' => Comment::STATUS_APPROVED,
                ],
                'order' => 'comments.lft',
            ],
            'linkedProductsRelation' => [self::HAS_MANY, 'ProductLink', 'product_id', 'joinType' => 'INNER JOIN'],
            'linkedProducts' => [
                self::HAS_MANY,
                'Product',
                ['linked_product_id' => 'id'],
                'through' => 'linkedProductsRelation',
                'joinType' => 'INNER JOIN',
            ],
            'attributesValues' => [
                self::HAS_MANY,
                'AttributeValue',
                'product_id',
                'with' => ['attribute' => ['alias' => 'attr']],
            ],
        ];
    }

    /**
     * @return array
     */
    public function scopes()
    {
        return [
            'published' => [
                'condition' => 't.status = :status',
                'params' => [':status' => self::STATUS_ACTIVE],
            ],
            'specialOffer' => [
                'condition' => 't.is_special = :is_special',
                'params' => [':is_special' => self::SPECIAL_ACTIVE],
            ],
        ];
    }

    /**
     * @return array customized attribute labels (name=>label)
     */
    public function attributeLabels()
    {
        return [
            'id' => Yii::t('StoreModule.store', 'ID'),
            'category_id' => Yii::t('StoreModule.store', 'Category'),
            'type_id' => Yii::t('StoreModule.store', 'Type'),
            'name' => Yii::t('StoreModule.store', 'Title'),
            'price' => Yii::t('StoreModule.store', 'Price'),
            'discount_price' => Yii::t('StoreModule.store', 'Discount price'),
            'discount' => Yii::t('StoreModule.store', 'Discount, %'),
            'sku' => Yii::t('StoreModule.store', 'SKU'),
            'image' => Yii::t('StoreModule.store', 'Image'),
            'short_description' => Yii::t('StoreModule.store', 'Short description'),
            'description' => Yii::t('StoreModule.store', 'Description'),
            'slug' => Yii::t('StoreModule.store', 'Alias'),
            'data' => Yii::t('StoreModule.store', 'Data'),
            'status' => Yii::t('StoreModule.store', 'Status'),
            'create_time' => Yii::t('StoreModule.store', 'Added'),
            'update_time' => Yii::t('StoreModule.store', 'Updated'),
            'user_id' => Yii::t('StoreModule.store', 'User'),
            'change_user_id' => Yii::t('StoreModule.store', 'Editor'),
            'is_special' => Yii::t('StoreModule.store', 'Special'),
            'length' => Yii::t('StoreModule.store', 'Length, m.'),
            'height' => Yii::t('StoreModule.store', 'Height, m.'),
            'width' => Yii::t('StoreModule.store', 'Width, m.'),
            'weight' => Yii::t('StoreModule.store', 'Weight, kg.'),
            'quantity' => Yii::t('StoreModule.store', 'Quantity'),
            'producer_id' => Yii::t('StoreModule.store', 'Producer'),
            'in_stock' => Yii::t('StoreModule.store', 'Stock status'),
            'meta_title' => Yii::t('StoreModule.store', 'Meta title'),
            'meta_keywords' => Yii::t('StoreModule.store', 'Meta keywords'),
            'meta_description' => Yii::t('StoreModule.store', 'Meta description'),
            'purchase_price' => Yii::t('StoreModule.store', 'Purchase price'),
            'average_price' => Yii::t('StoreModule.store', 'Average price'),
            'recommended_price' => Yii::t('StoreModule.store', 'Recommended price'),
            'position' => Yii::t('StoreModule.store', 'Position'),
            'external_id' => Yii::t('StoreModule.store', 'External id'),
            'title' => Yii::t('StoreModule.store', 'SEO_title'),
            'meta_canonical' => Yii::t('StoreModule.store', 'Canonical'),
            'image_alt' => Yii::t('StoreModule.store', 'Image alt'),
            'image_title' => Yii::t('StoreModule.store', 'Image title'),
            'view' => Yii::t('StoreModule.store', 'Template'),
        ];
    }

    /**
     * @return array customized attribute descriptions (name=>description)
     */
    public function attributeDescriptions()
    {
        return [
            'id' => Yii::t('StoreModule.store', 'ID'),
            'category_id' => Yii::t('StoreModule.store', 'Category'),
            'name' => Yii::t('StoreModule.store', 'Title'),
            'price' => Yii::t('StoreModule.store', 'Price'),
            'sku' => Yii::t('StoreModule.store', 'SKU'),
            'image' => Yii::t('StoreModule.store', 'Image'),
            'short_description' => Yii::t('StoreModule.store', 'Short description'),
            'description' => Yii::t('StoreModule.store', 'Description'),
            'slug' => Yii::t('StoreModule.store', 'Alias'),
            'data' => Yii::t('StoreModule.store', 'Data'),
            'status' => Yii::t('StoreModule.store', 'Status'),
            'create_time' => Yii::t('StoreModule.store', 'Added'),
            'update_time' => Yii::t('StoreModule.store', 'Edited'),
            'user_id' => Yii::t('StoreModule.store', 'User'),
            'change_user_id' => Yii::t('StoreModule.store', 'Editor'),
            'is_special' => Yii::t('StoreModule.store', 'Special'),
            'length' => Yii::t('StoreModule.store', 'Length, m.'),
            'height' => Yii::t('StoreModule.store', 'Height, m.'),
            'width' => Yii::t('StoreModule.store', 'Width, m.'),
            'weight' => Yii::t('StoreModule.store', 'Weight, kg.'),
            'quantity' => Yii::t('StoreModule.store', 'Quantity'),
            'producer_id' => Yii::t('StoreModule.store', 'Producer'),
            'purchase_price' => Yii::t('StoreModule.store', 'Purchase price'),
            'average_price' => Yii::t('StoreModule.store', 'Average price'),
            'recommended_price' => Yii::t('StoreModule.store', 'Recommended price'),
            'title' => Yii::t('StoreModule.store', 'SEO_title'),
            'meta_canonical' => Yii::t('StoreModule.store', 'Canonical'),
            'image_alt' => Yii::t('StoreModule.store', 'Image alt'),
            'image_title' => Yii::t('StoreModule.store', 'Image title'),
            'view' => Yii::t('StoreModule.store', 'Template'),
        ];
    }

    /**
     * Retrieves a list of models based on the current search/filter conditions.
     * @return CActiveDataProvider the data provider that can return the models based on the search/filter conditions.
     */
    public function search()
    {
        $module = Yii::app()->getModule('store');
        $criteria = new CDbCriteria;
        $criteria->with = ['category', 'categories'];

        $criteria->compare('id', $this->id);
        $criteria->compare('t.type_id', $this->type_id);
        $criteria->compare('t.name', $this->name, true);
        $criteria->compare('t.price', $this->price);
        $criteria->compare('t.discount_price', $this->discount_price);
        $criteria->compare('t.sku', $this->sku, true);
        $criteria->compare('t.short_description', $this->short_description, true);
        $criteria->compare('t.description', $this->description, true);
        $criteria->compare('t.slug', $this->slug, true);
        $criteria->compare('t.data', $this->data, true);
        $criteria->compare('t.is_special', $this->is_special);
        $criteria->compare('t.status', $this->status);
        $criteria->compare('t.create_time', $this->create_time, true);
        $criteria->compare('t.update_time', $this->update_time, true);
        $criteria->compare('t.producer_id', $this->producer_id);
        $criteria->compare('t.purchase_price', $this->purchase_price);
        $criteria->compare('t.average_price', $this->average_price);
        $criteria->compare('t.recommended_price', $this->recommended_price);
        $criteria->compare('t.in_stock', $this->in_stock);
        $criteria->compare('t.quantity', $this->quantity);

        if ($this->category_id) {
            $categoryCriteria = new CDbCriteria();
            $categoryCriteria->compare('t.category_id', $this->category_id);
            $categoryCriteria->addCondition(sprintf('t.id IN (SELECT product_id FROM {{store_product_category}} WHERE category_id = :category_id)'),
                'OR');
            $categoryCriteria->params = CMap::mergeArray($categoryCriteria->params,
                [':category_id' => $this->category_id]);
            $criteria->mergeWith($categoryCriteria);
        }

        return new CActiveDataProvider(
            'Product', [
                'criteria' => $criteria,
                'sort' => [
                    'defaultOrder' => $module->getDefaultSort(),
                ],
            ]
        );
    }

    /**
     * @return array
     */
    public function behaviors()
    {
        $module = Yii::app()->getModule('store');

        return [
            'time' => [
                'class' => 'zii.behaviors.CTimestampBehavior',
                'setUpdateOnCreate' => true,
                'createAttribute' => 'create_time',
                'updateAttribute' => 'update_time',
            ],
            'upload' => [
                'class' => 'yupe\components\behaviors\ImageUploadBehavior',
                'attributeName' => 'image',
                'minSize' => $module->minSize,
                'maxSize' => $module->maxSize,
                'types' => $module->allowedExtensions,
                'uploadPath' => $module->uploadPath.'/product',
                'resizeOnUpload' => true,
                'resizeOptions' => [
                    'maxWidth' => 900,
                    'maxHeight' => 900,
                ],
            ],
            'sortable' => [
                'class' => 'yupe\components\behaviors\SortableBehavior',
            ],
        ];
    }

    /**
     * @return bool
     */
    public function beforeValidate()
    {
        foreach ($this->getTypeAttributes() as $attribute) {

            if ($attribute->isType(Attribute::TYPE_CHECKBOX)) {
                continue;
            }

            if ($attribute->isRequired() && (!isset($this->_typeAttributes[$attribute->id]) || '' === $this->_typeAttributes[$attribute->id])
            ) {
                $this->addError(
                    $attribute->title,
                    Yii::t("StoreModule.store", "{title} attribute is required", ['title' => $attribute->title])
                );
            }
        }

        if (!$this->isInStock()) {
            $this->setEmptyQuantity();
        }

        return parent::beforeValidate();
    }

    /**
     * @return $this
     */
    public function setEmptyQuantity()
    {
        $this->quantity = 0;

        return $this;
    }

    /**
     * @return array
     */
    public function getStatusList()
    {
        return [
            self::STATUS_ACTIVE => Yii::t('StoreModule.store', 'Active'),
            self::STATUS_NOT_ACTIVE => Yii::t('StoreModule.store', 'Not active'),
        ];
    }

    /**
     * @return string
     */
    public function getStatusTitle()
    {
        $data = $this->getStatusList();

        return isset($data[$this->status]) ? $data[$this->status] : Yii::t('StoreModule.store', '*unknown*');
    }

    /**
     * @return array
     */
    public function getSpecialList()
    {
        return [
            self::SPECIAL_NOT_ACTIVE => Yii::t('StoreModule.store', 'No'),
            self::SPECIAL_ACTIVE => Yii::t('StoreModule.store', 'Yes'),
        ];
    }

    /**
     * @return int
     */
    public function isSpecial()
    {
        return $this->is_special;
    }

    /**
     * @return string
     */
    public function getSpecial()
    {
        $data = $this->getSpecialList();

        return isset($data[$this->is_special]) ? $data[$this->is_special] : Yii::t('StoreModule.store', '*unknown*');
    }

    /**
     * @return array
     */
    public function getInStockList()
    {
        return [
            self::STATUS_IN_STOCK => Yii::t('StoreModule.store', 'In stock'),
            self::STATUS_NOT_IN_STOCK => Yii::t('StoreModule.store', 'Not in stock'),
        ];
    }

    /**
     * Устанавливает дополнительные категории товара
     *
     * @param array $categoriesId - список id категорий
     * @return bool
     *
     */
    public function saveCategories(array $categoriesId)
    {
        $categoriesId = array_diff($categoriesId, (array)$this->category_id);

        $currentCategories = Yii::app()->getDb()->createCommand()
            ->select('category_id')
            ->from('{{store_product_category}}')
            ->where('product_id = :id', [':id' => $this->id])
            ->queryColumn();

        if ($categoriesId == $currentCategories) {
            return true;
        }

        $transaction = Yii::app()->getDb()->beginTransaction();

        try {

            Yii::app()->getDb()->createCommand()
                ->delete('{{store_product_category}}', 'product_id = :id', [':id' => $this->id]);

            if (!empty($categoriesId)) {

                $data = [];

                foreach ($categoriesId as $id) {
                    $data[] = [
                        'product_id' => $this->id,
                        'category_id' => (int)$id,
                    ];
                }

                Yii::app()->getDb()->getCommandBuilder()
                    ->createMultipleInsertCommand('{{store_product_category}}', $data)
                    ->execute();
            }

            $transaction->commit();

            return true;
        } catch (Exception $e) {
            $transaction->rollback();

            return false;
        }
    }

    /**
     * @return mixed
     */
    public function getCategoriesId()
    {
        return Yii::app()->getDb()->createCommand()
            ->select('category_id')
            ->from('{{store_product_category}}')
            ->where('product_id = :id', [':id' => $this->id])
            ->queryColumn();
    }

    /**
     * @param array $attributes
     * @return bool
     */
    public function saveTypeAttributes(array $attributes)
    {
        $transaction = Yii::app()->getDb()->beginTransaction();

        try {

            $existAttributes = Yii::app()->db->createCommand()->select('attribute_id')->from(AttributeValue::model()->tableName())->where('product_id = :product', [
                ':product' => $this->id
            ])->group('attribute_id')->queryColumn();

            $unselectedAttributes = array_diff( $existAttributes, array_keys($attributes));

            if ($unselectedAttributes) {
                AttributeValue::model()->deleteAll('product_id = :product AND attribute_id IN(:attributes)', [
                    ':product' => $this->id,
                    ':attributes' => implode(',', $unselectedAttributes),
                ]);
            }
            
            foreach ($attributes as $attribute => $value) {

                if (null === $value) {
                    continue;
                }

                $model = AttributeValue::model()->find('product_id = :product AND attribute_id = :attribute', [
                    ':product' => $this->id,
                    ':attribute' => $attribute,
                ]);

                //множественные значения
                if (is_array($value)) {

                    AttributeValue::model()->deleteAll('product_id = :product AND attribute_id = :attribute', [
                        ':product' => $this->id,
                        ':attribute' => $attribute,
                    ]);

                    foreach ($value as $val) {
                        $model = new AttributeValue();
                        if (false === $model->store($attribute, $val, $this)) {
                            throw new InvalidArgumentException('Error store attribute!');
                        }
                    }

                } else {

                    $model = $model ?: new AttributeValue();

                    if (false === $model->store($attribute, $value, $this)) {
                        throw new InvalidArgumentException('Error store attribute!');
                    }
                }
            }

            $transaction->commit();

            return true;
        } catch (Exception $e) {
            $transaction->rollback();

            return false;
        }
    }


    /**
     * @param $attribute
     * @return string
     */
    public function attributeFile($attribute)
    {
        $value = $this->attribute($attribute);

        if (null === $value) {
            return null;
        }

        return Yii::app()->getRequest()->getBaseUrl(true).'/'.Yii::app()->getModule('yupe')->uploadPath.'/'.Yii::app()->getModule('store')->uploadPath.'/product/'.$value;
    }

    /**
     * @param $attribute
     * @param null $default
     * @return bool|float|int|null|string|array
     */
    public function attribute($attribute, $default = null)
    {
        if ($this->getIsNewRecord()) {
            return null;
        }

        $this->loadAttributes();

        $attributeName = $attribute instanceof Attribute ? $attribute->name : $attribute;

        if (!array_key_exists($attributeName, $this->_attributesValues)) {
            return $default;
        }

        //если атрибут имеет множество значений - вернем их массив
        if (is_array($this->_attributesValues[$attributeName])) {
            $values = [];
            foreach ($this->_attributesValues[$attributeName] as $attribute) {
                $value = $attribute->value($default);

                if (is_null($value)) {
                    continue;
                }

                $values[] = $value;
            }

            return $values;
        }

        return $this->_attributesValues[$attributeName]->value($default);
    }

    /**
     *
     */
    protected function loadAttributes()
    {
        if (null === $this->_attributesValues) {

            $this->_attributesValues = [];

            foreach ($this->attributesValues as $attribute) {

                //собираем массив multiple values attributes
                if ($attribute->attribute->isMultipleValues()) {
                    $this->_attributesValues[$attribute->attribute->name][] = $attribute;
                } else {
                    $this->_attributesValues[$attribute->attribute->name] = $attribute;
                }
            }
        }
    }


    /**
     *
     */
    public function attributes()
    {
        return $this->attributesValues;
    }


    /**
     * @return array
     */
    public function getTypesAttributesValues()
    {
        $data = [];

        foreach ($this->attributesValues as $attribute) {
            $data[$attribute->attribute_id] = $attribute->value();
        }

        return $data;
    }

    /**
     * @return bool
     */
    public function beforeDelete()
    {
        // чтобы удалились файлики
        foreach ((array)$this->images as $image) {
            $image->delete();
        }

        return parent::beforeDelete();
    }

    /**
     * @param array $attributes
     * @param array $typeAttributes
     * @param array $variants
     * @param array $categories
     * @return bool
     */
    public function saveData(array $attributes, array $typeAttributes, array $variants, array $categories = [])
    {
        $transaction = Yii::app()->getDb()->beginTransaction();

        try {

            $this->setAttributes($attributes);
            $this->setTypeAttributes($typeAttributes);

            if ($this->save()) {

                $this->saveVariants($variants);
                $this->saveCategories($categories);
                $this->saveTypeAttributes($typeAttributes);

                $transaction->commit();

                return true;
            }

            $transaction->rollback();
            return false;
        } catch (Exception $e) {
            $transaction->rollback();

            return false;
        }
    }

    /**
     * @param array $variants
     * @return bool
     */
    private function saveVariants(array $variants)
    {
        $transaction = Yii::app()->getDb()->beginTransaction();

        try {
            $productVariants = [];
            foreach ($variants as $var) {
                $variant = null;
                if (isset($var['id'])) {
                    $variant = ProductVariant::model()->findByPk($var['id']);
                }
                $variant = $variant ?: new ProductVariant();
                $variant->attributes = $var;
                $variant->product_id = $this->id;
                if ($variant->save()) {
                    $productVariants[] = $variant->id;
                }
            }

            $criteria = new CDbCriteria();
            $criteria->params = [':product_id' => $this->id];
            $criteria->addCondition('product_id = :product_id');
            $criteria->addNotInCondition('id', $productVariants);
            ProductVariant::model()->deleteAll($criteria);
            $transaction->commit();

            return true;
        } catch (Exception $e) {
            $transaction->rollback();

            return false;
        }
    }

    /**
     * @return float
     */
    public function getBasePrice()
    {
        return (float)$this->price;
    }

    /**
     * @return float
     */
    public function getResultPrice()
    {
        return (float)$this->getDiscountPrice() ?: (float)$this->price * (1 - ((float)$this->discount ?: 0) / 100);
    }

    /**
     * @return float
     */
    public function getDiscountPrice()
    {
        return $this->discount_price;
    }

    /**
     * @return bool
     */
    public function hasDiscount()
    {
        if ($this->discount_price || $this->discount) {
            return true;
        }

        return false;
    }

    /**
     * @return mixed id
     */
    public function getId()
    {
        $variantIds = array_map(
            function ($var) {
                return $var->id;
            },
            $this->selectedVariants
        );
        sort($variantIds);

        return 'product_'.$this->id.'_'.implode('_', $variantIds);
    }

    /**
     * @param array $variantsIds
     * @return float|mixed
     */
    public function getPrice(array $variantsIds = [])
    {
        $variants = [];
        if (!empty($variantsIds)) {
            $criteria = new CDbCriteria();
            $criteria->addInCondition("id", $variantsIds);
            $variants = ProductVariant::model()->findAll($criteria);
        } else {
            $variants = $this->selectedVariants;
        }
        $basePrice = $this->getResultPrice();
        /* выбираем вариант, который меняет базовую цену максимально */
        /* @var $variants ProductVariant[] */

        $hasBasePriceVariant = false;
        foreach ($variants as $variant) {
            if ($variant->type == ProductVariant::TYPE_BASE_PRICE) {
                if (!$hasBasePriceVariant) {
                    $hasBasePriceVariant = true;
                    $basePrice = $variant->amount;
                } else {
                    if ($basePrice < $variant->amount) {
                        $basePrice = $variant->amount;
                    }
                }
            }
        }
        $newPrice = $basePrice;
        foreach ($variants as $variant) {
            switch ($variant->type) {
                case ProductVariant::TYPE_SUM:
                    $newPrice += $variant->amount;
                    break;
                case ProductVariant::TYPE_PERCENT:
                    $newPrice += $basePrice * ($variant->amount / 100);
                    break;
            }
        }

        return $newPrice;
    }

    /**
     * @return string
     */
    public function getTitle()
    {
        return $this->title ?: $this->name;
    }

    /**
     * @return mixed
     */
    public function getLink()
    {
        return ProductHelper::getUrl($this);
    }

    /**
     * @return null|string
     */
    public function getCategoryId()
    {
        return is_object($this->category) ? $this->category->id : null;
    }

    /**
     * @return array
     */
    public function getTypeAttributes()
    {
        if (empty($this->type)) {
            return [];
        }

        return (array)$this->type->typeAttributes;
    }

    /**
     * @return null|string
     */
    public function getProducerName()
    {
        if (empty($this->producer)) {
            return null;
        }

        return $this->producer->name;
    }

    /**
     * @return string
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * @return boolean
     */
    public function isInStock()
    {
        return $this->in_stock == self::STATUS_IN_STOCK
            && (!Yii::app()->getModule('store')->controlStockBalances || $this->getAvailableQuantity() > 0);
    }

    /**
     * @return string
     */
    public function getMetaTitle()
    {
        return $this->meta_title ?: $this->name;
    }

    /**
     * @return string
     */
    public function getMetaDescription()
    {
        return $this->meta_description;
    }

    /**
     * @return string
     */
    public function getMetaKeywords()
    {
        return $this->meta_keywords;
    }

    /**
     * Get canonical url
     *
     * @return string
     */
    public function getMetaCanonical()
    {
        return $this->meta_canonical;
    }

    /**
     * @return ProductImage[]
     */
    public function getImages()
    {
        return $this->images;
    }

    /**
     * @return array
     */
    public function getAttributeGroups()
    {
        if (empty($this->type)) {
            return [];
        }

        return $this->type->getAttributeGroups();
    }

    /**
     * @return ProductVariant[]
     */
    public function getVariants()
    {
        return $this->variants;
    }

    /**
     * @return array
     */
    public function getVariantsGroup()
    {
        $variantsGroups = [];

        foreach ((array)$this->variants as $variant) {
            $variantsGroups[$variant->attribute->title][] = $variant;
        }

        return $variantsGroups;
    }

    /**
     * Функция для подготовки специфичных настроек элементов option в select при выводе вариантов, которые будут использоваться в js при работе с вариантами
     * @return array
     */
    public function getVariantsOptions()
    {
        if ($this->_variantsOptions !== false) {
            return $this->_variantsOptions;
        }
        $options = [];

        foreach ((array)$this->variants as $variant) {
            $options[$variant->id] = ['data-type' => $variant->type, 'data-amount' => $variant->amount];
        }
        $this->_variantsOptions = $options;

        return $this->_variantsOptions;
    }

    /**
     * @return null|Product
     * @throws CDbException
     */
    public function copy()
    {
        $transaction = Yii::app()->getDb()->beginTransaction();
        $model = new Product();
        try {
            $model->setAttributes($this->getAttributes());
            $model->slug = null;

            $similarNamesCount = Yii::app()->getDb()->createCommand()
                ->select('count(*)')
                ->from($this->tableName())
                ->where("name like :name", [':name' => $this->name.' [%]'])
                ->queryScalar();

            $model->name = $this->name.' ['.($similarNamesCount + 1).']';
            $model->slug = \yupe\helpers\YText::translit($model->name);

            $attributes = $model->attributes;
            $typeAttributes = $this->getTypesAttributesValues();
            $variantAttributes = $categoriesIds = [];

            if ($variants = $this->variants) {
                foreach ($variants as $variant) {
                    $variantAttributes[] = $variant->getAttributes(
                        ['attribute_id', 'attribute_value', 'amount', 'type', 'sku']
                    );
                }
            }

            if ($categories = $this->categories) {
                foreach ($categories as $category) {
                    $categoriesIds[] = $category->id;
                }
            }

            // Создание копии изображения товара
            if ($newFileName = $this->copyImage($model)) {
                $model->setAttribute('image', $newFileName);
            } else {
                throw new CException('Error copy image!');
            }

            // Сохранение данных
            if (!$model->saveData($attributes, $typeAttributes, $variantAttributes, $categoriesIds)) {
                throw new CDbException('Error copy product!');
            }

            $transaction->commit();

            return $model;
        } catch (Exception $e) {
            $transaction->rollback();
            // Удаление копии изображения товара
            if (!empty($model->image)) {
                \yupe\helpers\YFile::rmIfExists($model->upload->getFilePath());
            }
        }

        return null;
    }


    /**
     * @param $product
     * @param null $typeId
     * @return bool
     */
    public function link($product, $typeId = null)
    {
        $link = new ProductLink();

        $link->setAttributes([
            'product_id' => $this->id,
            'linked_product_id' => ($product instanceof Product ? $product->id : $product),
            'type_id' => $typeId,
        ]);

        return $link->save();
    }


    /**
     * @param array $attributes
     */
    public function setTypeAttributes(array $attributes)
    {
        $this->_typeAttributes = $attributes;
    }

    /**
     * Get image alt tag text
     *
     * @return string
     */
    public function getImageAlt()
    {
        return $this->image_alt ?: $this->getTitle();
    }

    /**
     * Get image title tag text
     *
     * @return string
     */
    public function getImageTitle()
    {
        return $this->image_title ?: $this->getTitle();
    }

    /**
     * @return integer
     */
    public function getAvailableQuantity()
    {
        return $this->quantity;
    }

    /**
     * Создание копии изображения товара
     * @param Product $clone
     * @return bool|mixed|string
     */
    public function copyImage(Product $clone)
    {
        // Генерация нового имени файла
        if (is_callable($clone->upload->fileName)) {
            $newFileName = call_user_func($clone->upload->fileName);
        } else {
            $newFileName = md5(uniqid($this->image, true));
        }
        $newFileName .= '.' . pathinfo($this->image, PATHINFO_EXTENSION);

        // Копирование файла
        $imagePathFrom = $this->upload->getFilePath();
        $imagePathTo = Yii::app()->uploadManager->getFilePath(
            $newFileName,
            $clone->upload->getUploadPath()
        );

        if (\yupe\helpers\YFile::cpFile($imagePathFrom, $imagePathTo)) {
            return $newFileName;
        }

        return false;
    }
}