mirror of
https://github.com/avecms/AVE.cms.git
synced 2026-08-01 08:45:44 +00:00
698 lines
27 KiB
PHP
698 lines
27 KiB
PHP
<?php
|
|
|
|
/*
|
|
|--------------------------------------------------------------------------------------
|
|
| AVE.cms
|
|
|--------------------------------------------------------------------------------------
|
|
| @package AVE.cms
|
|
| @file adminx/modules/Rubrics/RubricRevisions.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\Rubrics;
|
|
|
|
defined('BASEPATH') || die('Direct access to this location is not allowed.');
|
|
|
|
use App\Content\ContentTables;
|
|
use App\Content\Fields\FieldRegistry;
|
|
use App\Content\Revisions\JsonRevisionStore;
|
|
use App\Helpers\Json;
|
|
use DB;
|
|
|
|
class RubricRevisions
|
|
{
|
|
protected static $store;
|
|
|
|
public static function table()
|
|
{
|
|
return ContentTables::table('rubric_schema_revisions');
|
|
}
|
|
|
|
public static function available()
|
|
{
|
|
return (bool) DB::query('SHOW TABLES LIKE %s', self::table())->getValue();
|
|
}
|
|
|
|
public static function labels()
|
|
{
|
|
return array(
|
|
'baseline' => array('label' => 'Исходная схема', 'badge' => 'badge-gray'),
|
|
'create' => array('label' => 'Создание', 'badge' => 'badge-green'),
|
|
'field_create' => array('label' => 'Новое поле', 'badge' => 'badge-green'),
|
|
'field_update' => array('label' => 'Поле изменено', 'badge' => 'badge-blue'),
|
|
'field_delete' => array('label' => 'Поле удалено', 'badge' => 'badge-red'),
|
|
'group_create' => array('label' => 'Новая группа', 'badge' => 'badge-green'),
|
|
'group_update' => array('label' => 'Группа изменена', 'badge' => 'badge-blue'),
|
|
'group_delete' => array('label' => 'Группа удалена', 'badge' => 'badge-red'),
|
|
'layout' => array('label' => 'Конструктор', 'badge' => 'badge-blue'),
|
|
'reorder' => array('label' => 'Порядок', 'badge' => 'badge-cyan'),
|
|
'field_set' => array('label' => 'Набор полей', 'badge' => 'badge-violet'),
|
|
'restore' => array('label' => 'Восстановление', 'badge' => 'badge-cyan'),
|
|
'restore_backup' => array('label' => 'Перед восстановлением', 'badge' => 'badge-amber'),
|
|
);
|
|
}
|
|
|
|
public static function listForRubric($rubricId, $limit = 80)
|
|
{
|
|
if (!self::available()) { return array(); }
|
|
$out = array();
|
|
foreach (self::store()->listing($rubricId, $limit) as $row) {
|
|
$out[] = self::format($row, false);
|
|
}
|
|
|
|
return $out;
|
|
}
|
|
|
|
public static function one($id)
|
|
{
|
|
if (!self::available()) { return null; }
|
|
$row = self::store()->one($id);
|
|
return $row ? self::format($row, true) : null;
|
|
}
|
|
|
|
public static function capture($rubricId, $action, $authorId = 0, $comment = '', array $snapshot = null, $sourceRevisionId = 0)
|
|
{
|
|
if (!self::available()) { return 0; }
|
|
$rubricId = (int) $rubricId;
|
|
$snapshot = $snapshot === null ? Model::schemaSnapshot($rubricId) : Model::normalizeSchemaSnapshot($snapshot);
|
|
if (!$snapshot) { return 0; }
|
|
|
|
return self::store()->capture(
|
|
$rubricId,
|
|
$action,
|
|
$snapshot,
|
|
(int) $authorId,
|
|
mb_substr(trim((string) $comment), 0, 500, 'UTF-8'),
|
|
array('source_revision_id' => (int) $sourceRevisionId),
|
|
true
|
|
);
|
|
}
|
|
|
|
public static function previewRestore($revisionId)
|
|
{
|
|
$revision = self::one((int) $revisionId);
|
|
if (!$revision || empty($revision['snapshot'])) {
|
|
throw new \RuntimeException('Ревизия не найдена');
|
|
}
|
|
|
|
$current = Model::schemaSnapshot((int) $revision['rubric_id']);
|
|
if (!$current) { throw new \RuntimeException('Рубрика не найдена'); }
|
|
$impact = self::compareSnapshots($current, $revision['snapshot']);
|
|
$impact['documents_total'] = Model::documentCount((int) $revision['rubric_id']);
|
|
$impact['documents_with_values'] = self::documentsWithValues(
|
|
(int) $revision['rubric_id'],
|
|
$impact['affected_field_ids']
|
|
);
|
|
$impact['dependencies'] = RubricSchemaImpact::analyze(
|
|
(int) $revision['rubric_id'],
|
|
self::fieldReferences($current, $revision['snapshot'], $impact['affected_field_ids'])
|
|
);
|
|
$impact['fingerprint'] = self::restoreFingerprint(
|
|
$revision,
|
|
$current,
|
|
(string) $impact['dependencies']['fingerprint']
|
|
);
|
|
unset($impact['affected_field_ids']);
|
|
|
|
return $impact;
|
|
}
|
|
|
|
public static function restore($revisionId, $fingerprint, $authorId = 0)
|
|
{
|
|
$revision = self::one((int) $revisionId);
|
|
if (!$revision || empty($revision['snapshot'])) {
|
|
throw new \RuntimeException('Ревизия не найдена');
|
|
}
|
|
|
|
$current = Model::schemaSnapshot((int) $revision['rubric_id']);
|
|
$impact = self::compareSnapshots($current, $revision['snapshot']);
|
|
$dependencies = RubricSchemaImpact::analyze(
|
|
(int) $revision['rubric_id'],
|
|
self::fieldReferences($current, $revision['snapshot'], $impact['affected_field_ids'])
|
|
);
|
|
$expected = self::restoreFingerprint($revision, $current, (string) $dependencies['fingerprint']);
|
|
if ($fingerprint === '' || !hash_equals($expected, (string) $fingerprint)) {
|
|
throw new \RuntimeException('Схема изменилась после предпросмотра. Проверьте ревизию ещё раз.');
|
|
}
|
|
|
|
if (!empty($impact['blockers'])) {
|
|
throw new \RuntimeException('Восстановление заблокировано: ' . implode('; ', $impact['blockers']));
|
|
}
|
|
|
|
DB::startTransaction();
|
|
try {
|
|
self::capture(
|
|
(int) $revision['rubric_id'],
|
|
'restore_backup',
|
|
(int) $authorId,
|
|
'Снимок перед восстановлением ревизии #' . (int) $revisionId,
|
|
$current,
|
|
(int) $revisionId
|
|
);
|
|
$result = Model::applySchemaSnapshot((int) $revision['rubric_id'], $revision['snapshot']);
|
|
self::capture(
|
|
(int) $revision['rubric_id'],
|
|
'restore',
|
|
(int) $authorId,
|
|
'Восстановлено из ревизии #' . (int) $revisionId,
|
|
null,
|
|
(int) $revisionId
|
|
);
|
|
DB::commit();
|
|
} catch (\Throwable $e) {
|
|
DB::rollback();
|
|
throw $e;
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
public static function delete($revisionId)
|
|
{
|
|
if (!self::available()) { return 0; }
|
|
$owner = self::store()->delete($revisionId);
|
|
|
|
return $owner === false ? 0 : (int) $owner;
|
|
}
|
|
|
|
public static function deleteForRubric($rubricId)
|
|
{
|
|
if (!self::available()) { return 0; }
|
|
|
|
return self::store()->deleteFor($rubricId);
|
|
}
|
|
|
|
public static function compareSnapshots(array $current, array $target)
|
|
{
|
|
$current = Model::normalizeSchemaSnapshot($current);
|
|
$target = Model::normalizeSchemaSnapshot($target);
|
|
$currentGroups = self::mapById($current['groups']);
|
|
$targetGroups = self::mapById($target['groups']);
|
|
$currentFields = self::mapById($current['fields']);
|
|
$targetFields = self::mapById($target['fields']);
|
|
$groups = self::describeGroupDiff(
|
|
self::entityDiff($currentGroups, $targetGroups, 'group_title', 'Группа'),
|
|
$currentGroups,
|
|
$targetGroups
|
|
);
|
|
$fields = self::describeFieldDiff(
|
|
self::entityDiff($currentFields, $targetFields, 'rubric_field_title', 'Поле'),
|
|
$currentFields,
|
|
$targetFields,
|
|
$currentGroups,
|
|
$targetGroups
|
|
);
|
|
$blockers = self::restoreBlockers(
|
|
$currentFields,
|
|
$targetFields,
|
|
isset($current['rubric']['id']) ? (int) $current['rubric']['id'] : 0
|
|
);
|
|
$affected = array_values(array_unique(array_merge($fields['restore_ids'], $fields['changed_ids'])));
|
|
|
|
return array(
|
|
'conditions_before' => !empty($current['rubric']['form_conditions_enabled']),
|
|
'conditions_after' => !empty($target['rubric']['form_conditions_enabled']),
|
|
'groups' => $groups,
|
|
'fields' => $fields,
|
|
'summary' => array(
|
|
'groups_restore' => count($groups['restore']),
|
|
'groups_change' => count($groups['change']),
|
|
'groups_preserve' => count($groups['preserve']),
|
|
'fields_restore' => count($fields['restore']),
|
|
'fields_change' => count($fields['change']),
|
|
'fields_preserve' => count($fields['preserve']),
|
|
),
|
|
'blockers' => $blockers,
|
|
'can_restore' => empty($blockers),
|
|
'affected_field_ids' => $affected,
|
|
);
|
|
}
|
|
|
|
protected static function entityDiff(array $current, array $target, $titleKey, $fallback)
|
|
{
|
|
$out = array(
|
|
'restore' => array(), 'change' => array(), 'preserve' => array(),
|
|
'restore_ids' => array(), 'changed_ids' => array(),
|
|
);
|
|
foreach ($target as $id => $item) {
|
|
$title = trim((string) (isset($item[$titleKey]) ? $item[$titleKey] : ''));
|
|
$entry = array('id' => (int) $id, 'title' => $title !== '' ? $title : $fallback . ' #' . (int) $id);
|
|
if (!isset($current[$id])) {
|
|
$out['restore'][] = $entry;
|
|
$out['restore_ids'][] = (int) $id;
|
|
continue;
|
|
}
|
|
|
|
$changed = self::changedKeys($current[$id], $item);
|
|
if ($changed) {
|
|
$entry['changed_keys'] = $changed;
|
|
$out['change'][] = $entry;
|
|
$out['changed_ids'][] = (int) $id;
|
|
}
|
|
}
|
|
|
|
foreach ($current as $id => $item) {
|
|
if (isset($target[$id])) { continue; }
|
|
$title = trim((string) (isset($item[$titleKey]) ? $item[$titleKey] : ''));
|
|
$out['preserve'][] = array(
|
|
'id' => (int) $id,
|
|
'title' => $title !== '' ? $title : $fallback . ' #' . (int) $id,
|
|
);
|
|
}
|
|
|
|
return $out;
|
|
}
|
|
|
|
protected static function changedKeys(array $current, array $target)
|
|
{
|
|
$ignore = array('rubric_id');
|
|
$keys = array_unique(array_merge(array_keys($current), array_keys($target)));
|
|
$out = array();
|
|
foreach ($keys as $key) {
|
|
if (in_array($key, $ignore, true)) { continue; }
|
|
$before = isset($current[$key]) ? $current[$key] : null;
|
|
$after = isset($target[$key]) ? $target[$key] : null;
|
|
if (in_array($key, array('rubric_field_settings', 'group_settings'), true)
|
|
&& self::sameValue(Json::toArray($before), Json::toArray($after))) { continue; }
|
|
if ((string) $before !== (string) $after) { $out[] = (string) $key; }
|
|
}
|
|
|
|
return $out;
|
|
}
|
|
|
|
protected static function describeGroupDiff(array $diff, array $current, array $target)
|
|
{
|
|
foreach ($diff['change'] as &$entry) {
|
|
$id = (int) $entry['id'];
|
|
$entry['changes'] = self::groupChanges($current[$id], $target[$id]);
|
|
}
|
|
|
|
unset($entry);
|
|
return $diff;
|
|
}
|
|
|
|
protected static function describeFieldDiff(array $diff, array $current, array $target, array $currentGroups, array $targetGroups)
|
|
{
|
|
foreach (array('restore', 'change', 'preserve') as $section) {
|
|
foreach ($diff[$section] as &$entry) {
|
|
$id = (int) $entry['id'];
|
|
$field = isset($target[$id]) ? $target[$id] : $current[$id];
|
|
$entry['meta'] = self::fieldTypeLabel(isset($field['rubric_field_type']) ? $field['rubric_field_type'] : '');
|
|
$alias = trim((string) (isset($field['rubric_field_alias']) ? $field['rubric_field_alias'] : ''));
|
|
if ($alias !== '') { $entry['meta'] .= ' · ' . $alias; }
|
|
if ($section === 'change') {
|
|
$entry['changes'] = self::fieldChanges($current[$id], $target[$id], $currentGroups, $targetGroups);
|
|
}
|
|
}
|
|
|
|
unset($entry);
|
|
}
|
|
|
|
return $diff;
|
|
}
|
|
|
|
protected static function groupChanges(array $current, array $target)
|
|
{
|
|
$changes = array();
|
|
self::addChange($changes, 'Название', $current['group_title'], $target['group_title']);
|
|
self::addChange($changes, 'Описание', $current['group_description'], $target['group_description']);
|
|
self::addChange($changes, 'Порядок', $current['group_position'], $target['group_position'], 'number');
|
|
$before = Json::toArray(isset($current['group_settings']) ? $current['group_settings'] : '');
|
|
$after = Json::toArray(isset($target['group_settings']) ? $target['group_settings'] : '');
|
|
self::addSettingsChanges($changes, $before, $after, '', '');
|
|
|
|
return $changes;
|
|
}
|
|
|
|
protected static function fieldChanges(array $current, array $target, array $currentGroups, array $targetGroups)
|
|
{
|
|
$changes = array();
|
|
self::addChange($changes, 'Название', $current['rubric_field_title'], $target['rubric_field_title']);
|
|
self::addChange($changes, 'Системное имя', $current['rubric_field_alias'], $target['rubric_field_alias']);
|
|
self::addChange(
|
|
$changes,
|
|
'Тип поля',
|
|
self::fieldTypeLabel($current['rubric_field_type']),
|
|
self::fieldTypeLabel($target['rubric_field_type'])
|
|
);
|
|
self::addChange(
|
|
$changes,
|
|
'Группа',
|
|
self::groupLabel((int) $current['rubric_field_group'], $currentGroups),
|
|
self::groupLabel((int) $target['rubric_field_group'], $targetGroups)
|
|
);
|
|
self::addChange($changes, 'Порядок', $current['rubric_field_position'], $target['rubric_field_position'], 'number');
|
|
self::addChange($changes, 'Значение по умолчанию', $current['rubric_field_default'], $target['rubric_field_default']);
|
|
self::addChange($changes, 'Числовое значение', $current['rubric_field_numeric'], $target['rubric_field_numeric'], 'bool');
|
|
self::addChange($changes, 'Участвует в поиске', $current['rubric_field_search'], $target['rubric_field_search'], 'bool');
|
|
self::addChange($changes, 'Подсказка редактору', $current['rubric_field_description'], $target['rubric_field_description']);
|
|
self::addChange($changes, 'Шаблон в документе', $current['rubric_field_template'], $target['rubric_field_template'], 'template');
|
|
self::addChange($changes, 'Шаблон в запросе', $current['rubric_field_template_request'], $target['rubric_field_template_request'], 'template');
|
|
self::addSettingsChanges(
|
|
$changes,
|
|
Json::toArray($current['rubric_field_settings']),
|
|
Json::toArray($target['rubric_field_settings']),
|
|
(string) $current['rubric_field_type'],
|
|
(string) $target['rubric_field_type']
|
|
);
|
|
|
|
return $changes;
|
|
}
|
|
|
|
protected static function addSettingsChanges(array &$changes, array $before, array $after, $beforeType, $afterType)
|
|
{
|
|
$descriptors = array_replace(
|
|
self::settingsDescriptors($beforeType),
|
|
self::settingsDescriptors($afterType),
|
|
array(
|
|
'placed' => array('label' => 'Размещено на форме', 'type' => 'bool'),
|
|
'width' => array('label' => 'Ширина поля', 'type' => 'width'),
|
|
'prefix' => array('label' => 'Префикс'),
|
|
'suffix' => array('label' => 'Суффикс'),
|
|
'form_condition' => array('label' => 'Условие формы', 'type' => 'condition'),
|
|
)
|
|
);
|
|
$keys = array_unique(array_merge(array_keys($before), array_keys($after)));
|
|
foreach ($keys as $key) {
|
|
$old = array_key_exists($key, $before) ? $before[$key] : null;
|
|
$new = array_key_exists($key, $after) ? $after[$key] : null;
|
|
if (self::sameValue($old, $new)) { continue; }
|
|
$descriptor = isset($descriptors[$key]) ? $descriptors[$key] : array();
|
|
$label = isset($descriptor['label']) ? (string) $descriptor['label'] : self::settingLabel($key);
|
|
$type = isset($descriptor['type']) ? (string) $descriptor['type'] : '';
|
|
$options = isset($descriptor['options']) && is_array($descriptor['options']) ? $descriptor['options'] : array();
|
|
$changes[] = array(
|
|
'label' => $label,
|
|
'before' => self::displayValue($old, $type, $options),
|
|
'after' => self::displayValue($new, $type, $options),
|
|
);
|
|
}
|
|
}
|
|
|
|
protected static function settingsDescriptors($type)
|
|
{
|
|
$field = FieldRegistry::get((string) $type);
|
|
if (!$field) { return array(); }
|
|
$out = array();
|
|
foreach (array_merge((array) $field->settingsSchema(), (array) $field->validationSchema()) as $descriptor) {
|
|
if (!is_array($descriptor) || empty($descriptor['key'])) { continue; }
|
|
$out[(string) $descriptor['key']] = $descriptor;
|
|
}
|
|
|
|
return $out;
|
|
}
|
|
|
|
protected static function addChange(array &$changes, $label, $before, $after, $type = '')
|
|
{
|
|
if (self::sameValue($before, $after)) { return; }
|
|
$changes[] = array(
|
|
'label' => (string) $label,
|
|
'before' => self::displayValue($before, $type),
|
|
'after' => self::displayValue($after, $type),
|
|
);
|
|
}
|
|
|
|
protected static function sameValue($before, $after)
|
|
{
|
|
if (is_array($before) || is_array($after)) {
|
|
return Json::encode($before, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
|
|
=== Json::encode($after, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
}
|
|
|
|
return (string) $before === (string) $after;
|
|
}
|
|
|
|
protected static function displayValue($value, $type = '', array $options = array())
|
|
{
|
|
if ($type === 'condition') { return self::conditionLabel(is_array($value) ? $value : array()); }
|
|
if ($type === 'bool') { return !empty($value) && (string) $value !== '0' ? 'Да' : 'Нет'; }
|
|
if ($type === 'width') {
|
|
$widths = array('full' => '12 колонок', 'half' => '6 колонок', 'third' => '4 колонки', 'quarter' => '3 колонки');
|
|
return isset($widths[(string) $value]) ? $widths[(string) $value] : self::plainValue($value);
|
|
}
|
|
|
|
if ($type === 'template') {
|
|
$value = trim((string) $value);
|
|
return $value === '' ? 'Не задан' : 'Задан · ' . mb_strlen($value, 'UTF-8') . ' симв.';
|
|
}
|
|
|
|
if ($type === 'select' && !is_array($value)) {
|
|
foreach ($options as $key => $label) {
|
|
$optionValue = is_int($key) ? $label : $key;
|
|
if ((string) $optionValue === (string) $value) { return (string) $label; }
|
|
}
|
|
}
|
|
|
|
if (is_array($value)) { return self::listValue($value); }
|
|
|
|
return self::plainValue($value);
|
|
}
|
|
|
|
protected static function plainValue($value)
|
|
{
|
|
$value = trim((string) $value);
|
|
if ($value === '') { return 'Не задано'; }
|
|
$value = preg_replace('/\s+/u', ' ', $value);
|
|
return mb_strlen($value, 'UTF-8') > 90 ? mb_substr($value, 0, 87, 'UTF-8') . '...' : $value;
|
|
}
|
|
|
|
protected static function listValue(array $value)
|
|
{
|
|
$labels = array();
|
|
foreach ($value as $key => $item) {
|
|
if (is_array($item)) {
|
|
$label = isset($item['label']) ? $item['label'] : (isset($item['title']) ? $item['title'] : (isset($item['value']) ? $item['value'] : ''));
|
|
} else {
|
|
$label = is_int($key) ? $item : $item;
|
|
}
|
|
|
|
$label = trim((string) $label);
|
|
if ($label !== '') { $labels[] = $label; }
|
|
}
|
|
|
|
if (!$labels) { return 'Не задано'; }
|
|
$shown = array_slice($labels, 0, 3);
|
|
return count($labels) . ' знач.: ' . implode(', ', $shown) . (count($labels) > 3 ? '...' : '');
|
|
}
|
|
|
|
protected static function conditionLabel(array $condition)
|
|
{
|
|
if (!$condition) { return 'Нет'; }
|
|
$modes = array('show' => 'Показать', 'hide' => 'Скрыть', 'lock' => 'Запретить изменение');
|
|
$mode = isset($condition['mode']) && isset($modes[$condition['mode']]) ? $modes[$condition['mode']] : 'Применить';
|
|
$tree = isset($condition['tree']) ? $condition['tree'] : array();
|
|
$ruleCount = self::conditionRuleCount($tree);
|
|
$parts = array($mode, $ruleCount . ' ' . ($ruleCount === 1 ? 'правило' : 'правил'));
|
|
$rules = array();
|
|
self::conditionRules($tree, $rules);
|
|
if ($rules) {
|
|
$parts[] = implode('; ', array_slice($rules, 0, 2)) . (count($rules) > 2 ? '; ...' : '');
|
|
}
|
|
|
|
if (!empty($condition['required'])) { $parts[] = 'обязательное'; }
|
|
if (!empty($condition['allowed_values']) && is_array($condition['allowed_values'])) {
|
|
$parts[] = count($condition['allowed_values']) . ' доступных вариантов';
|
|
}
|
|
|
|
if (isset($condition['value_action']) && $condition['value_action'] === 'set') { $parts[] = 'установить значение'; }
|
|
if (isset($condition['value_action']) && $condition['value_action'] === 'clear') { $parts[] = 'очистить значение'; }
|
|
|
|
return implode(' · ', $parts);
|
|
}
|
|
|
|
protected static function conditionRuleCount(array $node)
|
|
{
|
|
if (isset($node['items']) && is_array($node['items'])) {
|
|
$count = 0;
|
|
foreach ($node['items'] as $item) { $count += is_array($item) ? self::conditionRuleCount($item) : 0; }
|
|
return $count;
|
|
}
|
|
|
|
return isset($node['field']) ? 1 : 0;
|
|
}
|
|
|
|
protected static function conditionRules(array $node, array &$out)
|
|
{
|
|
if (isset($node['items']) && is_array($node['items'])) {
|
|
foreach ($node['items'] as $item) {
|
|
if (is_array($item)) { self::conditionRules($item, $out); }
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (empty($node['field'])) { return; }
|
|
$operators = array(
|
|
'empty' => 'не заполнено', 'not_empty' => 'заполнено', 'equals' => '=', 'not_equals' => '≠',
|
|
'contains' => 'содержит', 'not_contains' => 'не содержит', 'greater' => '>',
|
|
'greater_or_equal' => '≥', 'less' => '<', 'less_or_equal' => '≤',
|
|
'in' => 'одно из', 'not_in' => 'не входит в',
|
|
);
|
|
$field = preg_replace('/^(?:id|alias):/', '', (string) $node['field']);
|
|
$operator = isset($node['operator']) && isset($operators[$node['operator']]) ? $operators[$node['operator']] : '=';
|
|
$value = isset($node['value']) ? self::plainValue(is_array($node['value']) ? implode(', ', $node['value']) : $node['value']) : '';
|
|
$out[] = trim($field . ' ' . $operator . ' ' . ($value === 'Не задано' ? '' : $value));
|
|
}
|
|
|
|
protected static function fieldTypeLabel($type)
|
|
{
|
|
$field = FieldRegistry::get((string) $type);
|
|
return $field ? (string) $field->name() : ((string) $type !== '' ? (string) $type : 'Тип не задан');
|
|
}
|
|
|
|
protected static function groupLabel($id, array $groups)
|
|
{
|
|
if ($id <= 0) { return 'Без группы'; }
|
|
return isset($groups[$id]) && trim((string) $groups[$id]['group_title']) !== ''
|
|
? (string) $groups[$id]['group_title']
|
|
: 'Группа #' . $id;
|
|
}
|
|
|
|
protected static function settingLabel($key)
|
|
{
|
|
$known = array(
|
|
'upload_dir' => 'Папка загрузки', 'rubric' => 'Рубрика-источник',
|
|
'options' => 'Список значений', 'height' => 'Высота редактора',
|
|
'editor' => 'Режим редактора', 'language' => 'Язык кода',
|
|
);
|
|
if (isset($known[$key])) { return $known[$key]; }
|
|
return mb_convert_case(str_replace('_', ' ', (string) $key), MB_CASE_TITLE, 'UTF-8');
|
|
}
|
|
|
|
protected static function restoreBlockers(array $current, array $target, $rubricId)
|
|
{
|
|
$blockers = array();
|
|
$aliases = array();
|
|
foreach ($target as $id => $field) {
|
|
$alias = strtolower(trim((string) $field['rubric_field_alias']));
|
|
if ($alias !== '' && isset($aliases[$alias]) && $aliases[$alias] !== (int) $id) {
|
|
$blockers[] = 'В ревизии повторяется alias поля «' . $alias . '»';
|
|
}
|
|
|
|
if ($alias !== '') { $aliases[$alias] = (int) $id; }
|
|
$type = (string) $field['rubric_field_type'];
|
|
if (!FieldTypes::exists($type)) { $blockers[] = 'Тип поля «' . $type . '» не установлен'; }
|
|
}
|
|
|
|
foreach ($current as $id => $field) {
|
|
if (isset($target[$id])) { continue; }
|
|
$alias = strtolower(trim((string) $field['rubric_field_alias']));
|
|
if ($alias !== '' && isset($aliases[$alias])) {
|
|
$blockers[] = 'Сохранённое новое поле «' . $field['rubric_field_title'] . '» использует alias «' . $alias . '» из ревизии';
|
|
}
|
|
}
|
|
|
|
foreach ($target as $id => $field) {
|
|
if (isset($current[$id])) { continue; }
|
|
$owner = DB::query('SELECT rubric_id FROM ' . Model::fieldsTable() . ' WHERE Id=%i LIMIT 1', (int) $id)->getValue();
|
|
if ($owner && (int) $owner !== (int) $rubricId) {
|
|
$blockers[] = 'ID поля #' . (int) $id . ' уже занят другой рубрикой';
|
|
}
|
|
}
|
|
|
|
return array_values(array_unique($blockers));
|
|
}
|
|
|
|
protected static function restoreFingerprint(array $revision, array $current, $dependencyFingerprint)
|
|
{
|
|
$state = DB::query(
|
|
"SELECT COUNT(*) count,COALESCE(MAX(Id),0) max_id,COALESCE(MAX(document_changed),0) changed"
|
|
. " FROM " . Model::docsTable() . " WHERE rubric_id=%i AND document_deleted!='1'",
|
|
(int) $revision['rubric_id']
|
|
)->getAssoc() ?: array();
|
|
$payload = array(
|
|
'revision_id' => (int) $revision['id'],
|
|
'target_hash' => (string) $revision['snapshot_hash'],
|
|
'current_hash' => sha1(Json::encode(Model::normalizeSchemaSnapshot($current), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)),
|
|
'documents' => $state,
|
|
'dependencies' => (string) $dependencyFingerprint,
|
|
);
|
|
|
|
return hash('sha256', Json::encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
|
}
|
|
|
|
protected static function fieldReferences(array $current, array $target, array $fieldIds)
|
|
{
|
|
$wanted = array_fill_keys(array_map('intval', $fieldIds), true);
|
|
$references = array();
|
|
foreach (array($current, $target) as $snapshot) {
|
|
foreach (isset($snapshot['fields']) && is_array($snapshot['fields']) ? $snapshot['fields'] : array() as $field) {
|
|
$id = isset($field['Id']) ? (int) $field['Id'] : 0;
|
|
if ($id <= 0 || !isset($wanted[$id])) { continue; }
|
|
if (!isset($references[$id])) {
|
|
$references[$id] = array('id' => $id, 'title' => '', 'aliases' => array());
|
|
}
|
|
|
|
$title = trim((string) (isset($field['rubric_field_title']) ? $field['rubric_field_title'] : ''));
|
|
$alias = strtolower(trim((string) (isset($field['rubric_field_alias']) ? $field['rubric_field_alias'] : '')));
|
|
if ($title !== '') { $references[$id]['title'] = $title; }
|
|
if ($alias !== '' && !in_array($alias, $references[$id]['aliases'], true)) {
|
|
$references[$id]['aliases'][] = $alias;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $references;
|
|
}
|
|
|
|
protected static function documentsWithValues($rubricId, array $fieldIds)
|
|
{
|
|
if (!$fieldIds) { return 0; }
|
|
return (int) DB::query(
|
|
'SELECT COUNT(DISTINCT df.document_id) FROM ' . Model::docFieldsTable() . ' df'
|
|
. ' LEFT JOIN ' . Model::docFieldsTextTable() . ' dft ON dft.document_id=df.document_id AND dft.rubric_field_id=df.rubric_field_id'
|
|
. ' INNER JOIN ' . Model::docsTable() . ' d ON d.Id=df.document_id'
|
|
. " WHERE d.rubric_id=%i AND d.document_deleted!='1' AND df.rubric_field_id IN %li"
|
|
. " AND (df.field_value!='' OR COALESCE(dft.field_value,'')!='')",
|
|
(int) $rubricId,
|
|
$fieldIds
|
|
)->getValue();
|
|
}
|
|
|
|
protected static function mapById(array $items)
|
|
{
|
|
$out = array();
|
|
foreach ($items as $item) {
|
|
if (is_array($item) && !empty($item['Id'])) { $out[(int) $item['Id']] = $item; }
|
|
}
|
|
|
|
return $out;
|
|
}
|
|
|
|
protected static function format(array $row, $withSnapshot)
|
|
{
|
|
$base = self::store()->formatRow($row, $withSnapshot);
|
|
$decoded = Json::toArray(isset($row['snapshot_json']) ? (string) $row['snapshot_json'] : '');
|
|
|
|
return array(
|
|
'id' => (int) $base['id'],
|
|
'rubric_id' => (int) $base['rubric_id'],
|
|
'action' => $base['action'],
|
|
'action_label' => $base['action_label'],
|
|
'badge' => $base['badge'],
|
|
'comment' => (string) (isset($row['comment']) ? $row['comment'] : ''),
|
|
'author_id' => (int) $base['author_id'],
|
|
'author_name' => (string) $base['author_name'],
|
|
'source_revision_id' => (int) (isset($row['source_revision_id']) ? $row['source_revision_id'] : 0),
|
|
'created_at' => $base['created_at'],
|
|
'created_label' => $base['created_label'],
|
|
'snapshot_hash' => (string) (isset($row['snapshot_hash']) ? $row['snapshot_hash'] : ''),
|
|
'groups_count' => isset($decoded['groups']) ? count($decoded['groups']) : 0,
|
|
'fields_count' => isset($decoded['fields']) ? count($decoded['fields']) : 0,
|
|
'snapshot' => $withSnapshot ? $decoded : null,
|
|
);
|
|
}
|
|
|
|
protected static function store()
|
|
{
|
|
if (!self::$store) {
|
|
self::$store = new JsonRevisionStore(self::table(), 'rubric_id', self::labels());
|
|
}
|
|
|
|
return self::$store;
|
|
}
|
|
}
|