mirror of
https://github.com/avecms/AVE.cms.git
synced 2026-08-01 08:45:44 +00:00
1243 lines
46 KiB
PHP
1243 lines
46 KiB
PHP
<?php
|
|
|
|
/*
|
|
|--------------------------------------------------------------------------------------
|
|
| AVE.cms
|
|
|--------------------------------------------------------------------------------------
|
|
| @package AVE.cms
|
|
| @file adminx/modules/Requests/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\Requests;
|
|
|
|
defined('BASEPATH') || die('Direct access to this location is not allowed.');
|
|
|
|
use DB;
|
|
use App\Common\Auth;
|
|
use App\Common\FileCacheInvalidator;
|
|
use App\Common\Lock;
|
|
use App\Common\SystemTables;
|
|
use App\Content\ContentTables;
|
|
use App\Content\Fields\FieldRegistry;
|
|
use App\Content\Fields\FieldSettings;
|
|
use App\Content\Requests\RequestConditionValue;
|
|
|
|
/**
|
|
* Запросы (листинги) и их условия. Данные ведём как есть; шаблоны (item/main) и
|
|
* значения условий (в т.ч. legacy-PHP) сохраняются без изменений и не исполняются.
|
|
*/
|
|
class Model
|
|
{
|
|
protected static function req() { return ContentTables::table('request'); }
|
|
protected static function cond() { return ContentTables::table('request_conditions'); }
|
|
protected static function groups() { return ContentTables::table('request_condition_groups'); }
|
|
|
|
/** Редактируемые скалярные поля запроса. */
|
|
protected static $scalar = array(
|
|
'request_alias', 'rubric_id', 'request_title', 'request_description',
|
|
'request_order_by', 'request_asc_desc', 'request_items_per_page',
|
|
'request_pagination', 'request_template_item', 'request_template_main',
|
|
'request_cache_lifetime', 'request_result_contract',
|
|
'request_preview_renderer', 'request_executor_mode', 'request_order_by_nat',
|
|
'request_order_tiebreaker', 'request_sort_rules',
|
|
);
|
|
|
|
/** Поля-флаги enum('0','1'). */
|
|
public static $flags = array(
|
|
'request_show_pagination', 'request_count_items', 'request_hide_current',
|
|
'request_only_owner', 'request_ajax', 'request_external',
|
|
'request_cache_elements', 'request_show_statistic',
|
|
'request_show_sql', 'request_use_query',
|
|
);
|
|
|
|
/** Операторы условий (легаси-коды). */
|
|
public static function compareOptions()
|
|
{
|
|
return array(
|
|
'==' => 'Равно',
|
|
'!=' => 'Не равно',
|
|
'%%' => 'Содержит текст',
|
|
'%' => 'Начинается с',
|
|
'--' => 'Не содержит текст',
|
|
'!-' => 'Не начинается с',
|
|
'>' => 'Больше',
|
|
'<' => 'Меньше',
|
|
'>=' => 'Больше или равно',
|
|
'<=' => 'Меньше или равно',
|
|
'N>' => 'Число больше',
|
|
'N<' => 'Число меньше',
|
|
'N>=' => 'Число больше или равно',
|
|
'N<=' => 'Число меньше или равно',
|
|
'SEGMENT' => 'Диапазон, включая границы',
|
|
'INTERVAL' => 'Диапазон, не включая границы',
|
|
'IN=' => 'Входит в список',
|
|
'NOTIN=' => 'Не входит в список',
|
|
'ANY' => 'Совпадает с любым',
|
|
'FRE' => 'Legacy SQL (совместимость)',
|
|
);
|
|
}
|
|
|
|
public static function conditionValueModes()
|
|
{
|
|
return array(
|
|
'string_scalar' => 'Взять текст как есть',
|
|
'integer_scalar' => 'Взять целое число',
|
|
'decimal_scalar' => 'Взять десятичное число',
|
|
'string_list' => 'Взять список текстовых значений',
|
|
'integer_list' => 'Взять список чисел',
|
|
'integer_range' => 'Взять диапазон от/до',
|
|
'decimal_range' => 'Взять десятичный диапазон',
|
|
'integer_member' => 'Обернуть значение: |значение|',
|
|
'integer_member_list' => 'Обернуть список: |значение|',
|
|
'presence_constant' => 'Подставить константу, если заполнено',
|
|
);
|
|
}
|
|
|
|
/** Сводка для карточек списка. */
|
|
public static function stats()
|
|
{
|
|
return array(
|
|
'requests' => (int) DB::query('SELECT COUNT(*) FROM ' . self::req())->getValue(),
|
|
'conditions' => (int) DB::query('SELECT COUNT(*) FROM ' . self::cond())->getValue(),
|
|
'groups' => (int) DB::query('SELECT COUNT(*) FROM ' . self::groups())->getValue(),
|
|
'rubrics' => (int) DB::query('SELECT COUNT(DISTINCT rubric_id) FROM ' . self::req())->getValue(),
|
|
);
|
|
}
|
|
|
|
public static function all()
|
|
{
|
|
return DB::query(
|
|
'SELECT r.Id, r.request_alias, r.rubric_id, r.request_title, r.request_order_by,
|
|
r.request_asc_desc, r.request_items_per_page, r.request_changed,
|
|
r.request_executor_mode, r.request_native_verified_hash,
|
|
(SELECT COUNT(*) FROM ' . self::cond() . ' c WHERE c.request_id = r.Id) AS cond_count
|
|
FROM ' . self::req() . ' r
|
|
ORDER BY r.request_alias ASC'
|
|
)->getAll();
|
|
}
|
|
|
|
public static function nativeAuditRows()
|
|
{
|
|
$rows = array();
|
|
foreach (DB::query(
|
|
'SELECT Id,request_alias,request_title,rubric_id,request_executor_mode,request_native_verified_hash,request_native_plan,'
|
|
. 'request_native_audit_status,request_native_audit_details'
|
|
. ' FROM ' . self::req() . ' ORDER BY request_title ASC,Id ASC'
|
|
)->getAll() as $request) {
|
|
$request = (object) $request;
|
|
$plan = \App\Content\Requests\NativeRequestPlanCompiler::compile((int) $request->Id);
|
|
$details = isset($request->request_native_audit_details)
|
|
? json_decode((string) $request->request_native_audit_details, true)
|
|
: array();
|
|
$rows[] = array(
|
|
'id' => (int) $request->Id,
|
|
'alias' => (string) $request->request_alias,
|
|
'title' => (string) $request->request_title,
|
|
'rubric_id' => (int) $request->rubric_id,
|
|
'mode' => self::executorMode($request),
|
|
'eligible' => !empty($plan['eligible']),
|
|
'verified' => self::nativeVerified($request, $plan),
|
|
'reasons' => isset($plan['reasons']) ? array_values($plan['reasons']) : array(),
|
|
'audit_status' => isset($request->request_native_audit_status)
|
|
? (string) $request->request_native_audit_status
|
|
: '',
|
|
'audit_details' => is_array($details) ? $details : array(),
|
|
);
|
|
}
|
|
|
|
return $rows;
|
|
}
|
|
|
|
public static function nativeAuditStats(array $rows)
|
|
{
|
|
$stats = array(
|
|
'total' => count($rows),
|
|
'eligible' => 0,
|
|
'verified' => 0,
|
|
'legacy' => 0,
|
|
'shadow' => 0,
|
|
'native' => 0,
|
|
'unstable' => 0,
|
|
'order_diff' => 0,
|
|
);
|
|
foreach ($rows as $row) {
|
|
if (!empty($row['eligible'])) { $stats['eligible']++; }
|
|
if (!empty($row['verified'])) { $stats['verified']++; }
|
|
$mode = isset($row['mode']) && isset($stats[$row['mode']]) ? $row['mode'] : 'legacy';
|
|
$stats[$mode]++;
|
|
$status = isset($row['audit_status']) ? (string) $row['audit_status'] : '';
|
|
if (isset($stats[$status])) { $stats[$status]++; }
|
|
}
|
|
|
|
return $stats;
|
|
}
|
|
|
|
public static function setAllExecutorMode($mode)
|
|
{
|
|
$mode = in_array($mode, array('legacy', 'shadow'), true) ? $mode : 'legacy';
|
|
$requests = DB::query('SELECT Id,request_alias FROM ' . self::req())->getAll();
|
|
DB::query('UPDATE ' . self::req() . ' SET request_executor_mode=%s', $mode);
|
|
$affected = (int) DB::affectedRows();
|
|
\App\Frontend\RequestRepository::reset();
|
|
foreach ($requests as $request) {
|
|
FileCacheInvalidator::request((int) $request['Id'], (string) $request['request_alias']);
|
|
}
|
|
|
|
return $affected;
|
|
}
|
|
|
|
/** Adds typed descriptors beside legacy PHP; public Legacy execution is unchanged. */
|
|
public static function stageLegacyConditionValues()
|
|
{
|
|
$count = 0;
|
|
$requests = array();
|
|
$rows = DB::query(
|
|
'SELECT * FROM ' . self::cond() . " WHERE condition_value LIKE '%<?%' ORDER BY request_id,Id"
|
|
)->getAll();
|
|
foreach ($rows as $row) {
|
|
if (isset($row['condition_value_source']) && $row['condition_value_source'] === 'input') { continue; }
|
|
$descriptor = RequestConditionValue::legacyDescriptor($row['condition_value'], $row['condition_compare']);
|
|
if ($descriptor === null) { continue; }
|
|
DB::Update(self::cond(), array(
|
|
'condition_value_source' => 'input',
|
|
'condition_value_key' => $descriptor['key'],
|
|
'condition_value_config' => RequestConditionValue::encodeConfig($descriptor),
|
|
), 'Id=%i', (int) $row['Id']);
|
|
$requests[(int) $row['request_id']] = true;
|
|
$count++;
|
|
}
|
|
|
|
foreach (array_keys($requests) as $requestId) {
|
|
self::touch((int) $requestId);
|
|
}
|
|
|
|
return array('conditions' => $count, 'requests' => count($requests));
|
|
}
|
|
|
|
public static function activateVerifiedNative()
|
|
{
|
|
$activated = 0;
|
|
$invalidated = array();
|
|
foreach (DB::query('SELECT * FROM ' . self::req() . ' ORDER BY Id')->getAll() as $row) {
|
|
$request = (object) $row;
|
|
$plan = \App\Content\Requests\NativeRequestPlanCompiler::compile((int) $request->Id);
|
|
if (empty($plan['eligible']) || !self::nativeVerified($request, $plan)) {
|
|
continue;
|
|
}
|
|
|
|
$auditDetails = isset($request->request_native_audit_details)
|
|
? json_decode((string) $request->request_native_audit_details, true)
|
|
: array();
|
|
$baseline = is_array($auditDetails) && isset($auditDetails['runtime_verification'])
|
|
? (array) $auditDetails['runtime_verification']
|
|
: array();
|
|
$finalized = self::finalizeStagedConditionValues((int) $request->Id, $baseline);
|
|
if (!$finalized) { continue; }
|
|
if (is_array($finalized)) { self::recordNativeVerification((int) $request->Id, $finalized); }
|
|
DB::Update(self::req(), array('request_executor_mode' => 'native'), 'Id=%i', (int) $request->Id);
|
|
$invalidated[] = array((int) $request->Id, (string) $request->request_alias);
|
|
$activated++;
|
|
}
|
|
|
|
\App\Frontend\RequestRepository::reset();
|
|
foreach ($invalidated as $request) {
|
|
FileCacheInvalidator::request($request[0], $request[1]);
|
|
}
|
|
|
|
return $activated;
|
|
}
|
|
|
|
public static function conditionValuesEquivalent(array $comparison)
|
|
{
|
|
$verification = isset($comparison['runtime_verification']) ? $comparison['runtime_verification'] : array();
|
|
if (!$verification) { return false; }
|
|
foreach ($verification as $item) {
|
|
if (!in_array(isset($item['status']) ? $item['status'] : '', array('matched', 'unstable', 'order_diff'), true)) {
|
|
return false;
|
|
}
|
|
|
|
if ((int) $item['legacy_total'] !== (int) $item['native_total']) { return false; }
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public static function finalizeStagedConditionValues($requestId, array $baseline = array())
|
|
{
|
|
$rows = DB::query(
|
|
'SELECT * FROM ' . self::cond() . " WHERE request_id=%i AND condition_value_source='input' AND condition_value LIKE '%<?%' ORDER BY Id",
|
|
(int) $requestId
|
|
)->getAll();
|
|
if (!$rows) { return true; }
|
|
|
|
$backup = array();
|
|
foreach ($rows as $row) {
|
|
$descriptor = RequestConditionValue::descriptor($row);
|
|
if ($descriptor === null) { return false; }
|
|
$backup[(int) $row['Id']] = (string) $row['condition_value'];
|
|
DB::Update(self::cond(), array(
|
|
'condition_value' => RequestConditionValue::tag($descriptor['key']),
|
|
), 'Id=%i', (int) $row['Id']);
|
|
}
|
|
|
|
\App\Content\Requests\NativeRequestPlanCompiler::persist((int) $requestId);
|
|
try {
|
|
$comparison = (new \App\Content\Requests\RequestExecutorComparator())->compare((int) $requestId, 20);
|
|
} catch (\Throwable $e) {
|
|
$comparison = array('matched' => false);
|
|
}
|
|
|
|
if (!self::sameLegacyVerification($baseline, isset($comparison['runtime_verification']) ? $comparison['runtime_verification'] : array())) {
|
|
foreach ($backup as $conditionId => $value) {
|
|
DB::Update(self::cond(), array('condition_value' => $value), 'Id=%i', (int) $conditionId);
|
|
}
|
|
|
|
\App\Content\Requests\NativeRequestPlanCompiler::persist((int) $requestId);
|
|
return false;
|
|
}
|
|
|
|
return $comparison;
|
|
}
|
|
|
|
protected static function sameLegacyVerification(array $before, array $after)
|
|
{
|
|
if (!$before || count($before) !== count($after)) { return false; }
|
|
foreach ($before as $index => $item) {
|
|
$current = isset($after[$index]) ? $after[$index] : array();
|
|
if ((string) (isset($item['label']) ? $item['label'] : '') !== (string) (isset($current['label']) ? $current['label'] : '')) {
|
|
return false;
|
|
}
|
|
|
|
if ((int) (isset($item['legacy_total']) ? $item['legacy_total'] : -1) !== (int) (isset($current['legacy_total']) ? $current['legacy_total'] : -2)) {
|
|
return false;
|
|
}
|
|
|
|
if ((string) (isset($item['legacy_ids_hash']) ? $item['legacy_ids_hash'] : '') !== (string) (isset($current['legacy_ids_hash']) ? $current['legacy_ids_hash'] : '')) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/** Add an ID tiebreaker only when it preserves the complete current Legacy order. */
|
|
public static function stabilizeNativeOrder($requestId)
|
|
{
|
|
$requestId = (int) $requestId;
|
|
return Lock::run('request-native-order:' . $requestId, function () use ($requestId) {
|
|
$request = self::find($requestId);
|
|
if (!$request) {
|
|
return array('status' => 'missing', 'message' => 'Запрос не найден');
|
|
}
|
|
|
|
$sortRules = self::sortRules($request);
|
|
$currentTie = '';
|
|
if ($sortRules) {
|
|
$lastRule = end($sortRules);
|
|
if ($lastRule['source'] === 'document' && (string) $lastRule['key'] === 'Id') {
|
|
$currentTie = (string) $lastRule['direction'];
|
|
}
|
|
}
|
|
|
|
if ($currentTie !== '') {
|
|
return array('status' => 'already_set', 'message' => 'Финальный порядок уже настроен');
|
|
}
|
|
|
|
$recommendation = (new \App\Content\Requests\RequestTieBreakerAdvisor())->recommend($requestId);
|
|
if ($recommendation['status'] !== 'recommended') {
|
|
return array(
|
|
'status' => $recommendation['status'],
|
|
'message' => isset($recommendation['reasons'][0])
|
|
? (string) $recommendation['reasons'][0]
|
|
: 'Безопасный финальный порядок не найден',
|
|
);
|
|
}
|
|
|
|
$direction = (string) $recommendation['direction'];
|
|
$originalMode = self::executorMode($request);
|
|
$changed = false;
|
|
try {
|
|
$sortRules[] = array('source' => 'document', 'key' => 'Id', 'direction' => $direction);
|
|
$legacySort = \App\Frontend\RequestSort::legacyStorage($sortRules);
|
|
DB::Update(self::req(), array_merge($legacySort, array(
|
|
'request_sort_rules' => \App\Frontend\RequestSort::encodeRules($sortRules),
|
|
'request_executor_mode' => 'shadow',
|
|
)), 'Id=%i', $requestId);
|
|
$changed = true;
|
|
\App\Content\Requests\NativeRequestPlanCompiler::persist($requestId);
|
|
FileCacheInvalidator::request($requestId, (string) $request->request_alias);
|
|
|
|
$comparison = (new \App\Content\Requests\RequestExecutorComparator())->compare($requestId);
|
|
$afterFingerprint = isset($comparison['legacy'])
|
|
? \App\Content\Requests\RequestExecutorComparator::resultFingerprint($comparison['legacy'])
|
|
: '';
|
|
if (empty($comparison['matched'])
|
|
|| !hash_equals((string) $recommendation['legacy_fingerprint'], $afterFingerprint)) {
|
|
self::restoreTieBreaker($request, $originalMode);
|
|
return array(
|
|
'status' => 'rolled_back',
|
|
'message' => 'Порядок изменился при контрольной проверке. Настройка отменена.',
|
|
);
|
|
}
|
|
|
|
self::recordNativeVerification($requestId, $comparison);
|
|
DB::Update(self::req(), array('request_executor_mode' => 'native'), 'Id=%i', $requestId);
|
|
\App\Frontend\RequestRepository::reset();
|
|
FileCacheInvalidator::request($requestId, (string) $request->request_alias);
|
|
|
|
return array(
|
|
'status' => 'applied',
|
|
'message' => 'Порядок закреплён без изменения Legacy-результата',
|
|
'direction' => $direction,
|
|
'total' => isset($recommendation['total']) ? (int) $recommendation['total'] : 0,
|
|
'comparison' => $comparison,
|
|
);
|
|
} catch (\Throwable $e) {
|
|
if ($changed) {
|
|
self::restoreTieBreaker($request, $originalMode);
|
|
}
|
|
|
|
throw $e;
|
|
}
|
|
}, 10.0, true);
|
|
}
|
|
|
|
protected static function restoreTieBreaker($request, $mode)
|
|
{
|
|
DB::Update(self::req(), array(
|
|
'request_order_by' => isset($request->request_order_by) ? (string) $request->request_order_by : '',
|
|
'request_order_by_nat' => isset($request->request_order_by_nat) ? (int) $request->request_order_by_nat : 0,
|
|
'request_asc_desc' => isset($request->request_asc_desc) ? (string) $request->request_asc_desc : 'ASC',
|
|
'request_order_tiebreaker' => isset($request->request_order_tiebreaker) ? (string) $request->request_order_tiebreaker : '',
|
|
'request_sort_rules' => isset($request->request_sort_rules) ? $request->request_sort_rules : null,
|
|
'request_executor_mode' => (string) $mode,
|
|
), 'Id=%i', (int) $request->Id);
|
|
\App\Content\Requests\NativeRequestPlanCompiler::persist((int) $request->Id);
|
|
\App\Frontend\RequestRepository::reset();
|
|
FileCacheInvalidator::request((int) $request->Id, (string) $request->request_alias);
|
|
}
|
|
|
|
public static function find($id)
|
|
{
|
|
return DB::query('SELECT * FROM ' . self::req() . ' WHERE Id = %i LIMIT 1', (int) $id)->getObject();
|
|
}
|
|
|
|
public static function aliasExists($alias, $exceptId = 0)
|
|
{
|
|
return (bool) DB::query(
|
|
'SELECT Id FROM ' . self::req() . ' WHERE request_alias = %s AND Id != %i LIMIT 1',
|
|
(string) $alias, (int) $exceptId
|
|
)->getValue();
|
|
}
|
|
|
|
/** Собрать writable-набор полей из входных данных. */
|
|
protected static function fields(array $data)
|
|
{
|
|
$fields = array();
|
|
foreach (self::$scalar as $f) {
|
|
if (array_key_exists($f, $data)) {
|
|
$fields[$f] = $data[$f];
|
|
}
|
|
}
|
|
|
|
foreach (self::$flags as $f) {
|
|
if (array_key_exists($f, $data)) {
|
|
$fields[$f] = !empty($data[$f]) ? '1' : '0';
|
|
}
|
|
}
|
|
|
|
return $fields;
|
|
}
|
|
|
|
public static function create(array $data)
|
|
{
|
|
$now = time();
|
|
$fields = self::fields($data);
|
|
$fields += array(
|
|
'request_author_id' => Auth::id(),
|
|
'request_created' => $now,
|
|
'request_changed' => $now,
|
|
'request_changed_elements' => $now,
|
|
'request_where_cond' => isset($data['request_where_cond']) ? (string) $data['request_where_cond'] : '',
|
|
);
|
|
DB::Insert(self::req(), $fields);
|
|
$id = DB::insertId();
|
|
self::createRootGroup($id);
|
|
\App\Content\Requests\NativeRequestPlanCompiler::persist($id);
|
|
return $id;
|
|
}
|
|
|
|
public static function update($id, array $data)
|
|
{
|
|
$current = self::find($id);
|
|
$fields = self::fields($data);
|
|
$fields['request_changed'] = $fields['request_changed_elements'] = time();
|
|
$fields['request_where_cond'] = '';
|
|
DB::Update(self::req(), $fields, 'Id = %i', (int) $id);
|
|
\App\Content\Requests\NativeRequestPlanCompiler::persist($id, $fields);
|
|
FileCacheInvalidator::request($id, $current ? $current->request_alias : '');
|
|
}
|
|
|
|
public static function copy($id)
|
|
{
|
|
$row = self::find($id);
|
|
if (!$row) {
|
|
return 0;
|
|
}
|
|
|
|
$data = (array) $row;
|
|
unset($data['Id']);
|
|
$data['request_alias'] = self::uniqueAlias((string) $row->request_alias . '_copy');
|
|
$data['request_title'] = (string) $row->request_title . ' (копия)';
|
|
$data['request_author_id'] = Auth::id();
|
|
$data['request_created'] = time();
|
|
$data['request_changed'] = time();
|
|
$data['request_changed_elements'] = $data['request_changed'];
|
|
$data['request_where_cond'] = '';
|
|
DB::Insert(self::req(), $data);
|
|
$newId = DB::insertId();
|
|
|
|
//-- Сначала копируем дерево групп, затем привязанные к нему условия.
|
|
$groupMap = array();
|
|
foreach (self::groupsFlat($id) as $group) {
|
|
$group = (array) $group;
|
|
$oldId = (int) $group['Id'];
|
|
$parentId = (int) $group['parent_id'];
|
|
DB::Insert(self::groups(), array(
|
|
'request_id' => $newId,
|
|
'parent_id' => $parentId > 0 && isset($groupMap[$parentId]) ? $groupMap[$parentId] : 0,
|
|
'group_title' => (string) $group['group_title'],
|
|
'group_operator' => (string) $group['group_operator'],
|
|
'group_position' => (int) $group['group_position'],
|
|
));
|
|
$groupMap[$oldId] = DB::insertId();
|
|
}
|
|
|
|
if (!$groupMap) {
|
|
$groupMap[0] = self::createRootGroup($newId);
|
|
}
|
|
|
|
foreach (self::conditions($id) as $condition) {
|
|
$copy = (array) $condition;
|
|
unset($copy['Id']);
|
|
$copy['request_id'] = $newId;
|
|
$oldGroup = isset($copy['condition_group_id']) ? (int) $copy['condition_group_id'] : 0;
|
|
$copy['condition_group_id'] = isset($groupMap[$oldGroup])
|
|
? $groupMap[$oldGroup]
|
|
: reset($groupMap);
|
|
DB::Insert(self::cond(), $copy);
|
|
}
|
|
|
|
\App\Content\Requests\NativeRequestPlanCompiler::persist($newId);
|
|
|
|
return $newId;
|
|
}
|
|
|
|
protected static function uniqueAlias($base)
|
|
{
|
|
$base = substr(preg_replace('/[^a-z0-9_-]/', '', strtolower($base)), 0, 20) ?: 'request';
|
|
$alias = $base;
|
|
$i = 1;
|
|
while (self::aliasExists($alias)) {
|
|
$suffix = (string) $i++;
|
|
$alias = substr($base, 0, 20 - strlen($suffix)) . $suffix;
|
|
}
|
|
|
|
return $alias;
|
|
}
|
|
|
|
public static function delete($id)
|
|
{
|
|
$current = self::find($id);
|
|
if (!$current) {
|
|
return false;
|
|
}
|
|
|
|
$dependencies = \App\Content\ContentTagDependencies::request(
|
|
(int) $id,
|
|
isset($current->request_alias) ? (string) $current->request_alias : ''
|
|
);
|
|
if ($dependencies) {
|
|
throw new \RuntimeException(
|
|
'Запрос используется: ' . implode(', ', $dependencies) . '. Сначала уберите эти вызовы.'
|
|
);
|
|
}
|
|
|
|
DB::Delete(self::cond(), 'request_id = %i', (int) $id);
|
|
DB::Delete(self::groups(), 'request_id = %i', (int) $id);
|
|
DB::Delete(self::req(), 'Id = %i', (int) $id);
|
|
FileCacheInvalidator::request($id, $current ? $current->request_alias : '');
|
|
return true;
|
|
}
|
|
|
|
// ---------------- Условия ----------------
|
|
|
|
public static function conditions($requestId)
|
|
{
|
|
return DB::query(
|
|
'SELECT * FROM ' . self::cond() . ' WHERE request_id = %i ORDER BY condition_position ASC, Id ASC',
|
|
(int) $requestId
|
|
)->getAll();
|
|
}
|
|
|
|
public static function rubricOptions()
|
|
{
|
|
return DB::query(
|
|
'SELECT Id, rubric_title FROM ' . ContentTables::table('rubrics') . ' ORDER BY rubric_title ASC, Id ASC'
|
|
)->getAll();
|
|
}
|
|
|
|
public static function rubricFields($rubricId)
|
|
{
|
|
$rows = DB::query(
|
|
'SELECT Id, rubric_field_title, rubric_field_alias, rubric_field_type, rubric_field_numeric,
|
|
rubric_field_default, rubric_field_settings
|
|
FROM ' . ContentTables::table('rubric_fields') . '
|
|
WHERE rubric_id = %i
|
|
ORDER BY rubric_field_position ASC, Id ASC',
|
|
(int) $rubricId
|
|
)->getAll();
|
|
foreach ($rows as $index => $row) {
|
|
$rows[$index] = self::decorateConditionField($row);
|
|
}
|
|
|
|
return $rows;
|
|
}
|
|
|
|
protected static function decorateConditionField($row)
|
|
{
|
|
$row = is_object($row) ? $row : (object) $row;
|
|
$definition = (array) $row;
|
|
$typeCode = isset($definition['rubric_field_type']) ? (string) $definition['rubric_field_type'] : '';
|
|
$type = FieldRegistry::get($typeCode);
|
|
$settings = FieldSettings::effective($definition);
|
|
$kind = self::conditionFieldKind($typeCode, $type, $definition);
|
|
$options = self::conditionFieldOptions(isset($settings['options']) ? $settings['options'] : array());
|
|
$rubrics = array();
|
|
foreach ((array) (isset($settings['rubric']) ? $settings['rubric'] : array()) as $rubricId) {
|
|
$rubricId = (int) $rubricId;
|
|
if ($rubricId > 0) { $rubrics[$rubricId] = $rubricId; }
|
|
}
|
|
|
|
$row->condition_kind = $kind;
|
|
$row->condition_type_label = $type ? (string) $type->name() : $typeCode;
|
|
$row->condition_options = array_values($options);
|
|
$row->condition_rubrics = array_values($rubrics);
|
|
$row->condition_value_mode = self::conditionFieldMode($kind, $typeCode, $options);
|
|
$row->condition_operators = self::conditionFieldOperators($kind, $typeCode);
|
|
return $row;
|
|
}
|
|
|
|
protected static function conditionFieldKind($typeCode, $type, array $definition)
|
|
{
|
|
if (in_array($typeCode, array('checkbox', 'boolean'), true)) { return 'boolean'; }
|
|
if ($typeCode === 'date') { return 'date'; }
|
|
if ($typeCode === 'date_time') { return 'datetime'; }
|
|
if (in_array($typeCode, array('doc_from_rub'), true)) { return 'relation'; }
|
|
if (in_array($typeCode, array('doc_from_rub_all', 'doc_from_rub_check', 'doc_from_rub_search', 'analoque', 'teasers'), true)) { return 'relation_list'; }
|
|
if ($type && $type->isChoice()) { return $type->isMultiple() ? 'choice_list' : 'choice'; }
|
|
if ($type && $type->isNumeric()) { return 'number'; }
|
|
if (!empty($definition['rubric_field_numeric'])) { return 'number'; }
|
|
|
|
return 'text';
|
|
}
|
|
|
|
protected static function conditionFieldOptions($raw)
|
|
{
|
|
$options = array();
|
|
foreach ((array) $raw as $key => $item) {
|
|
if (is_array($item)) {
|
|
$value = isset($item['value']) ? $item['value'] : (isset($item['key']) ? $item['key'] : $key);
|
|
$label = isset($item['label']) ? $item['label'] : (isset($item['title']) ? $item['title'] : $value);
|
|
} else {
|
|
$value = is_string($key) && !ctype_digit($key) ? $key : $item;
|
|
$label = $item;
|
|
}
|
|
|
|
$value = trim((string) $value);
|
|
$label = trim((string) $label);
|
|
if ($value !== '') { $options[$value] = array('value' => $value, 'label' => $label !== '' ? $label : $value); }
|
|
}
|
|
|
|
return $options;
|
|
}
|
|
|
|
protected static function conditionFieldMode($kind, $typeCode, array $options)
|
|
{
|
|
if ($kind === 'relation' || $kind === 'boolean') { return 'integer_scalar'; }
|
|
if ($kind === 'number' || $kind === 'datetime') { return 'decimal_scalar'; }
|
|
if ($typeCode === 'catalog') { return 'integer_member'; }
|
|
if ($kind === 'relation_list') { return 'integer_list'; }
|
|
if ($kind === 'choice_list') {
|
|
return self::optionsAreInteger($options) ? 'integer_list' : 'string_list';
|
|
}
|
|
|
|
if ($kind === 'choice' && self::optionsAreInteger($options)) { return 'integer_scalar'; }
|
|
|
|
return 'string_scalar';
|
|
}
|
|
|
|
protected static function optionsAreInteger(array $options)
|
|
{
|
|
if (!$options) { return false; }
|
|
foreach (array_keys($options) as $value) {
|
|
if (!preg_match('/^-?[0-9]+$/', (string) $value)) { return false; }
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
protected static function conditionFieldOperators($kind, $typeCode)
|
|
{
|
|
if ($kind === 'boolean') { return array('==', '!='); }
|
|
if (in_array($kind, array('number', 'date', 'datetime', 'relation'), true)) {
|
|
return array('==', '!=', 'N>', 'N<', 'N>=', 'N<=', 'SEGMENT', 'INTERVAL', 'IN=', 'NOTIN=');
|
|
}
|
|
|
|
if (in_array($kind, array('choice', 'choice_list', 'relation_list'), true)) {
|
|
return array('==', '!=', '%%', '--', 'IN=', 'NOTIN=');
|
|
}
|
|
|
|
if ($typeCode === 'catalog') { return array('==', '!=', '%%', '--', 'IN=', 'NOTIN='); }
|
|
|
|
return array('==', '!=', '%%', '%', '--', '!-', 'IN=', 'NOTIN=');
|
|
}
|
|
|
|
public static function documentPicker($query, $rubricIds = '', $limit = 30)
|
|
{
|
|
return (new \App\Content\Documents\DocumentPickerRepository())->search($query, $rubricIds, $limit);
|
|
}
|
|
|
|
public static function paginationOptions()
|
|
{
|
|
try {
|
|
return DB::query(
|
|
'SELECT id,pagination_name FROM ' . SystemTables::table('paginations') . ' ORDER BY id ASC'
|
|
)->getAll();
|
|
} catch (\Throwable $e) {
|
|
return array();
|
|
}
|
|
}
|
|
|
|
public static function resultContract($request)
|
|
{
|
|
$rubricId = is_object($request) && isset($request->rubric_id) ? (int) $request->rubric_id : 0;
|
|
$value = is_object($request) && isset($request->request_result_contract)
|
|
? $request->request_result_contract
|
|
: null;
|
|
|
|
return \App\Content\Requests\RequestResultContract::decode($value, self::rubricFieldIds($rubricId));
|
|
}
|
|
|
|
public static function encodeResultContract($rubricId, array $value)
|
|
{
|
|
return \App\Content\Requests\RequestResultContract::encode($value, self::rubricFieldIds($rubricId));
|
|
}
|
|
|
|
public static function previewRenderer($request)
|
|
{
|
|
$value = is_object($request) && isset($request->request_preview_renderer)
|
|
? $request->request_preview_renderer
|
|
: 'data_cards';
|
|
return \App\Content\Requests\RequestRendererRegistry::normalizePreviewCode($value);
|
|
}
|
|
|
|
public static function sortRules($request)
|
|
{
|
|
return \App\Frontend\RequestSort::rulesForRequest($request);
|
|
}
|
|
|
|
public static function previewPlan($requestId)
|
|
{
|
|
$request = self::find($requestId);
|
|
if (!$request) { return array(); }
|
|
|
|
return array(
|
|
'rubric_id' => (int) $request->rubric_id,
|
|
'groups' => (int) DB::query(
|
|
'SELECT COUNT(*) FROM ' . self::groups() . ' WHERE request_id=%i',
|
|
(int) $requestId
|
|
)->getValue(),
|
|
'conditions' => (int) DB::query(
|
|
"SELECT COUNT(*) FROM " . self::cond() . " WHERE request_id=%i AND condition_status='1'",
|
|
(int) $requestId
|
|
)->getValue(),
|
|
'order' => self::sortSummary($request),
|
|
'publication' => 'Опубликован, не удалён и входит в срок публикации',
|
|
);
|
|
}
|
|
|
|
protected static function sortSummary($request)
|
|
{
|
|
$rules = self::sortRules($request);
|
|
if (!$rules) { return 'Без явной сортировки'; }
|
|
|
|
$labels = array();
|
|
$options = \App\Frontend\RequestSort::storedOptions();
|
|
$fields = array();
|
|
foreach (self::rubricFields((int) $request->rubric_id) as $field) {
|
|
$fields[(int) $field->Id] = (string) $field->rubric_field_title;
|
|
}
|
|
|
|
foreach ($rules as $rule) {
|
|
if ($rule['source'] === 'field') {
|
|
$label = isset($fields[(int) $rule['key']]) ? $fields[(int) $rule['key']] : 'Поле #' . (int) $rule['key'];
|
|
} elseif ($rule['source'] === 'random') {
|
|
$labels[] = 'Случайный порядок';
|
|
continue;
|
|
} else {
|
|
$label = isset($options[$rule['key']]) ? $options[$rule['key']] : (string) $rule['key'];
|
|
}
|
|
|
|
$labels[] = $label . ($rule['direction'] === 'DESC' ? ' ↓' : ' ↑');
|
|
}
|
|
|
|
return implode(' → ', $labels);
|
|
}
|
|
|
|
public static function nativePlan($requestId)
|
|
{
|
|
return \App\Content\Requests\NativeRequestPlanCompiler::compile((int) $requestId);
|
|
}
|
|
|
|
public static function nativeVerified($request, array $plan)
|
|
{
|
|
$verified = is_object($request) && isset($request->request_native_verified_hash)
|
|
? (string) $request->request_native_verified_hash
|
|
: '';
|
|
return $verified !== '' && isset($plan['source_hash']) && hash_equals($verified, (string) $plan['source_hash']);
|
|
}
|
|
|
|
public static function recordNativeVerification($requestId, array $comparison)
|
|
{
|
|
$hash = !empty($comparison['matched']) && isset($comparison['plan']['source_hash'])
|
|
? (string) $comparison['plan']['source_hash']
|
|
: null;
|
|
$details = array(
|
|
'reasons' => isset($comparison['reasons']) ? array_values((array) $comparison['reasons']) : array(),
|
|
'legacy_total' => isset($comparison['legacy']['total']) ? (int) $comparison['legacy']['total'] : null,
|
|
'native_total' => isset($comparison['native']['total']) ? (int) $comparison['native']['total'] : null,
|
|
'missing_ids' => isset($comparison['missing_ids']) ? array_slice(array_values((array) $comparison['missing_ids']), 0, 20) : array(),
|
|
'extra_ids' => isset($comparison['extra_ids']) ? array_slice(array_values((array) $comparison['extra_ids']), 0, 20) : array(),
|
|
'order_diagnostics' => isset($comparison['order_diagnostics']) && is_array($comparison['order_diagnostics'])
|
|
? $comparison['order_diagnostics']
|
|
: array(),
|
|
'runtime_verification' => isset($comparison['runtime_verification']) && is_array($comparison['runtime_verification'])
|
|
? $comparison['runtime_verification']
|
|
: array(),
|
|
'checked_at' => time(),
|
|
);
|
|
$fields = array(
|
|
'request_native_verified_hash' => $hash,
|
|
'request_native_audit_status' => isset($comparison['status']) ? (string) $comparison['status'] : 'error',
|
|
'request_native_audit_details' => json_encode($details, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
|
);
|
|
if (isset($comparison['plan']) && is_array($comparison['plan'])) {
|
|
$fields['request_native_plan'] = \App\Content\Requests\NativeRequestPlanCompiler::encode($comparison['plan']);
|
|
}
|
|
|
|
DB::Update(self::req(), $fields, 'Id=%i', (int) $requestId);
|
|
\App\Frontend\RequestRepository::reset();
|
|
}
|
|
|
|
public static function executorMode($request)
|
|
{
|
|
$mode = is_object($request) && isset($request->request_executor_mode)
|
|
? strtolower((string) $request->request_executor_mode)
|
|
: 'legacy';
|
|
return in_array($mode, array('legacy', 'shadow', 'native'), true) ? $mode : 'legacy';
|
|
}
|
|
|
|
public static function explainDocuments($requestId, $query = '', $limit = 12)
|
|
{
|
|
$request = self::find($requestId);
|
|
if (!$request) { return array(); }
|
|
|
|
$query = trim((string) $query);
|
|
$limit = max(1, min(20, (int) $limit));
|
|
$sql = 'SELECT Id,document_title,document_alias,document_status,document_deleted'
|
|
. ' FROM ' . ContentTables::table('documents') . ' WHERE rubric_id=%i';
|
|
$args = array((int) $request->rubric_id);
|
|
if ($query !== '') {
|
|
$sql .= ' AND (document_title LIKE %ss OR document_alias LIKE %ss OR Id=%i)';
|
|
$args[] = $query;
|
|
$args[] = $query;
|
|
$args[] = (int) $query;
|
|
}
|
|
|
|
$sql .= ' ORDER BY document_changed DESC,Id DESC LIMIT ' . $limit;
|
|
$rows = call_user_func_array(array('DB', 'query'), array_merge(array($sql), $args))->getAll();
|
|
$result = array();
|
|
foreach ($rows ?: array() as $row) {
|
|
$result[] = array(
|
|
'id' => (int) $row['Id'],
|
|
'title' => htmlspecialchars_decode((string) $row['document_title'], ENT_QUOTES),
|
|
'alias' => (string) $row['document_alias'],
|
|
'status' => (int) $row['document_status'],
|
|
'deleted' => (int) $row['document_deleted'],
|
|
);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
protected static function rubricFieldIds($rubricId)
|
|
{
|
|
$result = array();
|
|
foreach (self::rubricFields((int) $rubricId) as $field) {
|
|
$result[] = (int) $field->Id;
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
public static function groupsFlat($requestId)
|
|
{
|
|
return DB::query(
|
|
'SELECT * FROM ' . self::groups() . '
|
|
WHERE request_id = %i ORDER BY parent_id ASC, group_position ASC, Id ASC',
|
|
(int) $requestId
|
|
)->getAll();
|
|
}
|
|
|
|
public static function conditionTree($requestId)
|
|
{
|
|
$groups = self::groupsFlat($requestId);
|
|
if (!$groups) {
|
|
self::createRootGroup($requestId);
|
|
$groups = self::groupsFlat($requestId);
|
|
}
|
|
|
|
$nodes = array();
|
|
foreach ($groups as $group) {
|
|
$item = (array) $group;
|
|
$item['conditions'] = array();
|
|
$item['children'] = array();
|
|
$item['depth'] = 0;
|
|
$nodes[(int) $group->Id] = $item;
|
|
}
|
|
|
|
foreach (self::conditions($requestId) as $condition) {
|
|
$groupId = isset($condition->condition_group_id) ? (int) $condition->condition_group_id : 0;
|
|
if (!isset($nodes[$groupId])) {
|
|
$groupId = self::rootGroupId($requestId);
|
|
}
|
|
|
|
if (isset($nodes[$groupId])) {
|
|
$nodes[$groupId]['conditions'][] = self::conditionPresentation((array) $condition);
|
|
}
|
|
}
|
|
|
|
$roots = array();
|
|
foreach (array_keys($nodes) as $groupId) {
|
|
$parentId = (int) $nodes[$groupId]['parent_id'];
|
|
if ($parentId > 0 && isset($nodes[$parentId])) {
|
|
$nodes[$groupId]['depth'] = min(4, (int) $nodes[$parentId]['depth'] + 1);
|
|
$nodes[$parentId]['children'][] =& $nodes[$groupId];
|
|
} else {
|
|
$roots[] =& $nodes[$groupId];
|
|
}
|
|
}
|
|
|
|
return $roots ? $roots[0] : array();
|
|
}
|
|
|
|
public static function rootGroupId($requestId)
|
|
{
|
|
$id = (int) DB::query(
|
|
'SELECT Id FROM ' . self::groups() . ' WHERE request_id = %i AND parent_id = 0 ORDER BY Id LIMIT 1',
|
|
(int) $requestId
|
|
)->getValue();
|
|
return $id > 0 ? $id : self::createRootGroup($requestId);
|
|
}
|
|
|
|
protected static function createRootGroup($requestId)
|
|
{
|
|
DB::Insert(self::groups(), array(
|
|
'request_id' => (int) $requestId,
|
|
'parent_id' => 0,
|
|
'group_title' => '',
|
|
'group_operator' => 'AND',
|
|
'group_position' => 0,
|
|
));
|
|
return DB::insertId();
|
|
}
|
|
|
|
public static function findGroup($id)
|
|
{
|
|
return DB::query('SELECT * FROM ' . self::groups() . ' WHERE Id = %i LIMIT 1', (int) $id)->getObject();
|
|
}
|
|
|
|
public static function groupSave($requestId, array $data)
|
|
{
|
|
$id = isset($data['id']) ? (int) $data['id'] : 0;
|
|
$parentId = isset($data['parent_id']) ? (int) $data['parent_id'] : 0;
|
|
$operator = isset($data['group_operator']) && $data['group_operator'] === 'OR' ? 'OR' : 'AND';
|
|
$title = trim(isset($data['group_title']) ? (string) $data['group_title'] : '');
|
|
$title = function_exists('mb_substr') ? mb_substr($title, 0, 100) : substr($title, 0, 100);
|
|
|
|
if ($id > 0) {
|
|
$group = self::findGroup($id);
|
|
if (!$group || (int) $group->request_id !== (int) $requestId) {
|
|
return 0;
|
|
}
|
|
|
|
DB::Update(self::groups(), array(
|
|
'group_title' => $title,
|
|
'group_operator' => $operator,
|
|
), 'Id = %i', $id);
|
|
self::touch($requestId);
|
|
return $id;
|
|
}
|
|
|
|
$parent = self::findGroup($parentId);
|
|
if (!$parent || (int) $parent->request_id !== (int) $requestId || self::groupDepth($parentId) >= 3) {
|
|
return 0;
|
|
}
|
|
|
|
$position = (int) DB::query(
|
|
'SELECT COALESCE(MAX(group_position), -1) + 1 FROM ' . self::groups() . '
|
|
WHERE request_id = %i AND parent_id = %i',
|
|
(int) $requestId, $parentId
|
|
)->getValue();
|
|
DB::Insert(self::groups(), array(
|
|
'request_id' => (int) $requestId,
|
|
'parent_id' => $parentId,
|
|
'group_title' => $title,
|
|
'group_operator' => $operator,
|
|
'group_position' => $position,
|
|
));
|
|
$newId = DB::insertId();
|
|
self::touch($requestId);
|
|
return $newId;
|
|
}
|
|
|
|
protected static function groupDepth($id)
|
|
{
|
|
$depth = 0;
|
|
$seen = array();
|
|
while ($id > 0 && $depth < 10 && !isset($seen[$id])) {
|
|
$seen[$id] = true;
|
|
$group = self::findGroup($id);
|
|
if (!$group || (int) $group->parent_id === 0) {
|
|
break;
|
|
}
|
|
|
|
$id = (int) $group->parent_id;
|
|
$depth++;
|
|
}
|
|
|
|
return $depth;
|
|
}
|
|
|
|
public static function groupDelete($requestId, $id)
|
|
{
|
|
$group = self::findGroup($id);
|
|
if (!$group || (int) $group->request_id !== (int) $requestId || (int) $group->parent_id === 0) {
|
|
return false;
|
|
}
|
|
|
|
$parentId = (int) $group->parent_id;
|
|
DB::Update(self::cond(), array('condition_group_id' => $parentId), 'request_id = %i AND condition_group_id = %i', (int) $requestId, (int) $id);
|
|
DB::Update(self::groups(), array('parent_id' => $parentId), 'request_id = %i AND parent_id = %i', (int) $requestId, (int) $id);
|
|
DB::Delete(self::groups(), 'Id = %i', (int) $id);
|
|
self::touch($requestId);
|
|
return true;
|
|
}
|
|
|
|
public static function findCondition($id)
|
|
{
|
|
return DB::query('SELECT * FROM ' . self::cond() . ' WHERE Id = %i LIMIT 1', (int) $id)->getObject();
|
|
}
|
|
|
|
public static function conditionSave($requestId, array $data)
|
|
{
|
|
$request = self::find($requestId);
|
|
if (!$request) { throw new \InvalidArgumentException('Запрос не найден.'); }
|
|
$fieldId = (int) (isset($data['condition_field_id']) ? $data['condition_field_id'] : 0);
|
|
$fieldIds = self::rubricFieldIds((int) $request->rubric_id);
|
|
if ($fieldId <= 0 || !in_array($fieldId, $fieldIds, true)) {
|
|
throw new \InvalidArgumentException('Выберите поле текущей рубрики.');
|
|
}
|
|
|
|
$compare = (string) (isset($data['condition_compare']) ? $data['condition_compare'] : '==');
|
|
if (!array_key_exists($compare, self::compareOptions())) {
|
|
throw new \InvalidArgumentException('Выберите поддерживаемое сравнение.');
|
|
}
|
|
|
|
$valueFields = self::conditionValueFields($data);
|
|
//-- Позиция управляется drag-and-drop (reorder), в форме её нет.
|
|
$fields = array(
|
|
'condition_field_id' => $fieldId,
|
|
'condition_compare' => $compare,
|
|
'condition_join' => (isset($data['condition_join']) && $data['condition_join'] === 'OR') ? 'OR' : 'AND',
|
|
'condition_status' => !empty($data['condition_status']) ? '1' : '0',
|
|
);
|
|
$fields += $valueFields;
|
|
|
|
$groupId = isset($data['condition_group_id']) ? (int) $data['condition_group_id'] : 0;
|
|
$group = self::findGroup($groupId);
|
|
$fields['condition_group_id'] = $group && (int) $group->request_id === (int) $requestId
|
|
? $groupId
|
|
: self::rootGroupId($requestId);
|
|
|
|
$id = (int) (isset($data['id']) ? $data['id'] : 0);
|
|
if ($id > 0) {
|
|
$condition = self::findCondition($id);
|
|
if (!$condition || (int) $condition->request_id !== (int) $requestId) {
|
|
return 0;
|
|
}
|
|
|
|
DB::Update(self::cond(), $fields, 'Id = %i AND request_id = %i', $id, (int) $requestId);
|
|
self::touch($requestId);
|
|
return $id;
|
|
}
|
|
|
|
//-- новое условие — в конец
|
|
$fields['request_id'] = (int) $requestId;
|
|
$fields['condition_position'] = (int) DB::query(
|
|
'SELECT COALESCE(MAX(condition_position), -1) + 1 FROM ' . self::cond() . ' WHERE request_id = %i AND condition_group_id = %i',
|
|
(int) $requestId, (int) $fields['condition_group_id']
|
|
)->getValue();
|
|
DB::Insert(self::cond(), $fields);
|
|
$newId = DB::insertId();
|
|
self::touch($requestId);
|
|
return $newId;
|
|
}
|
|
|
|
protected static function conditionValueFields(array $data)
|
|
{
|
|
$source = isset($data['condition_value_source']) ? strtolower(trim((string) $data['condition_value_source'])) : 'literal';
|
|
if ($source !== 'input') {
|
|
return array(
|
|
'condition_value' => (string) (isset($data['condition_value']) ? $data['condition_value'] : ''),
|
|
'condition_value_source' => $source === 'legacy' ? 'legacy' : 'literal',
|
|
'condition_value_key' => '',
|
|
'condition_value_config' => null,
|
|
);
|
|
}
|
|
|
|
$key = isset($data['condition_value_key']) ? trim((string) $data['condition_value_key']) : '';
|
|
if (RequestConditionValue::tag($key) === '') {
|
|
throw new \InvalidArgumentException('Alias публичного параметра должен начинаться с буквы или _.');
|
|
}
|
|
|
|
$mode = isset($data['condition_value_mode']) ? (string) $data['condition_value_mode'] : 'string_scalar';
|
|
$constant = isset($data['condition_value_constant']) ? $data['condition_value_constant'] : '';
|
|
$descriptor = self::descriptorForMode($mode, $key, $constant);
|
|
return array(
|
|
'condition_value' => RequestConditionValue::tag($key),
|
|
'condition_value_source' => 'input',
|
|
'condition_value_key' => $key,
|
|
'condition_value_config' => RequestConditionValue::encodeConfig($descriptor),
|
|
);
|
|
}
|
|
|
|
protected static function descriptorForMode($mode, $key, $constant)
|
|
{
|
|
$modes = array(
|
|
'string_scalar' => array('cast' => 'string', 'shape' => 'scalar', 'transform' => 'plain'),
|
|
'integer_scalar' => array('cast' => 'integer', 'shape' => 'scalar', 'transform' => 'plain'),
|
|
'decimal_scalar' => array('cast' => 'decimal', 'shape' => 'scalar', 'transform' => 'plain'),
|
|
'string_list' => array('cast' => 'string', 'shape' => 'list', 'transform' => 'plain'),
|
|
'integer_list' => array('cast' => 'integer', 'shape' => 'list', 'transform' => 'plain'),
|
|
'integer_range' => array('cast' => 'integer', 'shape' => 'range', 'transform' => 'plain'),
|
|
'decimal_range' => array('cast' => 'decimal', 'shape' => 'range', 'transform' => 'plain'),
|
|
'integer_member' => array('cast' => 'integer', 'shape' => 'scalar', 'transform' => 'pipe_member'),
|
|
'integer_member_list' => array('cast' => 'integer', 'shape' => 'list', 'transform' => 'pipe_member'),
|
|
'presence_constant' => array('cast' => 'string', 'shape' => 'presence', 'transform' => 'plain'),
|
|
);
|
|
if (!isset($modes[$mode])) { throw new \InvalidArgumentException('Неизвестный способ обработки параметра.'); }
|
|
$modes[$mode]['source'] = 'input';
|
|
$modes[$mode]['key'] = $key;
|
|
$modes[$mode]['constant'] = $mode === 'presence_constant' ? (string) $constant : '';
|
|
if ($mode === 'presence_constant' && trim((string) $constant) === '') {
|
|
throw new \InvalidArgumentException('Укажите значение константы.');
|
|
}
|
|
|
|
return $modes[$mode];
|
|
}
|
|
|
|
protected static function conditionPresentation(array $condition)
|
|
{
|
|
$descriptor = RequestConditionValue::descriptor($condition);
|
|
if ($descriptor !== null) {
|
|
$condition['value_source_ui'] = 'input';
|
|
$condition['value_key_ui'] = $descriptor['key'];
|
|
$condition['value_mode_ui'] = self::modeForDescriptor($descriptor);
|
|
$condition['value_constant_ui'] = $descriptor['constant'];
|
|
return $condition;
|
|
}
|
|
|
|
$value = isset($condition['condition_value']) ? (string) $condition['condition_value'] : '';
|
|
$condition['value_source_ui'] = preg_match('/<\?(?:php|=)?|\?>/i', $value) ? 'legacy' : 'literal';
|
|
$condition['value_key_ui'] = '';
|
|
$condition['value_mode_ui'] = 'string_scalar';
|
|
$condition['value_constant_ui'] = '';
|
|
return $condition;
|
|
}
|
|
|
|
protected static function modeForDescriptor(array $descriptor)
|
|
{
|
|
if ($descriptor['shape'] === 'presence') { return 'presence_constant'; }
|
|
if ($descriptor['transform'] === 'pipe_member') {
|
|
return $descriptor['shape'] === 'list' ? 'integer_member_list' : 'integer_member';
|
|
}
|
|
|
|
return $descriptor['cast'] . '_' . $descriptor['shape'];
|
|
}
|
|
|
|
public static function conditionDelete($id)
|
|
{
|
|
$condition = self::findCondition($id);
|
|
DB::Delete(self::cond(), 'Id = %i', (int) $id);
|
|
if ($condition) { self::touch((int) $condition->request_id); }
|
|
}
|
|
|
|
/** Переупорядочить условия запроса по массиву Id (drag-and-drop). */
|
|
public static function reorderConditions($requestId, array $items)
|
|
{
|
|
$positions = array();
|
|
foreach ($items as $item) {
|
|
$id = is_array($item) && isset($item['id']) ? (int) $item['id'] : (int) $item;
|
|
$groupId = is_array($item) && isset($item['group_id']) ? (int) $item['group_id'] : self::rootGroupId($requestId);
|
|
if ($id <= 0) {
|
|
continue;
|
|
}
|
|
|
|
$group = self::findGroup($groupId);
|
|
if (!$group || (int) $group->request_id !== (int) $requestId) {
|
|
continue;
|
|
}
|
|
|
|
if (!isset($positions[$groupId])) {
|
|
$positions[$groupId] = 0;
|
|
}
|
|
|
|
DB::Update(self::cond(), array(
|
|
'condition_group_id' => $groupId,
|
|
'condition_position' => $positions[$groupId]++,
|
|
), 'Id = %i AND request_id = %i', $id, (int) $requestId);
|
|
}
|
|
|
|
self::touch($requestId);
|
|
}
|
|
|
|
protected static function touch($requestId)
|
|
{
|
|
$request = self::find($requestId);
|
|
$now = time();
|
|
DB::Update(self::req(), array(
|
|
'request_changed' => $now,
|
|
'request_changed_elements' => $now,
|
|
'request_where_cond' => '',
|
|
), 'Id = %i', (int) $requestId);
|
|
\App\Content\Requests\NativeRequestPlanCompiler::persist($requestId);
|
|
FileCacheInvalidator::request($requestId, $request ? $request->request_alias : '');
|
|
}
|
|
}
|