Version: current

       1 <?php
       2
       3 /**
       4  * Раздел каталога, содержит товары и другие разделы
       5  *
       6  * @package Catalog
       7  * @author inxo, dvp, menedem
       8  */
       9 class Catalog extends Page {
      10
      11     static $icon = array('cms/images/treeicons/folder', 'folder');
      12     static $allowed_children = array('Product', '*Catalog', 'SpecialCatalog');
      13     static $default_child = 'Product';
      14
      15     static $default_sort = 'Title ASC';
      16
      17     static $db = array(
      18         'Description' => 'Text',
      19         'OwnFilter' => 'Boolean',
      20         'OwnParams' => 'Boolean',
      21
      22         'StandartView' => 'Varchar',
      23         'ImportID' => 'Varchar', // ID-импорта
      24
      25         'GroupTitle' => 'Varchar(255)', //Название блока группировки, по-умолчанию «Возможные варианты»
      26     );
      27
      28     static $defaults = array(
      29         'AutoChild' => 0,
      30         'OwnFilter' => false,
      31         'OwnParams' => false,
      32     );
      33
      34     static $has_one = array(
      35         'Photo' => 'Image',
      36         'CatalogVAT' => 'VAT', // Ставка НДС каталога
      37     );
      38
      39     static $has_many = array(
      40         'Params' => 'ProductParam',
      41         'CatalogFilters' => 'CatalogFilter'
      42     );
      43
      44     // список задействованных в каталоге параметров и фильтров
      45     static $many_many = array(
      46         'EnabledParams' => 'ProductParam',
      47         'EnabledCatalogFilters' => 'CatalogFilter'
      48     );
      49
      50     static $indexes = array(
      51         'ImportID' => true,
      52     );
      53
      54     static $subpage_children = 'Product';
      55
      56     /**
      57      * Проброс возможных вариантов сортировки от товара
      58      *
      59      * @return array
      60      */
      61     static function get_sort_options() {
      62         $productClass = self::$subpage_children;
      63         return array_keys($productClass::get_sort_options());
      64     }
      65
      66     /**
      67      * Варианты отображения товаров в рубриках (как правило tile, table и list)
      68      */
      69     private static $view_options = array('tile');
      70
      71     /**
      72      * Изменяет список отображений товаров.
      73      * Используется в _00config.php для настройки параметров каталога
      74      *
      75      * @param array $data - новый список отображений
      76      */
      77     static function set_view_options(array $data) {
      78         self::$view_options = $data;
      79     }
      80
      81     /**
      82      * Возвращает текущий список отображений товаров
      83      *
      84      * @return array - текущий список отображений
      85      */
      86     static function get_view_options() {
      87         return self::$view_options;
      88     }
      89
      90     /**
      91      * Возвращает локализованный список отображений для использования в селектах
      92      *
      93      * @param bool $addDefault - добавлять ли пункт "по-умолчанию"
      94      *
      95      * @return array - список для селектов
      96      */
      97     static function view_options_dropdown_map($addDefault = false) {
      98         $map = array();
      99         if ($addDefault) {
     100             $map[''] = _t('Catalog.ViewOption_default', 'Default');
     101         }
     102         foreach (self::get_view_options() as $key) {
     103             $map[$key] = _t('Catalog.ViewOption_'.$key, ucfirst($key));
     104         }
     105         return $map;
     106     }
     107
     108     /**
     109      * Флаг может ли пользователь сам менять отображание товаров в рубриках
     110      */
     111     private static $user_can_change_view = false;
     112
     113     /**
     114      * Устанавливает флаг $user_can_change_view
     115      *
     116      * @param bool $val
     117      */
     118     static function allow_user_change_view($val = true) {
     119         self::$user_can_change_view = $val;
     120     }
     121
     122     /**
     123      * варианты размера страницы каталога
     124      * значения: число или 'all'
     125      */
     126     static $pagesize_options = array();
     127
     128     /**
     129      * Устанавливает варианты размера страницы каталога
     130      *
     131      * @param array $list
     132      */
     133     static function set_pagesize_options($list) {
     134         self::$pagesize_options = $list;
     135     }
     136
     137     /**
     138      * Возвращает список вариантов размера страницы каталога
     139      *
     140      * @param array $list
     141      */
     142     static function get_pagesize_options() {
     143         return self::$pagesize_options;
     144     }
     145
     146     /**
     147      * Возвращает локализованный список вариантов размера страницы каталога
     148      *
     149      * @param bool $addDefault - добавлять ли пункт "по-умолчанию"
     150      *
     151      * @return array - список для селектов
     152      */
     153     static function pagesize_dropdown_map($addDefault = false) {
     154         $map = array();
     155         if ($addDefault) {
     156             $map[''] = _t('Catalog.PageSize_default', 'Default');
     157         }
     158         foreach (self::get_pagesize_options() as $key) {
     159             $map[$key] = _t('Catalog.PageSize_'. $key, ucfirst($key));
     160         }
     161         return $map;
     162     }
     163
     164     /**
     165      * Флаг может ли пользователь сам менять отображание товаров в рубриках
     166      */
     167     private static $user_can_change_pagesize = false;
     168
     169     /**
     170      * Устанавливает флаг $user_can_change_pagesize
     171      *
     172      * @param bool $val
     173      */
     174     static function allow_user_change_pagesize($val = true) {
     175         self::$user_can_change_pagesize = $val;
     176     }
     177
     178     /**
     179      * Флаг использования доп.параметров каталога
     180      */
     181     static $use_additional_params = true;
     182
     183     /**
     184      * Устанавливает флаг использования доп.параметров каталога
     185      *
     186      * @param bool $val
     187      */
     188     static function disable_additional_params() {
     189         self::$use_additional_params = false;
     190     }
     191
     192     /**
     193      * Флаг использования доп.фильтров каталога
     194      */
     195     static $use_additional_filters = true;
     196
     197     /**
     198      * Устанавливает флаг использования доп.фильтров каталога
     199      *
     200      * @param bool $val
     201      */
     202     static function disable_additional_filters() {
     203         self::$use_additional_filters = false;
     204     }
     205
     206     /**
     207      * Флаг сужения фильтров каталога (выбрасывание опций фильтра, которых нет в текущей выборке)
     208      */
     209     static $reduce_filters = false;
     210
     211     /**
     212      * Устанавливает флаг сужения фильтров каталога
     213      *
     214      * @param bool $val
     215      */
     216     static function enable_reduce_filters() {
     217         self::$reduce_filters = true;
     218     }
     219
     220     /**
     221      * Флаг использования вариаций каталога
     222      */
     223     static $use_variations = false;
     224
     225     /**
     226      * Включение вариаций
     227      *
     228      * @param bool $val
     229      */
     230     static function enable_variations() {
     231         self::$use_variations = true;
     232     }
     233
     234      /**
     235      * Флаг скрытия в каталоге товаров с AllowPurchase == 0
     236      * FIXME перенести в SiteConfig
     237      */
     238     static $hide_allow_purchase_products = false;
     239
     240     /**
     241      * Устанавливает флаг показа в каталога товаров с AllowPurchase == 0
     242      *
     243      * @param bool $val
     244      */
     245     static function hide_allow_purchase_products($val) {
     246         self::$hide_allow_purchase_products = $val;
     247     }
     248
     249     /**
     250      * Режим подсчета возможных значений с выводом в фильтре каталога
     251      */
     252     static $filter_calc_suitable_products_mode = false;
     253
     254     /**
     255      * Устанавливает режим подсчета возможных значений с выводом в фильтре каталога
     256      * Возможные значения: false, one_filter, all_filters
     257      * false - подсчет отключен;
     258      * one_filter - считать кол-во товаров с этим значением фильтра (не зависит от других фильтров);
     259      * all_filters - считать кол-во товаров с учетом других фильтров (зависит от других фильтров, надо пересчитывать при каждом применении фильтров)
     260      *
     261      * @param string $val
     262      */
     263     static function set_filter_calc_suitable_products_mode($val) {
     264         if (in_array($val, array(false, 'one_filter', 'all_filters'))) {
     265             self::$filter_calc_suitable_products_mode = $val;
     266         }
     267     }
     268
     269     /**
     270      * Поиск каталога по вводу пользователя (для автодополнения)
     271     *
     272      * @return DataObjectSet
     273      */
     274     static function CatalogSearch($text, $neededCount) {
     275         $text = Convert::raw2sql(trim(preg_replace('/([%_])/', '\\\$1', $text))); //Экранируем % и _ в том что будем совать в LIKE
     276         $where = "Title LIKE '%$text%'";
     277         $rs = DataObject::get('Catalog', $where, 'Title', '', $neededCount);
     278         return $rs;
     279     }
     280
     281     /*--------------- Функции для работы с кешированием -------------------*/
     282
     283     static $cache = false;
     284
     285     /**
     286      * Получение адаптера для кеширования
     287      */
     288     static function get_cache() {
     289         if (!self::$cache) {
     290             self::$cache = SS_Cache::factory('catalog');
     291         }
     292         return self::$cache;
     293     }
     294
     295     /**
     296      * Получение данных из кеша
     297      *
     298      * @param $key - название кеша
     299      */
     300     static function get_cached_data($key) {
     301         if (!$key) return false;
     302         return unserialize(self::get_cache()->load($key));
     303     }
     304
     305     /**
     306      * Сохранение данных в кеш
     307      *
     308      * @param $key - название кеша
     309      * @param $data - данные, для сохранения в кеш
     310      */
     311     static function set_cached_data($key, $data) {
     312         if (!$key) return false;
     313         return self::get_cache()->save(serialize($data), $key);
     314     }
     315
     316     /**
     317      * Очистка кеша
     318      */
     319     static function clean_cache() {
     320         return self::get_cache()->clean();
     321     }
     322
     323     /*--------------- Функции для импорта -------------------*/
     324     /**
     325      * Список полей, которые могут быть в данных импорта
     326      */
     327     static $possibleFields = array('Title', 'Description', 'Content', 'URLSegment', 'MenuTitle', 'MetaTitle', 'MetaDescription', 'MetaKeywords', 'Sort');
     328
     329     /**
     330      * Добавление полей, которые могут быть в данных импорта
     331      *
     332      * @param array $fields
     333      */
     334     static function addPossibleFields($fields) {
     335         if ($fields)
     336             foreach($fields as $field)
     337                 self::$possibleFields[] = $field;
     338     }
     339
     340      /**
     341       * Возвращает объект по его ImportID
     342       * @return DataObject||null
     343       */
     344     static function import_find($importID) {
     345         return DataObject::get_one('Catalog', "ImportID = '" . Convert::raw2sql($importID) . "'");
     346     }
     347
     348     /**
     349      * Обновляет объект
     350      *
     351      * @param $importLog - объект для протоколиорвания импорта (или сама задача), для возможности записать сообщения об ошибках
     352      * @param $data - массив с данными для импорта
     353      * @return bool - флаг можно ли продолжать импорт
     354      */
     355     function importUpdate($importLog, $data) {
     356         if (!$this->importValidate($importLog, $data)) {
     357             return false;
     358         }
     359
     360         $rs = $this->extend('onBeforeImport', $importLog, $data);
     361         if ($rs && max($rs) == false) return false;
     362
     363         if (isset($data['id'])) {
     364             $this->ImportID = $data['id'];
     365         }
     366
     367         foreach(self::$possibleFields as $field) {
     368             if (isset($data[$field]))
     369             $this->{$field} = $data[$field];
     370         }
     371
     372         if (isset($data['Photo'])) {
     373             $this->PhotoID = ($data['Photo']) ? $data['Photo']->ID : 0;
     374         }
     375
     376         if (isset($data['ParentID']) && !$this->ParentID) { //не обновляем родителя у уже импортированных разделов  (для возможности ручного изменения структуры каталога на сайте).
     377             $this->ParentID = $data['ParentID'];
     378         }
     379         if (isset($data['Publish'])) {
     380             if ($data['Publish']) { // Если нету родителя, хотя должен быть (не корневой раздел), то не публикуем
     381                 $this->doPublish();
     382             } else {
     383                 $this->doUnpublish();
     384             }
     385         } else {
     386             //если флаг публикациии не задан, то сохраняем текущее состояние
     387             if ($this->isPublished()) {
     388                 $this->doPublish();
     389             } else {
     390                 $this->write();
     391             }
     392         }
     393         if (isset($data['Unpublish']) && $data['Unpublish']) {
     394             $this->doUnpublish();
     395         }
     396         $this->extend('onAfterImport', $importLog, $data);
     397         return true;
     398     }
     399
     400     /**
     401      * Проверяет данные полей объекта на соответствие типам
     402      * @param $importLog - объект для протоколиорвания импорта (или сама задача), для возможности записать сообщения об ошибках
     403      * @return bool - флаг можно ли продолжать импорт
     404      */
     405     function importValidate($importLog, $data) {
     406         if ((!$this->Title) && (!isset($data['Title']) || trim($data['Title']) == '')) { // если у каталога нет Title и Title нет в импорте, то ругаемся
     407             $importLog->addLog("Раздел каталога с id='{$data['id']}' не имеет названия!", 'error');
     408             return false;
     409         }
     410         if (isset($data['Title']) && trim($data['Title']) == '') { // если тег <Title> задан, но пустой, то ругаемся
     411             $importLog->addLog("Раздел каталога с id='{$data['id']}' не имеет названия!", 'error');
     412             return false;
     413         }
     414         if ((!$this->Title) && (!isset($data['Title']) || trim($data['Title']) == '')) {
     415             $importLog->addLog("Раздел каталога с id='{$data['id']}' не имеет названия!", 'error');
     416             return false;
     417         }
     418         if (isset($data['Sort']) && $data['Sort'] != (int)$data['Sort']) {
     419             $importLog->addLog("Параметр Sort раздела каталога {$data['Title']} не является целым числом!", 'warning');
     420             $data['Sort'] = 0;
     421         }
     422         $rs = $this->extend('importValidate', $importLog, $data);
     423         if ($rs && max($rs) == false) return false;
     424         return true;
     425     }
     426
     427      /**
     428      * Выполняет удаление всех объектов перед импортом
     429      */
     430     function importClearAll($importLog) {
     431         $oldMode = Versioned::get_reading_mode();
     432
     433         Versioned::reading_stage('Stage');
     434         $catalogs = DataObject::get('Catalog', "ClassName <> 'SpecialCatalog' AND ClassName <> 'StartCatalog'");
     435         if ($catalogs)
     436             foreach($catalogs as $catalog) {
     437                 $catalog->doUnpublish();
     438                 $catalog->delete(); // !!! что будет с детьми ?
     439             }
     440         unset($catalogs);
     441
     442         Versioned::reading_stage('Live');
     443         $catalogs = DataObject::get('Catalog', "ClassName <> 'SpecialCatalog' AND ClassName <> 'StartCatalog'");
     444         if ($catalogs)
     445             foreach($catalogs as $catalog) {
     446                 $catalog->doUnpublish();
     447                 $catalog->delete(); // !!! что будет с детьми ?
     448             }
     449
     450         Versioned::set_reading_mode($oldMode);
     451     }
     452     /*--------------- Конец функций для импорта -------------------*/
     453
     454     /**
     455      * Получение уникальных значений поля по фильтру (с учетом ограничений в $productsSQL - текущая рубрика и параметры фильтра)
     456      *
     457      * @param CatalogFilter $filter
     458      * @param string $productsSQL
     459      *
     460      * @return array
     461      */
     462     static function get_unique_values($filter, $productsSQL) {
     463         // FIXME кеширование результатов этого запроса
     464         $stage = (Versioned::current_stage() == 'Live' ? '_Live' : '');
     465         if ($table = self::productFieldTable($filter->FilterField)) {
     466             $data = DB::Query("SELECT DISTINCT {$filter->FilterField} as Value FROM \"{$table}{$stage}\" WHERE \"{$table}{$stage}\".ID IN (SELECT \"SiteTree{$stage}\".ID FROM \"SiteTree{$stage}\" JOIN \"Product{$stage}\" ON \"SiteTree{$stage}\".\"ID\" = \"Product{$stage}\".\"ID\" WHERE {$productsSQL}) ORDER BY {$filter->FilterField}");
     467         } else {
     468             $sort = 'ORDER BY Value';
     469             // берем параметр, по которому фильтруем
     470             $productParam = DataObject::get_one('ProductParam', "TechTitle = '{$filter->FilterField}'");
     471             // если он числовой
     472             if ($productParam && $productParam->Type == 'number') {
     473                 $sort = 'ORDER BY (Value+0)'; // то сортируем символы как числа
     474             }
     475             $data = DB::Query("SELECT DISTINCT Value FROM ProductParamValue WHERE TechTitle = '{$filter->FilterField}' AND ProductID IN (SELECT \"SiteTree{$stage}\".ID FROM \"SiteTree{$stage}\" JOIN \"Product{$stage}\" ON \"SiteTree{$stage}\".\"ID\" = \"Product{$stage}\".\"ID\" WHERE {$productsSQL}) {$sort}");
     476         }
     477
     478         $rs = array();
     479         if ($data->numRecords()) {
     480             foreach($data as $entry) {
     481                 if ($entry['Value']) {
     482                     $rs[$entry['Value']] = $entry['Value'];
     483                 }
     484             }
     485         }
     486         return $rs;
     487     }
     488
     489     /**
     490      * Получение мин/макс значений поля по фильтру (с учетом ограничений в $productsSQL - текущая рубрика и параметры фильтра)
     491      *
     492      * @param CatalogFilter $filter
     493      * @param string $productsSQL
     494      *
     495      * @return SS_Query
     496      */
     497     static function get_min_max_values($filter, $productsSQL) {
     498         $stage = (Versioned::current_stage() == 'Live' ? '_Live' : '');
     499         if ($table = self::productFieldTable($filter->FilterField)) {
     500             $data = DB::Query("SELECT MIN({$filter->FilterField}) as MinVal, MAX({$filter->FilterField}) as MaxVal FROM \"{$table}{$stage}\" WHERE \"{$table}{$stage}\".ID IN (SELECT \"SiteTree{$stage}\".ID FROM \"SiteTree{$stage}\" JOIN \"Product{$stage}\" ON \"SiteTree{$stage}\".\"ID\" = \"Product{$stage}\".\"ID\" WHERE {$productsSQL})");
     501         } else {
     502             // берем ID всех продуктов, удовлетворяющих текущим фильтрам
     503             $data = DB::Query("SELECT MIN(Value+0) as MinVal, MAX(Value+0) as MaxVal FROM ProductParamValue WHERE TechTitle = '{$filter->FilterField}' AND ProductID IN (SELECT \"SiteTree{$stage}\".ID FROM \"SiteTree{$stage}\" JOIN \"Product{$stage}\" ON \"SiteTree{$stage}\".\"ID\" = \"Product{$stage}\".\"ID\" WHERE {$productsSQL})");
     504         }
     505         return $data;
     506     }
     507
     508     function getCMSFields() {
     509         SiteTree::disableCMSFieldsExtensions();
     510         $fields = parent::getCMSFields();
     511         SiteTree::enableCMSFieldsExtensions();
     512
     513         $fields->addFieldToTab('Root.Content.Main', new ImageField('Photo', $this->fieldLabel('Photo')), 'Content');
     514         $fields->addFieldToTab('Root.Content.Main', new TextField('ImportID', $this->fieldLabel('ImportID')), 'Content');
     515         $fields->addFieldToTab('Root.Content.Main', new TextareaField('Description', $this->fieldLabel('Description')), 'Content');
     516
     517         if (count(self::get_view_options()) > 1)
     518             $fields->addFieldToTab('Root.Content.Main', new DropdownField("StandartView", $this->fieldLabel('StandartView'), self::view_options_dropdown_map(true)), 'Description');
     519
     520         if ($allVATs = DataObject::get('VAT')) {
     521             $fields->addFieldToTab('Root.Content.Main', new DropdownField("CatalogVATID", $this->fieldLabel('CatalogVAT'), $allVATs->map('ID', 'Title', _t('VAT.SelectVAT'))));
     522         }
     523
     524         $fields->addFieldToTab('Root.Content', new Tab('FullContent', _t('Catalog.tab_FullContent', 'Content')), 'Metadata');
     525         $fields->addFieldToTab('Root.Content.FullContent', $fields->dataFieldByName('Content'));
     526         if ($allVATs = DataObject::get('VAT')) {
     527             $fields->addFieldToTab('Root.Content.Main', new DropdownField("CatalogVATID", $this->fieldLabel('CatalogVAT'), $allVATs->map('ID', 'Title', _t('VAT.SelectVAT'))), 'Description');
     528         }
     529
     530
     531         Requirements::css('catalog/css/ProductAdmin.css');
     532 //        Requirements::javascript('catalog/javascript/ProductAdmin.js');
     533
     534         /// Товары в виде вкладки
     535         $fields->addFieldToTab('Root.Content', new Tab('tabSubPages', _t('Catalog.tab_Products', 'Products')), 'Metadata');
     536
     537         $class = $this->class;
     538         // Таблица с товарами
     539         $sp = new SubpageListField('Subpages', $this, $class::$subpage_children);
     540         $sp->Dragable = true;
     541         $url = '<a href=\"admin/show/$ID\">$value</a>';
     542
     543         $sp->setFieldFormatting(array_combine(array_keys(singleton($class::$subpage_children)->summaryFields()), array_fill(0, count(singleton($class::$subpage_children)->summaryFields()), $url)));
     544         $fields->addFieldToTab('Root.Content.tabSubPages', $sp);
     545
     546         if (Catalog::$use_additional_params) {
     547             //параметры товаров
     548             $tab = $fields->findOrMakeTab('Root.Content.Params', _t('Catalog.ProductParams', 'Product Params'));
     549             $tab->push(new LiteralField('CatalogParams', '<h4>'.$this->fieldLabel('Params').'</h4>'));
     550             $ctf = new ComplexTableField(
     551                 $this,
     552                 'Params',
     553                 'ProductParam'
     554             );
     555             $ctf->setRelationAutoSetting(true);
     556             $tab->push($ctf);
     557
     558             $tab->push(new CheckboxField('OwnParams', $this->fieldLabel('OwnParams')));
     559             if (!$this->OwnParams) {
     560                 $tab->push(new LiteralField('OwnParamsOnly', '<p>'._t('Catalog.OwnParamsOnly').'</p>'));
     561             } else {
     562                 // все подходящие параметры
     563                 if (($params = $this->getAllCatalogParams()) && $params->Count()) {
     564                     $ctf = new ManyManyDataObjectManager(
     565                         $this,
     566                         'EnabledParams',
     567                         'ProductParam',
     568                         array('Title'=>'Title', 'TechTitle'=>'TechTitle', 'LocalType'=>'Type'),
     569                         null,
     570                         "ProductParam.ID IN (".implode(',', $params->map('ID', 'ID')).")"
     571                     );
     572                     $ctf->setPermissions(array());
     573                     $ctf->setPluralTitle($this->fieldLabel('EnabledParams'));
     574                     //$ctf->setCustomSourceItems($paramValues);
     575                     $tab->push($ctf);
     576                 }
     577             }
     578         }
     579
     580         if (Catalog::$use_additional_filters) {
     581             //фильтры каталога
     582             $tab = $fields->findOrMakeTab('Root.Content.Filters', _t('Catalog.CatalogFilters', 'Catalog Filters'));
     583
     584             $tab->push(new LiteralField('CatalogFilters', '<h4>'.$this->fieldLabel('CatalogFilters').'</h4>'));
     585             $ctf = new ComplexTableField(
     586                 $this,
     587                 'CatalogFilters',
     588                 'CatalogFilter'
     589             );
     590             $ctf->setRelationAutoSetting(true);
     591             $tab->push($ctf);
     592
     593             $tab->push(new CheckboxField('OwnFilter', $this->fieldLabel('OwnFilter')));
     594             if (!$this->OwnFilter) {
     595                 $tab->push(new LiteralField('OwnParamsOnly', '<p>'._t('Catalog.OwnFilterOnly').'</p>'));
     596             } else {
     597                 // все подходящие фильтры
     598                 if (($filters = $this->getAllCatalogFilters()) && $filters->Count()) {
     599                     $ctf = new ManyManyDataObjectManager(
     600                         $this,
     601                         'EnabledCatalogFilters',
     602                         'CatalogFilter',
     603                         array('Title'=>'Title', 'LocalFilterField'=>'FilterField', 'LocalType'=>'Type'),
     604                         null,
     605                         "CatalogFilter.ID IN (".implode(',', $filters->map('ID', 'ID')).")"
     606                     );
     607                     //$ctf->setCustomSourceItems($this->getAllCatalogFilters());
     608                     $ctf->setPermissions(array());
     609                     $ctf->setPluralTitle($this->fieldLabel('EnabledCatalogFilters'));
     610                     $tab->push($ctf);
     611                 }
     612             }
     613         }
     614
     615         $this->extend('updateCMSFields', $fields);
     616         return $fields;
     617     }
     618
     619     function onAfterWrite() {
     620         parent::onAfterWrite();
     621     }
     622
     623     function onAfterDelete() {
     624         if ($this->IsDeletedFromStage && !$this->ExistsOnLive) {
     625             if ($this->EnabledParams()) {
     626                 $this->EnabledParams()->removeAll();
     627             }
     628             if ($this->EnabledCatalogFilters()) {
     629                 $this->EnabledCatalogFilters()->removeAll();
     630             }
     631             if ($this->Params()) {
     632                 foreach ($this->Params() as $obj) {
     633                     $obj->delete();
     634                 }
     635             }
     636             if ($this->CatalogFilters()) {
     637                 foreach ($this->CatalogFilters() as $obj) {
     638                     $obj->delete();
     639                 }
     640             }
     641         }
     642         parent::onAfterDelete();
     643     }
     644
     645     function getVAT(){
     646         if ($this->CatalogVATID && ($vat = $this->CatalogVAT())) {
     647             return $vat;
     648         }
     649         if ($this->ParentID && ($parent = $this->Parent()) && is_a($parent, 'Catalog')) {
     650             return $this->Parent()->getVAT();
     651         }
     652         return SiteConfig::current_site_config()->DefaultVAT();
     653     }
     654
     655     /**
     656      * Метод для шаблонов для проверки флага $user_can_change_view
     657      *
     658      * @return bool
     659      */
     660     public function AllowChangeView() {
     661         return self::$user_can_change_view && (count(self::$view_options) > 1);
     662     }
     663
     664     /**
     665      * Метод для шаблонов для проверки возможности смены размера страницы
     666      *
     667      * @return bool
     668      */
     669     public function AllowChangePageSize() {
     670         return self::$user_can_change_pagesize && (count(self::$pagesize_options) > 1);
     671     }
     672
     673     /**
     674      * Возвращает список подрубрик
     675      *
     676      * @return DataObjectSet
     677      */
     678     public function Subcats() {
     679         return DataObject::get("Catalog", "ParentID = {$this->ID} AND ShowInMenus=1", "Sort");
     680     }
     681
     682     /**
     683      * Число товаров в рубрике
     684      *
     685      * @return int
     686      */
     687     public function CountItems() {
     688         $count = 0;
     689         if ($items = DataObject::get("Product", "ParentID = {$this->ID}")) {
     690             $count += $items->Count();
     691         }
     692         return $count;
     693     }
     694
     695     /**
     696      * Возвращает режим сортировки без учета выбора пользователя
     697      *
     698      * @return string
     699      */
     700     public function defaultSort() {
     701         return ($this->SiteConfig->CatalogDefaultSort)? $this->SiteConfig->CatalogDefaultSort : 'title';
     702     }
     703
     704     /**
     705      * Возвращает режим отображения без учета выбора пользователя
     706      *
     707      * @return string
     708      */
     709     public function defaultView() {
     710         if ($this->StandartView)
     711             return $this->StandartView;
     712
     713         return ($this->SiteConfig->CatalogDefaultView) ? $this->SiteConfig->CatalogDefaultView : 'tile';
     714     }
     715
     716     /**
     717      * Возвращает количество товаров на странице без учета выбора пользователя
     718      *
     719      * @return string
     720      */
     721     public function defaultPageSize() {
     722         return ($this->SiteConfig->ProductPerPage)? $this->SiteConfig->ProductPerPage : 30;
     723     }
     724
     725     /**
     726      * Возвращает список разрешенных полей для фильтра
     727      *
     728      * @return array список полей товара для фильтра в текущей рубрике
     729      */
     730     public function catalogFilterFields() {
     731         $filters = false;
     732         if ($this->OwnFilter) {
     733             $filters = $this->EnabledCatalogFilters();
     734             return $filters;
     735         }
     736         if ($this->ParentID && $this->Parent() && is_a($this->Parent(), 'Catalog')) {
     737             $filters = $this->Parent()->catalogFilterFields();
     738         }
     739         // подтягиваем глобавльный фильтры (с ShowDefault = 1)
     740         if (!$filters && ($rootFilters = DataObject::get('CatalogFilter', "ParentCatalogID = 0 AND ShowDefault = 1"))) {
     741             $filters = $rootFilters;
     742         }
     743         return $filters;
     744     }
     745
     746     // получение всех фильтров рубрики с учетом глобальных и унаследованных от родителей
     747     function getAllCatalogFilters() {
     748         $filters = $this->CatalogFilters();
     749         if ($this->getAncestors( )) {
     750             foreach($this->getAncestors( ) as $ancestor) {
     751                 if (is_a($ancestor, 'Catalog') && $ancestor->CatalogFilters()) {
     752                     $filters->merge($ancestor->CatalogFilters());
     753                 }
     754             }
     755         }
     756         // подтягиваем глобавльный фильтры (с ShowDefault = 1), только п
     757         if ($rootFilters = DataObject::get('CatalogFilter', "ParentCatalogID = 0")) {
     758             $filters->merge($rootFilters);
     759         }
     760         return $filters;
     761     }
     762
     763     /**
     764      * Возвращает список разрешенных параметров
     765      *
     766      * @return array список  параметров товара  в текущей рубрике
     767      */
     768     public function catalogParams() {
     769         $params = false;
     770         if ($this->OwnParams) {
     771             return $this->EnabledParams();
     772         }
     773         if ($this->ParentID && $this->Parent() && is_a($this->Parent(), 'Catalog')) {
     774             $params = $this->Parent()->catalogParams();
     775         }
     776         // подтягиваем глобавльный фильтры (с ShowDefault = 1)
     777         if ((!$params || !$params->Count()) && ($rootParams = DataObject::get('ProductParam', "ParentCatalogID = 0 AND ShowDefault = 1"))) {
     778             $params = $rootParams;
     779         }
     780         return $params;
     781     }
     782
     783     // получение всех Параметров рубрики с учетом глобавльных и унаследованных от родителей
     784     function getAllCatalogParams() {
     785         $params = $this->Params();
     786         if ($this->getAncestors( )) {
     787             foreach($this->getAncestors( ) as $ancestor) {
     788                 if (is_a($ancestor, 'Catalog') && $ancestor->Params()) {
     789                     $params->merge($ancestor->Params());
     790                 }
     791             }
     792         }
     793         if ($rootParam = DataObject::get('ProductParam', "ParentCatalogID = 0 AND ShowDefault = 1")) {
     794             $params->merge($rootParam);
     795         }
     796         return $params;
     797
     798     }
     799
     800     // получение списка параметров для вариаций товаров
     801     function getVariationCatalogParams() {
     802         if ($params = $this->catalogParams()) {
     803             $variationParams = new DataObjectSet();
     804             foreach($params as $param) {
     805                 if ($param->ParamForVariation) {
     806                     $variationParams->push($param);
     807                 }
     808             }
     809             return $variationParams;
     810         }
     811         return false;
     812     }
     813
     814     // получение списка параметров для вариаций товаров
     815     function getNonVariationCatalogParams() {
     816         if ($params = $this->catalogParams()) {
     817             $nonVariationParams = new DataObjectSet();
     818             foreach($params as $param) {
     819                 if (!$param->ParamForVariation) {
     820                     $nonVariationParams->push($param);
     821                 }
     822             }
     823             return $nonVariationParams;
     824         }
     825         return false;
     826     }
     827
     828     /*
     829      * Имеет ли дочерний Товар заданной поле
     830      *
     831      * @param string $field
     832      * @return bool
     833      */
     834     static function productFieldTable($field) {
     835         if (!singleton(self::$subpage_children)->hasField($field)) {
     836             return false;
     837         }
     838         $classes = array_reverse(ClassInfo::ancestry(singleton(self::$subpage_children)));
     839         foreach($classes as $class) {
     840             $fields = Object::uninherited_static($class, 'db'); // ??? фильтрация по has_one параметрам ???
     841             if (isset($fields[$field])) {
     842                 return $class;
     843             }
     844         }
     845         return false;
     846     }
     847
     848     /**
     849      * Возвращает список товаров с учетом фильтрации и сортировки
     850      *
     851      * @param string $order - текущая сортировка
     852      * @param array $filters - массив условий фильтрации (из формы)
     853      * @param string $limit  - строка для sql limit
     854      *
     855      * @return DataObjectSet - текущая страница выборки товаров
     856      */
     857     public function filteredProducts($order=null, $filters=null, $limit=null) {
     858         $orderby = false;
     859         if (!is_null($order)) {
     860             $orderby = Product::sort_options_orderby($order);
     861         }
     862         if (!$orderby) {
     863             $orderby = Product::sort_options_orderby($this->defaultSort());
     864         }
     865
     866         $query = $this->getProductsListQuery($orderby, $limit);
     867         if ($filters) {
     868             // обновляем запроса на получение списка товаров по значениям пользовательских фильтров
     869             $query = $this->updateQueryByProductParamFilters($filters, $query);
     870         }
     871         $this->extend('updateFilteredProductsQuery', $query, $filters);
     872         $results = DataObject::buildDataObjectSet($query->execute());
     873         if($results) $results->parseQueryLimit($query);
     874         return $results;
     875     }
     876
     877     function getProductsListQuery($orderby=null, $limit=null) {
     878         $where = $this->getProductsListWhere();
     879         $query = singleton('SiteTree')->extendedSQL(implode(' AND ', $where), $orderby, $limit);
     880         return $query;
     881     }
     882
     883     /*
     884      * Базовое условие для получения список товаров данной рубрики (всех)
     885      *
     886      * @param (bool) $forceShowProductsFromSubCategories - принудительно брать товары из подкаталогов
     887      * @return  array
     888      */
     889     function getProductsListWhere($forceShowProductsFromSubCategories=false) {
     890         $sc = SiteConfig::current_site_config();
     891         $where = array();
     892
     893         $productClasses = array();
     894         if($subclasses = ClassInfo::subclassesFor(self::$subpage_children)) {
     895             foreach ($subclasses as $subclass)
     896                 $productClasses[] = $subclass;
     897         } else {
     898             $productClasses[] = self::$subpage_children;
     899         }
     900         $where[] = "(ClassName IN ('".implode("','", $productClasses)."'))";
     901
     902         $categoryIDs = array($this->ID);
     903         // товары из подкатегорий
     904         if ($sc->ShowProductsFromSubCategories || $forceShowProductsFromSubCategories) {
     905             $categoryIDs = $this->getDescendantIDList();
     906             $categoryIDs[] = $this->ID;
     907         }
     908         $where[] = "(ParentID IN (".implode(',', $categoryIDs) ."))";
     909
     910         // скрытие товаров с AllowPurchase == 0
     911         if (self::$hide_allow_purchase_products) {
     912             $where[] = "(AllowPurchase = 1)";
     913         }
     914         return $where;
     915     }
     916
     917     // обновление запроса на получение списка товаров по значениям пользовательских фильтров
     918     function updateQueryByProductParamFilters($filters, $query) {
     919         /*
     920          * Возможно попадание товаров (по Значение параметров) из других категорий (т.к. они ищутся по служебному имени, которое может совпадать - напр. vendor), но
     921          * они все равно отфильтруются в filteredProducts(), где есть фильтрация товаров по текущей и вложенным в нее категориям
     922          *
     923          */
     924
     925         $stage = (Versioned::current_stage() == 'Live' ? '_Live' : '');
     926         $query->renameTable('SiteTree', 'SiteTree'. $stage);
     927         $paramsWhere = array();
     928
     929         foreach($filters as $key=>$value) {
     930             $filter = DataObject::get_one('CatalogFilter', "FilterField = '".Convert::raw2sql($key)."'"); // AND CatalogID = {$this->ID} // !!!! фильтры имеют уникальный FilterField - УТОЧНИТЬ!!!
     931             if ($filter && $value) {
     932                 switch ($filter->Type) {
     933                     case 'bool':
     934                     case 'boolgroup':
     935                     case 'list':
     936                         $value = Convert::raw2sql($value);
     937                         if (self::productFieldTable($filter->FilterField)) {
     938                             $query->where[] = "{$filter->FilterField} = '{$value}'";
     939                         } else {
     940                             //$query->where[] = "\"SiteTree\".ID IN (SELECT DISTINCT ProductID FROM ProductParamValue WHERE TechTitle = '{$filter->FilterField}' AND Value = '{$value}')";
     941                             $paramsWhere[] = "SELECT DISTINCT ProductID FROM ProductParamValue WHERE TechTitle = '{$filter->FilterField}' AND Value = '{$value}'";
     942                         }
     943                         break;
     944                     case 'slider':
     945                         // для слайдера смотрим края интервала по отдельности
     946                         if (self::productFieldTable($filter->FilterField)) {
     947                             $sliderWhere = array();
     948                             if (isset($value['min'])) {
     949                                 $sliderWhere[] = "{$filter->FilterField} >= ".((int)$value['min']);
     950                             }
     951                             if (isset($value['max'])) {
     952                                 $sliderWhere[] = "{$filter->FilterField} <= ".((int)$value['max']);
     953                             }
     954                             if (count($sliderWhere)) {
     955                                 $query->where[] = "(". implode(' AND ', $sliderWhere).")";
     956                             }
     957                         } else {
     958                             $sliderWhere = array();
     959                             if (isset($value['min'])) {
     960                                 $sliderWhere[] = "(CONVERT(Value, SIGNED) >= ".((int)$value['min']) . ")";
     961                             }
     962                             if (isset($value['max'])) {
     963                                 $sliderWhere[] = "(CONVERT(Value, SIGNED) <= ".((int)$value['max']) . ")";
     964                             }
     965                             if (count($sliderWhere)) {
     966                                 $paramsWhere[] = "SELECT DISTINCT ProductID FROM ProductParamValue WHERE TechTitle = '{$filter->FilterField}' AND (". implode(' AND ', $sliderWhere).")";
     967                             }
     968                         }
     969                         break;
     970                     case 'multiselect':
     971                         if (is_array($value) && count($value)) {
     972                             foreach($value as $key=>$val) {
     973                                 $value[$key] = Convert::raw2sql($val);
     974                             }
     975                             if (self::productFieldTable($filter->FilterField)) {
     976                                 $query->where[] = "{$filter->FilterField} in('" . implode("','", $value)."')";
     977                             } else {
     978                                 //$query->where[] = "\"SiteTree\".ID IN (SELECT DISTINCT ProductID FROM ProductParamValue WHERE TechTitle = '{$filter->FilterField}' AND Value in ('" . implode("','", $value) . "'))";
     979                                 $paramsWhere[] = "SELECT DISTINCT ProductID FROM ProductParamValue WHERE TechTitle = '{$filter->FilterField}' AND Value in ('" . implode("','", $value) . "')";
     980                             }
     981                             break;
     982                         }
     983                     case 'text':
     984                         $value = Convert::raw2sql($value);
     985                         $condition = "LIKE '%{$value}%'";
     986                         // !!! для числовых полей используем точное сравнение
     987                         if (self::productFieldTable($filter->FilterField)) {
     988                             if (self::productFieldTable($filter->FilterField) == 'Int') {
     989                                 $condition = " = '$value'";
     990                             }
     991                             $query->where[] = "{$filter->FilterField} $condition";
     992                         } else {
     993                             $param = DataObject::get_one('ProductParam', "TechTitle = '{$filter->FilterField}'");
     994                             if ($param && $param->Type == 'number') {
     995                                 $condition = " = '$value'";
     996                             }
     997                             //$query->where[] = "\"SiteTree\".ID IN (SELECT DISTINCT ProductID FROM ProductParamValue WHERE TechTitle = '{$filter->FilterField}' AND Value LIKE '%{$value}%')";
     998                             $paramsWhere[] = "SELECT DISTINCT ProductID FROM ProductParamValue WHERE TechTitle = '{$filter->FilterField}' AND Value $condition";
     999                         }
    1000                         break;
    1001                 }
    1002             }
    1003         }
    1004
    1005         // спец. where для поиска по параметрам - работает быстро
    1006         if (count($paramsWhere)) {
    1007             $fullWhere = false;
    1008             foreach($paramsWhere as $where) {
    1009                 if (!$fullWhere) {
    1010                     $fullWhere = $where;
    1011                 } else {
    1012                     $fullWhere .= " AND ProductID IN ({$where}";
    1013                 }
    1014             }
    1015             $fullWhere .= str_repeat(")", count($paramsWhere)-1);
    1016             $query->where[] = "SiteTree{$stage}.ID IN ({$fullWhere})"; // вручную проставляем Stage, т.к. этот запрос используется еще при reduce_filters в $Filters
    1017         }
    1018
    1019         return $query;
    1020     }
    1021
    1022     /*
    1023      * SQL для получения список товаров данной рубрики (всех)
    1024      *
    1025      * @return  string
    1026      */
    1027     function getProductsListForFiltersSQL() {
    1028         $where = $this->getProductsListWhere();
    1029         $stage = (Versioned::current_stage() == 'Live' ? '_Live' : '');
    1030         return "SELECT \"SiteTree{$stage}\".ID FROM \"SiteTree{$stage}\" JOIN \"Product{$stage}\" ON \"SiteTree{$stage}\".\"ID\" = \"Product{$stage}\".\"ID\" WHERE " . implode(' AND ', $where);
    1031     }
    1032
    1033
    1034 }
    1035
    1036 class Catalog_Controller extends Page_Controller {
    1037
    1038     // текущие значения параметров отображения
    1039     public $CurrentSort;
    1040     public $CurrentView;
    1041     public $CurrentPageSize;
    1042     // текущие данные фильтра
    1043     public $FilterActive = null;
    1044
    1045     public $UseShowMore = false; // флаг используем подгрузку товаров ajax-ом
    1046     public $UseChangeView = false; // флаг меняем вид отображения ajax-ом
    1047
    1048     /**
    1049      * Устанавливает значение параметра отображения
    1050      *
    1051      * @param string $name - имя параметра
    1052      * @param mixed $newVal - новое значение (например из get параметров) TODO возможно вытаскивать автоматически
    1053      *
    1054      * @return bool была ли смена сохраненного значения
    1055      */
    1056     function setupCatalogtVar($name, $newVal = null) {
    1057         $currName = 'Current' . $name; // имя переменной объекта
    1058         $cookieName = 'Catalog' . $name; // имя куки и параметра у пользователя
    1059         $checkName = 'AllowChange' . $name; // имя метода проверки возможности смены (если метода не будет - считаем что можно)
    1060         $defName = 'default' . $name; // имя метода получения значения по-умолчанию
    1061         $valuesName = 'get_' . strtolower($name) . '_options'; // имя метода получения возможных значений
    1062
    1063         $changed = false; // меняли ли переменную
    1064         $default = $this->$defName();
    1065         $possibleValues = Catalog::$valuesName();
    1066
    1067         // сначала - значение по-умолчанию
    1068         $this->$currName = $default;
    1069
    1070         // проверим можно ли менять
    1071         if ($this->hasMethod($checkName) && !$this->$checkName()) {
    1072             return false;
    1073         }
    1074
    1075         // вытащим сохраненное в куках
    1076         $cookieVal = Cookie::get($cookieName);
    1077         if (isset($cookieVal) && in_array($cookieVal, $possibleValues)) {
    1078             $this->$currName = $cookieVal;
    1079         }
    1080
    1081         // если в куках нет - попробуем восстановить из пользователя
    1082         if (!$cookieVal && $member = Member::currentUser()) {
    1083             if ($member->hasMethod('getCatalogOption')) {
    1084                 $cookieVal = $member->getCatalogOption($cookieName);
    1085                 if ($cookieVal && in_array($cookieVal, $possibleValues)) {
    1086                     $this->$currName = $cookieVal;
    1087                     Cookie::set($cookieName, $cookieVal);
    1088                 }
    1089             }
    1090         }
    1091
    1092         // задано новое значение
    1093         if (isset($newVal)) {
    1094             $member = Member::currentUser();
    1095             if ($newVal === '' || $newVal == 'default') {
    1096                 $changed = ($default != $this->$currName);
    1097                 $this->$currName = $default;
    1098
    1099                 Cookie::set($cookieName, '');
    1100                 if ($member && $member->hasMethod('setCatalogOption')) {
    1101                     $member->setCatalogOption($cookieName, '');
    1102                 }
    1103                 return $changed;
    1104             }
    1105             if (in_array($newVal, $possibleValues)) {
    1106                 $changed = ($newVal != $this->$currName);
    1107                 $this->$currName = $newVal;
    1108
    1109                 Cookie::set($cookieName, $newVal);
    1110                 if ($member && $member->hasMethod('setCatalogOption')) {
    1111                     $member->setCatalogOption($cookieName, $newVal);
    1112                 }
    1113                 // если устанавливаем значение - уже нестандарт (даже если совпадает с дефолтным)
    1114                 $this->data()->SeoIsAlternative = true;
    1115             }
    1116         }
    1117
    1118         // нестандартное значение
    1119         /*
    1120         if ($this->$currName !== $default) {
    1121             $this->data()->SeoIsAlternative = true;
    1122         }
    1123         */
    1124         return $changed;
    1125     }
    1126
    1127     function isEmptyContent() {
    1128         return parent::isEmptyContent() && $this->CountItems() == 0;
    1129     }
    1130
    1131     /**
    1132      * Настраивает css классы для поля формы фильтров
    1133      *
    1134      * @param FormField $field
    1135      * @param CatalogFilter $filter
    1136      *
    1137      * @return FormField
    1138      */
    1139     function setupFilterClasses($field, $filter) {
    1140         if ($filter->Important) {
    1141             $field->addExtraClass('form_fieldContent__important important');
    1142             $field->addExtraClass('form_fieldContent__active active');
    1143         }
    1144         else {
    1145             $field->addExtraClass('not_important');
    1146             $filterData = $this->cleanParams();
    1147             if (isset($filterData[$filter->FilterField]) && ($filterData[$filter->FilterField] !== '')) {
    1148                 $field->addExtraClass('form_fieldContent__active active');
    1149             }
    1150         }
    1151         return $field;
    1152     }
    1153
    1154     /**
    1155      * Получение поля фильтра для текстового фильтра
    1156      *
    1157      * @param  ProductParam
    1158      * @return  FormField
    1159      *
    1160      */
    1161     function getTextFilter($filter) {
    1162         return $this->setupFilterClasses(new TextField($filter->FilterField, $filter->Title), $filter);
    1163     }
    1164
    1165     /**
    1166      * Получение поля фильтра для Флагов
    1167      *
    1168      * @param  ProductParam
    1169      * @return  FormField
    1170      *
    1171      */
    1172     function getBoolFilter($filter) {
    1173         return $this->setupFilterClasses(new CheckboxField($filter->FilterField, $filter->Title), $filter);
    1174     }
    1175
    1176     /**
    1177      * Получение поля фильтра для Флагов
    1178      *
    1179      * @param  ProductParam
    1180      * @return  FormField
    1181      *
    1182      */
    1183     function getMultiSelectFilter($filter, $productsSQL) {
    1184         $rs = Catalog::get_unique_values($filter, $productsSQL);
    1185         if (count($rs) > 0) {
    1186             $f = new CheckboxSetField($filter->FilterField, $filter->Title, $rs);
    1187             if (trim($filter->DefaultValue)) {
    1188                 $f->setValue($filter->DefaultValue);
    1189             }
    1190             return $this->setupFilterClasses($f, $filter);
    1191         }
    1192         return false;
    1193     }
    1194
    1195     /**
    1196      * Получение поля фильтра для Списка
    1197      *
    1198      * @param  ProductParam
    1199      * @return  FormField
    1200      *
    1201      */
    1202     function getListFilter($filter, $productsSQL) {
    1203         $rs = Catalog::get_unique_values($filter, $productsSQL);
    1204         if (count($rs) > 0) {
    1205             $f = (trim($filter->DefaultValue)) ? new DropdownField($filter->FilterField, $filter->Title, $rs, $filter->DefaultValue) : new DropdownField($filter->FilterField, $filter->Title, $rs, '', null, 'Не важно');
    1206             return $this->setupFilterClasses($f, $filter);
    1207         }
    1208         return false;
    1209     }
    1210
    1211     /**
    1212      * Получение поля фильтра для Слайдера
    1213      *
    1214      * @param  ProductParam
    1215      * @return  FormField
    1216      *
    1217      */
    1218     function getSliderFilter($filter, $productsSQL) {
    1219         $data = Catalog::get_min_max_values($filter, $productsSQL);
    1220         $data = $data->First();
    1221         if (isset($data['MinVal']) && isset($data['MaxVal']) && ($data['MinVal'] < $data['MaxVal'])) {
    1222             $min_max_data = array('min' => floor($data['MinVal']), 'max' => ceil($data['MaxVal'])); // округляем левую часть в меньшую сторону, а правую - в большую, чтобы учесть копейки
    1223             $f = new RangeField($filter->FilterField, $filter->Title);
    1224             $f->setValue($min_max_data);
    1225             $f->setMinMaxValue($min_max_data);
    1226             if (trim($filter->Unit)) {
    1227                 $f->addExtraAttribute('Unit', trim($filter->Unit));
    1228                 $f->setUnit(trim($filter->Unit));
    1229             }
    1230             return $this->setupFilterClasses($f, $filter);
    1231         }
    1232         return false;
    1233     }
    1234
    1235     /**
    1236      * Получение поля фильтра для Группы параметров
    1237      *
    1238      * @param  ProductParam
    1239      * @return  FormField
    1240      *
    1241      */
    1242     function getGroupFilter($key, $filters, &$paramFields) {
    1243         if (!$filters->Count()) {
    1244             return false;
    1245         }
    1246         $groupFields = new FieldSet();
    1247         $t = Convert::rus2lat($key);
    1248
    1249         $label = new LabelField($t . '_label', $key);
    1250         $label->addExtraClass('elem_label');
    1251         $groupFields->push($label);
    1252
    1253         $filterData = $this->cleanParams();
    1254
    1255         $minSort = false;
    1256         $important = 0;
    1257         $active = '';
    1258         $groupFields->push(new LiteralField('group_wrapper', '<div class="group_wrapper">'));
    1259
    1260         foreach($filters as $filter) {
    1261             if ($minSort === false) {
    1262                 $minSort = $filter->Sort;
    1263             } else {
    1264                 if ($filter->Sort && (($filter->Sort < $minSort) || (!$minSort))) {
    1265                     $minSort = $filter->Sort;
    1266                 }
    1267             }
    1268             if ($filter->Important) {
    1269                 $important = 1;
    1270             }
    1271             $paramTitle = $filter->Title;
    1272
    1273             if (isset($filterData[$filter->FilterField])) {
    1274                 $active = 'active';
    1275             }
    1276             $groupFields->push(new CheckboxField($filter->FilterField, $paramTitle));
    1277         }
    1278         $groupFields->push(new LiteralField('end_group_wrapper', '</div>'));
    1279
    1280         $groupFields->push(new LiteralField('end_wrapper', '</div>'));
    1281
    1282         $extraClass = 'not_important';
    1283         if ($important) {
    1284             $active = 'active';
    1285             $extraClass = 'important';
    1286         }
    1287         $extraClass = "{$extraClass} {$active}";
    1288         $groupFields->insertFirst(new LiteralField('wrapper', '<div id="'.$t.'" class="field '.$extraClass.'">'));
    1289
    1290         foreach($groupFields as $groupField) {
    1291             $paramFields[$important][$minSort][] = $groupField;
    1292         }
    1293     }
    1294
    1295     /**
    1296      * Возвращает параметры для формирования списка товаров
    1297      *
    1298      * @param array $values - массив кандидатов на параметры
    1299      *
    1300      * @return array
    1301      */
    1302     function cleanParams($values = false, $keepPage=false) {
    1303         if (!$values)
    1304             $values = $this->getRequest()->requestVars();
    1305
    1306         unset($values['url']);
    1307         if (!$keepPage)
    1308             unset($values['start']);
    1309
    1310         unset($values['sort']);
    1311         unset($values['view']);
    1312         unset($values['pagesize']);
    1313         unset($values['action_filter']);
    1314         unset($values['action_filterclear']);
    1315         unset($values['show_more']);
    1316         return $values;
    1317     }
    1318
    1319     /**
    1320      * Возвращает правильную ссылку на каталог с учетом фильтров и других параметров
    1321      *
    1322      * @param array $params - параметры для формирования ссылки
    1323      *
    1324      * @return string
    1325      */
    1326     function linkWithParams($params = array()) {
    1327         if (!$params)
    1328             $params = $this->cleanParams(false, true);
    1329         if (!$params)
    1330             return $this->Link();
    1331
    1332         $action = ($this->IsFilterActive()) ? 'filter' : '';
    1333         return $this->Link($action) . '?' . http_build_query($params);
    1334     }
    1335
    1336     /**
    1337      * Получение возможных вариантов сортировок
    1338      */
    1339     function Sorts($default = false) {
    1340         $values = $this->cleanParams();
    1341         $items = new DataObjectSet();
    1342         foreach (Product::sort_options_dropdown_map($default) as $id => $title) {
    1343             $htmlID = ($id) ? $id : 'default';
    1344             $values['sort'] = $htmlID;
    1345             $items->push(new ArrayData(array(
    1346                 'ID' => $htmlID,
    1347                 'Title' => $title,
    1348                 'Link' => $this->linkWithParams($values),
    1349                 'isCurrent' => ($id == $this->CurrentSort),
    1350                 'LinkOrCurrent' => ($id == $this->CurrentSort) ? 'current' : 'link',
    1351             )));
    1352
    1353         }
    1354         return $items;
    1355     }
    1356
    1357     /**
    1358      * Получение возможных вариантов отображений
    1359      */
    1360     function Views($default = false) {
    1361         if (!$this->AllowChangeView()) return false;
    1362
    1363         $values = $this->cleanParams(false, true); // при смене вида не меняем текущую страницу
    1364         $items = new DataObjectSet();
    1365         foreach (Catalog::view_options_dropdown_map($default) as $id => $title) {
    1366             $htmlID = ($id) ? $id : 'default';
    1367             $values['view'] = $htmlID;
    1368             $items->push(new ArrayData(array(
    1369                 'ID' => $htmlID,
    1370                 'Title' => $title,
    1371                 'Link' => $this->linkWithParams($values),
    1372                 'isCurrent' => ($id == $this->CurrentView),
    1373                 'LinkOrCurrent' => ($id == $this->CurrentView) ? 'current' : 'link',
    1374             )));
    1375         }
    1376         return $items;
    1377     }
    1378
    1379     /**
    1380      * Получение возможных вариантов отображений
    1381      */
    1382     function PageSizes($default = false) {
    1383         if (!$this->AllowChangePageSize()) return false;
    1384
    1385         $values = $this->cleanParams();
    1386         $items = new DataObjectSet();
    1387         foreach (Catalog::pagesize_dropdown_map($default) as $id => $title) {
    1388             $htmlID = ($id) ? $id : 'default';
    1389             $values['pagesize'] = $htmlID;
    1390             $items->push(new ArrayData(array(
    1391                 'ID' => $htmlID,
    1392                 'Title' => $title,
    1393                 'Link' => $this->linkWithParams($values),
    1394                 'isCurrent' => ($id == $this->CurrentPageSize),
    1395                 'LinkOrCurrent' => ($id == $this->CurrentPageSize) ? 'current' : 'link',
    1396             )));
    1397         }
    1398         return $items;
    1399     }
    1400
    1401     /**
    1402      * Возвращает ссылку на подсчет кол-ва товаров по заданным фильтрам
    1403      *
    1404      * @return string
    1405      */
    1406     function FilteredProductsCountLink() {
    1407         return $this->Link('filtered_products_count');
    1408     }
    1409
    1410     /**
    1411      * Получение формы фильтра товаров
    1412      *
    1413      * @return Form
    1414      */
    1415     function Filters() {
    1416         if ($this->TotalProductsCount(false) == 0) {
    1417             return false;
    1418         }
    1419
    1420         $fields = new FieldSet();
    1421         $paramFields = array();
    1422         $paramFields[0] = array(); //НЕ Важный параметр
    1423         $paramFields[1] = array(); //Важный параметр
    1424
    1425         $productsSQL = $this->getProductsListWhere();
    1426         $sliderProductsSQL = implode(' AND ', $productsSQL); // для слайдера не сужаем значения
    1427
    1428         $query = new SQLQuery();
    1429         $query->where = $productsSQL;
    1430
    1431         $filters = $this->catalogFilterFields();
    1432         if ($filters && $filters->Count()) {
    1433             $groupFilters = new DataObjectSet(); // групповые фильтры - отдельно
    1434
    1435             // отдельно складываем важные/не важные параметры (в массивы)
    1436             // далее отдельно складывем параметры по полю Sort (в массивы)
    1437             foreach($filters as $filter) {
    1438                 // дорабатываем запрос для сужения фильтров каталога
    1439                 // выкидываем текущий фильтр, чтоб можно было перевыбрать - !!! ДОП.ЗАПРОСЫ !!!
    1440                 // учитываем значения параметров по умолчанию
    1441                 if (Catalog::$reduce_filters && ($filterData = $this->setFiltersDefaultValue($this->cleanParams()))) {
    1442                     $query->where = $productsSQL;
    1443                     unset($filterData[$filter->TechTitle]);
    1444                     $this->updateQueryByProductParamFilters($filterData, $query);
    1445                 }
    1446                 $filteredProductSQL = implode(' AND ', $query->where);
    1447
    1448                 if ($filter->Type == 'text') {
    1449                     $filterField = $this->getTextFilter($filter);
    1450                 }
    1451                 elseif ($filter->Type == 'bool') {
    1452                     $filterField = $this->getBoolFilter($filter);
    1453                 }
    1454                 elseif ($filter->Type == 'slider') {
    1455                     $filterField = $this->getSliderFilter($filter, $sliderProductsSQL);
    1456                 }
    1457                 elseif ($filter->Type == 'list') {
    1458                     $filterField = $this->getListFilter($filter, $filteredProductSQL);
    1459                 }
    1460                 elseif ($filter->Type == 'multiselect') {
    1461                     $filterField = $this->getMultiSelectFilter($filter, $filteredProductSQL);
    1462                 }
    1463                 elseif ($filter->Type == 'boolgroup') {
    1464                     $filterField = false;
    1465                     $groupFilters->push($filter); // групповые пока просто складываем отдельно
    1466                 }
    1467
    1468                 if ($filterField) {
    1469                     if (!isset($paramFields[$filter->Important][$filter->Sort])) {
    1470                         $paramFields[$filter->Important][$filter->Sort] = array();
    1471                     }
    1472                     $paramFields[$filter->Important][$filter->Sort][] = $filterField;
    1473                 }
    1474             }
    1475
    1476             // групповые фильтры
    1477             if ($groupFilters) {
    1478                 // группируем по на званию группы
    1479                 $allGroupFilters = $groupFilters->groupBy('GroupTitle');
    1480                 foreach($allGroupFilters as $key=>$groupFilters) {
    1481                     $this->getGroupFilter($key, $groupFilters, $paramFields); // результат засовываем прямо в $paramFields
    1482                 }
    1483             }
    1484             // обрабатываем структуру данных полей фильтра
    1485             // сначала берем важные параметры с указанными Sort (Sort > 0)
    1486             foreach($paramFields[1] as $sort=>$importantParamFields) {
    1487                 if ($sort > 0) {
    1488                     foreach($importantParamFields as $paramField) {
    1489                         $fields->push($paramField);
    1490                     }
    1491                 }
    1492             }
    1493             // затем берем важные параметры с неуказанными Sort (Sort == 0)
    1494             if (isset($paramFields[1][0])) {
    1495                 foreach($paramFields[1][0] as $paramField) {
    1496                     $fields->push($paramField);
    1497                 }
    1498             }
    1499             // затем берем НЕ важные параметры с указанными Sort (Sort > 0)
    1500             // возможно понадобится вставить разделитель для Зои
    1501             foreach($paramFields[0] as $sort=>$notImportantparamFields) {
    1502                 if ($sort > 0) {
    1503                     foreach($notImportantparamFields as $paramField) {
    1504                         $fields->push($paramField);
    1505                     }
    1506                 }
    1507             }
    1508             // в конце берем НЕ важные параметры с неуказанными Sort (Sort == 0)
    1509             if (isset($paramFields[0][0])) {
    1510                 foreach($paramFields[0][0] as $paramField) {
    1511                     $fields->push($paramField);
    1512                 }
    1513             }
    1514         }
    1515
    1516         if (Catalog::$filter_calc_suitable_products_mode) {
    1517             $this->calculateSuitableProducts($fields, $filters);
    1518         }
    1519
    1520         $this->extend("updateFilterFields", $fields);
    1521         if ($fields->Count() == 0) {
    1522             return false;
    1523         }
    1524
    1525         $fields->First()->addExtraClass('first');
    1526         $actions = new FieldSet(
    1527             $fa1 = new FormAction('filter', _t('Catalog.FILTER_SELECT','Выбрать')),
    1528             $fa2 = new FormAction('filterclear', _t('Catalog.FILTER_CLEAR','Очистить'))
    1529         );
    1530         $fa1->addExtraClass('button__filters');
    1531         $fa2->addExtraClass('button__filters button__second');
    1532
    1533         $form = new Form($this, 'Filters', $fields, $actions);
    1534         $form->disableSecurityToken();
    1535         $form->setFormMethod('GET');
    1536         $form->getValidator()->setJavascriptValidationHandler('none');
    1537         $form->addExtraClass('filters');
    1538         $form->setTemplate('CatalogFilterForm');
    1539
    1540         $this->extend('updateFilterForm', $form);
    1541
    1542         if ($form->Fields()->Count() == 0)
    1543             return false;
    1544
    1545         if ($this->getRequest()) {
    1546             $form->loadDataFrom($this->getRequest()->requestVars());
    1547         }
    1548         return $form;
    1549     }
    1550
    1551     function calculateSuitableProducts($fields, $filters) {
    1552         foreach($fields as $field) {
    1553             if ($field->is_a('DropdownField') || $field->is_a('CheckboxField')) {
    1554                 $name = $field->Name();
    1555
    1556                 $map = array();
    1557                 $useAllFilters = true;
    1558                 if (Catalog::$filter_calc_suitable_products_mode == 'one_filter') {
    1559                     $key = "FilterData_{$this->ID}_{$name}";
    1560                     $map = Catalog::get_cached_data($key);
    1561                     $useAllFilters = false;
    1562                 }
    1563
    1564                 $productsSQL = $this->getProductsListWhere();
    1565                 $productsSQL = implode(' AND ', $productsSQL); // для слайдера не сужаем значения
    1566                 if (!is_array($map) || !count($map)) {
    1567                     $filter = $filters->find('FilterField', $name);
    1568                     $values = Catalog::get_unique_values($filter, $productsSQL);
    1569                     $map = array();
    1570                     foreach($values as $value) {
    1571                         $filterData = array();
    1572                         $where = array();
    1573                         if ($value) {
    1574                             $where[] = "{$name} = '{$value}'";
    1575                             $count = $this->TotalProductsCount($useAllFilters, $where); // добавить сюда $query->where
    1576                             $map[$value] = $count;
    1577                         }
    1578                     }
    1579                     Catalog::set_cached_data($key, $map);
    1580                 }
    1581
    1582                 if ($field->is_a('DropdownField')) {
    1583                     foreach ($map as $v => $c) {
    1584                         $map[$v] = "$v ($c)";
    1585                     }
    1586                     $field->setSource($map);
    1587                 } else if ($field->is_a('CheckboxField')) {
    1588                     $field->setTitle($field->Title(). " ({$map[1]})");
    1589                 }
    1590             }
    1591         }
    1592     }
    1593
    1594     /**
    1595      * выставляем значения фильтров по умолчанию, если они до этого не выставлены
    1596      *
    1597      * @param array $filterData
    1598      *
    1599      * @return array
    1600      */
    1601     function setFiltersDefaultValue($filterData) {
    1602         if ($this->catalogFilterFields() && $this->catalogFilterFields()->Count()) {
    1603             foreach($this->catalogFilterFields() as $filter) {
    1604                 if ($filter->Type == 'list' && trim($filter->DefaultValue)) {
    1605                     if (!isset($filterData[$filter->FilterField]) && $filter->DefaultValue) {
    1606                         $filterData[$filter->FilterField] = $filter->DefaultValue;
    1607                     }
    1608                 }
    1609             }
    1610         }
    1611         return $filterData;
    1612     }
    1613
    1614     /**
    1615      * Активен ли фильтр по товарам в текущем каталоге
    1616      *
    1617      * @return bool
    1618      */
    1619     function IsFilterActive() {
    1620         if ($this->FilterActive) {
    1621             return true;
    1622         }
    1623         $data = $this->cleanParams();
    1624         if (!count($data)) return false;
    1625
    1626         $filters = $this->Filters();
    1627         if (!$filters) {
    1628             return false;
    1629         }
    1630         $filterFields = $filters->Fields();
    1631         foreach ($data as $k => $v) {
    1632             if ($filterFields->fieldByName($k)) {
    1633                 if ($v !== '') return true;
    1634             }
    1635         }
    1636         return false;
    1637     }
    1638
    1639     /**
    1640      * Возвращает число товаров в каталоге
    1641      *
    1642      * @param bool $withFilters - с учетом фильтров или без
    1643      *
    1644      * @return int
    1645      */
    1646     function TotalProductsCount($withFilters=true, $additionalFilters=false) {
    1647         $query = new SQLQuery();
    1648         $query->where = $this->getProductsListWhere();
    1649         if ($withFilters) {
    1650             $filterData = $this->setFiltersDefaultValue($this->cleanParams());
    1651             $this->updateQueryByProductParamFilters($filterData, $query);
    1652         }
    1653         if ($additionalFilters && is_array($additionalFilters) && count($additionalFilters)) {
    1654             foreach($additionalFilters as $withFilter) {
    1655                 $query->where[] = $withFilter; // ???
    1656             }
    1657         }
    1658
    1659         $stage = (Versioned::current_stage() == 'Live' ? '_Live' : '');
    1660         $filteredProductSQL = implode(' AND ', $query->where);
    1661         // RNW
    1662         // Так как метод не расширяем
    1663         // а переделывать весь класс слишком накладно
    1664         // делаем проверку на наличие класса магазинов
    1665         // и модифицируем под него запрос
    1666
    1667         if(in_array("ShopsAdmin", get_declared_classes())){
    1668             $shopID = $this->cleanParams()['shopaddress'];
    1669             $sql = "SELECT count(*) FROM \"SiteTree{$stage}\" JOIN \"Product{$stage}\" ON \"SiteTree{$stage}\".\"ID\" = \"Product{$stage}\".\"ID\" LEFT JOIN \"Product_Shops\" ON \"Product_Shops\".\"ProductID\" = \"SiteTree{$stage}\".\"ID\" WHERE {$filteredProductSQL}";
    1670             if($shopID){
    1671                 $sql .= " AND ShopsItemID = {$shopID}";
    1672             }
    1673         }else{
    1674             $sql = "SELECT count(*) FROM \"SiteTree{$stage}\" JOIN \"Product{$stage}\" ON \"SiteTree{$stage}\".\"ID\" = \"Product{$stage}\".\"ID\" WHERE {$filteredProductSQL}";
    1675         }
    1676
    1677         // return DB::Query("SELECT count(*) FROM \"SiteTree{$stage}\" JOIN \"Product{$stage}\" ON \"SiteTree{$stage}\".\"ID\" = \"Product{$stage}\".\"ID\" WHERE {$filteredProductSQL}")->value();
    1678
    1679         return DB::Query($sql)->value();
    1680     }
    1681
    1682     /**
    1683      * Правильное именование числительного для товаров в рубрике
    1684      *
    1685      * @return string
    1686      */
    1687     function TotalProductsCountTitle() {
    1688         return Convert::number2name($this->TotalProductsCount(), _t('Product.Title_1'), _t('Product.Title_2_4'), _t('Product.Title_5_9'));
    1689     }
    1690
    1691     function init() {
    1692         parent::init();
    1693
    1694         $this->setupCatalogtVar('Sort');
    1695         $this->setupCatalogtVar('View');
    1696         $this->setupCatalogtVar('PageSize');
    1697         $this->data()->SeoCanonicalLink = $this->Link();
    1698     }
    1699
    1700     // page atction handlers
    1701
    1702     function index() {
    1703         $request = $this->getRequest();
    1704         $start = intval($request->getVar('start'));
    1705
    1706         // Установка режима просмотра
    1707         if ($this->setupCatalogtVar('View', $request->requestVar('view'))) {
    1708             if (Director::is_ajax()) $this->UseChangeView = true;
    1709         }
    1710
    1711         // Установка сортировки
    1712         if ($this->setupCatalogtVar('Sort', $request->requestVar('sort'))) {
    1713             if (Director::is_ajax()) $this->UseChangeView = true;
    1714             $start = 0;
    1715         }
    1716
    1717         // Установка кол-ва товаров на странице
    1718         if ($this->setupCatalogtVar('PageSize', $request->requestVar('pagesize'))) {
    1719             if (Director::is_ajax()) $this->UseChangeView = true;
    1720             $start = 0;
    1721         }
    1722
    1723         // на кнопку "показать еще"  в шаблонах требуется другое поведение
    1724         if ($request->requestVar('show_more')) {
    1725             if (Director::is_ajax()) $this->UseShowMore = true;
    1726             $_SERVER['REQUEST_URI'] = str_replace('&show_more=1', '', $_SERVER['REQUEST_URI']);
    1727         }
    1728
    1729         // Pagenation
    1730         $limit = '';
    1731         if ($this->CurrentPageSize == 'all') {
    1732             $this->data()->SeoIsAlternative = true;
    1733         } else {
    1734             $limit = "$start,{$this->CurrentPageSize}";
    1735         }
    1736
    1737         // Если фильтр задействован - то выставляем флаг SeoIsAlternative
    1738         if ($this->IsFilterActive()) {
    1739             $this->data()->SeoIsAlternative = true;
    1740         }
    1741
    1742         $filterData = $this->setFiltersDefaultValue($this->cleanParams());
    1743
    1744         $this->Products = $this->filteredProducts($this->CurrentSort, $filterData, $limit);
    1745         if ($this->hasMethod('setSEOVars')) {
    1746             $this->setSEOVars($this->Products); //Выставляем SEO-переменные (ф-я setSEOVars находится в Webylon Page_Controller)
    1747         }
    1748
    1749         // для изменения url при ajax запросах (требует поддержки в js)
    1750         if (Director::is_ajax()) {
    1751             $this->getResponse()->addHeader('X-Set-Url', $this->linkWithParams());
    1752         }
    1753
    1754         $action = (Director::is_ajax()) ? 'ajax' : 'index';
    1755         return parent::defaultAction($action);
    1756     }
    1757
    1758     /*
    1759      * Получаем количество товаров по текущим фильтрам
    1760      *
    1761      * @return  JSON
    1762      */
    1763     function filtered_products_count() {
    1764         $totalProductsCount = $this->TotalProductsCount();
    1765         $rs = array(
    1766             'ProductCount' => $totalProductsCount,
    1767             'ProductCountTitle' => Convert::number2name($totalProductsCount, _t('Product.Title_1'), _t('Product.Title_2_4'), _t('Product.Title_5_9')),
    1768         );
    1769         return json_encode($rs);
    1770     }
    1771
    1772     /**
    1773      * Обработчик формы - сбрасывает фильтр
    1774      *
    1775      * @param array $data - Данные формы
    1776      * @param Form $form - форма
    1777      *
    1778      * @return HTTP редирект на страницу каталога
    1779      */
    1780     function filterclear($data, $form) {
    1781         // Чистка фильтров (просто убирает GET-параметры)
    1782         if (!Director::is_ajax())
    1783             return $this->redirect($this->Link());
    1784
    1785         if ($form) {
    1786             $this->FilterActive = true;
    1787         }
    1788         return $this->index();
    1789     }
    1790
    1791     /*
    1792      * Обработчик формы - Фильтрация в пределах одной рубрики
    1793      */
    1794     function filter($data, $form=null) {
    1795         if ($form) {
    1796             if (!Director::is_ajax()) {
    1797                 return $this->redirect($this->linkWithParams());
    1798             }
    1799             $this->FilterActive = true;
    1800         }
    1801         return $this->index();
    1802     }
    1803 }
    1804

[Raise a SilverStripe Framework issue/bug](https://github.com/silverstripe/silverstripe-framework/issues/new)
- [Raise a SilverStripe CMS issue/bug](https://github.com/silverstripe/silverstripe-cms/issues/new)
- Please use the Silverstripe Forums to ask development related questions. -