Webylon 3.2 API Docs
  • Package
  • Class
  • Tree
  • Deprecated
  • Download
Version: current
  • 3.2
  • 3.1

Packages

  • 1c
    • exchange
      • catalog
  • auth
  • Booking
  • building
    • company
  • cart
    • shipping
    • steppedcheckout
  • Catalog
    • monument
  • cms
    • assets
    • batchaction
    • batchactions
    • bulkloading
    • comments
    • content
    • core
    • export
    • newsletter
    • publishers
    • reports
    • security
    • tasks
  • Dashboard
  • DataObjectManager
  • event
  • faq
  • forms
    • actions
    • core
    • fields-basic
    • fields-dataless
    • fields-datetime
    • fields-files
    • fields-formatted
    • fields-formattedinput
    • fields-relational
    • fields-structural
    • transformations
    • validators
  • googlesitemaps
  • guestbook
  • installer
  • newsletter
  • None
  • photo
    • gallery
  • PHP
  • polls
  • recaptcha
  • sapphire
    • api
    • bulkloading
    • control
    • core
    • cron
    • dev
    • email
    • fields-formattedinput
    • filesystem
    • formatters
    • forms
    • i18n
    • integration
    • misc
    • model
    • parsers
    • search
    • security
    • tasks
    • testing
    • tools
    • validation
    • view
    • widgets
  • seo
    • open
      • graph
  • sfDateTimePlugin
  • spamprotection
  • stealth
    • captha
  • subsites
  • userform
    • pagetypes
  • userforms
  • webylon
  • widgets

Classes

  • Catalog
  • CatalogAdmin
  • CatalogCMSActionDecorator
  • CatalogMemberDecorator
  • CatalogPrice
  • CMSSiteTreeFilter_Catalog
  • Monument
  • MonumentCatalog
  • MonumentForm
  • Orders1CExchange_SiteConfig
  • PaymentType
  • Product
  • ProductCatalogSiteConfig
  • ProductSearchPage
  • SimpleOrderButton
  • SimpleOrderData
  • SimpleOrderForm
  • SimpleOrderPage
  • StartCatalog
  • VirtualProduct

Interfaces

  • OrderButtonInterface
   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. -
Webylon 3.2 API Docs API documentation generated by ApiGen 2.8.0