| @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 App\Common\AdminAssets;
use App\Common\AuditLog;
use App\Common\Auth;
use App\Common\Controller as BaseController;
use App\Common\Permission;
use App\Common\ModuleManager;
use App\Common\Loader\Load;
use App\Common\Twig;
use App\Helpers\Request;
use App\Adminx\Rubrics\FieldAdminEditors;
use App\Adminx\Documents\Model as DocumentsModel;
use App\Adminx\Support\CodeEditor;
use App\Adminx\Support\SavedViews;
use App\Content\PaymentPrograms;
use App\Content\ProductShipping;
use App\Frontend\ThemeAssets;
use App\Adminx\Packages\Products\AttributeNormalization;
use App\Adminx\Packages\Products\Attributes;
use App\Adminx\Packages\Products\CardTemplateRevisions;
use App\Adminx\Packages\Products\CardTemplateSyntax;
use App\Adminx\Packages\Products\CardTemplates;
use App\Adminx\Packages\Products\CardContexts;
use App\Adminx\Packages\Products\FilterTemplateRevisions;
use App\Adminx\Packages\Products\FilterTemplates;
use App\Adminx\Packages\Products\QuickEditor;
use App\Adminx\Packages\Products\ProductRelations;
use App\Adminx\Packages\Products\ProductCollections;
use App\Adminx\Packages\Products\ProductDemands;
use App\Adminx\Packages\Products\ShippingPackageTemplates;
use App\Adminx\Packages\Products\VariantGroups;
use App\Modules\Products\VariantSelectorSettings;
use App\Modules\Products\ComparisonSettings;
use App\Modules\Products\DemandService;
use App\Modules\Products\DemandSettings;
class Controller extends BaseController
{
public function index(array $params = array())
{
if (!Permission::check('view_catalog')) { return $this->renderStatus('@adminx/404.twig', array('title' => 'Недостаточно прав'), 403); }
$this->assets();
$catalogs = Model::catalogs();
$items = 0; $active = 0; $filters = 0; $commerce = 0;
foreach ($catalogs as $catalog) { $items += $catalog['items_count']; $active += $catalog['active_count']; $filters += $catalog['filters_use'] ? 1 : 0; $commerce += $catalog['purpose'] === 'commerce' ? 1 : 0; }
$productsAvailable = $this->productsAvailable();
return $this->render('@catalog/index.twig', array(
'catalogs' => $catalogs,
'stats' => array('catalogs' => count($catalogs), 'items' => $items, 'active' => $active, 'filters' => $filters, 'commerce' => $commerce),
'rubric_options' => Model::rubricOptions(),
'products_available' => $productsAvailable,
'filter_templates_available' => $productsAvailable && FilterTemplates::available(),
'can_manage' => Permission::check('manage_catalog'),
));
}
public function products(array $params = array())
{
if (!Permission::check('view_catalog')) { return $this->renderStatus('@adminx/404.twig', array('title' => 'Недостаточно прав'), 403); }
if (!Model::hasCommerceCatalogs()) { return $this->renderStatus('@adminx/404.twig', array('title' => 'Товарные каталоги не настроены'), 404); }
$this->assets();
$filters = array(
'q' => Request::getStr('q', ''),
'state' => Request::getStr('state', ''),
'issue' => Request::getStr('issue', ''),
'payment' => Request::getStr('payment', ''),
'page' => Request::getInt('page', 1),
'per_page' => Request::getInt('per_page', 25),
);
return $this->render('@products/products.twig', array(
'result' => Model::products($filters),
'stats' => Model::productStats(),
'filters' => $filters,
'saved_views' => SavedViews::all('catalog_products', Auth::id(), $this->savedViewFields('products')),
'can_manage' => Permission::check('manage_catalog'),
));
}
public function productQuality(array $params = array())
{
if (!Permission::check('view_catalog')) { return $this->renderStatus('@adminx/404.twig', array('title' => 'Недостаточно прав'), 403); }
if (!Model::hasCommerceCatalogs()) { return $this->renderStatus('@adminx/404.twig', array('title' => 'Товарные каталоги не настроены'), 404); }
$this->assets();
$stats = Model::productStats();
$issues = Model::productQualityIssues($stats);
$allowed = array();
foreach ($issues as $item) { $allowed[(string) $item['code']] = true; }
$issue = Request::getStr('issue', '');
if ($issue !== '' && !isset($allowed[$issue])) { $issue = ''; }
$state = Request::getStr('state', '');
if (!in_array($state, array('', 'active', 'inactive'), true)) { $state = ''; }
$filters = array(
'q' => Request::getStr('q', ''),
'issue' => $issue,
'state' => $state,
'page' => Request::getInt('page', 1),
'per_page' => Request::getInt('per_page', 25),
);
return $this->render('@products/product-quality.twig', array(
'result' => Model::products($filters),
'stats' => $stats,
'issues' => $issues,
'filters' => $filters,
'saved_views' => SavedViews::all('catalog_quality', Auth::id(), $this->savedViewFields('quality')),
'can_manage' => Permission::check('manage_catalog'),
));
}
public function productComparison(array $params = array())
{
if (!Permission::check('view_catalog')) { return $this->renderStatus('@adminx/404.twig', array('title' => 'Недостаточно прав'), 403); }
if (!Model::hasCommerceCatalogs()) { return $this->renderStatus('@adminx/404.twig', array('title' => 'Товарные каталоги не настроены'), 404); }
$this->assets();
return $this->render('@products/product-comparison.twig', array(
'settings' => ComparisonSettings::get(),
'templates' => \App\Adminx\Rubrics\Model::templateOptions(),
'can_manage' => Permission::check('manage_catalog'),
));
}
public function saveProductComparison(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
try {
$settings = ComparisonSettings::save(Request::postAll());
} catch (\InvalidArgumentException $e) {
return $this->error($e->getMessage(), array(), 422);
}
$this->audit('catalog.product_comparison_settings_saved', 0, $settings);
return $this->success('Настройки сравнения сохранены', array(
'redirect' => $this->base() . '/catalog/product-comparison',
'data' => $settings,
));
}
public function productDemands(array $params = array())
{
if (!Permission::check('view_catalog')) { return $this->renderStatus('@adminx/404.twig', array('title' => 'Недостаточно прав'), 403); }
if (!Model::hasCommerceCatalogs()) { return $this->renderStatus('@adminx/404.twig', array('title' => 'Товарные каталоги не настроены'), 404); }
$this->assets();
$type = Request::getStr('type', '');
if (!in_array($type, array('', 'restock', 'preorder', 'price'), true)) { $type = ''; }
$status = Request::getStr('status', '');
if (!in_array($status, array('', 'new', 'ready', 'processing', 'notified', 'completed', 'cancelled'), true)) { $status = ''; }
$filters = array(
'q' => Request::getStr('q', ''),
'type' => $type,
'status' => $status,
'page' => Request::getInt('page', 1),
'per_page' => Request::getInt('per_page', 25),
);
return $this->render('@products/product-demands.twig', array(
'result' => ProductDemands::items($filters),
'stats' => ProductDemands::stats(),
'filters' => $filters,
'settings' => DemandSettings::get(),
'templates' => \App\Adminx\Rubrics\Model::templateOptions(),
'can_manage' => Permission::check('manage_catalog'),
));
}
public function saveProductDemandSettings(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
try {
$settings = DemandSettings::save(Request::postAll());
} catch (\InvalidArgumentException $e) {
return $this->error($e->getMessage(), array(), 422);
}
$this->audit('catalog.product_demand_settings_saved', 0, $settings);
return $this->success('Настройки обращений сохранены', array(
'redirect' => $this->base() . '/catalog/product-demands',
'data' => $settings,
));
}
public function saveProductDemandStatus(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
try {
$item = ProductDemands::setStatus(
isset($params['id']) ? (int) $params['id'] : 0,
Request::postStr('status', '')
);
} catch (\InvalidArgumentException $e) {
return $this->error($e->getMessage(), array(), 422);
}
$this->audit('catalog.product_demand_status_changed', (int) $item['id'], array('status' => $item['status']));
return $this->success('Состояние обращения изменено', array('data' => $item));
}
public function notifyProductDemands(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
$id = isset($params['id']) ? (int) $params['id'] : 0;
$result = DemandService::notifyReady($id > 0 ? 1 : 100, $id);
$this->audit('catalog.product_demand_notifications_sent', $id, $result);
$message = 'Отправлено: ' . (int) $result['sent'] . '.';
if ($result['skipped'] > 0) { $message .= ' Без email: ' . (int) $result['skipped'] . '.'; }
if ($result['failed'] > 0) { $message .= ' Ошибок: ' . (int) $result['failed'] . '.'; }
return $this->success($message, array('data' => $result));
}
public function deleteProductDemand(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
try {
$item = ProductDemands::delete(isset($params['id']) ? (int) $params['id'] : 0);
} catch (\InvalidArgumentException $e) {
return $this->error($e->getMessage(), array(), 404);
}
$this->audit('catalog.product_demand_deleted', (int) $item['id'], array(
'type' => $item['request_type'],
'document_id' => (int) $item['document_id'],
));
return $this->success('Обращение удалено');
}
public function saveSavedView(array $params = array())
{
if (($error = $this->savedViewGuard()) !== null) { return $error; }
$scope = isset($params['scope']) ? (string) $params['scope'] : '';
$definition = $this->savedViewDefinition($scope);
if (!$definition) { return $this->error('Раздел представления не найден', array(), 404); }
$filters = json_decode(Request::postStr('filters', '{}'), true);
if (!is_array($filters)) { return $this->error('Некорректный набор фильтров'); }
try {
$views = SavedViews::save($definition['scope'], Auth::id(), Request::postStr('title', ''), $filters, $definition['fields']);
} catch (\InvalidArgumentException $e) {
return $this->error($e->getMessage());
}
return $this->success('Представление сохранено', array('data' => array('views' => $views)));
}
public function deleteSavedView(array $params = array())
{
if (($error = $this->savedViewGuard()) !== null) { return $error; }
$scope = isset($params['scope']) ? (string) $params['scope'] : '';
$definition = $this->savedViewDefinition($scope);
if (!$definition) { return $this->error('Раздел представления не найден', array(), 404); }
try {
$views = SavedViews::delete($definition['scope'], Auth::id(), isset($params['id']) ? $params['id'] : '', $definition['fields']);
} catch (\InvalidArgumentException $e) {
return $this->error($e->getMessage(), array(), 404);
}
return $this->success('Представление удалено', array('data' => array('views' => $views)));
}
public function shipping(array $params = array())
{
if (!Permission::check('view_catalog')) { return $this->renderStatus('@adminx/404.twig', array('title' => 'Недостаточно прав'), 403); }
if (!Model::hasCommerceCatalogs()) { return $this->renderStatus('@adminx/404.twig', array('title' => 'Товарные каталоги не настроены'), 404); }
$this->shippingAssets();
$state = Request::getStr('shipping_state', '');
$issues = array('ready'=>'shipping_ready','incomplete'=>'shipping','disabled'=>'shipping_disabled');
if (!isset($issues[$state])) { $state = ''; }
$filters = array(
'q' => Request::getStr('q', ''),
'shipping_state' => $state,
'per_page' => Request::getInt('per_page', 25),
'page' => Request::getInt('page', 1),
);
return $this->render('@products/shipping.twig', array(
'result' => Model::products(array(
'q' => $filters['q'], 'issue' => $state !== '' ? $issues[$state] : '',
'per_page' => $filters['per_page'], 'page' => $filters['page'],
)),
'stats' => Model::productShippingStats(),
'filters' => $filters,
'package_templates' => ShippingPackageTemplates::all(),
'can_manage' => Permission::check('manage_catalog'),
));
}
public function productCollections(array $params = array())
{
if (!Permission::check('view_catalog')) { return $this->renderStatus('@adminx/404.twig', array('title' => 'Недостаточно прав'), 403); }
if (!Model::hasCommerceCatalogs()) { return $this->renderStatus('@adminx/404.twig', array('title' => 'Товарные каталоги не настроены'), 404); }
$this->collectionAssets();
return $this->render('@products/product-collections.twig', array(
'collections' => ProductCollections::all(),
'stats' => ProductCollections::stats(),
'categories' => ProductCollections::categories(),
'source_types' => ProductCollections::sourceTypes(),
'sort_modes' => ProductCollections::sortModes(),
'card_templates' => CardTemplates::options(),
'collections_available' => ProductCollections::available(),
'can_manage' => Permission::check('manage_catalog'),
));
}
public function productCollection(array $params = array())
{
if (!Permission::check('view_catalog')) { return $this->error('Недостаточно прав', array(), 403); }
$item = ProductCollections::one(isset($params['id']) ? (int) $params['id'] : 0);
return $item ? $this->success('', array('data' => array('collection' => $item)))
: $this->error('Подборка не найдена', array(), 404);
}
public function productCollectionProducts(array $params = array())
{
if (!Permission::check('view_catalog')) { return $this->error('Недостаточно прав', array(), 403); }
return $this->success('', array('data' => array(
'items' => ProductCollections::productOptions(Request::getStr('q', ''), 30),
)));
}
public function saveProductCollection(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
$id = isset($params['id']) ? (int) $params['id'] : 0;
try {
$item = ProductCollections::save($id, Request::postAll());
} catch (\Throwable $e) {
return $this->error($e->getMessage(), array(), 422);
}
$this->audit($id > 0 ? 'catalog.product_collection_saved' : 'catalog.product_collection_created', (int) $item['id'], array(
'code' => $item['code'],
'source_type' => $item['source_type'],
'manual_products' => count($item['manual_ids']),
));
return $this->success($id > 0 ? 'Подборка сохранена' : 'Подборка создана', array(
'data' => array('collection' => $item),
'redirect' => $this->base() . '/catalog/product-collections',
));
}
public function deleteProductCollection(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
$id = isset($params['id']) ? (int) $params['id'] : 0;
if (!ProductCollections::delete($id)) {
return $this->error('Подборка не найдена', array(), 404);
}
$this->audit('catalog.product_collection_deleted', $id);
return $this->success('Подборка удалена', array('redirect' => $this->base() . '/catalog/product-collections'));
}
public function productRelations(array $params = array())
{
if (!Permission::check('view_catalog')) { return $this->renderStatus('@adminx/404.twig', array('title' => 'Недостаточно прав'), 403); }
if (!Model::hasCommerceCatalogs()) { return $this->renderStatus('@adminx/404.twig', array('title' => 'Товарные каталоги не настроены'), 404); }
$this->relationAssets();
$type = Request::getStr('type', '');
return $this->render('@products/product-relations.twig', array(
'relations' => ProductRelations::relations($type), 'bundles' => ProductRelations::bundles(),
'types' => ProductRelations::types(), 'stats' => ProductRelations::stats(), 'selected_type' => $type,
'can_manage' => Permission::check('manage_catalog'),
));
}
public function productRelationProducts(array $params = array())
{
if (!Permission::check('view_catalog')) { return $this->error('Недостаточно прав', array(), 403); }
return $this->success('', array('data' => array('items' => ProductRelations::productOptions(Request::getStr('q', ''), 40))));
}
public function saveProductRelation(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
$id = isset($params['id']) ? (int) $params['id'] : 0;
try { $relation = ProductRelations::saveRelation($id, Request::postAll()); }
catch (\Throwable $e) { return $this->error($e->getMessage(), array(), 422); }
$this->audit($id > 0 ? 'catalog.product_relation_saved' : 'catalog.product_relation_created', (int) $relation['id'], array('type' => $relation['relation_type']));
return $this->success($id > 0 ? 'Связь сохранена' : 'Связь создана', array('data' => array('relation' => $relation), 'redirect' => $this->base() . '/catalog/product-relations'));
}
public function deleteProductRelation(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
$id = isset($params['id']) ? (int) $params['id'] : 0;
if (!ProductRelations::deleteRelation($id)) { return $this->error('Связь не найдена', array(), 404); }
$this->audit('catalog.product_relation_deleted', $id);
return $this->success('Связь удалена', array('redirect' => $this->base() . '/catalog/product-relations'));
}
public function productBundle(array $params = array())
{
if (!Permission::check('view_catalog')) { return $this->error('Недостаточно прав', array(), 403); }
$bundle = ProductRelations::bundle(isset($params['id']) ? (int) $params['id'] : 0);
return $bundle ? $this->success('', array('data' => array('bundle' => $bundle))) : $this->error('Комплект не найден', array(), 404);
}
public function saveProductBundle(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
$id = isset($params['id']) ? (int) $params['id'] : 0;
$items = Request::postJsonArray('items');
if (!is_array($items)) { return $this->error('Некорректный состав комплекта', array(), 422); }
try { $bundle = ProductRelations::saveBundle($id, Request::postAll(), $items); }
catch (\Throwable $e) { return $this->error($e->getMessage(), array(), 422); }
$this->audit($id > 0 ? 'catalog.product_bundle_saved' : 'catalog.product_bundle_created', (int) $bundle['id'], array('items' => count($bundle['items'])));
return $this->success($id > 0 ? 'Комплект сохранён' : 'Комплект создан', array('data' => array('bundle' => $bundle), 'redirect' => $this->base() . '/catalog/product-relations#bundles'));
}
public function deleteProductBundle(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
$id = isset($params['id']) ? (int) $params['id'] : 0;
if (!ProductRelations::deleteBundle($id)) { return $this->error('Комплект не найден', array(), 404); }
$this->audit('catalog.product_bundle_deleted', $id);
return $this->success('Комплект удалён', array('redirect' => $this->base() . '/catalog/product-relations#bundles'));
}
public function productShipping(array $params = array())
{
if (!Permission::check('view_catalog')) { return $this->error('Недостаточно прав', array(), 403); }
$productId = isset($params['id']) ? (int) $params['id'] : 0;
$product = Model::product($productId);
if (!$product) { return $this->error('Товар не найден', array(), 404); }
return $this->success('', array('data' => array(
'product' => array('id'=>$productId,'title'=>(string)$product['title'],'article'=>(string)$product['article']),
'profile' => ProductShipping::profile($productId),
)));
}
public function quickEdit(array $params = array())
{
if (!Permission::check('view_catalog')) { return $this->renderStatus('@adminx/404.twig', array('title' => 'Недостаточно прав'), 403); }
if (!Model::hasCommerceCatalogs()) { return $this->renderStatus('@adminx/404.twig', array('title' => 'Товарные каталоги не настроены'), 404); }
$this->assets();
AdminAssets::addStyle('/modules/products/admin/assets/quick-edit.css', 53);
AdminAssets::addScript('/modules/products/admin/assets/quick-edit.js', 52);
$data = $this->quickEditData();
if ($this->wantsPartial()) { return $this->partial('@catalog/partials/quick-edit-table.twig', $data); }
return $this->render('@products/quick-edit.twig', $data);
}
public function cardTemplates(array $params = array())
{
if (!Permission::check('view_catalog')) { return $this->renderStatus('@adminx/404.twig', array('title' => 'Недостаточно прав'), 403); }
$this->cardTemplateAssets();
return $this->render('@products/card-templates.twig', array(
'templates' => CardTemplates::all(Request::getStr('q', '')),
'card_contexts' => CardContexts::all(),
'card_template_options' => CardTemplates::options(),
'card_contexts_available' => CardContexts::available(),
'stats' => CardTemplates::stats(),
'filters' => array('q' => Request::getStr('q', '')),
'products' => CardTemplates::productOptions(100),
'default_markup' => CardTemplates::defaultMarkup(),
'can_manage' => Permission::check('manage_catalog'),
));
}
public function cardTemplate(array $params = array())
{
if (!Permission::check('view_catalog')) { return $this->error('Недостаточно прав', array(), 403); }
$item = CardTemplates::one(isset($params['id']) ? (int) $params['id'] : 0);
return $item ? $this->success('', array('data' => $item)) : $this->error('Представление не найдено', array(), 404);
}
public function saveCardContext(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
$code = isset($params['code']) ? (string) $params['code'] : '';
try { $context = CardContexts::save($code, Request::postAll(), Auth::id()); }
catch (\Throwable $e) { return $this->error($e->getMessage(), array(), 422); }
$this->audit('catalog.card_context_saved', null, array(
'context' => $context['code'],
'mode' => $context['mode'],
'template_id' => $context['template_id'],
));
return $this->success('Контекст карточки сохранён', array('data' => $context));
}
public function filterTemplates(array $params = array())
{
if (!Permission::check('view_catalog')) { return $this->renderStatus('@adminx/404.twig', array('title' => 'Недостаточно прав'), 403); }
$this->cardTemplateAssets();
return $this->render('@products/filter-templates.twig', array(
'templates' => FilterTemplates::all(Request::getStr('q', '')),
'stats' => FilterTemplates::stats(),
'filters' => array('q' => Request::getStr('q', '')),
'catalogs' => FilterTemplates::previewOptions(100),
'default_markup' => FilterTemplates::defaultMarkup(),
'can_manage' => Permission::check('manage_catalog'),
));
}
public function filterTemplate(array $params = array())
{
if (!Permission::check('view_catalog')) { return $this->error('Недостаточно прав', array(), 403); }
$item = FilterTemplates::one(isset($params['id']) ? (int) $params['id'] : 0);
return $item ? $this->success('', array('data' => $item)) : $this->error('Представление не найдено', array(), 404);
}
public function createFilterTemplate(array $params = array()) { return $this->persistFilterTemplate(0); }
public function saveFilterTemplate(array $params = array()) { return $this->persistFilterTemplate(isset($params['id']) ? (int) $params['id'] : 0); }
public function lintFilterTemplate(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
$result = CardTemplateSyntax::check(Request::postStr('draft_markup', ''));
return $result['ok'] ? $this->success($result['message'], array('data' => $result))
: $this->error($result['message'], array('draft_markup' => $result['message']), 422);
}
public function previewFilterTemplate(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
$markup = Request::postStr('draft_markup', ''); $css = Request::postStr('draft_css', '');
$syntax = CardTemplateSyntax::check($markup);
if (!$syntax['ok']) { return $this->error($syntax['message'], array('draft_markup' => $syntax['message']), 422); }
try {
$this->bootPublicCatalogPreview();
$catalog = \App\Modules\Products\Repository::fromDocumentId(Request::postInt('preview_catalog_id', 0));
if (!$catalog) { throw new \RuntimeException('Выберите раздел каталога с настроенными фильтрами'); }
$result = \App\Modules\Products\FilterTemplateRenderer::preview($catalog, $markup, $css);
$html = '
'
. ThemeAssets::styles()
. ''
. ''
. ($css !== '' ? '' : '') . ''
. $result['html'] . '';
} catch (\Throwable $e) { return $this->error($e->getMessage(), array(), 422); }
return $this->success('Предпросмотр обновлён', array('data' => array('html' => $html,
'json' => json_encode($result['data'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT))));
}
public function publishFilterTemplate(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; } $id = isset($params['id']) ? (int) $params['id'] : 0;
try { $item = FilterTemplates::publish($id, Auth::id()); } catch (\Throwable $e) { return $this->error($e->getMessage(), array(), 422); }
$this->audit('catalog.filter_template_published', $id, array('version' => $item['version']));
return $this->success('Фильтры опубликованы', array('data' => $item));
}
public function copyFilterTemplate(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
try { $id = FilterTemplates::copy(isset($params['id']) ? (int) $params['id'] : 0, Auth::id()); } catch (\Throwable $e) { return $this->error($e->getMessage(), array(), 422); }
$this->audit('catalog.filter_template_copied', $id); return $this->success('Копия создана', array('data' => array('id' => $id)));
}
public function deleteFilterTemplate(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; } $id = isset($params['id']) ? (int) $params['id'] : 0;
try { $deleted = FilterTemplates::delete($id); } catch (\Throwable $e) { return $this->error($e->getMessage(), array(), 422); }
if (!$deleted) { return $this->error('Представление не найдено', array(), 404); }
$this->audit('catalog.filter_template_deleted', $id); return $this->success('Представление удалено');
}
public function filterTemplateRevisions(array $params = array())
{
if (!Permission::check('view_catalog')) { return $this->error('Недостаточно прав', array(), 403); } $id = isset($params['id']) ? (int) $params['id'] : 0;
$item = FilterTemplates::one($id); if (!$item) { return $this->error('Представление не найдено', array(), 404); }
return $this->success('', array('data' => array('template' => array('id'=>$id,'title'=>$item['title']), 'revisions'=>FilterTemplateRevisions::listFor($id))));
}
public function filterTemplateRevision(array $params = array())
{
if (!Permission::check('view_catalog')) { return $this->error('Недостаточно прав', array(), 403); }
$item=FilterTemplateRevisions::one(isset($params['revision'])?(int)$params['revision']:0);return $item?$this->success('',array('data'=>$item)):$this->error('Ревизия не найдена',array(),404);
}
public function restoreFilterTemplateRevision(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
try{$id=FilterTemplateRevisions::restore(isset($params['revision'])?(int)$params['revision']:0,Auth::id());}catch(\Throwable $e){return $this->error($e->getMessage(),array(),422);}
$this->audit('catalog.filter_template_revision_restored',$id);return $this->success('Ревизия восстановлена в черновик',array('data'=>array('id'=>$id)));
}
public function deleteFilterTemplateRevision(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }$id=FilterTemplateRevisions::delete(isset($params['revision'])?(int)$params['revision']:0);
return $id?$this->success('Ревизия удалена',array('data'=>array('id'=>$id))):$this->error('Ревизия не найдена',array(),404);
}
public function deleteFilterTemplateRevisions(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }$id=isset($params['id'])?(int)$params['id']:0;
if(!FilterTemplates::one($id)){return $this->error('Представление не найдено',array(),404);}
return $this->success('Ревизии удалены',array('data'=>array('count'=>FilterTemplateRevisions::deleteFor($id))));
}
public function createCardTemplate(array $params = array())
{
return $this->persistCardTemplate(0);
}
public function saveCardTemplate(array $params = array())
{
return $this->persistCardTemplate(isset($params['id']) ? (int) $params['id'] : 0);
}
public function lintCardTemplate(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
$result = CardTemplateSyntax::check(Request::postStr('draft_markup', ''));
return $result['ok']
? $this->success($result['message'], array('data' => $result))
: $this->error($result['message'], array('draft_markup' => $result['message']), 422);
}
public function previewCardTemplate(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
$markup = Request::postStr('draft_markup', '');
$css = Request::postStr('draft_css', '');
$syntax = CardTemplateSyntax::check($markup);
if (!$syntax['ok']) { return $this->error($syntax['message'], array('draft_markup' => $syntax['message']), 422); }
try {
$this->bootPublicCatalogPreview();
$item = \App\Modules\Products\ProductCardRepository::find(Request::postInt('preview_product_id', 0));
if (!$item) { throw new \RuntimeException('Выберите существующий товар'); }
$card = Twig::twig()->createTemplate($markup, 'catalog-card-admin-preview')->render(array('item' => $item));
$html = ''
. ''
. ''
. ''
. ($css !== '' ? '' : '')
. '' . $card . '
';
} catch (\Throwable $e) {
return $this->error($e->getMessage(), array(), 422);
}
return $this->success('Предпросмотр обновлён', array('data' => array(
'html' => $html,
'json' => json_encode($item, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT),
)));
}
public function publishCardTemplate(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
$id = isset($params['id']) ? (int) $params['id'] : 0;
try { $item = CardTemplates::publish($id, Auth::id()); }
catch (\Throwable $e) { return $this->error($e->getMessage(), array(), 422); }
$this->audit('catalog.card_template_published', $id, array('version' => $item['version']));
return $this->success('Карточка опубликована', array('data' => $item));
}
public function copyCardTemplate(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
try { $id = CardTemplates::copy(isset($params['id']) ? (int) $params['id'] : 0, Auth::id()); }
catch (\Throwable $e) { return $this->error($e->getMessage(), array(), 422); }
$this->audit('catalog.card_template_copied', $id);
return $this->success('Копия создана', array('data' => array('id' => $id)));
}
public function deleteCardTemplate(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
$id = isset($params['id']) ? (int) $params['id'] : 0;
try { $deleted = CardTemplates::delete($id); }
catch (\Throwable $e) { return $this->error($e->getMessage(), array(), 422); }
if (!$deleted) { return $this->error('Представление не найдено', array(), 404); }
$this->audit('catalog.card_template_deleted', $id);
return $this->success('Представление удалено');
}
public function cardTemplateRevisions(array $params = array())
{
if (!Permission::check('view_catalog')) { return $this->error('Недостаточно прав', array(), 403); }
$id = isset($params['id']) ? (int) $params['id'] : 0;
$item = CardTemplates::one($id);
if (!$item) { return $this->error('Представление не найдено', array(), 404); }
return $this->success('', array('data' => array('template' => array('id' => $id, 'title' => $item['title']), 'revisions' => CardTemplateRevisions::listFor($id))));
}
public function cardTemplateRevision(array $params = array())
{
if (!Permission::check('view_catalog')) { return $this->error('Недостаточно прав', array(), 403); }
$item = CardTemplateRevisions::one(isset($params['revision']) ? (int) $params['revision'] : 0);
return $item ? $this->success('', array('data' => $item)) : $this->error('Ревизия не найдена', array(), 404);
}
public function restoreCardTemplateRevision(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
try { $id = CardTemplateRevisions::restore(isset($params['revision']) ? (int) $params['revision'] : 0, Auth::id()); }
catch (\Throwable $e) { return $this->error($e->getMessage(), array(), 422); }
$this->audit('catalog.card_template_revision_restored', $id);
return $this->success('Ревизия восстановлена в черновик', array('data' => array('id' => $id)));
}
public function deleteCardTemplateRevision(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
$id = CardTemplateRevisions::delete(isset($params['revision']) ? (int) $params['revision'] : 0);
return $id ? $this->success('Ревизия удалена', array('data' => array('id' => $id))) : $this->error('Ревизия не найдена', array(), 404);
}
public function deleteCardTemplateRevisions(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
$id = isset($params['id']) ? (int) $params['id'] : 0;
if (!CardTemplates::one($id)) { return $this->error('Представление не найдено', array(), 404); }
return $this->success('Ревизии удалены', array('data' => array('count' => CardTemplateRevisions::deleteFor($id))));
}
public function attributes(array $params = array())
{
if (!Permission::check('view_catalog')) { return $this->renderStatus('@adminx/404.twig', array('title' => 'Недостаточно прав'), 403); }
$this->attributeAssets();
$view = Request::getStr('view', 'attributes');
if (!in_array($view, array('normalization', 'attributes', 'sets', 'migration', 'sections'), true)) { $view = 'attributes'; }
return $this->render('@products/attributes.twig', array(
'view' => $view, 'stats' => Attributes::stats(), 'value_types' => Attributes::valueTypes(),
'attributes' => Attributes::all(Request::getStr('q', ''), Request::getStr('status', '')),
'sets' => Attributes::sets(), 'legacy_fields' => $view === 'migration' ? Attributes::legacyFields() : array(),
'sections' => $view === 'sections' ? Attributes::sections() : array(),
'normalization' => $view === 'normalization' ? AttributeNormalization::audit() : array(),
'filters' => array('q'=>Request::getStr('q',''),'status'=>Request::getStr('status','')),
'can_manage' => Permission::check('manage_catalog'),
));
}
public function prepareAttributeNormalization(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
try { $run = AttributeNormalization::prepare(Auth::id()); }
catch (\Throwable $e) { return $this->error($e->getMessage(), array(), 422); }
$this->audit('catalog.attribute_normalization_prepared', (int) $run['id'], $run['summary']);
return $this->success('Черновой контур характеристик подготовлен', array(
'redirect' => $this->base() . '/catalog/attributes?view=normalization',
'data' => $run,
));
}
public function approveAttributeNormalization(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
$id = isset($params['id']) ? (int) $params['id'] : 0;
try { $run = AttributeNormalization::approve($id, Auth::id()); }
catch (\Throwable $e) { return $this->error($e->getMessage(), array(), 422); }
$this->audit('catalog.attribute_normalization_approved', $id, $run['summary']);
return $this->success('Безопасные значения подтверждены, публичный режим не изменён', array(
'redirect' => $this->base() . '/catalog/attributes?view=normalization',
'data' => $run,
));
}
public function attributeNormalizationReport(array $params = array())
{
if (!Permission::check('view_catalog')) { return $this->error('Недостаточно прав', array(), 403); }
$run = AttributeNormalization::run(isset($params['id']) ? (int) $params['id'] : 0);
return $run ? $this->success('', array('data' => $run)) : $this->error('Прогон не найден', array(), 404);
}
public function saveAttribute(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
$id = isset($params['id']) ? (int) $params['id'] : 0;
try { $id = Attributes::save($id, Request::postAll(), Auth::id()); }
catch (\Throwable $e) { return $this->error($e->getMessage(), array(), 422); }
$this->audit('catalog.attribute_saved', $id);
return $this->success('Характеристика сохранена', array('data'=>array('id'=>$id)));
}
public function attribute(array $params = array())
{
if(!Permission::check('view_catalog')){return $this->error('Недостаточно прав',array(),403);}$item=Attributes::one(isset($params['id'])?(int)$params['id']:0);return $item?$this->success('',array('data'=>$item)):$this->error('Характеристика не найдена',array(),404);
}
public function deleteAttribute(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
try { Attributes::delete(isset($params['id']) ? (int) $params['id'] : 0); }
catch (\Throwable $e) { return $this->error($e->getMessage(), array(), 422); }
$this->audit('catalog.attribute_deleted', (int) $params['id']);
return $this->success('Характеристика удалена');
}
public function attributeSet(array $params = array())
{
if (!Permission::check('view_catalog')) { return $this->renderStatus('@adminx/404.twig', array('title' => 'Недостаточно прав'), 403); }
$set = Attributes::set(isset($params['id']) ? (int) $params['id'] : 0);
if (!$set) { return $this->renderStatus('@adminx/404.twig', array('title'=>'Набор не найден'), 404); }
$this->attributeAssets();
return $this->render('@products/attribute-set.twig', array('set'=>$set,'attributes'=>Attributes::all('', 'active'),'can_manage'=>Permission::check('manage_catalog')));
}
public function saveAttributeSet(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
$id=isset($params['id'])?(int)$params['id']:0;
try{$id=Attributes::saveSet($id,Request::postAll(),Auth::id());}catch(\Throwable $e){return $this->error($e->getMessage(),array(),422);}
$this->audit('catalog.attribute_set_saved',$id);return $this->success('Набор сохранён',array('redirect'=>$this->base().'/catalog/attribute-sets/'.$id,'data'=>array('id'=>$id)));
}
public function deleteAttributeSet(array $params = array())
{
if(($error=$this->guard())!==null){return $error;}try{Attributes::deleteSet((int)$params['id']);}catch(\Throwable $e){return $this->error($e->getMessage(),array(),422);}$this->audit('catalog.attribute_set_deleted',(int)$params['id']);return $this->success('Набор удалён',array('redirect'=>$this->base().'/catalog/attributes?view=sets'));
}
public function saveAttributeGroup(array $params = array())
{
if(($error=$this->guard())!==null){return $error;}try{$id=Attributes::saveGroup((int)$params['id'],Request::postInt('group_id',0),Request::postStr('name',''));}catch(\Throwable $e){return $this->error($e->getMessage(),array(),422);}return $this->success('Группа сохранена',array('data'=>array('id'=>$id)));
}
public function deleteAttributeGroup(array $params = array())
{
if(($error=$this->guard())!==null){return $error;}Attributes::deleteGroup((int)$params['id'],(int)$params['group']);return $this->success('Группа удалена, характеристики перенесены в «Без группы»');
}
public function saveAttributeSetItem(array $params = array())
{
if(($error=$this->guard())!==null){return $error;}try{$id=Attributes::saveSetItem((int)$params['id'],Request::postAll());}catch(\Throwable $e){return $this->error($e->getMessage(),array(),422);}return $this->success('Характеристика добавлена',array('data'=>array('id'=>$id)));
}
public function deleteAttributeSetItem(array $params = array())
{
if(($error=$this->guard())!==null){return $error;}Attributes::deleteSetItem((int)$params['id'],(int)$params['attribute']);return $this->success('Характеристика удалена из набора');
}
public function saveAttributeMapping(array $params = array())
{
if(($error=$this->guard())!==null){return $error;}try{$data=Attributes::saveLegacyMap((int)$params['field'],Request::postAll(),Auth::id());}catch(\Throwable $e){return $this->error($e->getMessage(),array(),422);}return $this->success('Сопоставление сохранено',array('data'=>$data));
}
public function attributeMappingPreview(array $params = array())
{
if(!Permission::check('view_catalog')){return $this->error('Недостаточно прав',array(),403);}try{$data=Attributes::migrationPreview((int)$params['field']);}catch(\Throwable $e){return $this->error($e->getMessage(),array(),422);}return $this->success('',array('data'=>$data));
}
public function migrateAttributeField(array $params = array())
{
if(($error=$this->guard())!==null){return $error;}try{$data=Attributes::migrate((int)$params['field'],Auth::id(),Request::postBool('verified',false));}catch(\Throwable $e){return $this->error($e->getMessage(),array(),422);}$this->audit('catalog.attribute_field_migrated',(int)$params['field'],$data);return $this->success('Значения перенесены в новый контур',array('data'=>$data));
}
public function verifyAttributeField(array $params = array())
{
if(($error=$this->guard())!==null){return $error;}try{$data=Attributes::verifyMigration((int)$params['field'],Auth::id());}catch(\Throwable $e){return $this->error($e->getMessage(),array(),422);}$this->audit('catalog.attribute_field_verified',(int)$params['field'],$data);return $this->success('Перенесённые значения подтверждены',array('data'=>$data));
}
public function saveAttributeSection(array $params = array())
{
if(($error=$this->guard())!==null){return $error;}try{$data=Attributes::saveSection((int)$params['id'],Request::postAll(),Auth::id());}catch(\Throwable $e){return $this->error($e->getMessage(),array(),422);}return $this->success('Режим раздела сохранён',array('data'=>$data));
}
public function rebuildAttributeShadow(array $params = array())
{
if(($error=$this->guard())!==null){return $error;}try{$data=Attributes::rebuildShadowIndex((int)$params['id']);}catch(\Throwable $e){return $this->error($e->getMessage(),array(),422);}$this->audit('catalog.attribute_shadow_rebuilt',(int)$params['id'],$data);return $this->success('Теневой индекс раздела перестроен',array('data'=>$data));
}
public function attributeShadowReport(array $params = array())
{
if(!Permission::check('view_catalog')){return $this->error('Недостаточно прав',array(),403);}try{$data=Attributes::shadowReport((int)$params['id']);}catch(\Throwable $e){return $this->error($e->getMessage(),array(),422);}return $this->success('',array('data'=>$data));
}
public function saveQuickEdit(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
$productId = isset($params['id']) ? (int) $params['id'] : 0;
$key = Request::postStr('key', '');
try { $saved = QuickEditor::save($productId, $key, Request::postStr('value', ''), Auth::id()); }
catch (\Throwable $e) { return $this->error($e->getMessage(), array('value' => $e->getMessage()), 422); }
$this->audit('catalog.product_quick_updated', $productId, $saved);
return $this->success('Изменение сохранено', array('data' => $saved));
}
public function setProductPaymentProgram(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
$productId = isset($params['id']) ? (int) $params['id'] : 0;
if (!Model::product($productId)) { return $this->error('Товар не найден', array(), 404); }
try { $enabled = PaymentPrograms::setProduct($productId, isset($params['program']) ? $params['program'] : '', Request::postBool('enabled', false)); }
catch (\Throwable $e) { return $this->error($e->getMessage(), array(), 422); }
$this->audit('catalog.product_payment_program_changed', $productId, array('program' => (string) $params['program'], 'enabled' => $enabled));
return $this->success($enabled ? 'Оплата через СФР доступна товару' : 'Оплата через СФР отключена для товара', array('data' => array('enabled' => $enabled)));
}
public function createCatalog(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
try { $catalog = Model::createCatalog(Request::postAll()); }
catch (\Throwable $e) { return $this->error($e->getMessage(), array(), 422); }
$this->audit('catalog.created', $catalog['field_id'], $catalog);
return $this->success('Каталог создан', array('redirect'=>$this->base().'/catalog/'.$catalog['rubric_id'].'/'.$catalog['field_id'],'data'=>$catalog));
}
public function variantGroups(array $params = array())
{
if (!Permission::check('view_catalog')) { return $this->renderStatus('@adminx/404.twig', array('title' => 'Недостаточно прав'), 403); }
if (!Model::hasCommerceCatalogs()) { return $this->renderStatus('@adminx/404.twig', array('title' => 'Товарные каталоги не настроены'), 404); }
$this->assets();
return $this->render('@products/variant-groups.twig', array(
'groups' => VariantGroups::all(),
'selector_settings' => VariantSelectorSettings::get(),
'can_manage' => Permission::check('manage_catalog'),
));
}
public function saveVariantSelectorSettings(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
$settings = VariantSelectorSettings::save(Request::postAll());
$this->audit('catalog.variant_selector_settings_saved', 0, $settings);
return $this->success('Вид выбора вариантов сохранён', array(
'redirect' => $this->base() . '/catalog/variant-groups',
'data' => $settings,
));
}
public function variantGroup(array $params = array())
{
if (!Permission::check('view_catalog')) { return $this->renderStatus('@adminx/404.twig', array('title' => 'Недостаточно прав'), 403); }
$group = VariantGroups::one(isset($params['id']) ? (int) $params['id'] : 0);
if (!$group) { return $this->renderStatus('@adminx/404.twig', array('title' => 'Группа не найдена'), 404); }
$this->shippingAssets();
$search = Request::getStr('q', '');
return $this->render('@products/variant-group.twig', array(
'group' => $group, 'search' => $search,
'variant_fields' => VariantGroups::fields((int) $group['id']),
'variant_attributes' => VariantGroups::nativeAttributes(),
'package_templates' => ShippingPackageTemplates::all(),
'candidates' => $search !== '' ? Model::products(array('q' => $search, 'page' => 1)) : array('items' => array()),
'can_manage' => Permission::check('manage_catalog'),
));
}
public function saveVariantGroup(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
$id = isset($params['id']) ? (int) $params['id'] : 0;
try { $id = VariantGroups::save($id, Request::postStr('title', ''), Request::postBool('status', false), Auth::id()); }
catch (\Throwable $e) { return $this->error($e->getMessage(), array(), 422); }
$this->audit('catalog.variant_group_saved', $id);
return $this->success('Группа вариантов сохранена', array('redirect' => $this->base() . '/catalog/variant-groups/' . $id));
}
public function addVariantProduct(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
try { VariantGroups::add((int) $params['id'], Request::postInt('product_id', 0)); }
catch (\Throwable $e) { return $this->error($e->getMessage(), array(), 422); }
$this->audit('catalog.variant_added', (int) $params['id'], array('product_id' => Request::postInt('product_id', 0)));
return $this->success('Товар добавлен в группу');
}
public function primaryVariantProduct(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
try { VariantGroups::primary((int) $params['id'], (int) $params['product']); }
catch (\Throwable $e) { return $this->error($e->getMessage(), array(), 422); }
$this->audit('catalog.variant_primary_changed', (int) $params['id'], array('product_id' => (int) $params['product']));
return $this->success('Основной вариант изменён');
}
public function removeVariantProduct(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
try { VariantGroups::remove((int) $params['id'], (int) $params['product']); }
catch (\Throwable $e) { return $this->error($e->getMessage(), array(), 422); }
$this->audit('catalog.variant_removed', (int) $params['id'], array('product_id' => (int) $params['product']));
return $this->success('Товар исключён из группы');
}
public function statusVariantProduct(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
$status = Request::postBool('status', false);
try { VariantGroups::status((int) $params['id'], (int) $params['product'], $status); }
catch (\Throwable $e) { return $this->error($e->getMessage(), array(), 422); }
$this->audit('catalog.variant_status_changed', (int) $params['id'], array('product_id' => (int) $params['product'], 'status' => $status));
return $this->success($status ? 'Вариант включён' : 'Вариант выключен');
}
public function saveVariantAttribute(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
try { VariantGroups::saveAttribute((int) $params['id'], (int) $params['product'], Request::postInt('field_id', 0), Request::postStr('label', ''), Request::postStr('swatch', '')); }
catch (\Throwable $e) { return $this->error($e->getMessage(), array(), 422); }
$this->audit('catalog.variant_attribute_saved', (int) $params['id'], array('product_id' => (int) $params['product'], 'field_id' => Request::postInt('field_id', 0)));
return $this->success('Атрибут варианта сохранён');
}
public function deleteVariantAttribute(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
VariantGroups::deleteAttribute((int) $params['id'], (int) $params['product'], (int) $params['field']);
$this->audit('catalog.variant_attribute_deleted', (int) $params['id'], array('product_id' => (int) $params['product'], 'field_id' => (int) $params['field']));
return $this->success('Атрибут удалён');
}
public function saveVariantNativeAttribute(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
$attributeId = Request::postInt('attribute_id', 0);
try {
VariantGroups::saveNativeAttribute(
(int) $params['id'],
(int) $params['product'],
$attributeId,
Request::postInt('option_id', 0),
Request::postStr('label', ''),
Request::postStr('swatch', '')
);
} catch (\Throwable $e) { return $this->error($e->getMessage(), array(), 422); }
$this->audit('catalog.variant_native_attribute_saved', (int) $params['id'], array(
'product_id' => (int) $params['product'],
'attribute_id' => $attributeId,
'option_id' => Request::postInt('option_id', 0),
));
return $this->success('Характеристика варианта сохранена');
}
public function deleteVariantNativeAttribute(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
VariantGroups::deleteNativeAttribute((int) $params['id'], (int) $params['product'], (int) $params['attribute']);
$this->audit('catalog.variant_native_attribute_deleted', (int) $params['id'], array(
'product_id' => (int) $params['product'],
'attribute_id' => (int) $params['attribute'],
));
return $this->success('Характеристика варианта удалена');
}
public function reorderVariantProducts(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
$order = Request::postJsonArray('order');
if (!is_array($order)) { return $this->error('Некорректный порядок', array(), 422); }
try { VariantGroups::reorder((int) $params['id'], $order); }
catch (\Throwable $e) { return $this->error($e->getMessage(), array(), 422); }
$this->audit('catalog.variants_reordered', (int) $params['id'], array('order' => $order));
return $this->success('Порядок вариантов сохранён');
}
public function copyVariantProduct(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
$sourceId = (int) $params['product'];
$newId = 0;
try {
$newId = DocumentsModel::copy($sourceId, Auth::id());
Model::reindexDocument($newId);
VariantGroups::add((int) $params['id'], $newId);
} catch (\Throwable $e) {
if ($newId > 0) { try { DocumentsModel::delete($newId); DocumentsModel::purge($newId); } catch (\Throwable $cleanupError) {} }
return $this->error($e->getMessage(), array(), 422);
}
$this->audit('catalog.variant_copied', (int) $params['id'], array('source_id' => $sourceId, 'product_id' => $newId));
return $this->success('Вариант создан копированием', array('redirect' => $this->base() . '/catalog/products/' . $newId . '/edit'));
}
public function deleteVariantGroup(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
try { VariantGroups::delete((int) $params['id']); }
catch (\Throwable $e) { return $this->error($e->getMessage(), array(), 422); }
$this->audit('catalog.variant_group_deleted', (int) $params['id']);
return $this->success('Группа удалена', array('redirect' => $this->base() . '/catalog/variant-groups'));
}
public function reindexProducts(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
try { $result = Model::reindexProducts(); }
catch (\Throwable $e) { return $this->error($e->getMessage(), array(), 500); }
$this->audit('catalog.products_reindexed', null, $result);
return $this->success('Индекс товаров перестроен', array('data' => $result));
}
public function reindexProduct(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
$id = isset($params['id']) ? (int) $params['id'] : 0;
if ($id <= 0 || !DocumentsModel::one($id)) { return $this->error('Товар не найден', array(), 404); }
try {
$result = Model::reindexDocument($id);
$product = Model::product($id);
} catch (\Throwable $e) {
return $this->error($e->getMessage(), array(), 500);
}
$this->audit('catalog.product_reindexed', $id, $result);
return $this->success('Товар переиндексирован', array('data' => array('product' => $product, 'result' => $result)));
}
public function createProductVariantGroup(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
$productId = isset($params['id']) ? (int) $params['id'] : 0;
$product = Model::product($productId);
if (!$product) { return $this->error('Товар не найден', array(), 404); }
if (VariantGroups::membership($productId)) { return $this->error('Товар уже состоит в группе', array(), 422); }
try {
$groupId = VariantGroups::save(0, Request::postStr('title', (string) $product['title']), true, Auth::id());
VariantGroups::add($groupId, $productId);
} catch (\Throwable $e) { return $this->error($e->getMessage(), array(), 422); }
$this->audit('catalog.variant_group_created_from_product', $groupId, array('product_id' => $productId));
return $this->success('Группа вариантов создана', array('redirect' => $this->base() . '/catalog/variant-groups/' . $groupId));
}
public function editProduct(array $params = array())
{
if (!Permission::check('manage_catalog') || !Permission::check('manage_documents')) {
return $this->renderStatus('@adminx/404.twig', array('title' => 'Недостаточно прав'), 403);
}
$id = isset($params['id']) ? (int) $params['id'] : 0;
$product = Model::product($id);
$document = DocumentsModel::one($id);
if (!$product || !$document) { return $this->renderStatus('@adminx/404.twig', array('title' => 'Товар не найден'), 404); }
AdminAssets::addStyle($this->base() . '/modules/Documents/assets/documents.css', 50);
AdminAssets::addScript($this->base() . '/modules/Documents/assets/documents.js', 50);
CodeEditor::useRichEditor();
$this->attributeAssets();
return $this->render('@documents/edit.twig', array(
'document' => $document,
'field_groups' => DocumentsModel::fieldsForDocument($id),
'rubrics' => DocumentsModel::rubrics(),
'templates' => DocumentsModel::rubricTemplates((int) $document['rubric_id']),
'navigation_items' => DocumentsModel::navigationItems(),
'authors' => DocumentsModel::authors(),
'is_new' => false, 'can_manage' => true,
'actor_id' => Auth::id(),
'catalog_mode' => true, 'product' => $product, 'variant_group' => VariantGroups::membership($id),
'shipping_profile' => ProductShipping::profile($id),
'package_templates' => ShippingPackageTemplates::all(),
'native_attributes' => Attributes::productEditor($id),
'return_url' => $this->base() . '/catalog/products',
'submit_url' => $this->base() . '/documents/' . $id,
));
}
public function saveProductShipping(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
$productId = isset($params['id']) ? (int) $params['id'] : 0;
$packages = Request::postJsonArray('packages');
if (!is_array($packages)) { return $this->error('Некорректный список грузовых мест', array(), 422); }
try { $profile = ProductShipping::save($productId, Request::postBool('shipping_enabled', false), $packages); }
catch (\Throwable $e) { return $this->error($e->getMessage(), array(), 422); }
$this->audit('catalog.product_shipping_saved', $productId, array('packages' => count($profile['packages']), 'complete' => $profile['complete']));
return $this->success('Упаковка товара сохранена', array('data' => array('profile' => $profile)));
}
public function saveProductAttributes(array $params = array())
{
if(($error=$this->guard())!==null){return $error;}$productId=isset($params['id'])?(int)$params['id']:0;if(!Model::product($productId)){return $this->error('Товар не найден',array(),404);}try{$data=Attributes::saveProductValues($productId,Request::postAll(),Auth::id());}catch(\Throwable $e){return $this->error($e->getMessage(),array(),422);}$this->audit('catalog.product_attributes_saved',$productId,$data);return $this->success('Характеристики товара сохранены',array('data'=>$data));
}
public function copyProductShippingToVariants(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
$productId = isset($params['id']) ? (int)$params['id'] : 0; $membership = VariantGroups::membership($productId);
if (!$membership) { return $this->error('Товар не входит в группу вариантов', array(), 422); }
$group = VariantGroups::one((int)$membership['id']); $targets = array();
foreach (isset($group['variants']) ? $group['variants'] : array() as $variant) { $targets[] = (int)$variant['product_id']; }
try { $done = ProductShipping::copy($productId, $targets); }
catch (\Throwable $e) { return $this->error($e->getMessage(), array(), 422); }
$this->audit('catalog.product_shipping_copied', $productId, array('group_id'=>(int)$membership['id'],'updated'=>$done));
return $this->success('Упаковка применена к вариантам: ' . $done, array('data'=>array('updated'=>$done)));
}
public function bulkProductShipping(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
$ids=Request::postJsonArray('ids');if(!is_array($ids)||count($ids)<1||count($ids)>200){return $this->error('Выберите от 1 до 200 товаров',array(),422);}
$enabled=Request::postBool('enabled',false);$done=ProductShipping::setEnabled($ids,$enabled);
$this->audit('catalog.product_shipping_bulk_status',null,array('enabled'=>$enabled,'updated'=>$done));
return $this->success(($enabled?'Расчёт включён: ':'Расчёт выключен: ').$done,array('data'=>array('updated'=>$done)));
}
public function shippingPackageTemplates(array $params = array())
{
if (!Permission::check('view_catalog')) { return $this->error('Недостаточно прав', array(), 403); }
return $this->success('', array('data' => array('templates' => ShippingPackageTemplates::all())));
}
public function saveShippingPackageTemplate(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
$id = isset($params['id']) ? (int) $params['id'] : 0;
try { $template = ShippingPackageTemplates::save($id, Request::postAll()); }
catch (\Throwable $e) { return $this->error($e->getMessage(), array(), 422); }
$this->audit($id > 0 ? 'catalog.shipping_template_saved' : 'catalog.shipping_template_created', $template['id'], array('title' => $template['title']));
return $this->success($id > 0 ? 'Шаблон упаковки сохранён' : 'Шаблон упаковки создан', array('data' => array(
'template' => $template,
'templates' => ShippingPackageTemplates::all(),
)));
}
public function deleteShippingPackageTemplate(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
$id = isset($params['id']) ? (int) $params['id'] : 0;
$template = ShippingPackageTemplates::one($id);
if (!$template) { return $this->error('Шаблон упаковки не найден', array(), 404); }
ShippingPackageTemplates::delete($id);
$this->audit('catalog.shipping_template_deleted', $id, array('title' => $template['title']));
return $this->success('Шаблон упаковки удалён', array('data' => array('templates' => ShippingPackageTemplates::all())));
}
public function createProduct(array $params = array())
{
if (!Permission::check('manage_catalog') || !Permission::check('manage_documents')) {
return $this->renderStatus('@adminx/404.twig', array('title' => 'Недостаточно прав'), 403);
}
$allowed = Model::productRubrics();
$rubricId = Request::getInt('rubric_id', 0);
if (!in_array($rubricId, $allowed, true)) { $rubricId = !empty($allowed) ? (int) reset($allowed) : 0; }
if ($rubricId <= 0) { return $this->renderStatus('@adminx/404.twig', array('title' => 'Товарная рубрика не настроена'), 404); }
$document = DocumentsModel::blank($rubricId);
AdminAssets::addStyle($this->base() . '/modules/Documents/assets/documents.css', 50);
AdminAssets::addScript($this->base() . '/modules/Documents/assets/documents.js', 50);
CodeEditor::useRichEditor(); $this->assets();
return $this->render('@documents/edit.twig', array(
'document' => $document, 'field_groups' => DocumentsModel::fieldsForRubric($rubricId, 0),
'rubrics' => array_values(array_filter(DocumentsModel::rubrics(), function ($rubric) use ($allowed) { return in_array((int) $rubric['Id'], $allowed, true); })),
'templates' => DocumentsModel::rubricTemplates($rubricId), 'navigation_items' => DocumentsModel::navigationItems(),
'authors' => DocumentsModel::authors(), 'is_new' => true, 'can_manage' => true,
'actor_id' => Auth::id(),
'catalog_mode' => true, 'return_url' => $this->base() . '/catalog/products',
'submit_url' => $this->base() . '/documents',
'stay_url_template' => $this->base() . '/catalog/products/{id}/edit',
));
}
public function edit(array $params = array())
{
if (!Permission::check('view_catalog')) { return $this->renderStatus('@adminx/404.twig', array('title' => 'Недостаточно прав'), 403); }
$rubricId = isset($params['rubric']) ? (int) $params['rubric'] : 0;
$fieldId = isset($params['field']) ? (int) $params['field'] : 0;
$catalog = Model::catalog($rubricId, $fieldId);
if (!$catalog) { return $this->renderStatus('@adminx/404.twig', array('title' => 'Каталог не найден'), 404); }
$this->assets();
$fields = Model::rubricFields($rubricId);
$fieldGroups = array();
foreach ($fields as &$field) {
$field['filter_options'] = Model::filterStyleOptions($field['type']);
$editor = FieldAdminEditors::describe($field['type']);
$field['type_title'] = isset($editor['title']) && $editor['title'] !== '' ? (string) $editor['title'] : (string) $field['type'];
$groupId = (int) $field['group_id'];
if (!isset($fieldGroups[$groupId])) {
$fieldGroups[$groupId] = array(
'id' => $groupId,
'title' => $field['group'] !== '' ? $field['group'] : 'Без группы',
'items' => array(),
);
}
$fieldGroups[$groupId]['items'][] = &$field;
}
unset($field);
$settings = Model::settings($rubricId, $fieldId);
$productsAvailable = $this->productsAvailable();
$filterTemplatesAvailable = $productsAvailable && FilterTemplates::available();
$settings['fields_default_ids'] = $this->ids(isset($settings['fields_default']) ? $settings['fields_default'] : '');
$settings['filters_default_ids'] = $this->ids(isset($settings['filters_default']) ? $settings['filters_default'] : '');
$styles = @unserialize(isset($settings['filters_default_settings']) ? (string) $settings['filters_default_settings'] : '', array('allowed_classes' => false));
$settings['filter_styles'] = is_array($styles) ? $styles : array();
return $this->render('@catalog/edit.twig', array(
'catalog' => $catalog, 'settings' => $settings,
'tree' => Model::tree($rubricId, $fieldId), 'flat' => Model::items($rubricId, $fieldId),
'fields' => $fields,
'field_groups' => array_values($fieldGroups),
'request_options' => Model::requestOptions(),
'navigation_options' => Model::navigationOptions(),
'rubric_options' => Model::rubricOptions(),
'card_template_options' => $productsAvailable ? CardTemplates::options() : array(),
'filter_template_options' => $filterTemplatesAvailable ? FilterTemplates::options() : array(),
'filter_templates_available' => $filterTemplatesAvailable,
'products_available' => $productsAvailable,
'can_manage' => Permission::check('manage_catalog'),
));
}
public function item(array $params = array())
{
if (!Permission::check('view_catalog')) { return $this->error('Недостаточно прав', array(), 403); }
$item = Model::item(isset($params['id']) ? (int) $params['id'] : 0);
if ($item) { $item['condition_context'] = Model::conditionContext($item['id']); }
return $item ? $this->success('', array('data' => $item)) : $this->error('Раздел не найден', array(), 404);
}
public function documents(array $params = array())
{
if (!Permission::check('view_catalog')) { return $this->error('Недостаточно прав', array(), 403); }
return $this->success('', array('data' => array(
'items' => Model::searchDocuments(Request::getStr('q', ''), Request::getInt('limit', 30)),
)));
}
public function storeItem(array $params = array()) { return $this->persistItem(0, $params); }
public function updateItem(array $params = array())
{
return $this->persistItem(isset($params['id']) ? (int) $params['id'] : 0, $params);
}
public function deleteItem(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
$id = isset($params['id']) ? (int) $params['id'] : 0;
$item = Model::item($id);
if (!$item || $item['rubric_id'] !== (int) $params['rubric'] || $item['field_id'] !== (int) $params['field']) {
return $this->error('Раздел не найден', array(), 404);
}
$deleted = Model::deleteItem($id);
$this->audit('catalog.item_deleted', $id, array('ids' => $deleted));
return $this->success('Раздел и вложенные разделы удалены');
}
public function reorder(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
$rows = Request::postJsonArray('order');
if (!is_array($rows)) { return $this->error('Некорректный порядок разделов', array(), 422); }
try { Model::reorder((int) $params['rubric'], (int) $params['field'], $rows); }
catch (\Throwable $e) { return $this->error($e->getMessage(), array(), 422); }
$this->audit('catalog.reordered', null, array('rubric_id' => (int) $params['rubric'], 'field_id' => (int) $params['field']));
return $this->success('Порядок разделов сохранён');
}
public function setItemStatus(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
$id = isset($params['id']) ? (int) $params['id'] : 0;
try { $status = Model::setItemStatus($id, (int) $params['rubric'], (int) $params['field'], Request::postBool('status', false)); }
catch (\Throwable $e) { return $this->error($e->getMessage(), array(), 404); }
$this->audit('catalog.item_status_changed', $id, array('status' => $status));
return $this->success($status ? 'Раздел включён' : 'Раздел скрыт', array('data' => array('status' => $status)));
}
public function recompileItem(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
$id = isset($params['id']) ? (int) $params['id'] : 0;
try { $result = Model::recompileItem((int) $params['rubric'], (int) $params['field'], $id); }
catch (\Throwable $e) { return $this->error($e->getMessage(), array(), 422); }
$this->audit('catalog.filters_recompiled', $id, array('rubric_id' => (int) $params['rubric'], 'field_id' => (int) $params['field']));
return $this->success('Фильтры раздела пересобраны', array('data' => $result));
}
public function saveSettings(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
try { $id = Model::saveSettings((int) $params['rubric'], (int) $params['field'], Request::postAll()); }
catch (\Throwable $e) { return $this->error($e->getMessage(), array(), 422); }
$this->audit('catalog.settings_saved', $id, array('rubric_id' => (int) $params['rubric'], 'field_id' => (int) $params['field']));
return $this->success('Настройки каталога сохранены');
}
public function syncCondition(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
$itemId = isset($params['id']) ? (int) $params['id'] : 0;
$filterId = isset($params['filter']) ? (int) $params['filter'] : 0;
if (!$this->itemInCatalog($itemId, $params)) { return $this->error('Раздел не найден', array(), 404); }
try { $context = Model::syncCondition($itemId, $filterId, Request::postBool('force', false)); }
catch (\Throwable $e) { return $this->error($e->getMessage(), array(), strpos($e->getMessage(), 'отличается') !== false ? 409 : 422); }
$this->audit('catalog.condition_synced', $itemId, array('field_id' => $filterId, 'request_id' => (int) $context['request']['id']));
return $this->success('Условие запроса синхронизировано', array('data' => array('condition_context' => $context)));
}
public function syncMissingConditions(array $params = array())
{
if (($error = $this->guard()) !== null) { return $error; }
$itemId = isset($params['id']) ? (int) $params['id'] : 0;
if (!$this->itemInCatalog($itemId, $params)) { return $this->error('Раздел не найден', array(), 404); }
try { $context = Model::syncMissingConditions($itemId); }
catch (\Throwable $e) { return $this->error($e->getMessage(), array(), 422); }
$this->audit('catalog.conditions_synced', $itemId, array('created' => (int) $context['created'], 'request_id' => (int) $context['request']['id']));
$message = (int) $context['created'] > 0 ? 'Недостающие условия созданы: ' . (int) $context['created'] : 'Все доступные условия уже созданы';
return $this->success($message, array('data' => array('condition_context' => $context)));
}
protected function persistItem($id, array $params)
{
if (($error = $this->guard()) !== null) { return $error; }
try { $saved = Model::saveItem($id, (int) $params['rubric'], (int) $params['field'], Request::postAll()); }
catch (\Throwable $e) { return $this->error($e->getMessage(), array('name' => $e->getMessage()), 422); }
$this->audit($id > 0 ? 'catalog.item_saved' : 'catalog.item_created', $saved, array('rubric_id' => (int) $params['rubric'], 'field_id' => (int) $params['field']));
return $this->success($id > 0 ? 'Раздел сохранён' : 'Раздел создан', array('data' => array(
'id' => $saved,
'condition_context' => Model::conditionContext($saved),
)));
}
protected function persistCardTemplate($id)
{
if (($error = $this->guard()) !== null) { return $error; }
try { $saved = CardTemplates::save((int) $id, Request::postAll(), Auth::id()); }
catch (\Throwable $e) { return $this->error($e->getMessage(), array('draft_markup' => $e->getMessage()), 422); }
$this->audit($id > 0 ? 'catalog.card_template_saved' : 'catalog.card_template_created', $saved);
return $this->success($id > 0 ? 'Черновик сохранён' : 'Представление создано', array('data' => array('id' => $saved)));
}
protected function persistFilterTemplate($id)
{
if (($error = $this->guard()) !== null) { return $error; }
try { $saved = FilterTemplates::save((int) $id, Request::postAll(), Auth::id()); }
catch (\Throwable $e) { return $this->error($e->getMessage(), array('draft_markup' => $e->getMessage()), 422); }
$this->audit($id > 0 ? 'catalog.filter_template_saved' : 'catalog.filter_template_created', $saved);
return $this->success($id > 0 ? 'Черновик сохранён' : 'Представление создано', array('data' => array('id' => $saved)));
}
protected function bootPublicCatalogPreview()
{
$path = BASEPATH . '/modules/products/app';
Load::regNamespace('App\\Modules\\Products', $path);
Twig::addPath($path . '/view', 'products_public');
ThemeAssets::registerViewOverrides();
}
protected function itemInCatalog($id, array $params)
{
$item = Model::item((int) $id);
return $item && $item['rubric_id'] === (int) $params['rubric'] && $item['field_id'] === (int) $params['field'];
}
protected function guard()
{
return $this->guardPermission('manage_catalog');
}
protected function savedViewGuard()
{
if (($error = $this->csrfGuard()) !== null) { return $error; }
if (!Permission::check('view_catalog')) { return $this->error('Недостаточно прав', array(), 403); }
return null;
}
protected function savedViewDefinition($scope)
{
$definitions = array(
'products' => array('scope' => 'catalog_products', 'fields' => $this->savedViewFields('products')),
'quality' => array('scope' => 'catalog_quality', 'fields' => $this->savedViewFields('quality')),
);
return isset($definitions[$scope]) ? $definitions[$scope] : null;
}
protected function savedViewFields($scope)
{
return $scope === 'quality'
? array('q', 'issue', 'state', 'per_page')
: array('q', 'state', 'issue', 'payment', 'per_page');
}
protected function quickEditData()
{
$filters = array(
'q' => Request::getStr('q', ''), 'rubric_id' => Request::getInt('rubric_id', 0),
'category_id' => Request::getInt('category_id', 0), 'state' => Request::getStr('state', ''),
'sfr' => Request::getStr('sfr', ''), 'sfr_code' => Request::getStr('sfr_code', ''),
'incomplete' => Request::getBool('incomplete', false), 'limit' => Request::getInt('limit', 25),
'page' => Request::getInt('page', 1),
);
return array('result' => QuickEditor::items($filters), 'filters' => $filters,
'rubrics' => QuickEditor::rubrics(), 'categories' => QuickEditor::categories(),
'can_manage' => Permission::check('manage_catalog'));
}
protected function assets()
{
AdminAssets::addStyle($this->base() . '/modules/Catalog/assets/catalog.css', 50);
AdminAssets::addStyle($this->base() . '/modules/Catalog/assets/catalog-drawer.css', 51);
AdminAssets::addScript($this->base() . '/modules/Catalog/assets/catalog.js', 50);
if ($this->productsAvailable()) {
if (is_file(BASEPATH . '/modules/products/admin/assets/products.css')) {
AdminAssets::addStyle('/modules/products/admin/assets/products.css', 52);
}
if (is_file(BASEPATH . '/modules/products/admin/assets/products.js')) {
AdminAssets::addScript('/modules/products/admin/assets/products.js', 51);
}
}
}
protected 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');
}
protected function attributeAssets()
{
$this->assets();
AdminAssets::addStyle('/modules/products/admin/assets/attributes.css', 54);
AdminAssets::addStyle('/modules/products/admin/assets/attributes-responsive.css', 55);
AdminAssets::addScript('/modules/products/admin/assets/attributes.js', 54);
}
protected function shippingAssets()
{
$this->assets();
AdminAssets::addStyle('/modules/products/admin/assets/shipping.css', 54);
AdminAssets::addScript('/modules/products/admin/assets/shipping.js', 54);
}
protected function relationAssets()
{
$this->assets();
AdminAssets::addStyle('/modules/products/admin/assets/product-relations.css', 54);
AdminAssets::addScript('/modules/products/admin/assets/product-relations.js', 54);
}
protected function collectionAssets()
{
$this->assets();
AdminAssets::addStyle('/modules/products/admin/assets/product-collections.css', 54);
AdminAssets::addScript('/modules/products/admin/assets/product-collections.js', 54);
}
protected function cardTemplateAssets()
{
$this->assets();
CodeEditor::useCodeMirror('htmlmixed');
CodeEditor::useCodeMirror('text/css');
AdminAssets::addStyle('/modules/products/admin/assets/card-templates.css', 56);
AdminAssets::addScript('/modules/products/admin/assets/card-templates.js', 56);
}
protected function audit($action, $targetId = null, array $meta = array())
{
$user = Auth::user();
AuditLog::record($action, array(
'actor_id' => Auth::id(), 'actor_name' => $user && isset($user['name']) ? (string) $user['name'] : '',
'target_type' => 'catalog', 'target_id' => $targetId, 'meta' => $meta,
));
}
protected function ids($value)
{
$out = array();
foreach (preg_split('/[^0-9]+/', (string) $value) ?: array() as $id) {
if ((int) $id > 0) { $out[] = (int) $id; }
}
return array_values(array_unique($out));
}
}