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 Product extends MediawebPage{
  10 
  11     //static $icon = 'cms/images/treeicons/element'; //!!!!
  12     static $can_be_root = false;
  13     static $allowed_children = 'none';
  14     static $default_parent = 'Catalog';
  15 
  16     static $db = array(     
  17         
  18         'Description' => 'Text', // Краткое описание
  19         'Vendor' => 'Varchar', // Производитель
  20         
  21         'Price' => 'CatalogPrice',        
  22         'BasePrice' => 'CatalogPrice', // Обычная цена
  23         'CostPrice' => 'CatalogPrice', // Цена по акции
  24         'Discount' => 'CatalogPrice', // Скидка
  25         
  26         'Available' => 'Boolean', // Флаг доступности
  27         'AllowPurchase' => 'Boolean', // Флаг "можно заказывать"
  28         
  29         'ImportID' => 'Varchar(100)', // ID-импорта, нигде не редактируется и не отображается используется только для импорта
  30         'SKU' => 'Varchar', // Артикул товара
  31 
  32         'Quantity' => 'Int',
  33         
  34         'Weight' => 'Int', // Вес товара в граммах
  35 
  36         'GroupID' => 'Varchar(255)', //Группировка схожих товаров
  37         'GroupTitle' => 'Varchar(255)', //Название товара в блоке группировки, по-умолчанию — MenuTitle
  38     );
  39 
  40     static $defaults = array(
  41         'ShowInMenus' => 1,
  42         'ShowOnlyInTab' => 1,
  43         'Available' => 1,
  44         'AllowPurchase' => 1,
  45         'BasePrice' => 0,
  46         'CostPrice' => 0,
  47     );
  48     
  49     static $casting = array(
  50         'KilosWeight' => 'Decimal',
  51     );
  52 
  53     static $has_one = array(
  54         'Photo' => 'Image',
  55         'ProductVAT' => 'VAT', // Ставка НДС продукта
  56     );
  57 
  58     static $has_many = array(
  59         'ParamValues' => 'ProductParamValue',
  60         'Variations' => 'ProductVariation',
  61     );
  62 
  63     static $indexes = array(        
  64         'ImportID' => true,
  65         'Vendor' => true,
  66     );
  67 
  68     static $summary_fields = array('SKU', 'Title', 'BasePrice', 'CostPrice', 'Vendor', 'ImportID', 'Available', 'AllowPurchase');   
  69 
  70     static $fulltext_fields = array('Title', 'Content', 'Description', 'BasePrice', 'CostPrice', 'Vendor');
  71 
  72     static $searchable_fields = array(
  73         'Title' => 'PartialMatchFilter',
  74         'ImportID' => array('field' => 'TextFieldWithEmptyFlag', 'filter'=>'ExactMatchFilterWithEmpty'),
  75         'SKU' => 'ExactMatchFilter',
  76         'Available' => 'ExactMatchFilter',      
  77         'Price' => array('field'=>'RangeField', 'filter'=>'WithinRangeFilter'), 
  78         'Vendor' => array('field' => 'TextFieldWithEmptyFlag', 'filter'=>'ExactMatchFilterWithEmpty')
  79     );
  80 
  81     // возможные поля для использования в фильтрах каталога
  82     static $possible_filter_fields = array('Title', 'Content', 'Description', 'Price', 'Vendor', 'Available', 'AllowPurchase');
  83     
  84     static function get_possible_filter_fields() {
  85         $rs = array();
  86         foreach(self::$possible_filter_fields as $field) {
  87             $rs[$field] = singleton(Catalog::$subpage_children)->fieldLabel($field);
  88         }
  89         return $rs;
  90     }
  91     
  92     /**
  93      * Позволяет задать поля для фильтра товаров
  94      * Фильтр исопльзуется в админке и при фильтрации товаров в каталоге
  95      * 
  96      * @param array $data 
  97      */
  98     static function set_searchable_fields(array $data) {
  99         self::$searchable_fields = $data;
 100     }
 101 
 102     /**
 103      * Возвращает текущие настроки полей для фильтрации
 104      * 
 105      * @return array
 106      */
 107     static function get_searchable_fields() {
 108         return self::$searchable_fields;
 109     }
 110 
 111     /**
 112      * Настройки сортировки товаров в рубриках
 113      * Формат: служебное_имя => "строка для ORDER BY"
 114      */
 115     private static $sort_options = array('title' => 'Title ASC', 'pricea' => 'Price', 'priced' => 'Price DESC');
 116 
 117     /**
 118      * Изменяет список сортировок товаров. 
 119      * Используется в _config.php для настройки параметров каталога
 120      * 
 121      * @param array $data - новый список сортировок
 122      */
 123     static function set_sort_options(Array $data) {
 124         self::$sort_options = $data;
 125     }
 126 
 127     /**
 128      * Возвращает текущий список сортировок товаров
 129      * 
 130      * @return array - текущий список сортировок
 131      */
 132     static function get_sort_options() {
 133         return self::$sort_options;
 134     }
 135 
 136     /**
 137      * Возвращает выражение order by для выбранной сортировки
 138      *
 139      * @return string
 140      */
 141     static function sort_options_orderby($sort) {
 142         return (array_key_exists($sort, self::$sort_options)) ? self::$sort_options[$sort] : '';
 143     }
 144 
 145     /**
 146      * Возвращает локализованный список сортировок для использования в селектах
 147      * 
 148      * @param bool $addDefault - добавлять ли пункт "по-умолчанию"
 149      * 
 150      * @return array - список для селектов
 151      */
 152     static function sort_options_dropdown_map($addDefault = false) {
 153         $map = array();
 154         if ($addDefault) {
 155             $map[''] = _t('Product.SortOption_default', 'Default');
 156         }
 157         foreach (self::get_sort_options() as $key => $val) {
 158             $map[$key] = _t('Product.SortOption_'.$key, ucfirst($key));
 159         }
 160         return $map;
 161     }
 162         
 163     /** 
 164     * устанавливает поведение при нулевой (0) цене продукта 
 165     * если false, то не добавлять в корзину
 166     * если true, то перенаправлять на 
 167     **/
 168     static $order_empty_price = false;
 169 
 170     static function allow_order_empty_price($val = true) {
 171         self::$order_empty_price = $val;
 172     }
 173 
 174     /**
 175      * Класс кнопки заказа, класс должен реализовать OrderButtonInterface
 176      */
 177     static $order_button_class = 'SimpleOrderButton';
 178 
 179     static function set_order_button_class($value) {
 180         self::$order_button_class = $value;
 181     }
 182 
 183     /**
 184      * Класс кнопки заказа для товаров без цены, класс должен реализовать OrderButtonInterface
 185      */
 186     static $empty_order_button_class = 'SimpleOrderButton';
 187 
 188     static function set_empty_order_button_class($value) {
 189         self::$empty_order_button_class = $value;
 190     }
 191     
 192     // список дополнительных полей для формы простого заказа
 193     protected static $simple_order_additional_fields = array(       
 194     );
 195     
 196     /**
 197      * Получить список дополнительных полей для формы простого заказа
 198      * 
 199      * @return array
 200      */ 
 201     static function get_simple_order_additional_fields() {
 202         return self::$simple_order_additional_fields;
 203     }
 204     
 205     /**
 206      * Добавить дополнительное поле в форму простого заказа
 207      * 
 208      * @param string $name - название
 209      * @param array $params - параметры
 210      */ 
 211     static function add_simple_order_additional_field($name, $params) {
 212         self::$simple_order_additional_fields[$name] = $params;
 213     }
 214     
 215     /**
 216      * Задать дополнительные поля формы простого заказа
 217      *      
 218      * @param array $fields - поля формы
 219      */
 220     static function set_simple_order_additional_fields($fields) {
 221         self::$simple_order_additional_fields = $fields;
 222     }
 223     
 224     function WeightNice() {
 225         if ($this->Weight > 100000) { // Если вес больше 100 кг, то выводим в кг, тип Int
 226             return sprintf(_t("Product.NiceKiloWeight"), $this->dbObject('KilosWeight')->Nice(0));
 227         }
 228         elseif ($this->Weight > 1000) { // Если вес больше 1000 грамм, то выводим в кг, тип Decimal
 229             return sprintf(_t("Product.NiceKiloWeight"), $this->dbObject('KilosWeight')->Nice(1));
 230         }
 231         return sprintf(_t("Product.NiceGrammWeight"), $this->dbObject('Weight')->Nice()); // иначе в граммах красиво, тип Int
 232     }
 233     
 234     function getKilosWeight() {
 235         return $this->Weight / 1000;
 236     }
 237     
 238     /**
 239      * Возвращаем основное фото товара
 240      * Если есть has_one Photo, то его, иначе первое из прикрепленной галереи
 241      * 
 242      * @return Image
 243      */
 244     
 245     function MainPhoto() {
 246         if ($this->PhotoID && $this->Photo()->ID) {
 247             return $this->Photo();
 248         }
 249         if ($this->Photos()->Count()) {
 250             return $this->Photos()->First()->Photo();
 251         }
 252         return false;
 253     }
 254     
 255     /**
 256      * Список всех фото товара
 257      * Если есть has_one Photo, то вставляем его первым
 258      * 
 259      * @return ComponentSet
 260      */
 261     
 262     function AllPhotos() {
 263         $photos = $this->Photos();
 264         if ($this->PhotoID && $this->Photo()->ID) {
 265             $mainPhoto = new MediawebPage_Photo();
 266             $mainPhoto->Caption = $this->Title;
 267             $mainPhoto->MediawebPageID = $this->ID;
 268             $mainPhoto->PhotoID = $this->Photo()->ID;
 269             $photos->unshift($mainPhoto);
 270         }
 271         return $photos;
 272     }
 273 
 274     // --------- ПАРАМЕТРЫ --------
 275     
 276     // Список параметров для вывода в списке товаров 
 277     function ListParamValues() {
 278         $join = "JOIN ProductParam ON ProductParam.ID = ProductParamValue.ProductParamID";
 279         if ($this->Parent()->OwnParams) {
 280             $join .= " JOIN Catalog_EnabledParams ON ProductParam.ID = Catalog_EnabledParams.ProductParamID AND Catalog_EnabledParams.CatalogID = {$this->ParentID}";
 281         }
 282         $values = DataObject::get('ProductParamValue', "ProductID = {$this->ID} AND ProductParam.ShowInList = 1", "ProductParam.Sort", $join);
 283         // группируем разные значения одного параметра (если ProductParam->MultiValues == true) в строку через запятую
 284         $rs = ProductParam::group_values_list($values);
 285         return $rs;
 286     }
 287     
 288     // Список параметров для вывода на карточке товара
 289     function ViewParamValues() {
 290         $join = "JOIN ProductParam ON ProductParam.ID = ProductParamValue.ProductParamID";
 291         if ($this->Parent()->OwnParams) {
 292             $join .= " JOIN Catalog_EnabledParams ON ProductParam.ID = Catalog_EnabledParams.ProductParamID AND Catalog_EnabledParams.CatalogID = {$this->ParentID}";
 293         }
 294         $values = DataObject::get('ProductParamValue', "ProductID = {$this->ID} AND ProductParam.ShowInView = 1", "ProductParam.Sort", $join);
 295         // группируем разные значения одного параметра (если ProductParam->MultiValues == true) в строку через запятую
 296         $rs = ProductParam::group_values_list($values);
 297         return $rs;
 298     }
 299 
 300     // Список параметров для вывода на карточке товара
 301     function ParamValue($title) {
 302         $join = "JOIN ProductParam ON ProductParam.ID = ProductParamValue.ProductParamID";
 303         if ($this->Parent()->OwnParams) {
 304             $join .= " JOIN Catalog_EnabledParams ON ProductParam.ID = Catalog_EnabledParams.ProductParamID AND Catalog_EnabledParams.CatalogID = {$this->ParentID}";
 305         }
 306         $title = Convert::raw2sql($title);
 307         $value = DataObject::get('ProductParamValue', "ProductVariationID = {$this->ID} AND ProductParam.TechTitle = '{$title}' ", "", $join);
 308         if ($value) {
 309             return $value->First();
 310         }
 311         return false;
 312     }
 313 
 314     //Связанные товары
 315     function GroupedProducts() {
 316         if ($this->GroupID) {
 317             return DataObject::get('Product', "GroupID = '".Convert::raw2sql($this->GroupID)."'");  // AND ImportID <> '{$this->ImportID}' - вроде не выкидываем сам товар из списка связанных            
 318         }
 319     }   
 320         
 321     function AvailableTitle() {
 322         return ($this->Available) ? 'Есть' : 'Нет';
 323     }
 324     
 325     public function defaultSearchColumns() {
 326         return implode(',', $this->stat('fulltext_fields'));
 327     }
 328 
 329     public function __construct($record = null, $isSingleton = false) {
 330         if (!isset($record['Price']) && isset($record['ID'])) {
 331             $p = DataObject::get_by_id('Product', $record['ID']);
 332             $record = $p->record;
 333         }
 334         parent::__construct($record, $isSingleton);
 335     }
 336     
 337     
 338     /*--------------- Функции для импорта -------------------*/    
 339     static function import_find($importID) {
 340         return DataObject::get_one('Product', "ImportID = '" . Convert::raw2sql($importID) . "'");
 341     }
 342     
 343     /**
 344      * Список полей, которые могут быть в данных импорта  !!!!! У текущего Product нет поля SKU!!!!!
 345      */  
 346     static $possibleFields = array(
 347         'Title', 'Available', 'AllowPurchase', 'BasePrice', 'CostPrice', 'Vendor', 'SKU', 'Description', 'Content', 'URLSegment', 'MenuTitle', 'MetaTitle', 'MetaDescription', 'MetaKeywords', 'Sort'
 348     );
 349     
 350     /**
 351      * Добавление полей, которые могут быть в данных импорта 
 352      *
 353      * @param array $fields
 354      */
 355     static function addPossibleFields($fields) {
 356         if ($fields)
 357             foreach($fields as $field)
 358                 self::$possibleFields[] = $field;
 359     }
 360 
 361     function getProductAdminLink(){
 362         return "<a target='_blank' href='/admin/show/{$this->ID}'>{$this->Title}</a>";
 363     }
 364     
 365      /**
 366      * Обновляет объект,
 367      * @param $importLog - объект для протоколиорвания импорта (или сама задача), для возможности записать сообщения об ошибках
 368      * @param $data - массив с данными для импорта
 369      * @return bool - флаг можно ли продолжать импорт
 370      */
 371     function importUpdate($importLog, $data) {
 372         if (!$this->importValidate($importLog, $data)) {
 373             return false;
 374         }
 375             
 376         $rs = $this->extend('onBeforeImport', $importLog, $data);
 377         if ($rs && max($rs) == false) return false;
 378         
 379         $this->ImportID = $data['id'];
 380         foreach(self::$possibleFields as $field) {
 381             if (isset($data[$field]))
 382             $this->{$field} = Convert::raw2sql($data[$field]);
 383         }
 384 
 385         //цепляем фотку
 386         if (isset($data['Photo'])) {
 387             $this->PhotoID = ($data['Photo']) ? $data['Photo']->ID : 0;
 388         }   
 389         
 390         //Обрабатываем списочные параметры (если есть)
 391         if (isset($data['params'])) {
 392             foreach($data['params'] as $param) {
 393                 // !!! то есть не проверяем есть ли такое поле и разрешено оно для записи?
 394                 $name = Convert::raw2sql($param->attributes()->name);               
 395                 //Проверяем, есть ли такое поле у Product
 396                 if ($this->db($name)) {                 
 397                     $value = Convert::raw2sql($param);
 398                     $this->{$name} = $value;
 399                 } else {
 400                     $importLog->addLog("У товара не определено поле {$name}!", 'error');
 401                 }
 402             }
 403         }           
 404         //Сначала сохраняем, чтоб потом привязывать изображения и файлы
 405         if (!$this->ID) //не обновляем родителя у уже импортированных товаров  (для возможности ручного изменения структуры каталога на сайте).
 406             $this->ParentID = ($data['ParentID'] !== false) ? $data['ParentID'] : 0;
 407                             
 408         if (isset($data['ParentID']) && $data['ParentID'] === false) { // Если нету родителя, хотя должен быть (не корневой раздел), то не публикуем
 409             $this->write();
 410         } else {
 411             $this->doPublish();
 412         }           
 413         
 414         //Обрабатываем списочные фотографии (если есть)
 415         // !!!!!!
 416         if ($data['Photos']) {
 417             //прикрепляем фотки
 418             $oldPhotos = $this->Photos();
 419             $oldPhotosIDs = $oldPhotos->getIdList(); //сохраняем список ID прикрепленных фоток
 420             $this->Photos()->removeAll();
 421 
 422             foreach($data['Photos'] as $importedImage) {
 423                 if ($mwPhoto = $oldPhotos->find('PhotoID', $importedImage->ID)) {
 424                     unset($oldPhotosIDs[$mwPhoto->ID]); //если фотка осталась, то удаляем ее ID из списка
 425                 } else {
 426                     $mwPhoto = new MediawebPage_Photo();
 427                     $mwPhoto->PhotoID = $importedImage->ID;
 428                 }
 429                 $mwPhoto->MediawebPageID = $this->ID; //Цепляем к Product
 430                 $mwPhoto->Caption = $importedImage->Title;
 431                 $mwPhoto->write();
 432             }
 433 
 434             if (count($oldPhotosIDs) > 0) {
 435                 foreach($oldPhotosIDs as $oldPhotosID) { // все, что осталось в списке - чистим!
 436                     if ($oldPhoto = $oldPhotos->find('ID', $oldPhotosID)) {
 437                         $oldPhoto->delete();
 438                     }
 439                 }
 440             }
 441         }
 442         
 443         //Обрабатываем списочные файлы (если есть)
 444         if ($data['Files']) {
 445             //прикрепляем файлы
 446             $oldFiles = $this->Files();
 447             $oldFilesIDs = $oldFiles->getIdList(); //сохраняем список ID прикрепленных фоток
 448             $this->Files()->removeAll(); 
 449 
 450             foreach($data['Files'] as $importedFile) {
 451                 if ($mwFile = $oldFiles->find('AttachID', $importedFile->ID)) {
 452                     unset($oldFilesIDs[$mwFile->ID]); //если фотка осталась, то удаляем ее ID из списка
 453                 } else {
 454                     $mwFile = new MediawebPage_File();
 455                     $mwFile->AttachID = $importedFile->ID; //Цепляем к Product
 456                 }
 457                 $mwFile->Caption = $importedFile->Title;
 458                 $mwFile->MediawebPageID = $this->ID; //Цепляем к Product
 459                 $mwFile->write();
 460             }
 461 
 462             if (count($oldFilesIDs) > 0) {
 463                 foreach($oldFilesIDs as $oldFilesID) { // все, что осталось в списке - чистим!
 464                     if ($oldFile = $oldFiles->find('ID', $oldFilesID)) {
 465                         $oldFile->delete();
 466                     }
 467                 }
 468             }
 469         }
 470 
 471         //Обрабатываем списочные спец.каталоги (если есть)
 472         if ($data['SpecialCatalogs']) {
 473             $productSpecialCatalogs = array(); //список ID-шники спецкаталогов, в которых изначально был продукт
 474             //складываем в список ID всех спец.каталогов, в которых есть товар
 475             $allSpecialCatalogs = DataObject::get('SpecialCatalog');
 476             if ($allSpecialCatalogs) {
 477                 foreach($allSpecialCatalogs as $specialCatalog) {
 478                     $specialCatalogProducts = $specialCatalog->Products()->getIdList();
 479                     if (isset($specialCatalogProducts[$this->ID])) {
 480                         $productSpecialCatalogs[$specialCatalog->ID] = $specialCatalog->ID;
 481                     }
 482                 }
 483             }
 484 
 485             //Добавляем товар в указанные спец.каталоги
 486             foreach($data['SpecialCatalogs'] as $specialCatalog) {
 487                 if (isset($productSpecialCatalogs[$specialCatalog->ID])) { //если товар уже есто в новом спец.каталоге
 488                     unset($productSpecialCatalogs[$specialCatalog->ID]); //то удаляем ID спец.каталога из списка
 489                 } else {
 490                     $specialCatalog->Products()->add($this); // иначе добавляем товар в спец.каталог
 491                 }
 492             }
 493 
 494             if (count($productSpecialCatalogs)) {
 495                 foreach($productSpecialCatalogs as $productSpecialCatalog) { //чистим все, что осталось в списке
 496                     if ($oldSpecialCatalog = $allSpecialCatalogs->find('ID', $productSpecialCatalog)) {
 497                         $oldSpecialCatalog->Products()->remove($this);
 498                     }
 499                 }
 500             }
 501         }
 502         $this->extend('onAfterImport');
 503         return true;
 504     }
 505     
 506     static function import_parse_bool_value($value) {       
 507         if (in_array($rs, array('true', '1'))) {
 508             return 1;
 509         }
 510         if (in_array($rs, array('false', '0'))) {
 511             return 0;
 512         }
 513         return null;
 514     }
 515     
 516     /**
 517      * Проверяет данные полей объекта на соответствие типам
 518      * @param $importLog - объект для протоколиорвания импорта (или сама задача), для возможности записать сообщения об ошибках     
 519      * @return bool - флаг можно ли продолжать импорт
 520      */
 521     function importValidate($importLog, & $data) {
 522         // !!! по идее Title обязательно отлько для новых товаров
 523         if ((!$this->Title) && (!isset($data['Title']) || trim($data['Title']) == '')) { // если у каталога нет Title и Title нет в импорте, то ругаемся
 524             $importLog->addLog("Товар с id='{$data['id']}' не имеет названия!", 'error');
 525             return false;           
 526         }       
 527         if (isset($data['Title']) && trim($data['Title']) == '') { // если тег <Title> задан, но пустой, то ругаемся
 528             $importLog->addLog("Товар с id='{$data['id']}' не имеет названия!", 'error');
 529             return false;           
 530         }   
 531         if (isset($data['Available'])) {
 532             $rs = self::import_parse_bool_value($data['Available']);
 533             if ($rs === null) {
 534                 $importLog->addLog(sprintf(_t('Product.ImportBadBoolValue'), "Наличие товара на складе", $data['Title']), 'warning');
 535             } else {
 536                 $data['Available'] = $rs; 
 537             }           
 538         }
 539         if (isset($data['AllowPurchase'])) {
 540             $rs = self::import_parse_bool_value($data['AllowPurchase']);
 541             if ($rs === null) {
 542                 $importLog->addLog(sprintf(_t('Product.ImportBadBoolValue'), "Разрешено покупать", $data['Title']), 'warning');
 543             } else {
 544                 $data['AllowPurchase'] = $rs; 
 545             }           
 546         }
 547         if (isset($data['BasePrice']) && !is_numeric($data['BasePrice'])) {
 548             $importLog->addLog("Параметр 'Цена' товара {$data['Title']} должен быть числовым!", 'error');
 549             return false;
 550         }   
 551         if (isset($data['CostPrice']) && !is_numeric($data['CostPrice'])) {
 552             $importLog->addLog("Параметр 'Цена по акции' товара {$data['Title']} должен быть числовым!", 'error');
 553             $data['CostPrice'] = 0;
 554         }       
 555         if (isset($data['Sort']) && $data['Sort'] != (int)$data['Sort']) {
 556             $importLog->addLog("Параметр Sort товара {$data['Title']} не является целым числом!", 'warning');
 557             $data['Sort'] = 0; 
 558         }
 559         
 560         $rs = $this->extend('importValidate', $importLog, $data);       
 561         if ($rs && max($rs) == false) return false;
 562         
 563         return true;
 564     }
 565     
 566      /**
 567      * Выполняет удаление всех объектов перед импортом
 568      */
 569     function importClearAll($importLog) {
 570         $oldMode = Versioned::get_reading_mode();
 571         
 572         Versioned::reading_stage('Stage');
 573         $products = DataObject::get('Product');
 574         if ($products)
 575             foreach($products as $product) {
 576                 $product->doUnpublish();
 577                 $product->delete();
 578             }
 579             
 580         Versioned::reading_stage('Live');
 581         $products = DataObject::get('Product');
 582         if ($products)
 583             foreach($products as $product) {
 584                 $product->doUnpublish();
 585                 $product->delete();
 586             }
 587             
 588         Versioned::set_reading_mode($oldMode);
 589     }       
 590     /*--------------- Конец функций для импорта -------------------*/
 591     
 592         
 593 
 594     public function getCMSFields() {
 595         SiteTree::disableCMSFieldsExtensions();
 596         $fields = parent::getCMSFields();
 597         SiteTree::enableCMSFieldsExtensions();
 598         
 599         $fields->replaceField('ParentType', new HiddenField('ParentType', '', 'subpage'));
 600         
 601         $fields->replaceField('Title', new TextField('Title', _t('Product.db_Title', 'Наименование')));
 602         
 603         if ($allVATs = DataObject::get('VAT')) {
 604             $fields->addFieldToTab('Root.Content.Main', new DropdownField("ProductVATID", $this->fieldLabel('ProductVAT'), $allVATs->map('ID', 'Title', _t('VAT.SelectVAT'))));
 605         }
 606         
 607         // фильтруем каталоги
 608         // !!! сейчас показывается только куст текущего каталога
 609         $cat = $this->Parent();
 610         while ($cat->Parent() && $cat->Parent()->ClassName == self::$default_parent) {
 611             $cat = $cat->Parent();
 612         }
 613         $parentIDField = new TreeDropdownField('ParentID', $this->fieldLabel('ParentID'), 'SiteTree');
 614         $parentIDField->setTreeBaseID($cat->ParentID);
 615         $parentIDField->setFilterFunction(create_function('$node', 'return ($node->ClassName == "'.self::$default_parent.'");')); //  || $node->ClassName == "StartCatalog"
 616         
 617         $fields->replaceField('ParentID', $parentIDField);
 618 
 619         $fields->addFieldToTab('Root.Content.Main', new NumericField('BasePrice', $this->fieldLabel('BasePrice')));
 620         $fields->addFieldToTab('Root.Content.Main', new NumericField('CostPrice', $this->fieldLabel('CostPrice')));
 621 
 622         $fields->addFieldToTab('Root.Content.Main', new CheckboxField('Available', $this->fieldLabel('Available')));
 623         $fields->addFieldToTab('Root.Content.Main', new CheckboxField('AllowPurchase', $this->fieldLabel('AllowPurchase')));
 624         
 625         $fields->addFieldToTab('Root.Content.Main', $imgField = new ImageField('Photo', $this->fieldLabel('Photo')));
 626         if ($folder = $this->getAssociatedFolder()) {
 627             $folderPath = substr_replace(str_replace(ASSETS_DIR.'/', '', $folder->Filename), "", -1);
 628             $imgField->setFolderName($folderPath);
 629         }
 630         
 631         $fields->addFieldToTab('Root.Content.Main', new TextField('Vendor', $this->fieldLabel('Vendor')));
 632         $fields->addFieldToTab('Root.Content.Main', new TextField('ImportID', $this->fieldLabel('ImportID')));
 633         $fields->addFieldToTab('Root.Content.Main', new TextField('SKU', $this->fieldLabel('SKU')));
 634         $fields->addFieldToTab('Root.Content.Main', new TextField('GroupID', $this->fieldLabel('GroupID')));
 635         
 636         if ($allVATs = DataObject::get('VAT')) {
 637             $fields->addFieldToTab('Root.Content.Main', new DropdownField("ProductVATID", $this->fieldLabel('ProductVAT'), $allVATs->map('ID', 'Title', _t('VAT.SelectVAT'))));
 638         }
 639         
 640         $fields->addFieldToTab('Root.Content.Main', new TextareaField('Description', $this->fieldLabel('Description')));
 641         $fields->addFieldToTab('Root.Content.Main', new TextField('Quantity', $this->fieldLabel('Quantity')));
 642         $fields->addFieldToTab('Root.Content.Main', new NumericField('Weight', $this->fieldLabel('Weight')));
 643         $fields->addFieldToTab('Root.Content.Main', new TextField('GroupID', $this->fieldLabel('GroupID')));
 644 
 645         if (HtmlEditorConfig::get_active() != 'cms') {
 646             HtmlEditorConfig::set_active('cms');
 647         }
 648         
 649         $fields->addFieldToTab('Root.Content', new Tab("FullContent", $this->fieldLabel('Content')), 'Metadata');
 650         $fields->addFieldToTab('Root.Content.FullContent', $fields->dataFieldByName('Content'));
 651         
 652         if (Catalog::$use_additional_params) {
 653             //параметры товаров
 654             if ($this->Parent() && $this->Parent()->hasMethod('Params') && ($categoryParams = $this->Parent()->getNonVariationCatalogParams()) && $categoryParams->Count()) {
 655                 $tab = $fields->findOrMakeTab('Root.Content.Params', 'Параметры');             
 656                 $paramValues = new DataObjectSet();
 657                 foreach($categoryParams as $categoryParam) {
 658                     if ($categoryParam->Type == 'bool') {
 659                         $tab->push(new ProductParamValue_BoolValueField('ParamValues', $categoryParam, $this->ID));
 660                     } else {
 661                         if ($categoryParam->MultiValues) {
 662                             if ($categoryParam->PossibleValuesList()) {
 663                                 $tab->push(new ProductParamValue_MultiValueSetField('ParamValues', $categoryParam, $this->ID));
 664                             } else {
 665                                 $tab->push(new LiteralField('ParamTitle', "<h4>{$categoryParam->Title}</h4>"));
 666                                 $rate = new ProductParamValue();
 667                                 $fieldList = array(                             
 668                                     "Value" => $rate->fieldLabel('Value'),                                  
 669                                 );  
 670                                 $fieldTypes = array(                                
 671                                     'Value' => 'TextField',             
 672                                 );
 673                                 // полю добавили суффикс _$categoryParam->ID, чтобы выводить несколько списков параметров
 674                                 $ctf = new TableField("ParamValues_{$categoryParam->ID}", "ProductParamValue", $fieldList, $fieldTypes, 'ProductParamID', $categoryParam->ID);
 675                                 $ctf->setExtraData(array('ProductID' => $this->ID)); // чтобы сохранялись новые значения
 676                                 $params = new DataObjectSet();
 677                                 if ($allParamValues = DataObject::get('ProductParamValue', "ProductParamID = {$categoryParam->ID} AND ProductID = {$this->ID}")) {
 678                                     $params = $allParamValues;
 679                                 }
 680                                 $ctf->setCustomSourceItems($params);
 681                                 $tab->push($ctf);
 682                             }
 683                         } else {
 684                             if ($categoryParam->PossibleValuesList()) {
 685                                 $tab->push(new ProductParamValue_MultiValueField('ParamValues', $categoryParam, $this->ID));
 686                             } else {
 687                                 $tab->push(new ProductParamValue_ValueField('ParamValues', $categoryParam, $this->ID));
 688                             }
 689                         }
 690                     }
 691                     //$paramValues->push($t);
 692                 }
 693                 
 694                 $rate = new ProductParamValue();
 695                 $fieldList = array(
 696                     "ID" => $rate->fieldLabel('ID'),
 697                     "ParamTitle" => $rate->fieldLabel('Title'),
 698                     "Value" => $rate->fieldLabel('Value'),
 699                     
 700                 );          
 701                 $fieldTypes = array(
 702                     'ID' => 'ReadonlyField',
 703                     'ParamTitle' => 'ReadonlyField',
 704                     'Value' => 'TextField',             
 705                 );
 706                 $tablefield = new TableField("ParamValues", "ProductParamValue", $fieldList, $fieldTypes);
 707                 $tablefield->setCustomSourceItems($paramValues);
 708             }
 709         }
 710 
 711         // вариации
 712         if (Catalog::$use_variations) {
 713             if ($this->Parent() && $this->Parent()->hasMethod('Params') && ($variationParams = $this->Parent()->getVariationCatalogParams())) {
 714                 $tab = $fields->findOrMakeTab('Root.Content.Variations', 'Вариации');
 715                 $ctf = $this->getVariationsTable();
 716                 $ctf->setPopupSize(1000, 600);
 717                 $tab->push($ctf);
 718             }
 719         }
 720         $this->extend('updateCMSFields', $fields);
 721         
 722         $this->extend('hideCMSFields', $fields); // extend для скрытия полей из админки
 723         return $fields;
 724     }
 725     
 726     function getVariationsTable() {
 727         $singleton = singleton('ProductVariation');
 728         $query = $singleton->buildSQL("\"ProductID\" = '{$this->ID}'");
 729         $variations = $singleton->buildDataObjectSet($query->execute());
 730         $filter = $variations ? "\"ID\" IN ('" . implode("','", $variations->column('ID')) . "')" : "\"ID\" < '0'";
 731 
 732         $summaryfields= $singleton->summaryFields();
 733         
 734         if ($this->Parent() && $this->Parent()->hasMethod('Params') && ($variationParams = $this->Parent()->getVariationCatalogParams())) {
 735             foreach($variationParams as $attribute){
 736                 $summaryfields["AttributeProxy.Val".$attribute->TechTitle] = $attribute->Title; //!! понять, как получается значение !!              
 737             }
 738         }
 739         $tableField = new HasManyComplexTableField(
 740             $this,
 741             'Variations',
 742             'ProductVariation',
 743             $summaryfields,
 744             null,
 745             $filter
 746         );
 747         $tableField->Markable = 0;
 748         $tableField->setRelationAutoSetting(true);
 749         return $tableField;
 750     }
 751 
 752     public function getCMSActions() {
 753         Requirements::javascript('catalog/javascript/ProductAction.js');
 754         $fields = parent::getCMSActions();
 755         $fields->push(new HiddenField('backLink', '', $this->Parent()->ID));
 756         $fields->insertFirst(new FormAction('goBack', _t('Product.BACKBUTTON', 'Back')));
 757         return $fields;
 758     }
 759 
 760     public function populateDefaults() {
 761         parent::populateDefaults();
 762 
 763         $sc = SiteConfig::current_site_config();
 764         $this->ShowInMenus = ($sc->ProductShowInTree) ? 1 : 0;
 765         $this->ShowOnlyInTab = ($sc->ProductShowInTree) ? 0 : 1;
 766     }
 767 
 768     function onBeforeWrite() {
 769         parent::onBeforeWrite();
 770         // если вариации есть, то высчитываем цену основного товара, как минимальную по вариациям
 771         if (Catalog::$use_variations && $this->Variations()->exists()) {
 772             if (($allVariations = $this->AllVariations()) && $allVariations->Count()) {
 773                 $allVariations->sort('VariationPrice');
 774                 $this->BasePrice = $allVariations->First()->getBasePrice();
 775                 $this->CostPrice = $allVariations->First()->getCostPrice();
 776             } else {
 777                 $this->BasePrice = 0;
 778                 $this->CostPrice = 0;
 779             }
 780             $this->Price = ($this->CostPrice > 0) ? $this->CostPrice : $this->BasePrice;
 781         } else {
 782             if ($this->isChanged('BasePrice') || $this->isChanged('CostPrice')) {
 783                 $this->Price = ($this->CostPrice > 0) ? $this->CostPrice : $this->BasePrice;
 784             }
 785         }
 786         $this->Discount = (($this->CostPrice > 0) ? ($this->BasePrice - $this->CostPrice) : 0);
 787         
 788         $sc = SiteConfig::current_site_config();
 789         if ($sc->ProductShowInTree) {
 790             $this->ShowOnlyInTab = 0;
 791         }
 792         else {
 793             $this->ShowInMenus = 0;
 794         }       
 795     }
 796     
 797     function onAfterDelete() {
 798         parent::onAfterDelete();
 799         
 800         if ($this->IsDeletedFromStage && !$this->ExistsOnLive) {
 801             DB::Query("DELETE FROM `ProductParamValue` WHERE ProductID = {$this->ID}");
 802         }
 803     }
 804 
 805     function getVAT(){
 806         if ($this->ProductVATID && ($vat = $this->ProductVAT())) {
 807             return $vat;
 808         }
 809         if ($this->ParentID && ($parent = $this->Parent())) {
 810             return $parent->getVAT();
 811         }
 812         return false;
 813     }
 814 
 815     /**
 816      * Список виртуальных товаров связанных с текущим
 817      * 
 818      * @return DataObjectSet
 819      */
 820     function VirtualPages() {
 821         if (!$this->ID)
 822             return null;
 823         if (class_exists('Subsite')) {
 824             return Subsite::get_from_all_subsites('VirtualProduct', "\"CopyContentFromID\" = " . (int) $this->ID);
 825         } else {
 826             return DataObject::get('VirtualProduct', "\"CopyContentFromID\" = " . (int) $this->ID);
 827         }
 828     }
 829 
 830     public function canCreate($member = null) {
 831         return (DataObject::get_one('Catalog')) ? parent::canCreate($member) : false;
 832     }
 833 
 834     function fieldLabels($includerelations = true) {
 835         $labels = parent::fieldLabels($includerelations);
 836         $labels['Title'] = _t('Product.db_Title', 'Title');
 837         return $labels;
 838     }
 839 
 840     /**
 841      * Возвращает класс для кнопки заказа товара в зависимости от цены и значений Product::$empty_order_button_class и Product::$order_button_class
 842      * 
 843      * @return string
 844      */
 845     function orderButtonClass() {
 846         if (!$this->AllowPurchase) {
 847             return false;
 848         }
 849         if ($this->Price == 0) {
 850             if (self::$order_empty_price)
 851                 return self::$empty_order_button_class;
 852             return false;
 853         }
 854         $class = self::$order_button_class;
 855         $this->extend('updateOrderButtonClass', $class);
 856         return $class;
 857     }
 858 
 859     /**
 860      * Возвращает объект для генерации кнопки заказа
 861      * 
 862      * @return OrderButtonInterface
 863      */
 864     
 865     function OrderButton() {        
 866         $buttonClass = $this->orderButtonClass();
 867         if (!$buttonClass) 
 868             return false;
 869         return new $buttonClass($this->ID);
 870     }
 871 
 872     /**
 873      * Возвращает html для кнопки заказа 1 товара
 874      * 
 875      * @return string
 876      */
 877     function OrderButtonOne() {
 878         if ($button = $this->OrderButton())
 879             return $button->One();
 880         return false;
 881     }
 882 
 883     /**
 884      * Возвращает html для кнопки заказа товара с оплем для кол-ва
 885      * 
 886      * @return string
 887      */
 888     function OrderButtonWithNum() {
 889         if ($button = $this->OrderButton()) {           
 890             return $button->WithNum();
 891         }
 892         return false;
 893     }
 894 
 895     /**
 896      * возвращает цену товара без скидок
 897      * 
 898      * @return CatalogPrice
 899      */
 900 
 901     function getRealPrice() {
 902         $price = $this->Price;
 903         $this->extend('updatePrice', $price);
 904         return $price;
 905     }
 906 
 907     /**
 908      * Возвращает центу товара с учетом скидок
 909      * 
 910      * @param bool $onlyreal - вернуть только 1 цену
 911      * 
 912      * @return mixed
 913      */
 914     function Price($onlyreal=false) {
 915         // Значения по умолчанию
 916         $real = $this->getRealPrice();
 917         $client = $real;
 918         $delta = 0;
 919         $percent = 0;
 920         $decimal = 2;
 921         
 922         if ($this->CostPrice == 0) {
 923             // персональная скидка (скидки на группу учтены там же)
 924             $member = Member::currentUser();
 925             if ($member){
 926                 if ($member->hasMethod('getPersonalDiscount') && $percent < $member->getPersonalDiscount()) {
 927                     $percent = $member->getPersonalDiscount();
 928                 }
 929             }
 930             
 931             $this->extend('updateDiscount', $percent);
 932         }
 933 
 934         $sc = SiteConfig::current_site_config();
 935         $ceil = $sc->CatalogDiscountCeil;
 936         
 937         if ($ceil) {
 938             $real = ceil($real);
 939             $client = ceil($client);
 940             $decimal = 0;
 941         }
 942 
 943         if ($percent) {
 944             $delta = ($ceil) ? ceil(($real / 100) * $percent) : round(($real / 100) * $percent, 2);
 945             $client = $real - $delta;
 946         }
 947         
 948         if ($onlyreal !== false) {
 949             return $client;
 950         }
 951         
 952         return new ArrayData(array(
 953             'Real' => DBField::create('CatalogPrice', $real),
 954             'Client' => DBField::create('CatalogPrice', $client),
 955             'Delta' => DBField::create('CatalogPrice', $delta),
 956             'Percent' => $percent,
 957             'Discount' => ($percent > 0) ? 1 : 0,
 958             'Currency' => $sc->CatalogCurrency,
 959         ));
 960     }
 961     
 962     // Старая цена товара
 963     function OldPrice() {
 964         if ($this->HasDiscount()) {
 965             return $this->BasePrice;
 966         }
 967         return 0;
 968     }
 969 
 970     function HasDiscount() {
 971         return ($this->CostPrice > 0);
 972     }
 973 
 974     function Currency() {
 975         return SiteConfig::current_site_config()->CatalogCurrency;
 976     }
 977 
 978     function getOrderItem() {
 979         //!!! TODO генерировать OrderItem самим
 980     }
 981     
 982     // Доступные вариации товара
 983     function AvailableVariations() {
 984         return $this->Variations("Available = 1");
 985     }
 986     
 987     // Разрешенные для покупки вариации товара
 988     function AllowPurchaseVariations() {
 989         return $this->Variations("AllowPurchase = 1");
 990     }
 991         
 992     /**
 993      * Вариации товара
 994      * В зависимости от настроек либо все вариации, либо только доступные (Available = 1)
 995      * 
 996      * @return ComponentSet
 997      */ 
 998     function AllVariations() {      
 999         if (SiteConfig::current_site_config()->UseOnlyAllowPurchaseVariations) {
1000             return $this->AllowPurchaseVariations();
1001         }
1002         return $this->Variations();
1003     }
1004     
1005     // цена вариации с минимальной ценой
1006     function MinPriceVariation() {
1007         if ($allVariations = $this->AllVariations()) {
1008             $allVariations->sort('VariationPrice');
1009             return $allVariations->First()->Price();
1010         }
1011     }
1012     
1013     // цена вариации с максимальной ценой
1014     function MaxPriceVariation() {
1015         if (($allVariations = $this->AllVariations()) && ($allVariations->Count() > 1)) { // только если больше одной вариации 
1016             $allVariations->sort('VariationPrice');
1017             return $allVariations->Last()->Price();
1018         }
1019         return false;
1020     }
1021     
1022     function HasSomePrices() {
1023         if ($this->MinPriceVariation() && $this->MaxPriceVariation() && ($this->MinPriceVariation()->Real != $this->MaxPriceVariation()->Real)) {
1024             return true;
1025         }
1026         return false;
1027     }
1028     
1029     /**
1030      * Селектор для выбора вариации
1031      * 
1032      * 
1033      * @return html
1034      */
1035     
1036     function VariationsSelector() {
1037         return $this->customise(array(
1038             'Variations' => $this->VariationsParamValues(),
1039         ))->renderWith('VariationsSelector');
1040     }
1041 
1042     // список уникальных значений по каждому параметру вариаций   
1043     function VariationsParamValues() {
1044         return self::combine_variations($this->AllVariations());
1045     }
1046     
1047     // возвращаем список уникальных значений по каждому параметру вариаций
1048     // используем для построения списков выбора значений параметров в карточке товара
1049     static function combine_variations($variations) {
1050         $rs = array();
1051         foreach($variations as $variation) {
1052             foreach($variation->ParamValues() as $paramValue) {
1053                 if (!isset($rs[$paramValue->ProductParamID])) {
1054                     $rs[$paramValue->ProductParamID] = new DataObjectSet();
1055                 }
1056                 $t = $rs[$paramValue->ProductParamID];
1057                 if (!$t->find('Value', $paramValue->Value)) {
1058                     $t->push($paramValue);
1059                 }
1060                 $t->sort('Value');
1061             }
1062         }
1063         $list = new DataObjectSet();
1064         foreach($rs as $paramID=>$paramValues) {
1065             if ($paramValues && $paramValues->Count()) {
1066                 $list->push(new ArrayData(array(
1067                     'Param' => $paramValues->First()->ProductParam(),
1068                     'Values' => $paramValues
1069                 )));
1070             }
1071         }
1072         return $list;
1073     }
1074     
1075     function VariationOrderButtonLink($ID='') {
1076         return $this->Link("variation_order_button/{$ID}");
1077     }
1078     
1079     function PurchaseStatus() {
1080         if (Catalog::$use_variations && $this->Variations()) {
1081             return 'HasVariations';
1082         }
1083         if (!$this->Available && !$this->AllowPurchase) {
1084             return 'NoOrder';
1085         }
1086         if (!$this->Available && $this->AllowPurchase) {
1087             return 'PreOrder';
1088         }
1089         if ($this->Available && !$this->AllowPurchase) {
1090             return 'NoOrder';
1091         }
1092         if ($this->Available && $this->AllowPurchase) {
1093             return 'InStock';
1094         }
1095         return 'NoOrder';
1096     }
1097     
1098     function PurchaseStatusTitle() {
1099         return _t('Product.PurchaseStatus_' . $this->PurchaseStatus());
1100     }
1101     
1102     function canOrder() {
1103         if (in_array($this->PurchaseStatus(), array('PreOrder', 'InStock'))) {
1104             return true;
1105         }
1106         return false;
1107     }
1108 }
1109 
1110 class Product_Controller extends Page_Controller {
1111     private $siblings = null;
1112     private $next = null;
1113     private $prev = null;
1114 
1115     /**
1116      * Возвращает "братьев" товара
1117      * 
1118      * @return DataObjectSet
1119      */
1120     function Siblings() {
1121         if (!isset($this->siblings)) {
1122             $catalog = new Catalog_Controller($this->Parent());
1123             $catalog->init();
1124             $this->siblings = $this->Parent()->filteredProducts($catalog->CurrentSort, $catalog->FilterData);
1125         }
1126         return $this->siblings;
1127     }
1128 
1129     /**
1130      * Возвращает следующий товар в списке
1131      * 
1132      * @return Product
1133      */
1134     function NextProduct() {
1135         if (!isset($this->next)) {
1136             $list = $this->Siblings()->toArray();
1137             while ($row = array_shift($list)) {
1138                 if ($row->ID == $this->dataRecord->ID) break;
1139             }
1140             $this->next = array_shift($list);
1141         }
1142         return $this->next;
1143     }
1144 
1145     /**
1146      * Возвращает предыдущий товар в списке
1147      * 
1148      * @return Product
1149      */
1150     function PrevProduct() {
1151         if (!isset($this->prev)) {
1152             $this->prev = false;
1153             $list = $this->Siblings()->toArray();
1154             $prev = array_shift($list);
1155             while ($row = array_shift($list)) {
1156                 if ($row->ID == $this->dataRecord->ID) {
1157                     $this->prev = $prev;
1158                     break;
1159                 }
1160                 $prev = $row;
1161             }
1162         }
1163         return $this->prev;
1164     }   
1165     
1166     /**
1167      * Возвращает список вариаций товара
1168      * 
1169      * @return json
1170      */
1171     function product_variations() {
1172         if ($this->Variations()->exists()) {
1173             $variations = $this->Variations()->data();
1174             return json_encode(array('Variations' => $variations));
1175         }
1176     }
1177     
1178     function variation_order_button($request) {
1179         if ($variation = DataObject::get_by_id('ProductVariation', (int)$request->param('ID'))) {
1180             return $this->customise(array(
1181                 'Variation' => $variation
1182             ));
1183         }
1184         return $this->httpError(404);
1185     }
1186 }
1187 
[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