<?php
/**
 * @author Semenov Alexander <semenov@skeeks.com>
 * @link http://skeeks.com/
 * @copyright 2010 SkeekS (СкикС)
 * @date 25.05.2015
 */

namespace skeeks\cms\shop\cmsWidgets\filters;

use skeeks\cms\base\WidgetRenderable;
use skeeks\cms\models\CmsContent;
use skeeks\cms\models\CmsContentElement;
use skeeks\cms\models\CmsContentProperty;
use skeeks\cms\models\CmsContentPropertyEnum;
use skeeks\cms\models\searchs\SearchChildrenRelatedPropertiesModel;
use skeeks\cms\models\searchs\SearchRelatedPropertiesModel;
use skeeks\cms\shop\models\ShopContent;
use skeeks\cms\shop\models\ShopTypePrice;
use yii\data\ActiveDataProvider;
use yii\helpers\ArrayHelper;
use yii\widgets\ActiveForm;

/**
 * @property ShopTypePrice $typePrice;
 * @property CmsContent    $cmsContent;
 * @property ShopContent   $shopContent;
 * @property CmsContent    $offerCmsContent;
 * @property []                 $childrenElementIds;
 *
 * Class ShopProductFiltersWidget
 * @package skeeks\cms\shop\cmsWidgets\filters
 */
class ShopProductFiltersWidget extends WidgetRenderable
{
    //Навигация
    public $content_id;
    public $searchModelAttributes = [];

    public $realatedProperties = [];
    public $offerRelatedProperties = [];

    public $type_price_id = "";

    /**
     * @var bool Учитывать только доступные фильтры для текущей выборки
     */
    public $onlyExistsFilters = false;

    /**
     * @var array (Массив ids записей, для показа только нужных фильтров)
     */
    public $elementIds = [];
    public $queryNeedElements = null;

    /**
     *
     * @var \skeeks\cms\shop\cmsWidgets\filters\models\SearchProductsModel
     */
    public $searchModel = null;

    /**
     * @var SearchRelatedPropertiesModel
     */
    public $searchRelatedPropertiesModel = null;

    /**
     * @var SearchChildrenRelatedPropertiesModel
     */
    public $searchOfferRelatedPropertiesModel = null;

    public $enableCache = false;

    public $cacheKey = null;
    protected $_relatedOptions = [];
    protected $_offerOptions = [];

    public static function descriptorConfig()
    {
        return array_merge(parent::descriptorConfig(), [
            'name' => \Yii::t('skeeks/shop/app', 'Filters'),
        ]);
    }

    public function init()
    {
        parent::init();

        if (!$this->searchModelAttributes) {
            $this->searchModelAttributes = [];
        }

        if (!$this->searchModel) {
            $this->searchModel = new \skeeks\cms\shop\cmsWidgets\filters\models\SearchProductsModel();
        }

        $this->searchModel->load(\Yii::$app->request->get());

        if ($this->cmsContent) {
            $this->searchRelatedPropertiesModel = new SearchRelatedPropertiesModel();
            $this->searchRelatedPropertiesModel->initCmsContent($this->cmsContent);
            $this->searchRelatedPropertiesModel->load(\Yii::$app->request->get());
        }

        if ($this->offerRelatedProperties && $this->cmsContent) {
            $this->searchOfferRelatedPropertiesModel = new SearchChildrenRelatedPropertiesModel();
            $this->searchOfferRelatedPropertiesModel->initCmsContent($this->offerCmsContent);
            $this->searchOfferRelatedPropertiesModel->load(\Yii::$app->request->get());
        }
    }

    public function attributeLabels()
    {
        return array_merge(parent::attributeLabels(),
            [
                'content_id'             => \Yii::t('skeeks/shop/app', 'Content'),
                'searchModelAttributes'  => \Yii::t('skeeks/shop/app', 'Fields'),
                'realatedProperties'     => \Yii::t('skeeks/shop/app', 'Properties'),
                'offerRelatedProperties' => \Yii::t('skeeks/shop/app', 'Offer properties'),
                'type_price_id'          => \Yii::t('skeeks/shop/app', 'Types of prices'),
            ]);
    }

    public function rules()
    {
        return ArrayHelper::merge(parent::rules(),
            [
                [['content_id'], 'integer'],
                [['searchModelAttributes'], 'safe'],
                [['offerRelatedProperties'], 'safe'],
                [['realatedProperties'], 'safe'],
                [['type_price_id'], 'integer'],
            ]);
    }

    public function renderConfigForm(ActiveForm $form)
    {
        echo \Yii::$app->view->renderFile(__DIR__.'/_form.php', [
            'form'  => $form,
            'model' => $this,
        ], $this);
    }

    /**
     * @return ShopTypePrice
     */
    public function getTypePrice()
    {
        if (!$this->type_price_id) {
            return null;
        }
        return ShopTypePrice::find()->where(['id' => $this->type_price_id])->one();
    }

    /**
     * @return CmsContent
     */
    public function getOfferCmsContent()
    {
        return $this->shopContent->offerContent;
    }

    /**
     * @return CmsContent
     */
    public function getCmsContent()
    {
        return CmsContent::findOne($this->content_id);
    }

    /**
     * @return ShopContent
     */
    public function getShopContent()
    {
        return ShopContent::findOne(['content_id' => $this->content_id]);
    }

    /**
     * @return array
     */
    public function getChildrenElementIds()
    {
        return array_keys(CmsContentElement::find()->andWhere([
            'parent_content_element_id' => $this->elementIds,
        ])->asArray()->indexBy('id')->all());
    }

    /**
     * @return \skeeks\cms\shop\models\ShopProductPrice
     */
    public function getMinPrice()
    {
        $minPrice = null;

        if ($this->elementIds && $this->typePrice) {
            $minPrice = \skeeks\cms\shop\models\ShopProductPrice::find()
                ->andWhere(['product_id' => $this->elementIds])
                ->andWhere(['type_price_id' => $this->typePrice->id])
                ->orderBy(['price' => SORT_ASC])
                ->one();
        }

        return $minPrice;
    }

    /**
     * @return \skeeks\cms\shop\models\ShopProductPrice
     */
    public function getMaxPrice()
    {
        $maxPrice = null;

        if ($this->elementIds && $this->typePrice) {
            $maxPrice = \skeeks\cms\shop\models\ShopProductPrice::find()
                ->andWhere(['product_id' => $this->elementIds])
                ->andWhere(['type_price_id' => $this->typePrice->id])
                ->orderBy(['price' => SORT_DESC])
                ->one();
        }

        return $maxPrice;
    }

    /**
     * @param ActiveDataProvider $activeDataProvider
     */
    public function search(ActiveDataProvider $activeDataProvider)
    {
        if ($this->onlyExistsFilters) {
            /**
             * @var $query \yii\db\ActiveQuery
             */
            $query = clone $activeDataProvider->query;
            //TODO::notice errors
            $query->with = [];
            $query->select(['cms_content_element.id as mainId', 'cms_content_element.id as id'])->indexBy('mainId');
            $ids = $query->asArray()->all();

            $this->cacheKey = md5($query->limit(10)->createCommand()->rawSql);

            $this->elementIds = array_keys($ids);
        }

        $this->searchModel->search($activeDataProvider);

        if ($this->searchRelatedPropertiesModel) {
            $this->searchRelatedPropertiesModel->search($activeDataProvider);
        }

        if ($this->searchOfferRelatedPropertiesModel) {
            $this->searchOfferRelatedPropertiesModel->search($activeDataProvider);
        }
    }

    /**
     * @param $property
     * @return bool
     */
    public function isShowRelatedProperty($property)
    {
        if (!in_array($property->code, $this->realatedProperties)) {
            return false;
        }

        if ($this->onlyExistsFilters === false) {
            return true;
        }

        if (in_array($property->property_type, [
            \skeeks\cms\relatedProperties\PropertyType::CODE_ELEMENT,
            \skeeks\cms\relatedProperties\PropertyType::CODE_LIST,
        ])) {
            $options = $this->getRelatedPropertyOptions($property);
            if (count($options) > 1) {
                return true;
            } else {
                return false;
            }
        }

        return true;
    }

    /**
     *
     * Получение доступных опций для свойства
     * @param CmsContentProperty $property
     * @return $this|array|\yii\db\ActiveRecord[]
     */
    public function getRelatedPropertyOptions($property)
    {
        $options = [];

        $cacheKey = $this->cacheKey."_rp_options_{$property->id}";
        $options = \Yii::$app->cache->get($cacheKey);
        if ($this->enableCache && $options) {
            return $options;
        }

        if (isset($this->_relatedOptions[$property->code])) {
            return $this->_relatedOptions[$property->code];
        }

        if ($this->onlyExistsFilters && !$this->elementIds) {
            return [];
        }

        if ($property->property_type == \skeeks\cms\relatedProperties\PropertyType::CODE_ELEMENT) {
            $propertyType = $property->handler;

            $options = \skeeks\cms\models\CmsContentElementProperty::find()->from([
                'map' => \skeeks\cms\models\CmsContentElementProperty::tableName(),
            ])
                ->leftJoin(['e' => CmsContentElement::tableName()], 'e.id = map.value_enum')
                ->select(['e.id as key', 'e.name as value', 'map.value_enum'])
                ->indexBy('key')
                ->groupBy('key')
                ->andWhere(['map.element_id' => $this->elementIds])
                ->andWhere(['map.property_id' => $property->id])
                ->andWhere(['>', 'map.value_enum', 0])
                ->andWhere(['>', 'e.id', 0])
                ->andWhere(['is not', 'map.value_enum', null])
                ->asArray()
                ->all();

            if ($this->onlyExistsFilters && !$options) {
                return [];
            }

            $options = \yii\helpers\ArrayHelper::map(
                $options, 'key', 'value'
            );

        } elseif ($property->property_type == \skeeks\cms\relatedProperties\PropertyType::CODE_LIST) {

            $options = \skeeks\cms\models\CmsContentElementProperty::find()->from([
                'map' => \skeeks\cms\models\CmsContentElementProperty::tableName(),
            ])
                ->leftJoin(['enum' => CmsContentPropertyEnum::tableName()], 'enum.id = map.value_enum')
                //->leftJoin(['p' => CmsContentProperty::tableName()], 'p.id = enum.property_id')
                ->select(['enum.id as key', 'enum.value as value', 'map.value_enum'])
                ->indexBy('key')
                ->groupBy('key')
                ->andWhere(['map.element_id' => $this->elementIds])
                ->andWhere(['map.property_id' => $property->id])
                ->andWhere(['>', 'map.value_enum', 0])
                ->andWhere(['>', 'enum.id', 0])
                ->andWhere(['is not', 'map.value_enum', null])
                ->asArray()
                ->all();

            /*$options =
                CmsContentPropertyEnum::find()
                    ->andWhere([CmsContentPropertyEnum::tableName() . '.id' => $this->elementIds])
                    ->joinWith('property as p')
                    ->joinWith('property.elementProperties as map')
                    ->andWhere(['p.id' => $property->id])
                    ->andWhere(['is not', 'map.value_enum', null])
                    ->select([
                        CmsContentPropertyEnum::tableName() . '.id',
                        CmsContentPropertyEnum::tableName() . '.value',
                        CmsContentPropertyEnum::tableName() . '.property_id'
                    ])
            ;*/

            if ($this->onlyExistsFilters && !$options) {
                return [];
            }

            $options = \yii\helpers\ArrayHelper::map(
                $options, 'key', 'value'
            );

        } elseif ($property->property_type == \skeeks\cms\relatedProperties\PropertyType::CODE_BOOL) {
            $availables = [];
            if ($this->elementIds) {
                $availables = \skeeks\cms\models\CmsContentElementProperty::find()
                    ->select(['value_bool'])
                    ->indexBy('value_bool')
                    ->groupBy('value_bool')
                    ->andWhere(['element_id' => $this->elementIds])
                    ->andWhere(['property_id' => $property->id])
                    ->asArray()
                    ->all();

                $availables = array_keys($availables);
            }

            if ($this->onlyExistsFilters && !$availables) {
                return [];
            }

            $options = [];
            foreach ($availables as $value) {
                $labal = $value;
                if ($value == 0) {
                    $label = \Yii::t('skeeks/cms', 'No');
                } else {
                    if ($value == 1) {
                        $label = \Yii::t('skeeks/cms', 'Yes');
                    }
                }
                $options[$value] = $label;
            }
        }

        $this->_relatedOptions[$property->code] = $options;

        if ($this->enableCache) {
            \Yii::$app->cache->set($cacheKey, $options);
        }

        return $options;
    }

    /**
     * @param $property
     *
     * @return null
     */
    public function getMaxValue($property)
    {
        $cacheKey = $this->cacheKey."_max_{$property->id}";
        $value = \Yii::$app->cache->get($cacheKey);
        if (!$this->enableCache) {
            $value = null;
        }

        if (!$value) {
            if ($this->elementIds) {
                $value = \skeeks\cms\models\CmsContentElementProperty::find()
                    ->select(['value_enum'])
                    ->andWhere(['element_id' => $this->elementIds])
                    ->andWhere(['property_id' => $property->id])
                    ->asArray()
                    ->orderBy(['value_enum' => SORT_DESC])
                    ->limit(1)
                    ->one();

                $value = (float)$value['value_enum'];

                if ($this->enableCache) {
                    \Yii::$app->cache->set($cacheKey, $value);
                }
            }
        }

        return $value;
    }

    /**
     * @param $property
     *
     * @return null
     */
    public function getMinValue($property)
    {
        $cacheKey = $this->cacheKey."_min_{$property->id}";
        $value = \Yii::$app->cache->get($cacheKey);
        if (!$this->enableCache) {
            $value = null;
        }

        if (!$value) {
            if ($this->elementIds) {
                $value = \skeeks\cms\models\CmsContentElementProperty::find()
                    ->select(['value_enum'])
                    ->andWhere(['element_id' => $this->elementIds])
                    ->andWhere(['property_id' => $property->id])
                    ->asArray()
                    ->orderBy(['value_enum' => SORT_ASC])
                    ->limit(1)
                    ->one();

                $value = (float)$value['value_enum'];

                if ($this->enableCache) {
                    \Yii::$app->cache->set($cacheKey, $value);
                }
            }
        }

        return $value;
    }

    /**
     * @param $property
     * @return bool
     */
    public function isShowOfferProperty($property)
    {
        if (!in_array($property->code, $this->offerRelatedProperties)) {
            return false;
        }

        if ($this->onlyExistsFilters === false) {
            return true;
        }

        if (in_array($property->property_type, [
            \skeeks\cms\relatedProperties\PropertyType::CODE_ELEMENT,
            \skeeks\cms\relatedProperties\PropertyType::CODE_LIST,
        ])) {
            $options = $this->getOfferRelatedPropertyOptions($property);
            if (count($options) > 1) {
                return true;
            } else {
                return false;
            }
        }

        return true;
    }

    /**
     *
     * Получение доступных опций для свойства
     * @param CmsContentProperty $property
     * @return $this|array|\yii\db\ActiveRecord[]
     */
    public function getOfferRelatedPropertyOptions($property)
    {
        if (isset($this->_offerOptions[$property->code])) {
            return $this->_offerOptions[$property->code];
        }

        if ($property->property_type == \skeeks\cms\relatedProperties\PropertyType::CODE_ELEMENT) {
            $propertyType = $property->handler;

            $availables = [];
            if ($this->childrenElementIds) {
                $availables = \skeeks\cms\models\CmsContentElementProperty::find()
                    ->select(['value_enum'])
                    ->indexBy('value_enum')
                    ->andWhere(['element_id' => $this->childrenElementIds])
                    ->andWhere(['property_id' => $property->id])
                    ->asArray()
                    ->all();

                $availables = array_keys($availables);
            }

            if ($this->onlyExistsFilters && !$availables) {
                return [];
            }

            $options = \skeeks\cms\models\CmsContentElement::find()
                ->active()
                ->andWhere(['content_id' => $propertyType->content_id]);
            if ($this->childrenElementIds) {
                $options->andWhere(['id' => $availables]);
            }

            $options = $options->select(['id', 'name'])->asArray()->all();

            $options = \yii\helpers\ArrayHelper::map(
                $options, 'id', 'name'
            );
        } elseif ($property->property_type == \skeeks\cms\relatedProperties\PropertyType::CODE_LIST) {
            $options = $property->getEnums()->select(['id', 'value']);

            $availables = [];
            if ($this->childrenElementIds) {
                $availables = \skeeks\cms\models\CmsContentElementProperty::find()
                    ->select(['value_enum'])
                    ->indexBy('value_enum')
                    ->andWhere(['element_id' => $this->childrenElementIds])
                    ->andWhere(['property_id' => $property->id])
                    ->asArray()
                    ->all();

                $availables = array_keys($availables);
                $options->andWhere(['id' => $availables]);
            }

            if ($this->onlyExistsFilters && !$availables) {
                return [];
            }

            $options = $options->asArray()->all();

            $options = \yii\helpers\ArrayHelper::map(
                $options, 'id', 'value'
            );
        }

        $this->_offerOptions[$property->code] = $options;

        return $options;
    }

    /**
     * @param $property
     * @return bool
     */
    public function isShowPriceFilter()
    {
        if (!$this->typePrice) {
            return false;
        }

        if ($this->onlyExistsFilters === false) {
            return true;
        }

        if ($this->searchModel->price_from == $this->searchModel->price_to) {
            return false;
        }

        return true;
    }

    protected function _run()
    {

        if ($this->elementIds && !$this->searchModel->price_from && $this->typePrice) {
            $this->searchModel->price_from = $this->minPrice->price;
        }

        if ($this->elementIds && !$this->searchModel->price_to && $this->typePrice) {
            $this->searchModel->price_to = $this->maxPrice->price;
        }

        return parent::_run();
    }
}


<?php
/**
 * @author Semenov Alexander <semenov@skeeks.com>
 * @link http://skeeks.com/
 * @copyright 2010 SkeekS (СкикС)
 * @date 22.03.2016
 */

namespace skeeks\cms\shop\console\controllers;

use skeeks\cms\shop\models\ShopProduct;
use skeeks\cms\shop\models\ShopProductQuantityChange;
use skeeks\cms\shop\models\ShopQuantityNoticeEmail;
use yii\console\Controller;
use yii\helpers\Console;

/**
 * Class NotifyController
 *
 * @package skeeks\cms\shop\console\controllers
 */
class NotifyController extends Controller
{

    /**
     * Просмотр созданных бекапов баз данных
     */
    public function actionQuantityEmails()
    {
        $countEmails = ShopQuantityNoticeEmail::find()
            ->andWhere(['is_notified' => 0])
            ->orderBy(['created_at' => SORT_ASC])
            ->count();

        if (!$countEmails) {
            $this->stdout("Уведомить некого\n", Console::BOLD);
            return;
        }
        $this->stdout("Количество клиентов для уведомлений: ".$countEmails."\n", Console::BOLD);

        //Самый старый запрос на уведомление, начинаем искать изменения от него
        $shopQuantityNoticeEmail = ShopQuantityNoticeEmail::find()
            ->andWhere(['is_notified' => 0])
            ->orderBy(['created_at' => SORT_ASC])
            ->one();

        $this->stdout("\tСамый старый запрос на уведомление: ".\Yii::$app->formatter->asDatetime($shopQuantityNoticeEmail->created_at)."\n");

        $productIds = ShopQuantityNoticeEmail::find()
            ->andWhere(['is_notified' => 0])
            ->orderBy(['>=', 'created_at', $shopQuantityNoticeEmail->created_at])
            ->orderBy(['created_at' => SORT_ASC])
            ->groupBy('shop_product_id')
            ->indexBy('shop_product_id')
            ->asArray()
            ->all();

        if (!$productIds) {
            $this->stdout("\tТоваров не найдено\n");
        }
        $this->stdout("\tНужно проверить изменения по товарам на которые подписались: ".count($productIds)."\n");

        $productIds = array_keys($productIds);

        //Какие товары появлялись в наличии за это время?
        $productIds = ShopProductQuantityChange::find()
            ->andWhere(['shop_product_id' => $productIds])
            ->andWhere(['>=', 'created_at', $shopQuantityNoticeEmail->created_at])
            ->andWhere(['>', 'quantity', 0])
            ->groupBy('shop_product_id')
            ->indexBy('shop_product_id')
            ->asArray()
            ->all();

        if (!$productIds) {
            $this->stdout("\tНе было изменений по товарам\n");
        }
        $productIds = array_keys($productIds);
        $this->stdout("\tИзменилось товаров: ".count($productIds)."\n");
        /**
         * @var ShopProduct $product
         */
        foreach (ShopProduct::find()->where(['id' => $productIds])->each(10) as $product) {
            $this->stdout("\t\tТовар: {$product->cmsContentElement->name}\n");
            $this->stdout("\t\tНаличие: {$product->quantity}\n");

            if ($product->quantity <= 0) {
                continue;
            }

            if ($noticeEmails = $product->getShopQuantityNoticeEmails()
                ->andWhere(['is_notified' => 0])
                ->groupBy('email')
                ->orderBy(['created_at' => SORT_DESC])
                ->all()
            ) {
                $count = count($noticeEmails);
                $this->stdout("\t\tУведомить клиентов: {$count}\n");
                /**
                 * @var ShopQuantityNoticeEmail $noticeEmail
                 */
                foreach ($noticeEmails as $noticeEmail) {
                    $this->stdout("\t\t\tКлиент: {$noticeEmail->email}\n");
                    $this->_notifyCleint($noticeEmail);
                }
            }
        }
    }

    protected function _notifyCleint(ShopQuantityNoticeEmail $noticeEmail)
    {
        if ($noticeEmail->is_notified == 1) {
            $this->stdout("\t\t\tУже уведомлен\n");
            return;
        }

        /*try
        {*/
        \Yii::$app->mailer->view->theme->pathMap['@app/mail'][] = '@skeeks/cms/shop/mail';

        \Yii::$app->mailer->compose('client-quantity-notice', [
            'model' => $noticeEmail,
        ])
            ->setFrom([\Yii::$app->cms->adminEmail => \Yii::$app->cms->appName.''])
            ->setTo($noticeEmail->email)
            ->setSubject(\Yii::t('skeeks/shop/app',
                    "We've got the goods interesting you")." (".$noticeEmail->shopProduct->cmsContentElement->name.")")
            ->send();

        $noticeEmail->notified_at = \Yii::$app->formatter->asTimestamp(time());
        $noticeEmail->is_notified = 1;
        $noticeEmail->save();

        $this->stdout("\t\t\tУведомлен\n", Console::FG_GREEN);

        ShopQuantityNoticeEmail::updateAll([
            'is_notified' => 1,
            'notified_at' => \Yii::$app->formatter->asTimestamp(time()),
        ], [
            'shop_product_id' => $noticeEmail->shop_product_id,
            'email'           => $noticeEmail->email,
            'is_notified'     => 0,
        ]);

        /*} catch (\Exception $e)
        {
            $this->stdout("\t\t\tEmail не отправлен: {$e->getMessage()}\n", Console::FG_RED);
        }*/
    }
}


<?php

namespace pietercolpaert\hardf;
/** a clone of the N3Lexer class from the N3js code by Ruben Verborgh **/
// **N3Lexer** tokenizes N3 documents.
class N3Lexer
{
    // Regular expression and replacement string to escape N3 strings.
    // Note how we catch invalid unicode sequences separately (they will trigger an error).
    private $escapeSequence = '/\\\\u([a-fA-F0-9]{4})|\\\\U([a-fA-F0-9]{8})|\\\\[uU]|\\\\(.)/';
    private $escapeReplacements;
    private $illegalIriChars = '/[\x00-\x20<>\\"\{\}\|\^\`]/';

    private $input;
    private $line = 1;

    private $prevTokenType;

    public function __construct($options = []) {
        $this->initTokenize();
        $this->escapeReplacements = [
            '\\' => '\\', "'"=> "'", '"' => '"',
            'n' => "\n", 'r' => "\r", 't' => "\t", 'f' => "\f", 'b' => chr(8),
            '_' => '_', '~' => '~', '.' => '.', '-' => '-', '!' => '!', '$' => '$', '&' => '&',
            '(' => '(', ')' => ')', '*' => '*', '+' => '+', ',' => ',', ';' => ';', '=' => '=',
            '/' => '/', '?' => '?', '#' => '#', '@' => '@', '%' => '%'
        ];
        // In line mode (N-Triples or N-Quads), only simple features may be parsed
        if ($options["lineMode"]) {
            // Don't tokenize special literals
            $this->tripleQuotedString = '/$0^/';
            $this->number = '/$0^/';
            $this->boolean = '/$0^/';
            // Swap the tokenize method for a restricted version
            $this->_oldTokenize = $this->_tokenize;
            $self = $this;
            $this->_tokenize = function ($input, $finalize = true) use ($self) {
                $tokens = call_user_func($this->_oldTokenize, $input, $finalize);
                foreach ($tokens as $token) {
                    if (!preg_match('/^(?:IRI|prefixed|literal|langcode|type|\.|eof)$/',$token["type"])) {
                        throw $self->syntaxError($token['type'], $token['line']);
                    }
                }
                return $tokens;
            };
        }
        // Enable N3 functionality by default
        $this->n3Mode = $options["n3"] !== false;

        // Disable comment tokens by default
        $this->comments = isset($options["comments"])?$options["comments"]:null;
    }

    // ## Regular expressions
    //_iri:        /^<((?:[^ <>{}\\]|\\[uU])+)>[ \t]*/, // IRI with escape sequences; needs sanity check after unescaping
    private $iri ='/^<((?:[^ <>{}\\\\]|\\\\[uU])+)>[ \\t]*/'; // IRI with escape sequences; needs sanity check after unescaping
    //      _unescapedIri:    /^<([^\x00-\x20<>\\"\{\}\|\^\`]*)>[ \t]*/, // IRI without escape sequences; no unescaping
    private $unescapedIri =  '/^<([^\\x00-\\x20<>\\\\"\\{\\}\\|\\^\\`]*)>[ \\t]*/'; // IRI without escape sequences; no unescaping
    //  _unescapedString:      /^"[^"\\]+"(?=[^"\\])/, // non-empty string without escape sequences
    private $unescapedString= '/^"[^\\\\"]+"(?=[^\\\\"])/'; // non-empty string without escape sequences
    //  _singleQuotedString:      /^"[^"\\]*(?:\\.[^"\\]*)*"(?=[^"\\])|^'[^'\\]*(?:\\.[^'\\]*)*'(?=[^'\\])/,
    private $singleQuotedString= '/^"[^"\\\\]*(?:\\\\.[^"\\\\]*)*"(?=[^"\\\\])|^\'[^\'\\\\]*(?:\\\\.[^\'\\\\]*)*\'(?=[^\'\\\\])/';
    //  _tripleQuotedString:       /^""("[^"\\]*(?:(?:\\.|"(?!""))[^"\\]*)*")""|^''('[^'\\]*(?:(?:\\.|'(?!''))[^'\\]*)*')''/,
    private $tripleQuotedString = '/^""("[^\\\\"]*(?:(?:\\\\.|"(?!""))[^\\\\"]*)*")""|^\'\'(\'[^\\\\\']*(?:(?:\\\\.|\'(?!\'\'))[^\\\\\']*)*\')\'\'/';
    private $langcode =  '/^@([a-z]+(?:-[a-z0-9]+)*)(?=[^a-z0-9\\-])/i';
    private $prefix = '/^((?:[A-Za-z\\xc0-\\xd6\\xd8-\\xf6])(?:\\.?[\\-0-9A-Z_a-z\\xb7\\xc0-\\xd6\\xd8-\\xf6])*)?:(?=[#\\s<])/';
    private $prefixed = "/^((?:[A-Za-z\\xc0-\\xd6\\xd8-\\xf6\\xf8-\\x{02ff}\\x{0370}-\\x{037d}\\x{037f}-\\x{1fff}\\x{200c}\\x{200d}\\x{2070}-\\x{218f}\\x{2c00}-\\x{2fef}\\x{3001}-\\x{d7ff}\\x{f900}-\\x{fdcf}\\x{fdf0}-\\x{fffd}])(?:\\.?[\\-0-9A-Z_a-z\\xb7\\xc0-\\xd6\\xd8-\\xf6\\xf8-\\x{037d}\\x{037f}-\\x{1fff}\\x{200c}\\x{200d}\\x{203f}\\x{2040}\\x{2070}-\\x{218f}\\x{2c00}-\\x{2fef}\\x{3001}-\\x{d7ff}\\x{f900}-\\x{fdcf}\\x{fdf0}-\\x{fffd}])*)?:((?:(?:[0-:A-Z_a-z\\xc0-\\xd6\\xd8-\\xf6\\xf8-\\x{02ff}\\x{0370}-\\x{037d}\\x{037f}-\\x{1fff}\\x{200c}\\x{200d}\\x{2070}-\\x{218f}\\x{2c00}-\\x{2fef}\\x{3001}-\\x{d7ff}\\x{f900}-\\x{fdcf}\\x{fdf0}-\\x{fffd}]|%[0-9a-fA-F]{2}|\\\\[!#-\\/;=?\\-@_~])(?:(?:[\\.\\-0-:A-Z_a-z\\xb7\\xc0-\\xd6\\xd8-\\xf6\\xf8-\\x{037d}\\x{037f}-\\x{1fff}\\x{200c}\\x{200d}\\x{203f}\\x{2040}\\x{2070}-\\x{218f}\\x{2c00}-\\x{2fef}\\x{3001}-\\x{d7ff}\\x{f900}-\\x{fdcf}\\x{fdf0}-\\x{fffd}]|%[0-9a-fA-F]{2}|\\\\[!#-\\/;=?\\-@_~])*(?:[\\-0-:A-Z_a-z\\xb7\\xc0-\\xd6\\xd8-\\xf6\\xf8-\\x{037d}\\x{037f}-\\x{1fff}\\x{200c}\\x{200d}\\x{203f}\\x{2040}\\x{2070}-\\x{218f}\\x{2c00}-\\x{2fef}\\x{3001}-\\x{d7ff}\\x{f900}-\\x{fdcf}\\x{fdf0}-\\x{fffd}]|%[0-9a-fA-F]{2}|\\\\[!#-\\/;=?\\-@_~]))?)?)(?:[ \\t]+|(?=\\.?[,;!\\^\\s#()\\[\\]\\{\\}\"'<]))/u";

    private $variable = '/^\\?(?:(?:[A-Z_a-z\\xc0-\\xd6\\xd8-\\xf6])(?:[\\-0-:A-Z_a-z\\xb7\\xc0-\\xd6\\xd8-\\xf6])*)(?=[.,;!\\^\\s#()\\[\\]\\{\\}"\'<])/';

    private $blank = '/^_:((?:[0-9A-Z_a-z\\xc0-\\xd6\\xd8-\\xf6])(?:\\.?[\\-0-9A-Z_a-z\\xb7\\xc0-\\xd6\\xd8-\\xf6])*)(?:[ \\t]+|(?=\\.?[,;:\\s#()\\[\\]\\{\\}"\'<]))/';
    private $number = "/^[\\-+]?(?:\\d+\\.?\\d*([eE](?:[\\-\\+])?\\d+)|\\d*\\.?\\d+)(?=[.,;:\\s#()\\[\\]\\{\\}\"'<])/";
    private $boolean = '/^(?:true|false)(?=[.,;\\s#()\\[\\]\\{\\}"\'<])/';
    private $keyword = '/^@[a-z]+(?=[\\s#<])/i';
    private $sparqlKeyword= '/^(?:PREFIX|BASE|GRAPH)(?=[\\s#<])/i';
    private $shortPredicates= '/^a(?=\\s+|<)/';
    private $newline= '/^[ \\t]*(?:#[^\\n\\r]*)?(?:\\r\\n|\\n|\\r)[ \\t]*/';
    private $comment= '/#([^\\n\\r]*)/';
    private $whitespace= '/^[ \\t]+/';
    private $endOfFile= '/^(?:#[^\\n\\r]*)?$/';

    // ## Private methods
    // ### `_tokenizeToEnd` tokenizes as for as possible, emitting tokens through the callback
    private function tokenizeToEnd($callback, $inputFinished) {

        // Continue parsing as far as possible; the loop will return eventually
        $input = $this->input;

        // Signals the syntax error through the callback
        $reportSyntaxError = function ($self)  use ($callback, &$input) {
            preg_match("/^\S*/", $input, $match);
            $callback($self->syntaxError($match[0], $self->line), null);
        };

        $outputComments = $this->comments;
        while (true) {
            // Count and skip whitespace lines
            $whiteSpaceMatch;
            $comment;
            while (preg_match($this->newline, $input, $whiteSpaceMatch)) {
                // Try to find a comment
                if ($outputComments && preg_match($this->comment, $whiteSpaceMatch[0], $comment))
                    callback(null, [ "line"=> $this->line, "type" => 'comment', "value"=> $comment[1], "prefix"=> '' ]);
                // Advance the input
                $input = substr($input,strlen($whiteSpaceMatch[0]), strlen($input));
                $this->line++;
            }
            // Skip whitespace on current line
            if (preg_match($this->whitespace, $input, $whiteSpaceMatch))
                $input = substr($input,strlen($whiteSpaceMatch[0]), strlen($input));

            // Stop for now if we're at the end
            if (preg_match($this->endOfFile, $input)) {
                // If the $input is finished, emit EOF
                if ($inputFinished) {
                    // Try to find a final comment
                    if ($outputComments && preg_match($this->comment, $input, $comment))
                        $callback(null, [ "line"=> $this->line, "type"=> 'comment', "value"=> $comment[1], "prefix"=> '' ]);
                    $callback($input = null, [ "line"=> $this->line, "type"=> 'eof', "value"=> '', "prefix"=> '' ]);
                }
                $this->input = $input;
                return $input;
            }

            // Look for specific token types based on the first character
            $line = $this->line;
            $type = '';
            $value = '';
            $prefix = '';
            $firstChar = $input[0];
            $match = null;
            $matchLength = 0;
            $unescaped = null;
            $inconclusive = false;

            switch ($firstChar) {
                case '^':
                    // We need at least 3 tokens lookahead to distinguish ^^<IRI> and ^^pre:fixed
                    if (strlen($input) < 3)
                        break;
                    // Try to match a type
                    else if ($input[1] === '^') {
                        $this->prevTokenType = '^^';
                        // Move to type IRI or prefixed name
                        $input = substr($input,2);
                        if ($input[0] !== '<') {
                            $inconclusive = true;
                            break;
                        }
                    }
                    // If no type, it must be a path expression
                    else {
                        if ($this->n3Mode) {
                            $matchLength = 1;
                            $type = '^';
                        }
                        break;
                    }
                    // Fall through in case the type is an IRI
                case '<':
                    // Try to find a full IRI without escape sequences
                    if (preg_match($this->unescapedIri, $input, $match)){
                        $type = 'IRI';
                        $value = $match[1];
                    }

                    // Try to find a full IRI with escape sequences
                    else if (preg_match($this->iri, $input, $match)) {
                        $unescaped = $this->unescape($match[1]);
                        if ($unescaped === null || preg_match($this->illegalIriChars,$unescaped))
                            return $reportSyntaxError($this);
                        $type = 'IRI';
                        $value = $unescaped;
                    }
                    // Try to find a backwards implication arrow
                    else if ($this->n3Mode && strlen($input) > 1 && $input[1] === '=') {
                        $type = 'inverse';
                        $matchLength = 2;
                        $value = 'http://www.w3.org/2000/10/swap/log#implies';
                    }
                    break;
                case '_':
                    // Try to find a blank node. Since it can contain (but not end with) a dot,
                    // we always need a non-dot character before deciding it is a prefixed name.
                    // Therefore, try inserting a space if we're at the end of the $input.
                    if ((preg_match($this->blank, $input, $match)) || $inputFinished && (preg_match($this->blank, $input . ' ', $match))) {
                        $type = 'blank';
                        $prefix = '_';
                        $value = $match[1];
                    }

                    break;

                case '"':
                case "'":
                    // Try to find a non-empty double-quoted literal without escape sequences
                    if (preg_match($this->unescapedString, $input, $match)){
                        $type = 'literal';
                        $value = $match[0];
                    }
                    // Try to find any other literal wrapped in a pair of single or double quotes
                    else if (preg_match($this->singleQuotedString, $input, $match)) {
                        $unescaped = $this->unescape($match[0]);
                        if ($unescaped === null)
                            return $reportSyntaxError($this);
                        $type = 'literal';
                        $value = preg_replace('/^\'|\'$/', '"',$unescaped);
                    }
                    // Try to find a literal wrapped in three pairs of single or double quotes
                    else if (preg_match($this->tripleQuotedString, $input, $match)) {
                        $unescaped = isset($match[1])?$match[1]:$match[2];
                        // Count the newlines and advance line counter
                        $this->line += sizeof(preg_split('/\r\n|\r|\n/',$unescaped)) - 1;
                        $unescaped = $this->unescape($unescaped);
                        if ($unescaped === null)
                            return $reportSyntaxError($this);
                        $type = 'literal';
                        $value = preg_replace("/^'|'$/", '"',$unescaped);
                    }
                break;

                case '?':
                    // Try to find a variable
                    if ($this->n3Mode && (preg_match($this->variable, $input, $match))) {
                        $type = 'var';
                        $value = $match[0];
                    }
                    break;

                case '@':
                    // Try to find a language code
                    if ($this->prevTokenType === 'literal' && preg_match($this->langcode, $input, $match)){
                        $type = 'langcode';
                        $value = $match[1];
                    }

                    // Try to find a keyword
                    else if (preg_match($this->keyword, $input, $match))
                        $type = $match[0];
                    break;

                case '.':
                    // Try to find a dot as punctuation
                    if (strlen($input) === 1 ? $inputFinished : ($input[1] < '0' || $input[1] > '9')) {
                        $type = '.';
                        $matchLength = 1;
                        break;
                    }
                    // Fall through to numerical case (could be a decimal dot)

                case '0':
                case '1':
                case '2':
                case '3':
                case '4':
                case '5':
                case '6':
                case '7':
                case '8':
                case '9':
                case '+':
                case '-':
                    // Try to find a number
                    if (preg_match($this->number, $input, $match)) {
                        $type = 'literal';
                        $value = '"' . $match[0] . '"^^http://www.w3.org/2001/XMLSchema#' . (isset($match[1]) ? 'double' : (preg_match("/^[+\-]?\d+$/",$match[0]) ? 'integer' : 'decimal'));
                    }
                    break;
                case 'B':
                case 'b':
                case 'p':
                case 'P':
                case 'G':
                case 'g':
                    // Try to find a SPARQL-style keyword
                    if (preg_match($this->sparqlKeyword, $input, $match))
                        $type = strtoupper($match[0]);
                    else
                        $inconclusive = true;
                    break;

                case 'f':
                case 't':
                    // Try to match a boolean
                    if (preg_match($this->boolean, $input, $match)){
                        $type = 'literal';
                        $value = '"' . $match[0] . '"^^http://www.w3.org/2001/XMLSchema#boolean';
                    } else
                        $inconclusive = true;
                    break;

                case 'a':
                    // Try to find an abbreviated predicate
                    if (preg_match($this->shortPredicates, $input, $match)) {
                        $type = 'abbreviation';
                        $value = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
                    }
                    else
                        $inconclusive = true;
                    break;
                case '=':
                    // Try to find an implication arrow or equals sign
                    if ($this->n3Mode && strlen($input) > 1) {
                        $type = 'abbreviation';
                        if ($input[1] !== '>') {
                            $matchLength = 1;
                            $value = 'http://www.w3.org/2002/07/owl#sameAs';
                        }
                        else{
                            $matchLength = 2;
                            $value = 'http://www.w3.org/2000/10/swap/log#implies';
                        }
                    }
                    break;

                case '!':
                    if (!$this->n3Mode)
                        break;
                case ',':
                case ';':
                case '[':
                case ']':
                case '(':
                case ')':
                case '{':
                case '}':
                    // The next token is punctuation
                    $matchLength = 1;
                    $type = $firstChar;
                    break;
                default:
                    $inconclusive = true;
            }

            // Some first characters do not allow an immediate decision, so inspect more
            if ($inconclusive) {
                // Try to find a prefix
                if (($this->prevTokenType === '@prefix' || $this->prevTokenType === 'PREFIX') && preg_match($this->prefix, $input, $match)){
                    $type = 'prefix';
                    $value = isset($match[1])?$match[1]:'';
                }
                // Try to find a prefixed name. Since it can contain (but not end with) a dot,
                // we always need a non-dot character before deciding it is a prefixed name.
                // Therefore, try inserting a space if we're at the end of the input.
                else if (preg_match($this->prefixed, $input, $match) || $inputFinished && (preg_match($this->prefixed, $input . ' ', $match))) {
                    $type = 'prefixed';
                    $prefix = isset($match[1])?$match[1]:'';
                    $value = $this->unescape($match[2]);
                }
            }

            // A type token is special: it can only be emitted after an IRI or prefixed name is read
            if ($this->prevTokenType === '^^') {
                switch ($type) {
                    case 'prefixed': $type = 'type';    break;
                    case 'IRI':      $type = 'typeIRI'; break;
                    default:         $type = '';
                }
            }

            // What if nothing of the above was found?
            if (!$type) {
                // We could be in streaming mode, and then we just wait for more input to arrive.
                // Otherwise, a syntax error has occurred in the input.
                // One exception: error on an unaccounted linebreak (= not inside a triple-quoted literal).
                if ($inputFinished || (!preg_match('/^\'\'\'|^"""/',$input) && preg_match('/\\n|\\r/',$input))) {
                    return $reportSyntaxError($this);
                } else {
                    $this->input = $input;
                    return $input;
                }
            }
            // Emit the parsed token
            $callback(null, [ "line"=> $line, "type"=> $type, "value"=>$value, "prefix"=> $prefix ]);
            $this->prevTokenType = $type;

            // Advance to next part to tokenize
            $input = substr($input,$matchLength>0?$matchLength:strlen($match[0]), strlen($input));
        }

    }

    // ### `_unescape` replaces N3 escape codes by their corresponding characters
    private function unescape($item) {
        return preg_replace_callback($this->escapeSequence, function ($match) {
            $sequence = $match[0];
            $unicode4 = isset($match[1])?$match[1]:null;
            $unicode8 = isset($match[2])?$match[2]:null;
            $escapedChar = isset($match[3])?$match[3]:null;
            $charCode;
            if ($unicode4) {
                $charCode = intval($unicode4, 16);
                return mb_convert_encoding('&#' . intval($charCode) . ';', 'UTF-8', 'HTML-ENTITIES');
            }
            else if ($unicode8) {
                $charCode = intval($unicode8, 16);
                return mb_convert_encoding('&#' . intval($charCode) . ';', 'UTF-8', 'HTML-ENTITIES');
            }
            else {
                if (!isset($this->escapeReplacements[$escapedChar]))
                    throw new \Exception();
                return $this->escapeReplacements[$escapedChar];
            }
        },$item);
    }

    // ### `_syntaxError` creates a syntax error for the given issue
    private function syntaxError($issue, $line = 0) {
        $this->input = null;
        return new \Exception('Unexpected "' . $issue . '" on line ' . $line . '.');
    }

    // When handling tokenize as a variable, we can hotswap its functionality when dealing with various serializations
    private function initTokenize()
    {
        $this->_tokenize = function ($input, $finalize) {
            // If the input is a string, continuously emit tokens through the callback until the end
            if (!isset($this->input))
                $this->input = "";
            $this->input .= $input;
            $tokens = [];
            $error = "";
            $this->input = $this->tokenizeToEnd(function ($e, $t) use (&$tokens,&$error) {
                if (isset($e)) {
                    $error = $e;
                }
                array_push($tokens, $t);
            }, $finalize);
            if ($error) throw $error;
            return $tokens;
        };
    }

    // ## Public methods

    // ### `tokenize` starts the transformation of an N3 document into an array of tokens.
    // The input can be a string or a stream.
    public function tokenize($input, $finalize = true) {
        try {
            return call_user_func($this->_tokenize, $input, $finalize);
        } catch (\Exception $e) {
            throw $e;
        }
    }

    // Adds the data chunk to the buffer and parses as far as possible
    public function tokenizeChunk($input)
    {
        return $this->tokenize($input, false);
    }

    public function end()
    {
        // Parses the rest
        return $this->tokenizeToEnd(true);
    }
}


<?php
/**
 * Вспомогательные иструменты
 *
 * @author Semenov Alexander <semenov@skeeks.com>
 * @link http://skeeks.com/
 * @copyright 2010 SkeekS (СкикС)
 * @date 28.04.2015
 */

namespace skeeks\cms\shop\controllers;

use skeeks\cms\base\Controller;

/**
 * Class ToolsController
 * @package skeeks\cms\controllers
 */
class ToolsController extends Controller
{

    /**
     * Выбор элемента контента
     * @return string
     */
    public function actionSelectCmsElement()
    {
        $this->layout = '@skeeks/cms/modules/admin/views/layouts/main.php';
        \Yii::$app->cmsToolbar->enabled = 0;

        return $this->render($this->action->id);
    }
}


<?php
/**
 * @author Semenov Alexander <semenov@skeeks.com>
 * @link http://skeeks.com/
 * @copyright 2010 SkeekS (СкикС)
 * @date 21.09.2015
 */

namespace skeeks\cms\shop\controllers;

use skeeks\cms\base\Controller;
use skeeks\cms\filters\CmsAccessControl;
use skeeks\cms\shop\models\ShopBill;
use skeeks\cms\shop\models\ShopOrder;
use yii\base\UserException;
use yii\helpers\ArrayHelper;
use yii\web\NotFoundHttpException;

/**
 * Class CartController
 * @package skeeks\cms\shop\controllers
 */
class OrderController extends Controller
{
    public $defaultAction = 'view';

    /**
     * @return array
     */
    public function behaviors()
    {
        return ArrayHelper::merge(parent::behaviors(), [

            'accessToView' => [
                'class' => CmsAccessControl::class,
                'only'  => ['view'],
                'rules' => [
                    // deny all POST request
                    //
                    [
                        'allow'         => true,
                        'matchCallback' => function ($rule, $action) {
                            $id = \Yii::$app->request->get('id');
                            $shopOrder = ShopOrder::findOne($id);

                            if (\Yii::$app->user->isGuest) {
                                return false;
                            }

                            if ($shopOrder->user_id == \Yii::$app->user->identity->id) {
                                return true;
                            }

                            return false;
                        },
                    ],
                ],
            ],

            'accessToList' => [
                'class' => CmsAccessControl::class,
                'only'  => ['list'],
                'rules' => [
                    [
                        'allow' => true,
                        'roles' => ['@'],
                    ],
                ],
            ],
        ]);
    }

    public function beforeAction($action)
    {
        if ($action->id == 'view') {
            $this->enableCsrfValidation = false;
        }

        return parent::beforeAction($action);
    }

    /**
     * @return string
     */
    public function actionList()
    {
        $this->view->title = \Yii::t('skeeks/shop/app', 'My orders').' | '.\Yii::t('skeeks/shop/app', 'Shop');

        return $this->render($this->action->id);
    }

    /**
     * @return string
     */
    public function actionView()
    {
        $this->view->title = \Yii::t('skeeks/shop/app', 'Order').' | '.\Yii::t('skeeks/shop/app', 'Shop');

        return $this->render($this->action->id, [
            'model' => ShopOrder::findOne(\Yii::$app->request->get('id')),
        ]);
    }

    /**
     * @return string
     */
    public function actionFinish()
    {
        $this->view->title = \Yii::t('skeeks/shop/app', 'Order').' | '.\Yii::t('skeeks/shop/app', 'Shop');

        return $this->render($this->action->id, [
            'model' => ShopOrder::find()->andWhere(['code' => \Yii::$app->request->get('code')])->one(),
        ]);
    }

    /**
     * Оплатить
     *
     * @return mixed
     * @throws NotFoundHttpException
     */
    public function actionPay()
    {
        /**
         * @var $shopOrder ShopOrder
         */
        if (\Yii::$app->request->get('code')) {
            $shopOrder = ShopOrder::find()->where(['code' => \Yii::$app->request->get('code')])->one();
        } else {
            $shopOrder = ShopOrder::findOne(\Yii::$app->request->get('id'));
        }

        if (!$shopOrder) {
            throw new NotFoundHttpException;
        }

        $shopBill = new ShopBill();
        $shopBill->shop_order_id = $shopOrder->id;
        $shopBill->shop_buyer_id = $shopOrder->shop_buyer_id;
        $shopBill->shop_pay_system_id = $shopOrder->shop_pay_system_id;
        $shopBill->amount = $shopOrder->amount;
        $shopBill->currency_code = $shopOrder->currency_code;
        $shopBill->description = "Оплата по заказу №".$shopOrder->id;
        if (!$shopBill->save()) {
            throw new UserException('Не создался счет: '.print_r($shopBill->errors, true));
        }

        return $shopBill->shopPaySystem->paySystemHandler->actionPaymentResponse($shopBill);

    }

    public function actionPayPal()
    {
        return $this->render($this->action->id, [
            'model' => ShopOrder::findOne(\Yii::$app->request->get('id')),
        ]);
    }

    /**
     * @deprecated
     * @return mixed
     * @throws NotFoundHttpException
     */
    public function actionFinishPay()
    {
        /**
         * @var $shopOrder ShopOrder
         */
        if (\Yii::$app->request->get('code')) {
            $shopOrder = ShopOrder::find()->where(['code' => \Yii::$app->request->get('code')])->one();
        } else {
            $shopOrder = ShopOrder::findOne(\Yii::$app->request->get('id'));
        }

        if (!$shopOrder) {
            throw new NotFoundHttpException;
        }

        return $shopOrder->paySystem->paySystemHandler->paymentResponse($shopOrder);

    }

}


<?php
/**
 * @author Semenov Alexander <semenov@skeeks.com>
 * @link http://skeeks.com/
 * @copyright 2010 SkeekS (СкикС)
 * @date 06.03.2016
 */

namespace skeeks\cms\shop\models;

use skeeks\cms\models\CmsContentElement;
use yii\helpers\ArrayHelper;

/**
 * @property ShopProduct             $shopProduct
 * @property ShopViewedProduct[]     $shopViewedProducts
 *
 * @property ShopCmsContentElement   $parentContentElement
 * @property ShopCmsContentElement[] $childrenContentElements
 *
 * @property ShopCmsContentElement[] $tradeOffers
 * @property ShopContent             $shopContent
 *
 * Class ShopCmsContentElement
 * @package skeeks\cms\shop\models
 */
class ShopCmsContentElement extends CmsContentElement
{

    /**
     * @inheritdoc
     */
    public function init()
    {
        parent::init();

        $this->on(self::EVENT_BEFORE_DELETE, [$this, "_deleteShops"]);
        $this->on(self::EVENT_AFTER_DELETE, [$this, "_updateParentAfterDelete"]);
    }

    //Удалить все что связано с элементом
    public function _deleteShops()
    {
        if ($this->shopProduct) {
            $this->shopProduct->delete();
        }

        if ($this->tradeOffers) {
            foreach ($this->tradeOffers as $tradeOffer) {
                $tradeOffer->delete();
            }
        }
    }

    /**
     * Проверка родителя если он есть, и изменение его типа если нужно после удаления текущего
     * @param $event
     */
    public function _updateParentAfterDelete($event)
    {
        //Если есть родительский элемент
        if ($this->parent_content_element_id) {
            if ($offers = $this->parentContentElement->getTradeOffers()->all()) {
                /**
                 * Если есть оферы, берем одного из них и обновляем цены, это повлечет за собой обновление цены у продукта
                 * @var $offer ShopCmsContentElement
                 */
                $offer = array_shift($offers);

                if ($offer->shopProduct && $offer->shopProduct->shopProductPrices) {
                    foreach ($offer->shopProduct->shopProductPrices as $shopPrice) {
                        $shopPrice->save();
                    }
                }

                $this->parentContentElement->shopProduct->product_type = ShopProduct::TYPE_OFFERS;
            } else {
                $this->parentContentElement->shopProduct->product_type = ShopProduct::TYPE_SIMPLE;
            }

            $this->parentContentElement->shopProduct->save();
        }
    }
    /**
     * @return \yii\db\ActiveQuery
     */
    public function getTradeOffers()
    {
        $childContentId = null;
        if ($this->shopContent) {
            $childContentId = $this->shopContent->children_content_id;
        }

        return $this
            ->hasMany(static::class, ['parent_content_element_id' => 'id'])
            ->andWhere(["content_id" => $childContentId])
            ->orderBy(['priority' => SORT_ASC]);
    }
    /**
     * @return \yii\db\ActiveQuery
     */
    public function getShopProduct()
    {
        return $this->hasOne(ShopProduct::class, ['id' => 'id'])->from(['shopProduct' => ShopProduct::tableName()]);
    }
    /**
     * @return \yii\db\ActiveQuery
     */
    public function getShopViewedProducts()
    {
        return $this->hasMany(ShopViewedProduct::class, ['shop_product_id' => 'id']);
    }
    /**
     * @return \yii\db\ActiveQuery
     */
    public function getShopContent()
    {
        return $this->hasOne(ShopContent::class, ['content_id' => 'content_id']);
    }
    /**
     * @return CmsContentElement|static
     */
    public function copy()
    {
        $transaction = \Yii::$app->db->beginTransaction();
        try {

            $model = parent::copy();
            $sp = $this->shopProduct;

            ArrayHelper::remove($sp, 'id');
            ArrayHelper::remove($sp, 'created_at');
            ArrayHelper::remove($sp, 'created_by');
            ArrayHelper::remove($sp, 'updated_by');
            ArrayHelper::remove($sp, 'updated_at');
            ArrayHelper::remove($sp, 'product_id');

            $spNew = new ShopProduct(ArrayHelper::merge($sp->toArray(), [
                'id' => $model->id,
            ]));
            $spNew->save();

            /**
             * @var $prices ShopProductPrice[]
             */
            if ($prices = $sp->getShopProductPrices()->all()) {
                foreach ($prices as $price) {
                    $priceData = $price->toArray();

                    ArrayHelper::remove($priceData, 'id');
                    ArrayHelper::remove($priceData, 'created_at');
                    ArrayHelper::remove($priceData, 'created_by');
                    ArrayHelper::remove($priceData, 'updated_by');
                    ArrayHelper::remove($priceData, 'updated_at');
                    ArrayHelper::remove($priceData, 'product_id');

                    if (!$priceNew = ShopProductPrice::findOne(['product_id' => $model->id, 'type_price_id' => $price->type_price_id])) {
                        $priceNew = new ShopProductPrice(ArrayHelper::merge($priceData, [
                            'product_id' => $model->id,
                        ]));
                    } else {
                        $priceNew->setAttributes(ArrayHelper::merge($priceData, [
                            'product_id' => $model->id,
                        ]));
                    }

                    $priceNew->save();
                }
            }

            $transaction->commit();

        } catch (\Exception $e) {

            $transaction->rollBack();
            throw $e;
        }

        return $model;

    }

}


<?php
/**
 * @link https://cms.skeeks.com/
 * @copyright Copyright (c) 2010 SkeekS
 * @license https://cms.skeeks.com/license/
 * @author Semenov Alexander <semenov@skeeks.com>
 */

namespace skeeks\cms\shop\models;

use skeeks\cms\models\behaviors\HasJsonFieldsBehavior;
use skeeks\cms\money\models\MoneyCurrency;
use skeeks\cms\money\Money;
use Yii;
use yii\db\AfterSaveEvent;
use yii\helpers\ArrayHelper;
use yii\helpers\Url;

/**
 * This is the model class for table "{{%shop_bill}}".
 *
 * @property int           $id
 * @property int           $created_by
 * @property int           $updated_by
 * @property int           $created_at
 * @property int           $updated_at
 * @property int           $shop_buyer_id Покупатель
 * @property int           $shop_order_id Заказ
 * @property int           $shop_pay_system_id Платежная система
 * @property int           $paid_at Дата оплаты
 * @property int           $shop_payment_id Платеж
 * @property int           $closed_at Дата отмены
 * @property string        $reason_closed Причина отмены
 * @property string        $amount
 * @property string        $currency_code
 * @property string        $description
 * @property string        $code Уникальный код счета
 * @property array         $external_data Внешние данные, например от платежной системы
 *
 * @property MoneyCurrency $currencyCode
 * @property ShopBuyer     $shopBuyer
 * @property ShopOrder     $shopOrder
 * @property ShopPayment   $shopPayment
 * @property ShopPaySystem $shopPaySystem
 *
 * @property string        $url Ссылка на страницу счета
 * @property string        $payUrl Ссылка на страницу оплаты
 *
 * @property Money         $money
 */
class ShopBill extends \skeeks\cms\base\ActiveRecord
{
    /**
     * {@inheritdoc}
     */
    public static function tableName()
    {
        return '{{%shop_bill}}';
    }

    public function behaviors()
    {
        return ArrayHelper::merge(parent::behaviors(), [
            HasJsonFieldsBehavior::class => [
                'class'  => HasJsonFieldsBehavior::class,
                'fields' => ['external_data'],
            ],
        ]);
    }
    /**
     *
     */
    public function init()
    {
        parent::init();

        $this->on(self::EVENT_AFTER_INSERT, [$this, '_notifyCreate']);
        $this->on(self::EVENT_AFTER_UPDATE, [$this, '_notifyUpdate']);
    }
    /**
     * Уведомление о создании счета на оплату заказчику услуги
     */
    public function _notifyCreate()
    {
        if ($this->shopBuyer && $this->shopBuyer->email) {

            \Yii::$app->mailer->view->theme->pathMap['@app/mail'][] = '@skeeks/cms/shop/mail/bill';

            \Yii::$app->mailer->compose('create', [
                'model' => $this,
            ])
                ->setFrom([\Yii::$app->cms->adminEmail => \Yii::$app->cms->appName.''])
                ->setTo($this->shopBuyer->email)
                ->setSubject('Выставлен счет №'.$this->id." от ".\Yii::$app->formatter->asDate($this->created_at)." по заказу №".$this->shopOrder->id)
                ->send();
        }
    }
    /**
     * Уведомление об оплате счета заказчику услуги
     *
     * @param AfterSaveEvent $event
     */
    public function _notifyUpdate(AfterSaveEvent $event)
    {
        if ($this->shopBuyer && $this->shopBuyer->email) {

            //Если счет стал оплаченым
            if (in_array("paid_at", array_keys($event->changedAttributes)) && $this->paid_at) {

                try {

                    //2 уведомить
                    \Yii::$app->mailer->view->theme->pathMap['@app/mail'][] = '@skeeks/cms/shop/mail/bill';

                    \Yii::$app->mailer->compose('paid', [
                        'model' => $this,
                    ])
                        ->setFrom([\Yii::$app->cms->adminEmail => \Yii::$app->cms->appName.''])
                        ->setTo($this->shopBuyer->email)
                        ->setSubject('Оплачен счет №'.$this->id." от ".\Yii::$app->formatter->asDate($this->created_at)." по заказу №".$this->shopOrder->id)
                        ->send();

                } catch (\Exception $e) {
                    \Yii::error($e->getMessage(), self::class);
                }

            }
        }
    }
    /**
     * {@inheritdoc}
     */
    public function rules()
    {
        return ArrayHelper::merge(parent::rules(), [
            [['created_by', 'updated_by', 'created_at', 'updated_at', 'shop_buyer_id', 'shop_order_id', 'shop_pay_system_id', 'paid_at', 'shop_payment_id', 'closed_at'], 'integer'],
            [['shop_buyer_id', 'shop_order_id', 'shop_pay_system_id'], 'required'],
            [['reason_closed', 'description'], 'string'],
            [['external_data'], 'safe'],
            [['amount'], 'number'],
            [['currency_code'], 'string', 'max' => 3],
            [['code'], 'string', 'max' => 255],
            [['code'], 'unique'],
            [['currency_code'], 'exist', 'skipOnError' => true, 'targetClass' => MoneyCurrency::class, 'targetAttribute' => ['currency_code' => 'code']],
            [['shop_buyer_id'], 'exist', 'skipOnError' => true, 'targetClass' => ShopBuyer::class, 'targetAttribute' => ['shop_buyer_id' => 'id']],
            [['shop_order_id'], 'exist', 'skipOnError' => true, 'targetClass' => ShopOrder::class, 'targetAttribute' => ['shop_order_id' => 'id']],
            [['shop_payment_id'], 'exist', 'skipOnError' => true, 'targetClass' => ShopPayment::class, 'targetAttribute' => ['shop_payment_id' => 'id']],
            [['shop_pay_system_id'], 'exist', 'skipOnError' => true, 'targetClass' => ShopPaySystem::class, 'targetAttribute' => ['shop_pay_system_id' => 'id']],

            [['code'], 'default', 'value' => \Yii::$app->security->generateRandomString()],
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function attributeLabels()
    {
        return ArrayHelper::merge(parent::attributeLabels(), [
            'id'                 => Yii::t('skeeks/shop/app', 'ID'),
            'shop_buyer_id'      => Yii::t('skeeks/shop/app', 'Покупатель'),
            'shop_order_id'      => Yii::t('skeeks/shop/app', 'Заказ'),
            'shop_pay_system_id' => Yii::t('skeeks/shop/app', 'Платежная система'),
            'paid_at'            => Yii::t('skeeks/shop/app', 'Дата оплаты'),
            'shop_payment_id'    => Yii::t('skeeks/shop/app', 'Платеж'),
            'closed_at'          => Yii::t('skeeks/shop/app', 'Дата отмены'),
            'reason_closed'      => Yii::t('skeeks/shop/app', 'Причина отмены'),
            'amount'             => Yii::t('skeeks/shop/app', 'Сумма'),
            'currency_code'      => Yii::t('skeeks/shop/app', 'Currency Code'),
            'description'        => Yii::t('skeeks/shop/app', 'Description'),
            'code'               => Yii::t('skeeks/shop/app', 'Уникальный код счета'),
        ]);
    }

    /**
     * @return \yii\db\ActiveQuery
     */
    public function getCurrencyCode()
    {
        return $this->hasOne(MoneyCurrency::class, ['code' => 'currency_code']);
    }

    /**
     * @return \yii\db\ActiveQuery
     */
    public function getShopBuyer()
    {
        return $this->hasOne(ShopBuyer::class, ['id' => 'shop_buyer_id']);
    }

    /**
     * @return \yii\db\ActiveQuery
     */
    public function getShopOrder()
    {
        return $this->hasOne(ShopOrder::class, ['id' => 'shop_order_id']);
    }

    /**
     * @return \yii\db\ActiveQuery
     */
    public function getShopPayment()
    {
        return $this->hasOne(ShopPayment::class, ['id' => 'shop_payment_id']);
    }

    /**
     * @return \yii\db\ActiveQuery
     */
    public function getShopPaySystem()
    {
        return $this->hasOne(ShopPaySystem::class, ['id' => 'shop_pay_system_id']);
    }

    /**
     * @return Money
     */
    public function getMoney()
    {
        return new Money($this->amount, (string)$this->currency_code);
    }

    /**
     * @param bool $scheme
     * @return string
     */
    public function getUrl($scheme = false)
    {
        return Url::to(['/shop/shop-bill/view', 'code' => $this->code], $scheme);
    }
    /**
     * @param bool $scheme
     * @return string
     */
    public function getPayUrl($scheme = false)
    {
        return Url::to(['/shop/shop-bill/go', 'code' => $this->code], $scheme);
    }
}


<?php
/**
 * @link https://cms.skeeks.com/
 * @copyright Copyright (c) 2010 SkeekS
 * @license https://cms.skeeks.com/license/
 * @author Semenov Alexander <semenov@skeeks.com>
 */

namespace skeeks\cms\shop\controllers;

use skeeks\cms\shop\models\ShopBill;
use skeeks\cms\shop\models\ShopPayment;
use skeeks\cms\shop\paySystems\SberbankPaySystem;
use yii\base\Exception;
use yii\helpers\ArrayHelper;
use yii\helpers\Json;
use yii\helpers\Url;
use yii\web\Controller;

/**
 * @author Semenov Alexander <semenov@skeeks.com>
 */
class SberbankController extends Controller
{
    /**
     * @inheritdoc
     */
    public $enableCsrfValidation = false;

    /**
     * Payment form
     *
     * @throws Exception
     */
    public function actionBillForm()
    {
        /**
         * @var $bill ShopBill
         */

        if (!$code = \Yii::$app->request->get('code')) {
            throw new Exception('Bill not found');
        }

        if (!$bill = ShopBill::find()->where(['code' => $code])->one()) {
            throw new Exception('Bill not found');
        }

        if (isset($bill->external_data['formUrl'])) {
            return $this->redirect($bill->external_data['formUrl']);
        }

        $data = [
            'userName'    => $bill->shopPaySystem->handler->username,
            'password'    => $bill->shopPaySystem->handler->password,
            'description' => "Заказ в магазине №{$bill->shopOrder->id}",
            'orderNumber' => urlencode($bill->id),
            'amount'      => urlencode($bill->money->amount * 100), // передача данных в копейках/центах
            'returnUrl'   => Url::toRoute(['/shop/sberbank/success', 'code' => urlencode($bill->code)], true),
            'failUrl'     => Url::toRoute(['/shop/sberbank/fail', 'code' => urlencode($bill->code)], true),
        ];
        if ($bill->shopBuyer->email) {
            $data['jsonParams'] = '{"email":"'.$bill->shopBuyer->email.'"}';
        }
        /**
         * ЗАПРОС РЕГИСТРАЦИИ ОДНОСТАДИЙНОГО ПЛАТЕЖА В ПЛАТЕЖНОМ ШЛЮЗЕ
         *        register.do
         *
         * ПАРАМЕТРЫ
         *        userName            Логин магазина.
         *        password            Пароль магазина.
         *        orderNumber            Уникальный идентификатор заказа в магазине.
         *        amount                Сумма заказа.
         *        returnUrl            Адрес, на который надо перенаправить пользователя в случае успешной оплаты.
         *
         * ОТВЕТ
         *        В случае ошибки:
         *            errorCode        Код ошибки. Список возможных значений приведен в таблице ниже.
         *            errorMessage    Описание ошибки.
         *
         *        В случае успешной регистрации:
         *            orderId            Номер заказа в платежной системе. Уникален в пределах системы.
         *            formUrl            URL платежной формы, на который надо перенаправить браузер клиента.
         *
         *    Код ошибки        Описание
         *        0            Обработка запроса прошла без системных ошибок.
         *        1            Заказ с таким номером уже зарегистрирован в системе.
         *        3            Неизвестная (запрещенная) валюта.
         *        4            Отсутствует обязательный параметр запроса.
         *        5            Ошибка значения параметра запроса.
         *        7            Системная ошибка.
         */
        /*$response = $bill->shopPaySystem->handler->gateway('getOrderStatusExtended.do', $data);
                print_r($response);die;*/

        $response = $bill->shopPaySystem->handler->gateway('register.do', $data);

        /**
         * ЗАПРОС РЕГИСТРАЦИИ ДВУХСТАДИЙНОГО ПЛАТЕЖА В ПЛАТЕЖНОМ ШЛЮЗЕ
         *        registerPreAuth.do
         *
         * Параметры и ответ точно такие же, как и в предыдущем методе.
         * Необходимо вызывать либо register.do, либо registerPreAuth.do.
         */
        //	$response = $module->gateway('registerPreAuth.do', $data);
        if (isset($response['errorCode'])) { // В случае ошибки вывести ее
            return $this->redirect(Url::toRoute(['/shop/sberbank/fail', 'code' => $code, 'response' => Json::encode($response)], true));
        } else { // В случае успеха перенаправить пользователя на плетжную форму
            $bill->external_data = $response;
            if (!$bill->save()) {

                //TODO: Add logs
                print_r($bill->errors);
                die;
            }

            return $this->redirect($bill->external_data['formUrl']);
        }

    }

    public function actionFail()
    {
        \Yii::warning("Sberbank fail: ".print_r(\Yii::$app->request->get(), true), self::class);

        if (!$code = \Yii::$app->request->get('code')) {
            throw new Exception('Bill not found');
        }

        if (!$bill = ShopBill::find()->where(['code' => $code])->one()) {
            throw new Exception('Bill not found');
        }

        print_r(\Yii::$app->request->get());
        die;

        return $this->redirect($shopOrder->getPublicUrl(\Yii::$app->request->get()));
    }

    /**
     * @throws Exception
     */
    public function actionSuccess()
    {
        \Yii::info("Sberbank success: ".print_r(\Yii::$app->request->get(), true), self::class);

        /**
         * @var $bill ShopBill
         */
        if (!$code = \Yii::$app->request->get('code')) {
            throw new Exception('Bill not found');
        }

        if (!$bill = ShopBill::find()->where(['code' => $code])->one()) {
            throw new Exception('Bill not found');
        }

        $data = [
            'userName'    => $bill->shopPaySystem->handler->username,
            'password'    => $bill->shopPaySystem->handler->password,
            'orderNumber' => urlencode($bill->id),
        ];

        $response = $bill->shopPaySystem->handler->gateway('getOrderStatusExtended.do', $data);

        /**
         * Оплата произведена
         */
        if (ArrayHelper::getValue($response, 'orderStatus') == SberbankPaySystem::ORDER_STATUS_2 && ArrayHelper::getValue($response, 'orderNumber') == $bill->id) {

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

            try {

                $payment = new ShopPayment();
                $payment->shop_buyer_id = $bill->shop_buyer_id;
                $payment->shop_pay_system_id = $bill->shop_pay_system_id;
                $payment->shop_order_id = $bill->shop_order_id;
                $payment->amount = $bill->amount;
                $payment->currency_code = $bill->currency_code;
                $payment->comment = "Оплата по счету №{$bill->id} от ".\Yii::$app->formatter->asDate($bill->created_at);
                $payment->external_data = $response;

                if (!$payment->save()) {
                    throw new Exception("Не сохранился платеж: ".print_r($payment->errors, true));
                }

                $bill->paid_at = time();
                $bill->shop_payment_id = $payment->id;

                if (!$bill->save()) {
                    throw new Exception("Не обновился счет: ".print_r($payment->errors, true));
                }

                $bill->shopOrder->paid_at = time();
                $bill->shopOrder->save();

                $transaction->commit();

                return $this->redirect($bill->shopOrder->url);

            } catch (\Exception $e) {
                $transaction->rollBack();
                \Yii::error($e->getMessage(), self::class);
                throw $e;
            }

        }

        return $this->redirect(Url::toRoute(['/shop/sberbank/fail', 'code' => $code, 'response' => Json::encode($response)], true));

        /*$orderId = \Yii::$app->request->get('OrderId');
        if (!$orderId) {
            throw new NotFoundHttpException('!!!');
        }

        if (!$shopOrder = ShopOrder::findOne($orderId)) {
            throw new NotFoundHttpException('!!!');
        }

        return $this->redirect($shopOrder->getPublicUrl(\Yii::$app->request->get()));*/
    }

    /*public function actionSuccess()
    {
        $orderId = \Yii::$app->request->get('OrderId');
        if (!$orderId) {
            throw new NotFoundHttpException('!!!');
        }

        if (!$shopOrder = ShopOrder::findOne($orderId)) {
            throw new NotFoundHttpException('!!!');
        }

        return $this->redirect($shopOrder->getPublicUrl(\Yii::$app->request->get()));
    }

    public function actionNotify()
    {
        \Yii::info("POST: " . Json::encode(\Yii::$app->request->post()), self::class);

        try {
            if (!\Yii::$app->request->post('OrderId')) {
                throw new Exception('Некорректны запрос от банка.');
            }

            /**
             * @var $shopOrder ShopOrder
            if (!$shopOrder = ShopOrder::findOne(\Yii::$app->request->post('OrderId'))) {
                throw new Exception('Заказ не найден в базе.');
            }

            if ($shopOrder->id != \Yii::$app->request->post('OrderId')) {
                throw new Exception('Не совпадает номер заказа.');
            }

            if ($shopOrder->money->getAmount() != \Yii::$app->request->post('Amount')) {
                throw new Exception('Не совпадает сумма заказа.');
            }

            if (\Yii::$app->request->post('Status') == "CONFIRMED") {
                \Yii::info("Успешный платеж", self::class);
                $shopOrder->processNotePayment();
            }

        } catch (\Exception $e) {
            \Yii::error($e->getMessage(), self::class);
        }

        $this->layout = false;
        return "OK";
    }*/
}


<?php
/**
 * @author Semenov Alexander <semenov@skeeks.com>
 * @link http://skeeks.com/
 * @copyright 2010 SkeekS (СкикС)
 * @date 22.03.2016
 */

namespace skeeks\cms\shop\console\controllers;

use skeeks\cms\shop\models\ShopProductPriceChange;
use yii\console\Controller;
use yii\helpers\Console;

/**
 * Массовая читска данных
 *
 * @package skeeks\cms\shop\console\controllers
 */
class FlushController extends Controller
{
    /**
     * Чистка лога изменения цен
     * @param int $countDay за последние количество дней
     */
    public function actionPriceChanges($countDay = 30)
    {
        if ($count = ShopProductPriceChange::find()->where([
            '<=',
            'created_at',
            time() - 3600 * 24 * $countDay,
        ])->count()
        ) {
            $this->stdout("Total price changes for delete: {$count}\n", Console::BOLD);
            $totalDeleted = ShopProductPriceChange::deleteAll(['<=', 'created_at', time() - 3600 * 24 * $countDay]);
            $this->stdout("Total deleted: {$totalDeleted}\n");
        } else {
            $this->stdout("Нечего удалять\n", Console::BOLD);
        }
    }
}


<?php
/**
 * @author Semenov Alexander <semenov@skeeks.com>
 * @link http://skeeks.com/
 * @copyright 2010 SkeekS (СкикС)
 * @date 22.03.2016
 */

namespace skeeks\cms\shop\console\controllers;

use skeeks\cms\shop\models\ShopCart;
use skeeks\cms\shop\models\ShopFuser;
use skeeks\cms\shop\models\ShopOrder;
use yii\console\Controller;
use yii\db\Expression;
use yii\helpers\ArrayHelper;

/**
 * Module shop agents
 *
 * Class AgentsController
 * @package skeeks\cms\shop\console\controllers
 */
class AgentsController extends Controller
{

    /**
     * Просмотр созданных бекапов баз данных
     */
    public function actionDeleteEmptyCarts($days = 1)
    {
        $condition = [
            //'and',
            //['shop_order.is_created' => 0],
            //['<=', 'shop_order.created_at', time()-3600*24*$days],
            //['shop_order.is_created' => 0],
            /*['shop_order.person_type_id' => null],
            ['shop_fuser.pay_system_id' => null],
            ['shop_fuser.delivery_id' => null],
            ['shop_fuser.buyer_id' => null],*/
            /*new Expression(<<<SQL
            (SELECT count(id) as count FROM shop_order_item WHERE shop_order_item.shop_order_id = shop_order.id) = 0
SQL
            ),*/
        ];
        //$forDelete = ShopOrder::find()->where($condition)->count(1);
        $forDeleteQuery = ShopOrder::find()->joinWith('shopOrderItems as shopOrderItems')
            ->andWhere([
                'and',
                ['shop_order.is_created' => 0], //Не созданные заказы
                ['<=', 'shop_order.created_at', time() - 3600 * 24 * $days] //старше 1 дня
            ])
            ->andWhere(['shopOrderItems.id' => null])//У которых нет ничего в корзине
            ->limit(5000)
            ->orderBy(['shop_order.id' => SORT_ASC])
            ->select(["shop_order.id"])
            ->asArray()
            ->all();

        $ids = ArrayHelper::map($forDeleteQuery, 'id', 'id');

        /*
                $counter = 0;
                $models = $query->all();
                $allCount = count($models);
                Console::startProgress(0, $allCount);

                foreach ($query->each() as $model)
                {
                    // $users is indexed by the "username" column
                    $counter ++;
                    $model->delete();
                    Console::updateProgress($counter, $allCount);
                }

                Console::endProgress();*/

        if ($ids) {
            $this->stdout("Empty orders for delete: " . count($ids) . "\n");
            $deleted = ShopOrder::deleteAll(['id' => $ids]);
            $this->stdout("Removed empty orders: ".$deleted."\n");
        } else {
            $this->stdout("Not found orders for delete\n");
        }

        $deleted = ShopCart::deleteAll([
            'shop_order_id' => null,
        ]);
        $this->stdout("Removed empty carts: ".$deleted."\n");
    }
}


<?php
/*
 * Copyright (c) 2012-2016, Hofmänner New Media.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This file is part of the N2N FRAMEWORK.
 *
 * The N2N FRAMEWORK is free software: you can redistribute it and/or modify it under the terms of
 * the GNU Lesser General Public License as published by the Free Software Foundation, either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * N2N is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Lesser General Public License for more details: http://www.gnu.org/licenses/
 *
 * The following people participated in this project:
 *
 * Andreas von Burg.....: Architect, Lead Developer
 * Bert Hofmänner.......: Idea, Frontend UI, Community Leader, Marketing
 * Thomas Günther.......: Developer, Hangar
 */
namespace n2n\impl\persistence\orm\property;

use n2n\reflection\property\AccessProxy;
use n2n\util\type\TypeConstraint;
use n2n\persistence\orm\property\BasicEntityProperty;
use n2n\persistence\orm\query\from\MetaTreePoint;
use n2n\persistence\orm\query\QueryState;
use n2n\persistence\orm\criteria\compare\ScalarColumnComparable;
use n2n\persistence\orm\query\select\SimpleSelection;
use n2n\persistence\orm\store\action\PersistAction;
use n2n\persistence\orm\store\operation\MergeOperation;
use n2n\persistence\orm\store\action\RemoveAction;
use n2n\persistence\meta\data\QueryItem;
use n2n\persistence\Pdo;
use n2n\persistence\orm\EntityManager;
use n2n\util\type\ArgUtils;
use n2n\persistence\orm\store\ValueHash;
use n2n\persistence\orm\store\CommonValueHash;
use n2n\util\type\TypeName;

class FloatEntityProperty extends ColumnPropertyAdapter implements BasicEntityProperty {
	/**
	 * @param AccessProxy $accessProxy
	 * @param string $columnName
	 */
	public function __construct(AccessProxy $accessProxy, $columnName) {
		$accessProxy->setConstraint(TypeConstraint::createSimple(TypeName::FLOAT, true, true));

		parent::__construct($accessProxy, $columnName);
	}
	/* (non-PHPdoc)
	 * @see \n2n\persistence\orm\property\ColumnComparableEntityProperty::createComparisonStrategy()
	 */
	public function createColumnComparable(MetaTreePoint $metaTreePoint, QueryState $queryState) {
		return new ScalarColumnComparable($this->createQueryColumn($metaTreePoint->getMeta()), $queryState);
	}
	/* (non-PHPdoc)
	 * @see \n2n\persistence\orm\property\BasicEntityProperty::createColumnComparableFromQueryItem()
	 */
	public function createColumnComparableFromQueryItem(QueryItem $queryItem, QueryState $queryState) {
		return new ScalarColumnComparable($queryItem, $queryState);
	}

	public function createSelection(MetaTreePoint $metaTreePoint, QueryState $queryState) {
		return new SimpleSelection($this->createQueryColumn($metaTreePoint->getMeta()), TypeName::FLOAT);
	}

	/**
	 * {@inheritDoc}
	 * @see \n2n\persistence\orm\property\BasicEntityProperty::valueToRep()
	 */
	public function valueToRep($value): string {
		ArgUtils::valScalar($value);
		return (string) $value;
	}

	/**
	 * {@inheritDoc}
	 * @see \n2n\persistence\orm\property\BasicEntityProperty::repToValue()
	 */
	public function repToValue(string $rep) {
		ArgUtils::assertTrue(is_numeric($rep));
		return (float) $rep;
	}

	/**
	 * {@inheritDoc}
	 * @see \n2n\persistence\orm\property\EntityProperty::supplyPersistAction()
	 */
	public function supplyPersistAction(PersistAction $persistAction, $mappedValue, ValueHash $valueHash, ?ValueHash $oldValueHash) {
// 		$pdoDataType = null;
// 		if (is_bool($mappedValue)) {
// 			$pdoDataType = PDO::PARAM_BOOL;
// 		}
		$persistAction->getMeta()->setRawValue($this->getEntityModel(), $this->getColumnName(), $mappedValue/*, $pdoDataType*/);
	}

	/**
	 * {@inheritDoc}
	 * @see \n2n\persistence\orm\property\EntityProperty::createValueHash()
	 */
	public function createValueHash($value, EntityManager $em): ValueHash {
		return new CommonValueHash($value);
	}

	/**
	 * {@inheritDoc}
	 * @see \n2n\persistence\orm\property\EntityProperty::mergeValue()
	 */
	public function mergeValue($value, $sameEntity, MergeOperation $mergeOperation) {
		return $value;
	}

	/**
	 * {@inheritDoc}
	 * @see \n2n\persistence\orm\property\EntityProperty::supplyRemoveAction()
	 */
	public function supplyRemoveAction(RemoveAction $removeAction, $value, ValueHash $oldValueHash) {
	}

	/**
	 * {@inheritDoc}
	 * @see \n2n\persistence\orm\property\BasicEntityProperty::parseValue()
	 */
	public function parseValue($raw, Pdo $pdo) {
		return $raw;
	}
	/* (non-PHPdoc)
	 * @see \n2n\persistence\orm\property\BasicEntityProperty::buildRaw()
	 */
	public function buildRaw($value, Pdo $pdo) {
		return $value;
	}
	/* (non-PHPdoc)
	 * @see \n2n\persistence\orm\property\BasicEntityProperty::createSelectionFromQueryItem()
	 */
	public function createSelectionFromQueryItem(QueryItem $queryItem, QueryState $queryState) {
		return new SimpleSelection($queryItem, TypeName::FLOAT);
	}

// 	public function getTypeConstraint() {
//  		return $this->getAccessProxy()->getConstraint();
// 	}

// 	public function buildComparableValue($value) {
// 		return $value;
// 	}

// 	public static function areConstraintsTypical(TypeConstraint $constraints = null) {
// 		return is_null($constraints)
// 				|| (is_null($constraints->getParamClass()) && !$constraints->isArray());
// 	}
}


<?php
/*
 * Copyright (c) 2012-2016, Hofmänner New Media.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This file is part of the N2N FRAMEWORK.
 *
 * The N2N FRAMEWORK is free software: you can redistribute it and/or modify it under the terms of
 * the GNU Lesser General Public License as published by the Free Software Foundation, either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * N2N is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Lesser General Public License for more details: http://www.gnu.org/licenses/
 *
 * The following people participated in this project:
 *
 * Andreas von Burg.....: Architect, Lead Developer
 * Bert Hofmänner.......: Idea, Frontend UI, Community Leader, Marketing
 * Thomas Günther.......: Developer, Hangar
 */
namespace n2n\impl\persistence\orm\property\relation\compare;

use n2n\persistence\orm\query\from\MetaTreePoint;
use n2n\persistence\orm\query\from\TreePath;
use n2n\persistence\orm\criteria\compare\CriteriaComparator;
use n2n\util\ex\IllegalStateException;
use n2n\persistence\orm\criteria\compare\ComparisonStrategy;
use n2n\persistence\meta\data\QueryPlaceMarker;
use n2n\util\type\ArgUtils;
use n2n\persistence\orm\query\QueryState;
use n2n\util\type\TypeConstraint;
use n2n\persistence\orm\model\EntityModel;
use n2n\persistence\orm\criteria\compare\CustomComparable;
use n2n\persistence\meta\data\QueryComparator;
use n2n\persistence\orm\criteria\CriteriaConflictException;
use n2n\util\type\ValueIncompatibleWithConstraintsException;
use n2n\persistence\orm\criteria\compare\QueryComparatorBuilder;
use n2n\persistence\orm\query\QueryModel;
use n2n\persistence\orm\query\from\Tree;
use n2n\persistence\orm\query\QueryItemSelect;
use n2n\persistence\orm\criteria\item\ConstantQueryPoint;
use n2n\persistence\meta\data\QueryItem;

class ToManyCustomComparable implements CustomComparable {
	private $metaTreePoint;
	private $targetIdTreePath;
	private $toManyQueryItemFactory;
	private $queryState;
	private $typeConstraint;

	private $entityColumnComparable;
	private $toManyQueryItem;

	public function __construct(MetaTreePoint $metaTreePoint, EntityModel $targetEntityModel,
			TreePath $targetIdTreePath, ToManyQueryItemFactory $toManyQueryItemFactory,
			QueryState $queryState) {
		$this->metaTreePoint = $metaTreePoint;
		$this->targetIdTreePath = $targetIdTreePath;
		$this->targetEntityModel = $targetEntityModel;
		$this->toManyQueryItemFactory = $toManyQueryItemFactory;
		$this->queryState = $queryState;
		$this->typeConstraint = TypeConstraint::createSimple($this->targetEntityModel->getClass()->getName(), true);
	}
	/* (non-PHPdoc)
	 * @see \n2n\persistence\orm\criteria\compare\ColumnComparable::getAvailableOperators()
	*/
	public function getAvailableOperators() {
		return array(CriteriaComparator::OPERATOR_CONTAINS, CriteriaComparator::OPERATOR_CONTAINS_NOT,
				CriteriaComparator::OPERATOR_CONTAINS_ANY, CriteriaComparator::OPERATOR_CONTAINS_NONE);
	}

	private function requestEntityColumnComparable() {
		if ($this->entityColumnComparable !== null) {
			return $this->entityColumnComparable;
		}

		$targetIdComparisonStrategy = $this->metaTreePoint->requestPropertyComparisonStrategy(
				$this->targetIdTreePath->copy());
		IllegalStateException::assertTrue($targetIdComparisonStrategy->getType() == ComparisonStrategy::TYPE_COLUMN);

		return $this->entityColumnComparable = new IdColumnComparableDecorator(
				$targetIdComparisonStrategy->getColumnComparable(),
				$this->targetEntityModel);
	}

// 	/* (non-PHPdoc)
// 	 * @see \n2n\persistence\orm\criteria\compare\ColumnComparable::buildQueryItem($operator)
// 	*/
// 	public function buildQueryItem($operator) {

// 	}
	/* (non-PHPdoc)
	 * @see \n2n\persistence\orm\criteria\compare\ColumnComparable::buildCounterpartQueryItemFromValue()
	 */
	public function buildCounterpartQueryItemFromValue($operator, $value) {
		if ($operator == CriteriaComparator::OPERATOR_CONTAINS) {
			return $this->requestEntityColumnComparable()
					->buildCounterpartQueryItemFromValue($operator, $value);
		}

		return new QueryPlaceMarker($this->queryState->registerPlaceholderValue(
				$this->parseFieldValue($value)));
	}

	private function validateOperator($operator) {
		if ($operator == CriteriaComparator::OPERATOR_CONTAINS
				|| $operator == CriteriaComparator::OPERATOR_CONTAINS_NOT
				|| $operator == CriteriaComparator::OPERATOR_CONTAINS_ANY
				|| $operator == CriteriaComparator::OPERATOR_CONTAINS_NONE) return;

		throw new CriteriaConflictException('Invalid operator \'' . $operator
				. '\' for comparison. Available operators: ' . CriteriaComparator::OPERATOR_CONTAINS
				. ', ' . CriteriaComparator::OPERATOR_CONTAINS_NOT);
	}
	/* (non-PHPdoc)
	 * @see \n2n\persistence\orm\criteria\compare\CustomComparable::compareWithValue()
	 */
	public function compareWithValue(QueryComparator $queryComparator, $operator, $value) {
		$this->validateOperator($operator);
		try {
			switch ($operator) {
				case CriteriaComparator::OPERATOR_CONTAINS:
				case CriteriaComparator::OPERATOR_CONTAINS_NOT:
					$this->typeConstraint->validate($value);
					break;
				case CriteriaComparator::OPERATOR_CONTAINS_ANY:
				case CriteriaComparator::OPERATOR_CONTAINS_NONE:
					TypeConstraint::createArrayLike(null, true, $this->typeConstraint)->validate($value);
			}
		} catch (ValueIncompatibleWithConstraintsException $e) {
			throw new CriteriaConflictException('Value can not be compared with property.', 0, $e);
		}

		if ($operator == CriteriaComparator::OPERATOR_CONTAINS) {
			$entityColumnComparable = $this->requestEntityColumnComparable();
			$queryComparator->match(
					$entityColumnComparable->buildQueryItem(CriteriaComparator::OPERATOR_EQUAL),
					QueryComparator::OPERATOR_EQUAL,
					$entityColumnComparable->buildCounterpartQueryItemFromValue(
							CriteriaComparator::OPERATOR_EQUAL, $value));
			return;
		}

		if ($operator == CriteriaComparator::OPERATOR_CONTAINS_NOT) {
			$queryComparator->match(
					new QueryPlaceMarker($this->queryState->registerPlaceholderValue(
							$this->parseFieldValue($value)),
							QueryComparator::OPERATOR_IN, $this->requestToManyQueryItem()));
			return;
		}

		$entityColumnComparable = $this->requestEntityColumnComparable();
		$testQueryResult = $this->createTestQueryResult(
				$entityColumnComparable->buildCounterpartQueryItemFromValue(
						QueryComparator::OPERATOR_IN, $value));

		if ($operator == CriteriaComparator::OPERATOR_CONTAINS_ANY) {
			$queryComparator->test(QueryComparator::OPERATOR_EXISTS, $testQueryResult);
			return;
		}

		if ($operator == CriteriaComparator::OPERATOR_CONTAINS_NONE) {
			$queryComparator->test(QueryComparator::OPERATOR_NOT_EXISTS, $testQueryResult);
			return;
		}
	}

	private function requestIdCc(MetaTreePoint $metaTreePoint, TreePath $treePath) {
		$idComparisonStrategy = $metaTreePoint->requestPropertyComparisonStrategy($treePath);
		IllegalStateException::assertTrue($idComparisonStrategy->getType() == ComparisonStrategy::TYPE_COLUMN);

		return $idComparisonStrategy->getColumnComparable();
	}

	/**
	 * @param object $entity
	 * @return string
	 */
	private function parseTargetIdRaw($entity) {
		$targetIdProperty = $this->targetEntityModel->getIdDef()->getEntityProperty();

		$id = null;
		if ($entity !== null) {
			ArgUtils::assertTrue(is_object($entity));
			$id = $targetIdProperty->readValue($entity);
		}

		return $targetIdProperty->buildRaw($id, $this->queryState->getEntityManager()->getPdo());
	}
	/* (non-PHPdoc)
	 * @see \n2n\persistence\orm\criteria\compare\CustomComparable::compareWith()
	 */
	public function compareWith(QueryComparator $queryComparator, $operator, ComparisonStrategy $comparisonStrategy) {
		$this->validateOperator($operator);

		if ($comparisonStrategy->getType() != ComparisonStrategy::TYPE_COLUMN) {
			throw new CriteriaConflictException('Incompatible comparison');
		}

		$columnComparable = $comparisonStrategy->getColumnComparable();

		$oppositeOperator = null;
		$testTypeConstraint = null;
		switch ($operator) {
			case CriteriaComparator::OPERATOR_CONTAINS_ANY:
			case CriteriaComparator::OPERATOR_CONTAINS_NONE:
				$oppositeOperator = CriteriaComparator::OPERATOR_CONTAINS;
				$testTypeConstraint = TypeConstraint::createArrayLike(null, false, $this->typeConstraint);
				break;
			default:
				$oppositeOperator = QueryComparatorBuilder::oppositeOperator($operator);
				$testTypeConstraint = $this->typeConstraint;

		}

		if (!$testTypeConstraint->isPassableBy($columnComparable->getTypeConstraint($oppositeOperator))) {
			$arrayTypeConstraint = TypeConstraint::createArrayLike(null, false, $this->typeConstraint);
			throw new CriteriaConflictException('Incompatible comparison: '
					. $arrayTypeConstraint->__toString() . ' ' . $operator . ' '
					. $columnComparable->getTypeConstraint($oppositeOperator) );
		}

		if ($operator == CriteriaComparator::OPERATOR_CONTAINS) {
			$entityColumnComparable = $this->requestEntityColumnComparable();
			$queryComparator->match(
					$entityColumnComparable->buildQueryItem(CriteriaComparator::OPERATOR_EQUAL),
					QueryComparator::OPERATOR_EQUAL,
					$columnComparable->buildQueryItem(CriteriaComparator::OPERATOR_EQUAL));
			return;
		}

		if ($operator == CriteriaComparator::OPERATOR_CONTAINS_NOT) {
			$queryComparator->match(
					$columnComparable->buildQueryItem(CriteriaComparator::OPERATOR_NOT_IN),
					QueryComparator::OPERATOR_NOT_IN, $this->requestToManyQueryItem());
			return;
		}

		$testQueryResult = $this->createTestQueryResult($columnComparable->buildQueryItem($oppositeOperator));

		if ($operator == CriteriaComparator::OPERATOR_CONTAINS_ANY) {
			$queryComparator->test(QueryComparator::OPERATOR_EXISTS, $testQueryResult);
			return;
		}

		if ($operator == CriteriaComparator::OPERATOR_CONTAINS_NONE) {
			$queryComparator->test(QueryComparator::OPERATOR_NOT_EXISTS, $testQueryResult);
			return;
		}
	}

	private function requestToManyQueryItem() {
		if ($this->toManyQueryItem !== null) {
			return $this->toManyQueryItem;
		}

		$entityModel = $this->metaTreePoint->getMeta()->getEntityModel();
		$idComparisonStrategy = $this->metaTreePoint->requestPropertyComparisonStrategy(
				$entityModel->getIdDef()->getPropertyName());

		return $this->toManyQueryItem = $this->toManyQueryItemFactory->createQueryItem(
				$idComparisonStrategy->getColumnComparable(), $this->queryState);
	}

	/**
	 *
	 * @return \n2n\persistence\orm\query\from\TreePath
	 */
	private function createIdTreePath() {
		return new TreePath([$this->metaTreePoint->getMeta()->getEntityModel()->getIdDef()->getPropertyName()]);
	}

	private function createTestQueryResult(QueryItem $counterQueryItem) {
		$entityModel = $this->metaTreePoint->getMeta()->getEntityModel();
		$idCc = $this->requestIdCc($this->metaTreePoint, $this->createIdTreePath());

		$tree = new Tree($this->queryState);
// @todo support inherit access
// 		$tree->setInheritedQueryPointResolver($inheritedQueryPointResolver);
		$subMetaTreePoint = $tree->createBaseTreePoint($entityModel);

		$subIdCc = $this->requestIdCc($subMetaTreePoint, $this->createIdTreePath());
		$subTargetIdCc = $this->requestIdCc($subMetaTreePoint, $this->targetIdTreePath->copy());

		$subQueryComparator = new QueryComparator();
		$subQueryComparator->match(
				$idCc->buildQueryItem(CriteriaComparator::OPERATOR_EQUAL),
				QueryComparator::OPERATOR_EQUAL,
				$subIdCc->buildQueryItem(CriteriaComparator::OPERATOR_EQUAL));
		$subQueryComparator->match(
				$subTargetIdCc->buildQueryItem(CriteriaComparator::OPERATOR_IN),
				QueryComparator::OPERATOR_IN,
				$counterQueryItem);

		$subQueryModel = new QueryModel($tree, new QueryItemSelect($this->queryState));
		$subQueryModel->addUnnamedSelectQueryPoint(new ConstantQueryPoint(1, $this->queryState));
		$subQueryModel->setWhereQueryComparator($subQueryComparator);

		$selectBuilder = $this->queryState->getPdo()->getMetaData()->createSelectStatementBuilder();
		$subQueryModel->apply($selectBuilder);

		return $selectBuilder->toQueryResult();

	}

}