Files
ave-cms/system/App/Content/Presentation/PresentationRepository.php
T
2026-07-30 11:56:32 +03:00

312 lines
12 KiB
PHP

<?php
/*
|--------------------------------------------------------------------------------------
| AVE.cms
|--------------------------------------------------------------------------------------
| @package AVE.cms
| @file system/App/Content/Presentation/PresentationRepository.php
| @author AVE.cms <support@ave-cms.ru>
| @copyright 2007-2026 (c) AVE.cms
| @link https://ave-cms.ru
| @version 3.3
*/
namespace App\Content\Presentation;
defined('BASEPATH') || die('Direct access to this location is not allowed.');
use App\Common\DatabaseSchema;
use App\Common\FileCacheInvalidator;
use App\Content\ContentTables;
use App\Helpers\Json;
use DB;
class PresentationRepository
{
protected static $kinds = array(
'card' => 'Карточка',
'compact_card' => 'Компактная карточка',
'list' => 'Список',
'table' => 'Таблица',
'slider' => 'Слайдер',
'tree' => 'Дерево',
'page' => 'Страница материала',
'filter' => 'Фильтры',
'custom' => 'Пользовательское',
);
public static function table()
{
return ContentTables::table('presentations');
}
public static function revisionsTable()
{
return ContentTables::table('presentation_revisions');
}
public static function assignmentsTable()
{
return ContentTables::table('presentation_assignments');
}
public static function available()
{
return DatabaseSchema::tableExists(self::table())
&& DatabaseSchema::tableExists(self::revisionsTable())
&& DatabaseSchema::tableExists(self::assignmentsTable());
}
public static function kinds()
{
return self::$kinds;
}
public static function all($query = '', $kind = '', $state = '')
{
if (!self::available()) { return array(); }
$sql = 'SELECT p.*,(SELECT COUNT(*) FROM ' . self::assignmentsTable()
. ' a WHERE a.presentation_id=p.id) usage_count FROM ' . self::table() . ' p WHERE 1=1';
$args = array();
$query = trim((string) $query);
if ($query !== '') {
$sql .= ' AND (p.title LIKE %ss OR p.code LIKE %ss OR p.id=%i)';
$args = array($query, $query, (int) $query);
}
if (isset(self::$kinds[$kind])) { $sql .= ' AND p.kind=%s'; $args[] = $kind; }
if ($state === 'published') { $sql .= ' AND p.is_published=1'; }
if ($state === 'draft') { $sql .= ' AND p.is_published=0'; }
if ($state === 'changed') {
$sql .= ' AND (p.is_published=0 OR p.draft_item_markup!=p.published_item_markup'
. ' OR p.draft_wrapper_markup!=p.published_wrapper_markup'
. ' OR p.draft_empty_markup!=p.published_empty_markup OR p.draft_css!=p.published_css)';
}
$sql .= ' ORDER BY p.updated_at DESC,p.id DESC';
$rows = call_user_func_array(array('DB', 'query'), array_merge(array($sql), $args))->getAll() ?: array();
return array_map(function ($row) { return self::format($row, false); }, $rows);
}
public static function one($id)
{
if (!self::available()) { return null; }
$row = DB::query(
'SELECT p.*,(SELECT COUNT(*) FROM ' . self::assignmentsTable()
. ' a WHERE a.presentation_id=p.id) usage_count FROM ' . self::table() . ' p WHERE p.id=%i LIMIT 1',
(int) $id
)->getAssoc();
return $row ? self::format($row, true) : null;
}
public static function raw($id)
{
if (!self::available()) { return null; }
return DB::query('SELECT * FROM ' . self::table() . ' WHERE id=%i LIMIT 1', (int) $id)->getAssoc() ?: null;
}
public static function runtime($id)
{
if (!self::available()) { return null; }
$row = DB::query(
'SELECT id presentation_id,code presentation_code,kind presentation_kind,version presentation_version,'
. ' is_published,published_item_markup,published_wrapper_markup,published_empty_markup,published_css,published_settings_json'
. ' FROM ' . self::table() . ' WHERE id=%i LIMIT 1',
(int) $id
)->getAssoc();
if (!$row || empty($row['is_published'])) { return null; }
$row['id'] = 0;
$row['mode'] = 'native';
$row['presentation_id'] = (int) $row['presentation_id'];
$row['presentation_version'] = (int) $row['presentation_version'];
$row['is_published'] = true;
$row['settings'] = Json::toArray((string) $row['published_settings_json'], array());
return $row;
}
public static function stats()
{
$items = self::all();
return array(
'total' => count($items),
'published' => count(array_filter($items, function ($item) { return $item['is_published']; })),
'changed' => count(array_filter($items, function ($item) { return $item['has_changes']; })),
'used' => count(array_filter($items, function ($item) { return $item['usage_count'] > 0; })),
);
}
public static function save($id, array $input, $authorId)
{
if (!self::available()) { throw new \RuntimeException('Обновите схему ядра'); }
$id = (int) $id;
$data = self::input($input, $id);
$data['author_id'] = (int) $authorId;
$data['updated_at'] = time();
if ($id > 0) {
if (!self::raw($id)) { throw new \RuntimeException('Представление не найдено'); }
DB::Update(self::table(), $data, 'id=%i', $id);
PresentationRevisionRepository::capture($id, 'update', $authorId, 'Сохранение черновика');
return $id;
}
$data += array(
'published_item_markup' => '',
'published_wrapper_markup' => '',
'published_empty_markup' => '',
'published_css' => '',
'published_settings_json' => '{}',
'is_published' => 0,
'version' => 0,
'created_at' => time(),
'published_at' => 0,
);
DB::Insert(self::table(), $data);
$id = (int) DB::insertId();
PresentationRevisionRepository::capture($id, 'create', $authorId, 'Создание представления');
return $id;
}
public static function publish($id, $authorId)
{
$row = self::raw((int) $id);
if (!$row) { throw new \RuntimeException('Представление не найдено'); }
if (trim((string) $row['draft_item_markup']) === '') {
throw new \InvalidArgumentException('Заполните шаблон элемента');
}
DB::Update(self::table(), array(
'published_item_markup' => (string) $row['draft_item_markup'],
'published_wrapper_markup' => (string) $row['draft_wrapper_markup'],
'published_empty_markup' => (string) $row['draft_empty_markup'],
'published_css' => (string) $row['draft_css'],
'published_settings_json' => (string) $row['draft_settings_json'],
'is_published' => 1,
'version' => max(1, (int) $row['version'] + 1),
'author_id' => (int) $authorId,
'updated_at' => time(),
'published_at' => time(),
), 'id=%i', (int) $id);
PresentationRevisionRepository::capture((int) $id, 'publish', $authorId, 'Публикация представления');
FileCacheInvalidator::publicPresentation();
return self::one((int) $id);
}
public static function copy($id, $authorId)
{
$row = self::one((int) $id);
if (!$row) { throw new \RuntimeException('Представление не найдено'); }
$base = $row['code'] . '_copy';
$code = $base;
$index = 2;
while (DB::query('SELECT id FROM ' . self::table() . ' WHERE code=%s LIMIT 1', $code)->getValue()) {
$code = $base . '_' . $index++;
}
$row['title'] .= ' (копия)';
$row['code'] = $code;
return self::save(0, $row, $authorId);
}
public static function delete($id)
{
$row = self::one((int) $id);
if (!$row) { return false; }
if ($row['usage_count'] > 0) { throw new \RuntimeException('Сначала удалите назначения представления'); }
DB::Delete(self::revisionsTable(), 'presentation_id=%i', (int) $id);
DB::Delete(self::table(), 'id=%i', (int) $id);
FileCacheInvalidator::publicPresentation();
return true;
}
public static function snapshot(array $row)
{
$keys = array(
'title', 'code', 'kind', 'description', 'draft_item_markup', 'draft_wrapper_markup',
'draft_empty_markup', 'draft_css', 'draft_settings_json', 'published_item_markup',
'published_wrapper_markup', 'published_empty_markup', 'published_css',
'published_settings_json', 'is_published', 'version',
);
$out = array('id' => isset($row['id']) ? (int) $row['id'] : 0);
foreach ($keys as $key) { $out[$key] = isset($row[$key]) ? $row[$key] : ''; }
return $out;
}
public static function applySnapshot($id, array $snapshot, $authorId)
{
$current = self::raw((int) $id);
if (!$current) { throw new \RuntimeException('Представление не найдено'); }
$snapshot['title'] = isset($snapshot['title']) ? $snapshot['title'] : $current['title'];
$snapshot['code'] = isset($snapshot['code']) ? $snapshot['code'] : $current['code'];
$data = self::input($snapshot, (int) $id);
$data['author_id'] = (int) $authorId;
$data['updated_at'] = time();
DB::Update(self::table(), $data, 'id=%i', (int) $id);
return (int) $id;
}
protected static function input(array $input, $id)
{
$title = trim(isset($input['title']) ? (string) $input['title'] : '');
$code = strtolower(trim(isset($input['code']) ? (string) $input['code'] : ''));
$kind = strtolower(trim(isset($input['kind']) ? (string) $input['kind'] : 'card'));
if ($title === '') { throw new \InvalidArgumentException('Укажите название представления'); }
if (!isset(self::$kinds[$kind])) { throw new \InvalidArgumentException('Выберите вид представления'); }
if (!preg_match('/^[a-z][a-z0-9_-]{1,79}$/', $code)) {
throw new \InvalidArgumentException('Код: латиница, цифры, дефис или подчёркивание');
}
if (DB::query('SELECT id FROM ' . self::table() . ' WHERE code=%s AND id!=%i LIMIT 1', $code, (int) $id)->getValue()) {
throw new \InvalidArgumentException('Такой код уже используется');
}
$settings = isset($input['draft_settings_json']) ? Json::toArray((string) $input['draft_settings_json']) : array();
$markup = array(
'Элемент' => isset($input['draft_item_markup']) ? (string) $input['draft_item_markup'] : '',
'Обёртка' => isset($input['draft_wrapper_markup']) ? (string) $input['draft_wrapper_markup'] : '',
'Пустой результат' => isset($input['draft_empty_markup']) ? (string) $input['draft_empty_markup'] : '',
);
$syntax = PresentationSyntax::checkMany($markup);
if (!$syntax['ok']) {
throw new \InvalidArgumentException($syntax['message']);
}
return array(
'title' => $title,
'code' => $code,
'kind' => $kind,
'description' => trim(isset($input['description']) ? (string) $input['description'] : ''),
'draft_item_markup' => $markup['Элемент'],
'draft_wrapper_markup' => $markup['Обёртка'],
'draft_empty_markup' => $markup['Пустой результат'],
'draft_css' => isset($input['draft_css']) ? (string) $input['draft_css'] : '',
'draft_settings_json' => Json::encode(is_array($settings) ? $settings : array(), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
);
}
protected static function format(array $row, $withCode)
{
$row['id'] = (int) $row['id'];
$row['version'] = (int) $row['version'];
$row['usage_count'] = isset($row['usage_count']) ? (int) $row['usage_count'] : 0;
$row['is_published'] = !empty($row['is_published']);
$row['kind_label'] = isset(self::$kinds[$row['kind']]) ? self::$kinds[$row['kind']] : $row['kind'];
$row['has_changes'] = !$row['is_published']
|| (string) $row['draft_item_markup'] !== (string) $row['published_item_markup']
|| (string) $row['draft_wrapper_markup'] !== (string) $row['published_wrapper_markup']
|| (string) $row['draft_empty_markup'] !== (string) $row['published_empty_markup']
|| (string) $row['draft_css'] !== (string) $row['published_css']
|| (string) $row['draft_settings_json'] !== (string) $row['published_settings_json'];
$row['updated_label'] = !empty($row['updated_at']) ? date('d.m.Y H:i', (int) $row['updated_at']) : '—';
if (!$withCode) {
foreach (array_keys($row) as $key) {
if (strpos($key, '_markup') !== false || strpos($key, '_css') !== false || strpos($key, '_settings_json') !== false) {
unset($row[$key]);
}
}
}
return $row;
}
}