Files
ave-cms/adminx/modules/Catalog/Model.php
T
2026-07-27 12:58:44 +03:00

1098 lines
49 KiB
PHP

<?php
/*
|--------------------------------------------------------------------------------------
| AVE.cms
|--------------------------------------------------------------------------------------
| @package AVE.cms
| @file adminx/modules/Catalog/Model.php
| @author AVE.cms <support@ave-cms.ru>
| @copyright 2007-2026 (c) AVE.cms
| @link https://ave-cms.ru
| @version 3.3
*/
namespace App\Adminx\Catalog;
defined('BASEPATH') || die('Direct access to this location is not allowed.');
use DB;
use App\Common\AdminLocation;
use App\Common\Cache;
use App\Common\CacheKey;
use App\Common\DatabaseSchema;
use App\Common\FileCacheInvalidator;
use App\Common\ModuleManager;
use App\Content\CatalogTables;
use App\Content\ContentTables;
use App\Content\Fields\FieldValueCodec;
use App\Content\Requests\RequestConditionValue;
use App\Helpers\Json;
use App\Adminx\Rubrics\Model as RubricsModel;
use App\Adminx\Documents\Model as DocumentsModel;
use App\Adminx\Packages\Products\ProductIndexer;
class Model
{
public static function settingsTable() { return CatalogTables::table('module_catalog_settings'); }
public static function itemsTable() { return CatalogTables::table('module_catalog_items'); }
public static function fieldsTable() { return ContentTables::table('rubric_fields'); }
public static function groupsTable() { return ContentTables::table('rubric_fields_group'); }
public static function rubricsTable() { return ContentTables::table('rubrics'); }
public static function documentsTable() { return ContentTables::table('documents'); }
public static function catalogs()
{
$rows = DB::query(
'SELECT f.Id AS field_id, f.rubric_id, f.rubric_field_title, f.rubric_field_alias,'
. ' r.rubric_title, s.*, COUNT(i.id) AS items_count,'
. ' SUM(CASE WHEN i.status = 1 THEN 1 ELSE 0 END) AS active_count'
. ' FROM ' . self::fieldsTable() . ' f'
. ' INNER JOIN ' . self::rubricsTable() . ' r ON r.Id = f.rubric_id'
. ' LEFT JOIN ' . self::settingsTable() . ' s ON s.rubric_id = f.rubric_id AND s.field_id = f.Id'
. ' LEFT JOIN ' . self::itemsTable() . ' i ON i.rubric_id = f.rubric_id AND i.field_id = f.Id'
. " WHERE f.rubric_field_type = 'catalog'"
. ' GROUP BY f.Id ORDER BY r.rubric_title ASC, f.rubric_field_position ASC, f.Id ASC'
)->getAll();
$out = array();
foreach ($rows as $row) {
$out[] = self::catalogRow($row);
}
return $out;
}
public static function hasCommerceCatalogs()
{
return (int) DB::query("SELECT COUNT(*) FROM " . self::settingsTable() . " WHERE purpose='commerce'")->getValue() > 0;
}
public static function createCatalog(array $input)
{
$rubricId = isset($input['rubric_id']) ? (int) $input['rubric_id'] : 0;
$title = trim(isset($input['title']) ? (string) $input['title'] : '');
$alias = strtolower(trim(isset($input['alias']) ? (string) $input['alias'] : ''));
$alias = trim(preg_replace('/[^a-z0-9_]+/', '_', $alias), '_');
$purpose = isset($input['purpose']) && $input['purpose'] === 'commerce' ? 'commerce' : 'content';
if ($purpose === 'commerce' && !self::productsAvailable()) {
throw new \InvalidArgumentException('Сначала установите и включите модуль «Товары»');
}
if ($rubricId < 1 || !DB::query('SELECT Id FROM ' . self::rubricsTable() . ' WHERE Id=%i LIMIT 1', $rubricId)->getValue()) { throw new \InvalidArgumentException('Выберите рубрику'); }
if ($title === '') { throw new \InvalidArgumentException('Укажите название каталога'); }
if ($alias === '') { throw new \InvalidArgumentException('Укажите alias латиницей'); }
if (strlen($alias) > 20) { throw new \InvalidArgumentException('Alias каталога должен быть не длиннее 20 символов'); }
if (DB::query('SELECT Id FROM ' . self::fieldsTable() . ' WHERE rubric_id=%i AND rubric_field_alias=%s LIMIT 1', $rubricId, $alias)->getValue()) { throw new \InvalidArgumentException('Такой alias уже используется в рубрике'); }
$fieldId = RubricsModel::saveField(0, $rubricId, array('rubric_field_title'=>$title,'rubric_field_alias'=>$alias,'rubric_field_type'=>'catalog','rubric_field_description'=>'Структура каталога «'.$title.'»'));
try {
self::saveSettings($rubricId, $fieldId, array('purpose'=>$purpose,'recursive'=>1,'doc_parent'=>1,'item_parent'=>1,'sort_parent'=>1));
} catch (\Throwable $e) {
DB::Delete(self::settingsTable(), 'rubric_id=%i AND field_id=%i', $rubricId, $fieldId);
RubricsModel::deleteField($fieldId);
throw $e;
}
return array('rubric_id'=>$rubricId,'field_id'=>$fieldId,'purpose'=>$purpose);
}
public static function productsAvailable()
{
$module = ModuleManager::get('products');
return is_array($module)
&& !empty($module['installed'])
&& !empty($module['enabled'])
&& is_file(BASEPATH . '/modules/products/app/module.php')
&& is_file(BASEPATH . '/modules/products/admin/module.php');
}
public static function catalog($rubricId, $fieldId)
{
foreach (self::catalogs() as $catalog) {
if ($catalog['rubric_id'] === (int) $rubricId && $catalog['field_id'] === (int) $fieldId) {
return $catalog;
}
}
return null;
}
public static function settings($rubricId, $fieldId)
{
$row = DB::query(
'SELECT * FROM ' . self::settingsTable() . ' WHERE rubric_id = %i AND field_id = %i LIMIT 1',
(int) $rubricId,
(int) $fieldId
)->getAssoc();
$defaults = array(
'id' => 0, 'rubric_id' => (int) $rubricId, 'field_id' => (int) $fieldId,
'purpose' => 'content',
'product_title_field_id'=>0,'product_article_field_id'=>0,'product_price_field_id'=>0,'product_old_price_field_id'=>0,'product_stock_field_id'=>0,'product_images_field_id'=>0,'product_card_template_id'=>0,
'filter_template_id'=>0,'filter_template_mode'=>'legacy','filter_template_settings'=>'',
'navi_id' => 0, 'request_id' => 0, 'doc_fileds' => 0, 'recursive' => 1,
'doc_parent' => 1, 'item_parent' => 1, 'sort_parent' => 1, 'save_names' => 0,
'filters_use' => 0, 'rub_cat_id' => 0, 'fields_default' => '',
'filters_default' => '', 'filters_default_settings' => '',
);
$settings = array_merge($defaults, $row ?: array());
$card = !empty($settings['product_card_settings'])
? Json::toArray((string) $settings['product_card_settings'])
: array();
$settings['product_card'] = self::productCardSettings(is_array($card) ? $card : array());
$filterPresentation = !empty($settings['filter_template_settings'])
? Json::toArray((string) $settings['filter_template_settings'])
: array();
$settings['filter_template'] = self::filterTemplateSettings(is_array($filterPresentation) ? $filterPresentation : array());
return $settings;
}
public static function saveSettings($rubricId, $fieldId, array $input)
{
self::requireCatalog($rubricId, $fieldId);
$data = array();
$data['purpose'] = isset($input['purpose']) && $input['purpose'] === 'commerce' ? 'commerce' : 'content';
if ($data['purpose'] === 'commerce' && !self::productsAvailable()) {
throw new \InvalidArgumentException('Сначала установите и включите модуль «Товары»');
}
if ($data['purpose'] === 'commerce' && DB::query(
"SELECT id FROM " . self::settingsTable() . " WHERE rubric_id=%i AND field_id!=%i AND purpose='commerce' LIMIT 1",
(int) $rubricId,
(int) $fieldId
)->getValue()) {
throw new \InvalidArgumentException('У рубрики уже есть товарный каталог');
}
foreach(array('product_title_field_id','product_article_field_id','product_price_field_id','product_old_price_field_id','product_stock_field_id','product_images_field_id') as $key){$data[$key]=isset($input[$key])?max(0,(int)$input[$key]):0;if($data[$key]&&!DB::query('SELECT Id FROM '.self::fieldsTable().' WHERE Id=%i AND rubric_id=%i LIMIT 1',$data[$key],(int)$rubricId)->getValue()){throw new \InvalidArgumentException('Коммерческое поле не принадлежит рубрике');}}
$data['product_card_template_id'] = isset($input['product_card_template_id']) ? max(0, (int) $input['product_card_template_id']) : 0;
if ($data['product_card_template_id'] > 0 && !DB::query(
'SELECT id FROM ' . CatalogTables::table('catalog_card_templates') . ' WHERE id=%i LIMIT 1',
$data['product_card_template_id']
)->getValue()) {
throw new \InvalidArgumentException('Выбранное представление карточки не найдено');
}
if (self::filterTemplateSchemaAvailable()) {
$data['filter_template_id'] = isset($input['filter_template_id']) ? max(0, (int) $input['filter_template_id']) : 0;
$data['filter_template_mode'] = isset($input['filter_template_mode']) ? (string) $input['filter_template_mode'] : 'legacy';
if (!in_array($data['filter_template_mode'], array('legacy', 'preview', 'native'), true)) { $data['filter_template_mode'] = 'legacy'; }
if ($data['filter_template_id'] > 0 && !DB::query(
'SELECT id FROM ' . CatalogTables::table('catalog_filter_templates') . ' WHERE id=%i LIMIT 1',
$data['filter_template_id']
)->getValue()) { throw new \InvalidArgumentException('Выбранный шаблон фильтров не найден'); }
if ($data['filter_template_mode'] !== 'legacy' && $data['filter_template_id'] <= 0) {
throw new \InvalidArgumentException('Для предпросмотра или нового режима выберите шаблон фильтров');
}
if ($data['filter_template_mode'] === 'native' && !DB::query(
'SELECT id FROM ' . CatalogTables::table('catalog_filter_templates') . ' WHERE id=%i AND is_published=1 LIMIT 1',
$data['filter_template_id']
)->getValue()) { throw new \InvalidArgumentException('Перед включением нового режима опубликуйте шаблон фильтров'); }
}
foreach (array('navi_id', 'request_id', 'rub_cat_id') as $key) {
$data[$key] = isset($input[$key]) ? max(0, (int) $input[$key]) : 0;
}
self::assertOptionExists($data['request_id'], self::requestsTable(), 'Id', 'Выбранный запрос не найден');
self::assertOptionExists($data['navi_id'], self::navigationTable(), 'navigation_id', 'Выбранная навигация не найдена');
self::assertOptionExists($data['rub_cat_id'], self::rubricsTable(), 'Id', 'Выбранная рубрика не найдена');
foreach (array('doc_fileds', 'recursive', 'doc_parent', 'item_parent', 'sort_parent', 'save_names', 'filters_use') as $key) {
$data[$key] = !empty($input[$key]) ? '1' : '0';
}
$data['fields_default'] = self::idsString(isset($input['fields_default']) ? $input['fields_default'] : array());
$data['filters_default'] = self::idsString(isset($input['filters_default']) ? $input['filters_default'] : array());
$styles = self::filterStyles(isset($input['filter_style']) ? $input['filter_style'] : array(), self::ids($data['filters_default']));
$data['filters_default_settings'] = !empty($styles) ? serialize($styles) : '';
$data['product_card_settings'] = Json::encode(
self::productCardSettings(isset($input['product_card']) && is_array($input['product_card']) ? $input['product_card'] : array(), true),
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
);
if (self::filterTemplateSchemaAvailable()) {
$data['filter_template_settings'] = Json::encode(
self::filterTemplateSettings(isset($input['filter_template']) && is_array($input['filter_template']) ? $input['filter_template'] : array(), true),
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
);
}
$existing = self::settings($rubricId, $fieldId);
if ((int) $existing['id'] > 0) {
DB::Update(self::settingsTable(), $data, 'id = %i', (int) $existing['id']);
if ($data['purpose'] !== $existing['purpose'] && self::productsAvailable()) {
try { ProductIndexer::rebuild(); }
catch (\Throwable $e) { DB::Update(self::settingsTable(), array('purpose'=>$existing['purpose']), 'id=%i', (int) $existing['id']); throw $e; }
}
Cache::forgetTag(CacheKey::tag('catalog'));
FileCacheInvalidator::publicPresentation();
return (int) $existing['id'];
}
$data['rubric_id'] = (int) $rubricId;
$data['field_id'] = (int) $fieldId;
DB::Insert(self::settingsTable(), $data);
$settingsId = (int) DB::insertId();
if ($data['purpose'] === 'commerce') {
try { ProductIndexer::rebuild(); }
catch (\Throwable $e) { DB::Delete(self::settingsTable(), 'id=%i', $settingsId); throw $e; }
}
Cache::forgetTag(CacheKey::tag('catalog'));
FileCacheInvalidator::publicPresentation();
return $settingsId;
}
protected static function productCardSettings(array $input, $submitted = false)
{
$settings = array(
'max_images' => isset($input['max_images']) ? max(1, min(5, (int) $input['max_images'])) : 5,
'delivery_threshold' => isset($input['delivery_threshold']) ? max(0, (float) $input['delivery_threshold']) : 50000,
);
foreach (array('show_badges', 'show_article', 'show_stock', 'show_category', 'show_delivery', 'show_buy_button', 'show_comparison_price') as $key) {
$settings[$key] = $submitted ? !empty($input[$key]) : (!array_key_exists($key, $input) || !empty($input[$key]));
}
return $settings;
}
protected static function filterTemplateSettings(array $input, $submitted = false)
{
$zero = isset($input['zero_behavior']) ? (string) $input['zero_behavior'] : 'disable';
if (!in_array($zero, array('show', 'disable', 'hide'), true)) { $zero = 'disable'; }
return array(
'show_counts' => $submitted ? !empty($input['show_counts']) : (!array_key_exists('show_counts', $input) || !empty($input['show_counts'])),
'zero_behavior' => $zero,
);
}
protected static function filterTemplateSchemaAvailable()
{
$table = self::settingsTable();
return DatabaseSchema::columnExists($table, 'filter_template_id')
&& DatabaseSchema::columnExists($table, 'filter_template_mode')
&& DatabaseSchema::columnExists($table, 'filter_template_settings');
}
public static function requestOptions()
{
$rows = DB::query(
'SELECT Id, request_title, request_alias, rubric_id FROM ' . self::requestsTable()
. ' ORDER BY request_title ASC, request_alias ASC, Id ASC'
)->getAll();
$out = array();
foreach ($rows as $row) {
$out[] = array(
'id' => (int) $row['Id'],
'title' => self::decode(isset($row['request_title']) ? $row['request_title'] : ''),
'alias' => (string) $row['request_alias'],
'rubric_id' => (int) $row['rubric_id'],
);
}
return $out;
}
public static function navigationOptions()
{
$rows = DB::query(
'SELECT navigation_id, title, alias FROM ' . self::navigationTable()
. ' ORDER BY title ASC, navigation_id ASC'
)->getAll();
$out = array();
foreach ($rows as $row) {
$out[] = array(
'id' => (int) $row['navigation_id'],
'title' => self::decode(isset($row['title']) ? $row['title'] : ''),
'alias' => (string) $row['alias'],
);
}
return $out;
}
public static function rubricOptions()
{
$rows = DB::query(
'SELECT Id, rubric_title, rubric_alias FROM ' . self::rubricsTable()
. ' ORDER BY rubric_position ASC, rubric_title ASC, Id ASC'
)->getAll();
$out = array();
foreach ($rows as $row) {
$out[] = array(
'id' => (int) $row['Id'],
'title' => self::decode(isset($row['rubric_title']) ? $row['rubric_title'] : ''),
'alias' => (string) $row['rubric_alias'],
);
}
return $out;
}
public static function items($rubricId, $fieldId, $activeOnly = false)
{
self::requireCatalog($rubricId, $fieldId);
$sql = 'SELECT i.*, d.document_title, d.document_breadcrumb_title'
. ' FROM ' . self::itemsTable() . ' i'
. ' LEFT JOIN ' . self::documentsTable() . ' d ON d.Id = i.document_id'
. ' WHERE i.rubric_id = %i AND i.field_id = %i';
if ($activeOnly) { $sql .= ' AND i.status = 1'; }
$sql .= ' ORDER BY i.level ASC, i.parent_id ASC, i.position ASC, i.id ASC';
$rows = DB::query($sql, (int) $rubricId, (int) $fieldId)->getAll();
$out = array();
foreach ($rows as $row) { $out[] = self::itemRow($row); }
return $out;
}
public static function tree($rubricId, $fieldId, $activeOnly = false)
{
$flat = self::items($rubricId, $fieldId, $activeOnly);
$map = array();
foreach ($flat as $item) { $item['children'] = array(); $map[$item['id']] = $item; }
$tree = array();
foreach ($map as $id => $item) {
$parentId = $item['parent_id'];
if ($parentId > 0 && isset($map[$parentId])) { $map[$parentId]['children'][] = &$map[$id]; }
else { $tree[] = &$map[$id]; }
}
return $tree;
}
public static function item($id)
{
$row = DB::query(
'SELECT i.*, d.document_title, d.document_breadcrumb_title FROM ' . self::itemsTable() . ' i'
. ' LEFT JOIN ' . self::documentsTable() . ' d ON d.Id = i.document_id WHERE i.id = %i LIMIT 1',
(int) $id
)->getAssoc();
return $row ? self::itemRow($row) : null;
}
public static function saveItem($id, $rubricId, $fieldId, array $input)
{
self::requireCatalog($rubricId, $fieldId);
$id = (int) $id;
$current = $id > 0 ? self::item($id) : null;
if ($id > 0 && (!$current || $current['rubric_id'] !== (int) $rubricId || $current['field_id'] !== (int) $fieldId)) {
throw new \RuntimeException('Раздел каталога не найден');
}
$name = trim(isset($input['name']) ? (string) $input['name'] : '');
if ($name === '') { throw new \RuntimeException('Укажите название раздела'); }
$parentId = isset($input['parent_id']) ? (int) $input['parent_id'] : 0;
self::assertParent($id, $parentId, $rubricId, $fieldId);
$documentId = isset($input['document_id']) ? max(0, (int) $input['document_id']) : 0;
if ($documentId > 0 && !self::documentExists($documentId)) { throw new \RuntimeException('Выбранный документ не найден'); }
$filterIds = self::ids(isset($input['filters_use']) ? $input['filters_use'] : array());
$filterIds = self::orderedIds(isset($input['filters_order']) ? $input['filters_order'] : array(), $filterIds);
$data = array(
'rubric_id' => (int) $rubricId, 'field_id' => (int) $fieldId,
'name' => $name, 'parent_id' => $parentId,
'status' => !empty($input['status']) ? 1 : 0,
'document_id' => $documentId ?: null,
'document_alias' => $documentId ? self::documentAlias($documentId) : '',
'level' => self::parentLevel($parentId) + 1,
'fields_use' => self::idsString(isset($input['fields_use']) ? $input['fields_use'] : array()),
'filters_use' => implode(',', $filterIds),
'filters_settings' => serialize(self::filterStyles(isset($input['filter_style']) ? $input['filter_style'] : array(), $filterIds)),
);
if ($id > 0) {
DB::Update(self::itemsTable(), $data, 'id = %i', $id);
} else {
$data['position'] = self::nextPosition($rubricId, $fieldId, $parentId);
DB::Insert(self::itemsTable(), $data);
$id = (int) DB::insertId();
}
self::recountLevels($rubricId, $fieldId);
self::syncCategoryDocumentParent($id);
$filtersChanged = !$current
|| serialize(isset($current['filters_use']) ? $current['filters_use'] : array()) !== serialize($filterIds)
|| serialize(isset($current['filter_styles']) ? $current['filter_styles'] : array()) !== serialize(self::filterStyles(isset($input['filter_style']) ? $input['filter_style'] : array(), $filterIds));
if ($filtersChanged && (int) self::settings($rubricId, $fieldId)['filters_use'] === 1) { self::recompileFilters($rubricId, array($id)); }
self::clearCache();
return $id;
}
public static function deleteItem($id)
{
$item = self::item($id);
if (!$item) { return false; }
$ids = self::descendantIds((int) $id);
$ids[] = (int) $id;
DB::query('DELETE FROM ' . self::itemsTable() . ' WHERE id IN (' . implode(',', $ids) . ')');
self::clearCache();
return $ids;
}
public static function reorder($rubricId, $fieldId, array $rows)
{
self::requireCatalog($rubricId, $fieldId);
$changed = array();
foreach ($rows as $row) {
if (!is_array($row) || empty($row['id'])) { continue; }
$id = (int) $row['id'];
$item = self::item($id);
if (!$item || $item['rubric_id'] !== (int) $rubricId || $item['field_id'] !== (int) $fieldId) { continue; }
$parentId = isset($row['parent_id']) ? (int) $row['parent_id'] : 0;
self::assertParent($id, $parentId, $rubricId, $fieldId);
DB::Update(self::itemsTable(), array(
'parent_id' => $parentId,
'position' => isset($row['position']) ? max(0, (int) $row['position']) : 0,
), 'id = %i', $id);
if ($parentId !== (int) $item['parent_id']) { $changed[] = $id; }
}
self::recountLevels($rubricId, $fieldId);
foreach (array_unique($changed) as $id) { self::syncCategoryDocumentParent($id); }
self::clearCache();
}
public static function setItemStatus($id, $rubricId, $fieldId, $active)
{
$item = self::item((int) $id);
if (!$item || $item['rubric_id'] !== (int) $rubricId || $item['field_id'] !== (int) $fieldId) {
throw new \RuntimeException('Раздел каталога не найден');
}
DB::Update(self::itemsTable(), array('status' => $active ? 1 : 0), 'id = %i', (int) $id);
self::clearCache();
return $active ? 1 : 0;
}
public static function recompileItem($rubricId, $fieldId, $itemId)
{
$item = self::item((int) $itemId);
if (!$item || $item['rubric_id'] !== (int) $rubricId || $item['field_id'] !== (int) $fieldId) {
throw new \RuntimeException('Раздел каталога не найден');
}
return self::runFilterRecompile((int) $rubricId, (int) $itemId);
}
public static function rubricFields($rubricId)
{
$rows = DB::query(
'SELECT f.Id, f.rubric_field_title, f.rubric_field_alias, f.rubric_field_type, f.rubric_field_group, g.group_title'
. ' FROM ' . self::fieldsTable() . ' f LEFT JOIN ' . self::groupsTable() . ' g ON g.Id = f.rubric_field_group'
. ' WHERE f.rubric_id = %i ORDER BY COALESCE(g.group_position,999999), f.rubric_field_position, f.Id',
(int) $rubricId
)->getAll();
$out = array();
foreach ($rows as $row) {
$out[] = array(
'id' => (int) $row['Id'], 'title' => html_entity_decode((string) $row['rubric_field_title'], ENT_QUOTES, 'UTF-8'),
'alias' => (string) $row['rubric_field_alias'], 'type' => (string) $row['rubric_field_type'],
'group_id' => (int) $row['rubric_field_group'],
'group' => html_entity_decode((string) $row['group_title'], ENT_QUOTES, 'UTF-8'),
);
}
return $out;
}
public static function searchDocuments($query, $limit = 30)
{
$query = trim((string) $query);
$limit = max(1, min(50, (int) $limit));
$sql = 'SELECT d.Id, d.rubric_id, d.document_title, d.document_alias, r.rubric_title'
. ' FROM ' . self::documentsTable() . ' d'
. ' LEFT JOIN ' . self::rubricsTable() . ' r ON r.Id = d.rubric_id'
. " WHERE d.document_deleted != '1'";
$args = array();
if ($query !== '') {
$sql .= ' AND (d.document_title LIKE %ss OR d.document_alias LIKE %ss OR d.Id = %i)';
$args[] = $query; $args[] = $query; $args[] = (int) $query;
}
$sql .= ' ORDER BY d.document_changed DESC, d.Id DESC LIMIT ' . $limit;
$rows = call_user_func_array(array('DB', 'query'), array_merge(array($sql), $args))->getAll();
$out = array();
foreach ($rows as $row) {
$out[] = array(
'id' => (int) $row['Id'], 'rubric_id' => (int) $row['rubric_id'],
'title' => self::decode($row['document_title']), 'alias' => (string) $row['document_alias'],
'rubric_title' => self::decode(isset($row['rubric_title']) ? $row['rubric_title'] : ''),
);
}
return $out;
}
public static function parseValue($value)
{
$data = FieldValueCodec::decodeStructured($value, array());
return array(
'catalog_ids' => self::ids(isset($data['catalogs']) ? $data['catalogs'] : array()),
'document_ids' => self::ids(isset($data['documents']) ? $data['documents'] : array()),
'names' => isset($data['names']) ? (string) $data['names'] : '',
);
}
public static function products(array $filters = array())
{
return self::productsAvailable() ? ProductIndexer::products($filters) : array();
}
public static function productShippingStats()
{
return self::productsAvailable() ? ProductIndexer::shippingStats() : array('total'=>0,'ready'=>0,'incomplete'=>0,'disabled'=>0);
}
public static function productStats()
{
return self::productsAvailable()
? ProductIndexer::stats()
: array('total' => 0, 'active' => 0, 'inactive' => 0, 'categories' => 0);
}
public static function productQualityIssues(array $stats)
{
return self::productsAvailable() ? ProductIndexer::qualityIssues($stats) : array();
}
public static function product($id)
{
return self::productsAvailable() ? ProductIndexer::product($id) : null;
}
public static function productRubrics()
{
return self::productsAvailable() ? ProductIndexer::productRubrics() : array();
}
public static function reindexProducts($limit = 0)
{
return self::productsAvailable()
? ProductIndexer::rebuild($limit)
: array('products' => 0, 'categories' => 0, 'filters' => 0);
}
public static function reindexDocument($documentId)
{
return self::productsAvailable()
? ProductIndexer::indexDocument((int) $documentId)
: array('categories' => 0, 'filters' => 0);
}
public static function selection($rubricId, $fieldId, $input)
{
$ids = self::ids($input);
if (empty($ids)) { return array(); }
$valid = array();
foreach (self::items($rubricId, $fieldId, false) as $item) { $valid[$item['id']] = true; }
return array_values(array_filter($ids, function ($id) use ($valid) { return isset($valid[$id]); }));
}
public static function allowedFieldIds($rubricId, $fieldId, array $selected)
{
$settings = self::settings($rubricId, $fieldId);
if ((int) $settings['doc_fileds'] !== 1) { return array(); }
$allowed = array((int) $fieldId);
$selectedMap = array_fill_keys($selected, true);
foreach (self::items($rubricId, $fieldId, false) as $item) {
if (isset($selectedMap[$item['id']])) { $allowed = array_merge($allowed, self::ids($item['fields_use'])); }
}
return array_values(array_unique(array_map('intval', $allowed)));
}
public static function serializeSelection($rubricId, $fieldId, array $selected)
{
$selected = self::selection($rubricId, $fieldId, $selected);
$settings = self::settings($rubricId, $fieldId);
$map = array();
foreach (self::items($rubricId, $fieldId, false) as $item) { $map[$item['id']] = $item; }
$documents = array();
$names = array();
foreach ($selected as $id) {
$names[] = isset($map[$id]) ? $map[$id]['name'] : '';
$chain = array();
$cursor = $id;
while ($cursor > 0 && isset($map[$cursor])) {
if ($map[$cursor]['document_id'] > 0) { $chain[] = $map[$cursor]['document_id']; }
if ((int) $settings['recursive'] !== 1) { break; }
$cursor = $map[$cursor]['parent_id'];
}
$documents = array_merge($documents, array_reverse($chain));
}
$documents = array_values(array_unique(array_filter(array_map('intval', $documents))));
$data = array('catalogs' => implode(',', $selected), 'documents' => !empty($documents) ? '|' . implode('|', $documents) . '|' : '');
if ((int) $settings['save_names'] === 1) { $data['names'] = implode(',', array_filter($names, 'strlen')); }
return serialize($data);
}
public static function afterDocumentSave($documentId, $rubricId, $fieldId, array $selected)
{
$settings = self::settings($rubricId, $fieldId);
if ((int) $settings['doc_parent'] === 1 && !empty($selected)) {
$item = self::item((int) reset($selected));
if ($item && $item['document_id'] > 0) {
DB::Update(self::documentsTable(), array('document_parent' => $item['document_id']), 'Id = %i', (int) $documentId);
DocumentsModel::markChanged((int) $documentId);
}
}
if ((int) $settings['filters_use'] === 1) { self::recompileFilters($rubricId, $selected); }
self::clearCache();
}
public static function filterStyleOptions($type)
{
$options = array('default' => 'Автоматически');
$map = array(
'single_line' => array('single_line_input' => 'Текстовое поле', 'single_line_minmax' => 'Диапазон от и до'),
'single_line_numeric' => array('single_line_numeric_input' => 'Числовое поле', 'single_line_numeric_minmax' => 'Диапазон от и до'),
'number' => array('single_line_numeric_input' => 'Числовое поле', 'single_line_numeric_minmax' => 'Диапазон от и до'),
'drop_down' => array('drop_down_select' => 'Выпадающий список', 'drop_down_checkbox' => 'Флажки'),
'drop_down_key' => array('drop_down_key_select' => 'Выпадающий список', 'drop_down_key_checkbox' => 'Флажки'),
'doc_from_rub' => array('doc_from_rub_select' => 'Выпадающий список', 'doc_from_rub_checkbox' => 'Флажки'),
'checkbox' => array('checkbox_checkbox' => 'Переключатель'),
'multi_checkbox' => array('multi_checkbox_select' => 'Выпадающий список', 'multi_checkbox_checkbox' => 'Флажки'),
'checkbox_multi' => array('checkbox_multi_select' => 'Выпадающий список', 'checkbox_multi_checkbox' => 'Флажки'),
);
return isset($map[$type]) ? array_merge($options, $map[$type]) : $options;
}
public static function conditionContext($itemId)
{
$item = self::item((int) $itemId);
if (!$item) { throw new \RuntimeException('Раздел каталога не найден'); }
$settings = self::settings($item['rubric_id'], $item['field_id']);
$requestId = (int) $settings['request_id'];
$requestTitle = '';
if ($requestId > 0) {
$requestTitle = (string) DB::query(
'SELECT request_title FROM ' . self::requestsTable() . ' WHERE Id = %i LIMIT 1',
$requestId
)->getValue();
$requestTitle = self::decode($requestTitle);
}
$conditions = array();
if ($requestId > 0) {
foreach (DB::query(
'SELECT * FROM ' . self::conditionsTable() . ' WHERE request_id = %i ORDER BY condition_position ASC, Id ASC',
$requestId
)->getAll() as $condition) {
$fieldId = (int) $condition['condition_field_id'];
if (!isset($conditions[$fieldId])) { $conditions[$fieldId] = $condition; }
}
}
$fields = array();
foreach (self::rubricFields($item['rubric_id']) as $field) { $fields[$field['id']] = $field; }
$states = array();
$summary = array('synced' => 0, 'staged' => 0, 'missing' => 0, 'different' => 0, 'unsupported' => 0);
foreach ($item['filters_use'] as $filterId) {
if (!isset($fields[$filterId])) { continue; }
$style = isset($item['filter_styles'][$filterId]) ? $item['filter_styles'][$filterId] : 'default';
$generated = self::generateCondition($fields[$filterId], $style);
$existing = isset($conditions[$filterId]) ? $conditions[$filterId] : null;
$state = 'missing';
if (!$generated) { $state = 'unsupported'; }
elseif ($existing && self::conditionMatches($existing, $generated)) { $state = 'synced'; }
elseif ($existing && self::conditionDescriptorMatches($existing, $generated)) { $state = 'staged'; }
elseif ($existing) { $state = 'different'; }
$summary[$state]++;
$states[$filterId] = array(
'field_id' => $filterId,
'state' => $requestId > 0 ? $state : 'no_request',
'style' => $style,
'condition_id' => $existing ? (int) $existing['Id'] : 0,
'generated' => $generated ?: array(
'compare' => '', 'value' => '', 'source' => '', 'key' => '', 'config' => '',
),
'existing' => $existing ? array(
'compare' => (string) $existing['condition_compare'],
'value' => (string) $existing['condition_value'],
'status' => (int) $existing['condition_status'],
) : null,
);
}
return array(
'request' => array('id' => $requestId, 'title' => $requestTitle),
'states' => $states,
'summary' => $summary,
);
}
public static function syncCondition($itemId, $filterId, $force = false, $rebuild = true)
{
$item = self::item((int) $itemId);
if (!$item || !in_array((int) $filterId, $item['filters_use'], true)) {
throw new \RuntimeException('Фильтр не включён в этом разделе');
}
$context = self::conditionContext($itemId);
$requestId = (int) $context['request']['id'];
if ($requestId <= 0) { throw new \RuntimeException('Сначала выберите запрос фильтра в настройках каталога'); }
if (!isset($context['states'][(int) $filterId])) { throw new \RuntimeException('Поле фильтра не найдено'); }
$state = $context['states'][(int) $filterId];
if ($state['state'] === 'unsupported') { throw new \RuntimeException('Для выбранного типа фильтра нельзя создать условие автоматически'); }
if ($state['state'] === 'different' && !$force) { throw new \RuntimeException('Существующее условие отличается от сгенерированного'); }
if ($state['state'] === 'synced' || $state['state'] === 'staged') { return $context; }
$value = $state['generated']['value'];
if ((int) $state['condition_id'] > 0 && $state['existing']) {
$legacy = RequestConditionValue::legacyDescriptor(
$state['existing']['value'],
$state['existing']['compare']
);
if ($legacy !== null && RequestConditionValue::encodeConfig($legacy) === $state['generated']['config']) {
$value = $state['existing']['value'];
}
}
$data = array(
'condition_compare' => $state['generated']['compare'],
'condition_value' => $value,
'condition_value_source' => $state['generated']['source'],
'condition_value_key' => $state['generated']['key'],
'condition_value_config' => $state['generated']['config'],
'condition_join' => 'AND',
'condition_status' => '1',
);
if ((int) $state['condition_id'] > 0) {
DB::Update(self::conditionsTable(), $data, 'Id = %i AND request_id = %i', (int) $state['condition_id'], $requestId);
} else {
$data['request_id'] = $requestId;
$data['condition_field_id'] = (int) $filterId;
$data['condition_position'] = (int) DB::query(
'SELECT COALESCE(MAX(condition_position), -1) + 1 FROM ' . self::conditionsTable() . ' WHERE request_id = %i',
$requestId
)->getValue();
DB::Insert(self::conditionsTable(), $data);
}
if ($rebuild) { self::rebuildRequest($requestId); }
self::clearCache();
return self::conditionContext($itemId);
}
public static function syncMissingConditions($itemId)
{
$context = self::conditionContext($itemId);
if ((int) $context['request']['id'] <= 0) { throw new \RuntimeException('Сначала выберите запрос фильтра в настройках каталога'); }
$created = 0;
foreach ($context['states'] as $fieldId => $state) {
if ($state['state'] !== 'missing') { continue; }
self::syncCondition((int) $itemId, (int) $fieldId, false, false);
$created++;
}
if ($created > 0) { self::rebuildRequest((int) $context['request']['id']); }
$result = self::conditionContext($itemId);
$result['created'] = $created;
return $result;
}
public static function generateCondition(array $field, $style)
{
$fieldId = (int) $field['id'];
$type = isset($field['type']) ? (string) $field['type'] : '';
$style = self::resolveFilterStyle($type, (string) $style);
if ($style === '') { return null; }
$alias = isset($field['alias']) ? trim((string) $field['alias']) : '';
$name = preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', $alias) ? $alias : 'field_' . $fieldId;
if ($style === 'checkbox_checkbox' || $style === 'drop_down_key_select') {
return self::conditionDefinition('==', $name, 'integer', 'scalar');
}
if ($style === 'single_line_input' || $style === 'single_line_numeric_input') {
return self::conditionDefinition('%%', $name, 'string', 'scalar');
}
if ($style === 'drop_down_select' || $style === 'doc_from_rub_select') {
return self::conditionDefinition('==', $name, 'string', 'scalar');
}
if ($style === 'single_line_minmax') {
return self::conditionDefinition('FRE', $name, 'integer', 'range', 'plain', array(
'range_min_default' => '0',
));
}
if ($style === 'single_line_numeric_minmax' && $type === 'number') {
return self::conditionDefinition('FRE', $name, 'decimal', 'range');
}
if ($style === 'single_line_numeric_minmax') {
return self::conditionDefinition('FRE', $name, 'integer', 'range', 'plain', array(
'range_min_default' => '0',
'range_max_positive' => true,
));
}
if (in_array($style, array('drop_down_checkbox', 'doc_from_rub_checkbox'), true)) {
return self::conditionDefinition('FRE', $name, 'string', 'list');
}
if ($style === 'drop_down_key_checkbox') {
return self::conditionDefinition('FRE', $name, 'integer', 'list');
}
if (in_array($style, array('multi_checkbox_select', 'checkbox_multi_select'), true)) {
return self::conditionDefinition('%%', $name, 'integer', 'scalar', 'pipe_member');
}
if (in_array($style, array('multi_checkbox_checkbox', 'checkbox_multi_checkbox'), true)) {
return self::conditionDefinition('FRE', $name, 'integer', 'list', 'pipe_member');
}
return null;
}
protected static function conditionDefinition($compare, $key, $cast, $shape, $transform = 'plain', array $options = array())
{
$descriptor = array(
'source' => 'input',
'key' => $key,
'cast' => $cast,
'shape' => $shape,
'transform' => $transform,
'constant' => '',
);
$descriptor += $options;
return array(
'compare' => $compare,
'value' => RequestConditionValue::tag($key),
'source' => 'input',
'key' => $key,
'config' => RequestConditionValue::encodeConfig($descriptor),
);
}
protected static function conditionMatches(array $condition, array $generated)
{
return (string) $condition['condition_compare'] === (string) $generated['compare']
&& trim((string) $condition['condition_value']) === trim((string) $generated['value'])
&& (string) (isset($condition['condition_value_source']) ? $condition['condition_value_source'] : '') === (string) $generated['source']
&& (string) (isset($condition['condition_value_key']) ? $condition['condition_value_key'] : '') === (string) $generated['key']
&& RequestConditionValue::decodeConfig(isset($condition['condition_value_config']) ? $condition['condition_value_config'] : '')
=== RequestConditionValue::decodeConfig($generated['config']);
}
protected static function conditionDescriptorMatches(array $condition, array $generated)
{
return strpos((string) $condition['condition_value'], '<?') !== false
&& (string) $condition['condition_compare'] === (string) $generated['compare']
&& (string) (isset($condition['condition_value_source']) ? $condition['condition_value_source'] : '') === (string) $generated['source']
&& (string) (isset($condition['condition_value_key']) ? $condition['condition_value_key'] : '') === (string) $generated['key']
&& RequestConditionValue::decodeConfig(isset($condition['condition_value_config']) ? $condition['condition_value_config'] : '')
=== RequestConditionValue::decodeConfig($generated['config']);
}
protected static function resolveFilterStyle($type, $style)
{
$defaults = array(
'single_line' => 'single_line_input', 'single_line_numeric' => 'single_line_numeric_input', 'number' => 'single_line_numeric_input',
'drop_down' => 'drop_down_select', 'drop_down_key' => 'drop_down_key_select',
'doc_from_rub' => 'doc_from_rub_select', 'checkbox' => 'checkbox_checkbox',
'multi_checkbox' => 'multi_checkbox_select', 'checkbox_multi' => 'checkbox_multi_select',
);
if ($style === '' || $style === 'default') { return isset($defaults[$type]) ? $defaults[$type] : ''; }
$allowed = self::filterStyleOptions($type);
return isset($allowed[$style]) ? $style : '';
}
protected static function requireCatalog($rubricId, $fieldId)
{
$exists = (int) DB::query(
'SELECT Id FROM ' . self::fieldsTable() . " WHERE Id = %i AND rubric_id = %i AND rubric_field_type = 'catalog' LIMIT 1",
(int) $fieldId, (int) $rubricId
)->getValue();
if ($exists <= 0) { throw new \RuntimeException('Каталог не найден'); }
}
protected static function assertOptionExists($id, $table, $column, $message)
{
if ((int) $id <= 0) { return; }
if (!preg_match('/^[A-Za-z0-9_]+$/', (string) $table) || !preg_match('/^[A-Za-z0-9_]+$/', (string) $column)) {
throw new \RuntimeException('Некорректный источник справочника');
}
$exists = (int) DB::query('SELECT `' . $column . '` FROM `' . $table . '` WHERE `' . $column . '` = %i LIMIT 1', (int) $id)->getValue();
if ($exists <= 0) { throw new \RuntimeException((string) $message); }
}
protected static function assertParent($id, $parentId, $rubricId, $fieldId)
{
if ($parentId <= 0) { return; }
if ($parentId === (int) $id) { throw new \RuntimeException('Раздел не может быть родителем самого себя'); }
$parent = self::item($parentId);
if (!$parent || $parent['rubric_id'] !== (int) $rubricId || $parent['field_id'] !== (int) $fieldId) {
throw new \RuntimeException('Родительский раздел не найден');
}
if ($id > 0 && in_array((int) $parentId, self::descendantIds($id), true)) {
throw new \RuntimeException('Нельзя переместить раздел внутрь его потомка');
}
}
protected static function descendantIds($id)
{
$out = array(); $queue = array((int) $id);
while (!empty($queue)) {
$parent = array_shift($queue);
$rows = DB::query('SELECT id FROM ' . self::itemsTable() . ' WHERE parent_id = %i', $parent)->getAll();
foreach ($rows as $row) { $child = (int) $row['id']; if (!in_array($child, $out, true)) { $out[] = $child; $queue[] = $child; } }
}
return $out;
}
protected static function recountLevels($rubricId, $fieldId)
{
$items = self::items($rubricId, $fieldId, false); $map = array();
foreach ($items as $item) { $map[$item['id']] = $item; }
foreach ($map as $id => $item) {
$level = 1; $cursor = $item['parent_id']; $seen = array($id => true);
while ($cursor > 0 && isset($map[$cursor]) && !isset($seen[$cursor])) { $seen[$cursor] = true; $level++; $cursor = $map[$cursor]['parent_id']; }
DB::Update(self::itemsTable(), array('level' => $level), 'id = %i', $id);
}
}
protected static function syncCategoryDocumentParent($id)
{
$item = self::item($id); if (!$item || $item['document_id'] <= 0) { return; }
$settings = self::settings($item['rubric_id'], $item['field_id']);
if ((int) $settings['item_parent'] !== 1) { return; }
$parent = $item['parent_id'] > 0 ? self::item($item['parent_id']) : null;
DB::Update(self::documentsTable(), array('document_parent' => $parent ? $parent['document_id'] : 0), 'Id = %i', $item['document_id']);
DocumentsModel::markChanged((int) $item['document_id']);
}
protected static function recompileFilters($rubricId, array $selected)
{
foreach ($selected as $itemId) {
try { self::runFilterRecompile((int) $rubricId, (int) $itemId); }
catch (\Throwable $e) { error_log('Catalog filter recompile failed: ' . $e->getMessage()); }
}
}
protected static function runFilterRecompile($rubricId, $itemId)
{
$runner = AdminLocation::path('modules/Catalog/bin/recompile.php');
if (!is_file($runner)) { throw new \RuntimeException('Сценарий пересборки фильтров не найден'); }
if (!function_exists('exec')) { throw new \RuntimeException('Функция exec недоступна'); }
$output = array(); $code = 0;
$command = escapeshellarg(self::phpCliBinary()) . ' ' . escapeshellarg($runner) . ' ' . (int) $rubricId . ' ' . (int) $itemId . ' 2>&1';
exec($command, $output, $code);
if ($code !== 0) { throw new \RuntimeException(trim(implode("\n", $output)) ?: 'Пересборка завершилась с ошибкой'); }
return array('item_id' => (int) $itemId, 'output' => trim(implode("\n", $output)));
}
protected static function phpCliBinary()
{
$candidates = array(getenv('PHP_CLI_BINARY'));
if (defined('PHP_BINDIR')) { $candidates[] = PHP_BINDIR . '/php'; }
if (defined('PHP_BINARY') && preg_match('/^php(?:[0-9.]*)?$/i', basename((string) PHP_BINARY))) { $candidates[] = PHP_BINARY; }
$candidates[] = '/usr/local/bin/php'; $candidates[] = '/usr/bin/php';
foreach (array_unique(array_filter($candidates)) as $candidate) {
if (is_file($candidate) && is_executable($candidate)) { return $candidate; }
}
throw new \RuntimeException('CLI-интерпретатор PHP не найден');
}
protected static function rebuildRequest($requestId)
{
\App\Content\Requests\RequestConditionCompiler::compile((int) $requestId, true);
}
protected static function clearCache()
{
DB::clearTags(array('documents', 'requests', 'modules', 'catalog'));
Cache::forgetTag(CacheKey::tag('catalog'));
}
protected static function documentAlias($id)
{
return (string) DB::query('SELECT document_alias FROM ' . self::documentsTable() . ' WHERE Id = %i LIMIT 1', (int) $id)->getValue();
}
protected static function documentExists($id)
{
return (bool) DB::query('SELECT Id FROM ' . self::documentsTable() . " WHERE Id = %i AND document_deleted != '1' LIMIT 1", (int) $id)->getValue();
}
protected static function requestsTable() { return ContentTables::table('request'); }
protected static function conditionsTable() { return ContentTables::table('request_conditions'); }
protected static function navigationTable() { return ContentTables::table('navigation'); }
protected static function decode($value)
{
return html_entity_decode(stripslashes((string) $value), ENT_QUOTES, 'UTF-8');
}
protected static function parentLevel($id)
{
return $id > 0 ? (int) DB::query('SELECT level FROM ' . self::itemsTable() . ' WHERE id = %i LIMIT 1', (int) $id)->getValue() : 0;
}
protected static function nextPosition($rubricId, $fieldId, $parentId)
{
return (int) DB::query('SELECT COALESCE(MAX(position), -1) + 1 FROM ' . self::itemsTable() . ' WHERE rubric_id = %i AND field_id = %i AND parent_id = %i', (int) $rubricId, (int) $fieldId, (int) $parentId)->getValue();
}
protected static function ids($value)
{
if (!is_array($value)) { $value = preg_split('/[^0-9]+/', (string) $value); }
$out = array(); foreach ($value ?: array() as $id) { $id = (int) $id; if ($id > 0) { $out[] = $id; } }
return array_values(array_unique($out));
}
protected static function idsString($value) { return implode(',', self::ids($value)); }
protected static function orderedIds($order, array $selected)
{
$selectedMap = array_fill_keys($selected, true); $out = array();
foreach (self::ids($order) as $id) { if (isset($selectedMap[$id])) { $out[] = $id; unset($selectedMap[$id]); } }
foreach ($selected as $id) { if (isset($selectedMap[$id])) { $out[] = $id; } }
return $out;
}
protected static function filterStyles($styles, array $allowed)
{
if (!is_array($styles)) { return array(); }
$out = array(); foreach ($allowed as $id) { $style = isset($styles[$id]) ? preg_replace('/[^a-z0-9_\-]/i', '', (string) $styles[$id]) : ''; if ($style !== '' && $style !== 'default') { $out[$id] = $style; } }
return $out;
}
protected static function catalogRow(array $row)
{
return array(
'rubric_id' => (int) $row['rubric_id'], 'field_id' => (int) $row['field_id'],
'rubric_title' => html_entity_decode((string) $row['rubric_title'], ENT_QUOTES, 'UTF-8'),
'field_title' => html_entity_decode((string) $row['rubric_field_title'], ENT_QUOTES, 'UTF-8'),
'field_alias' => (string) $row['rubric_field_alias'], 'items_count' => (int) $row['items_count'],
'active_count' => (int) $row['active_count'], 'configured' => !empty($row['id']),
'filters_use' => isset($row['filters_use']) ? (int) $row['filters_use'] : 0,
'doc_fileds' => isset($row['doc_fileds']) ? (int) $row['doc_fileds'] : 0,
'purpose' => isset($row['purpose']) && $row['purpose'] === 'commerce' ? 'commerce' : 'content',
);
}
protected static function itemRow(array $row)
{
$styles = @unserialize(isset($row['filters_settings']) ? (string) $row['filters_settings'] : '', array('allowed_classes' => false));
return array(
'id' => (int) $row['id'], 'rubric_id' => (int) $row['rubric_id'], 'field_id' => (int) $row['field_id'],
'name' => (string) $row['name'], 'parent_id' => (int) $row['parent_id'], 'status' => (int) $row['status'],
'document_id' => (int) $row['document_id'], 'document_alias' => (string) $row['document_alias'],
'document_title' => html_entity_decode((string) (!empty($row['document_breadcrumb_title']) ? $row['document_breadcrumb_title'] : (isset($row['document_title']) ? $row['document_title'] : '')), ENT_QUOTES, 'UTF-8'),
'level' => (int) $row['level'], 'position' => (int) $row['position'],
'fields_use' => self::ids(isset($row['fields_use']) ? $row['fields_use'] : ''),
'filters_use' => self::ids(isset($row['filters_use']) ? $row['filters_use'] : ''),
'filter_styles' => is_array($styles) ? $styles : array(),
'attribute_set_id' => isset($row['attribute_set_id']) ? (int) $row['attribute_set_id'] : 0,
'attributes_runtime' => isset($row['attributes_runtime']) ? (string) $row['attributes_runtime'] : 'legacy',
'filter_runtime' => isset($row['filter_runtime']) ? (string) $row['filter_runtime'] : 'legacy',
);
}
}