mirror of
https://github.com/avecms/AVE.cms.git
synced 2026-08-01 08:45:44 +00:00
1098 lines
49 KiB
PHP
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',
|
|
);
|
|
}
|
|
|
|
}
|