Files
2026-07-30 11:56:32 +03:00

417 lines
12 KiB
PHP

<?php
/*
|--------------------------------------------------------------------------------------
| AVE.cms
|--------------------------------------------------------------------------------------
| @package AVE.cms
| @file system/App/Content/Directories/DirectoryRepository.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\Directories;
defined('BASEPATH') || die('Direct access to this location is not allowed.');
use App\Common\Cache;
use App\Common\CacheKey;
use App\Common\DatabaseSchema;
use App\Content\ContentTables;
use App\Helpers\Json;
use App\Helpers\Str;
use DB;
/** Lightweight shared value lists used by choice fields. */
class DirectoryRepository
{
const CACHE_TTL = 3600;
public static function all($query = '', $includeInactive = true)
{
if (!self::schemaReady()) {
return array();
}
$query = trim((string) $query);
$sql = 'SELECT d.*, (SELECT COUNT(*) FROM ' . self::itemsTable()
. ' i WHERE i.directory_id = d.id) AS items_count,'
. ' (SELECT COUNT(*) FROM ' . self::itemsTable()
. ' i WHERE i.directory_id = d.id AND i.is_active = 1) AS active_items_count'
. ' FROM ' . self::table() . ' d WHERE 1=1';
$params = array();
if (!$includeInactive) {
$sql .= ' AND d.is_active = 1';
}
if ($query !== '') {
$sql .= ' AND (d.name LIKE %ss OR d.code LIKE %ss OR d.description LIKE %ss)';
$params = array($query, $query, $query);
}
$sql .= ' ORDER BY d.name ASC, d.id ASC';
$result = empty($params)
? DB::query($sql)
: call_user_func_array(array(DB::class, 'query'), array_merge(array($sql), $params));
return array_map(array(self::class, 'normalizeDirectory'), $result->getAll() ?: array());
}
public static function choices($includeInactive = false)
{
$out = array();
foreach (self::all('', $includeInactive) as $directory) {
$out[(string) $directory['id']] = $directory['name'];
}
return $out;
}
public static function find($id)
{
if (!self::schemaReady()) {
return null;
}
$row = DB::query('SELECT * FROM ' . self::table() . ' WHERE id = %i LIMIT 1', (int) $id)->getAssoc();
return $row ? self::normalizeDirectory($row) : null;
}
public static function save($id, array $input)
{
if (!self::schemaReady()) {
throw new \RuntimeException('Схема справочников не установлена');
}
$id = (int) $id;
$name = trim(isset($input['name']) ? (string) $input['name'] : '');
$code = Str::machineAlias(isset($input['code']) ? $input['code'] : '');
if ($name === '') {
throw new \InvalidArgumentException('Укажите название справочника');
}
if ($code === '') {
$code = Str::slug($name);
}
if ($code === '' || !preg_match('/^[a-z0-9][a-z0-9_-]{1,63}$/', $code)) {
throw new \InvalidArgumentException('Код: латиница, цифры, _ и -, от 2 до 64 символов');
}
$duplicate = DB::query(
'SELECT id FROM ' . self::table() . ' WHERE code = %s AND id != %i LIMIT 1',
$code,
$id
)->getValue();
if ($duplicate) {
throw new \InvalidArgumentException('Справочник с таким кодом уже существует');
}
$now = time();
$data = array(
'name' => mb_substr($name, 0, 190, 'UTF-8'),
'code' => $code,
'description' => trim(isset($input['description']) ? (string) $input['description'] : ''),
'is_active' => !empty($input['is_active']) ? 1 : 0,
'updated_at' => $now,
);
if ($id > 0) {
if (!self::find($id)) {
throw new \RuntimeException('Справочник не найден');
}
DB::Update(self::table(), $data, 'id = %i', $id);
} else {
$data['settings_json'] = '';
$data['created_at'] = $now;
DB::Insert(self::table(), $data);
$id = (int) DB::insertId();
}
self::clear($id);
return self::find($id);
}
public static function delete($id)
{
$id = (int) $id;
$directory = self::find($id);
if (!$directory) {
return false;
}
if (self::usageCount($id) > 0) {
throw new \RuntimeException('Справочник используется в полях. Сначала выберите для них другой источник.');
}
DB::startTransaction();
try {
DB::Delete(self::itemsTable(), 'directory_id = %i', $id);
DB::Delete(self::table(), 'id = %i', $id);
DB::commit();
} catch (\Throwable $e) {
DB::rollback();
throw $e;
}
self::clear($id);
return true;
}
public static function items($directoryId, $includeInactive = true)
{
if (!self::schemaReady()) {
return array();
}
$sql = 'SELECT * FROM ' . self::itemsTable() . ' WHERE directory_id = %i';
if (!$includeInactive) {
$sql .= ' AND is_active = 1';
}
$sql .= ' ORDER BY sort_order ASC, id ASC';
$rows = DB::query($sql, (int) $directoryId)->getAll() ?: array();
return array_map(array(self::class, 'normalizeItem'), $rows);
}
public static function item($id)
{
if (!self::schemaReady()) {
return null;
}
$row = DB::query('SELECT * FROM ' . self::itemsTable() . ' WHERE id = %i LIMIT 1', (int) $id)->getAssoc();
return $row ? self::normalizeItem($row) : null;
}
public static function saveItem($directoryId, $id, array $input)
{
$directoryId = (int) $directoryId;
$id = (int) $id;
if (!self::find($directoryId)) {
throw new \RuntimeException('Справочник не найден');
}
$label = trim(isset($input['label']) ? (string) $input['label'] : '');
$key = trim(isset($input['item_key']) ? (string) $input['item_key'] : '');
if ($label === '') {
throw new \InvalidArgumentException('Укажите подпись значения');
}
if ($key === '') {
$key = Str::slug($label);
}
$key = mb_substr($key, 0, 120, 'UTF-8');
if ($key === '') {
throw new \InvalidArgumentException('Укажите стабильный ключ значения');
}
if (!preg_match('/^[a-zA-Z0-9][a-zA-Z0-9_.:-]{0,119}$/', $key)) {
throw new \InvalidArgumentException('Ключ: латиница, цифры, _, -, точка или двоеточие');
}
$duplicate = DB::query(
'SELECT id FROM ' . self::itemsTable()
. ' WHERE directory_id = %i AND item_key = %s AND id != %i LIMIT 1',
$directoryId,
$key,
$id
)->getValue();
if ($duplicate) {
throw new \InvalidArgumentException('Такой ключ уже есть в этом справочнике');
}
$data = array(
'directory_id' => $directoryId,
'item_key' => $key,
'label' => mb_substr($label, 0, 255, 'UTF-8'),
'sort_order' => max(0, (int) (isset($input['sort_order']) ? $input['sort_order'] : 0)),
'is_active' => !empty($input['is_active']) ? 1 : 0,
'parent_id' => max(0, (int) (isset($input['parent_id']) ? $input['parent_id'] : 0)),
'data_json' => self::normalizeData(isset($input['data_json']) ? $input['data_json'] : ''),
'updated_at' => time(),
);
if ($id > 0) {
$current = self::item($id);
if (!$current || $current['directory_id'] !== $directoryId) {
throw new \RuntimeException('Значение справочника не найдено');
}
DB::Update(self::itemsTable(), $data, 'id = %i', $id);
} else {
if ($data['sort_order'] === 0) {
$data['sort_order'] = (int) DB::query(
'SELECT COALESCE(MAX(sort_order), 0) + 10 FROM ' . self::itemsTable() . ' WHERE directory_id = %i',
$directoryId
)->getValue();
}
$data['created_at'] = time();
DB::Insert(self::itemsTable(), $data);
$id = (int) DB::insertId();
}
self::clear($directoryId);
return self::item($id);
}
public static function deleteItem($directoryId, $id)
{
$item = self::item($id);
if (!$item || $item['directory_id'] !== (int) $directoryId) {
return false;
}
DB::Delete(self::itemsTable(), 'id = %i AND directory_id = %i', (int) $id, (int) $directoryId);
self::clear((int) $directoryId);
return true;
}
public static function reorderItems($directoryId, array $ids)
{
$directoryId = (int) $directoryId;
$position = 10;
foreach ($ids as $id) {
DB::Update(
self::itemsTable(),
array('sort_order' => $position, 'updated_at' => time()),
'id = %i AND directory_id = %i',
(int) $id,
$directoryId
);
$position += 10;
}
self::clear($directoryId);
}
public static function optionMap($directoryId, $includeInactive = false)
{
$directoryId = (int) $directoryId;
if ($directoryId < 1 || !self::schemaReady()) {
return array();
}
$key = 'content.directory.options.' . $directoryId . '.' . ($includeInactive ? 'all' : 'active');
return Cache::rememberTagged(
$key,
self::CACHE_TTL,
array(CacheKey::tag('directory'), CacheKey::tag('directory', $directoryId)),
function () use ($directoryId, $includeInactive) {
$out = array();
foreach (self::items($directoryId, $includeInactive) as $item) {
$out[(string) $item['item_key']] = (string) $item['label'];
}
return $out;
}
);
}
public static function usageCount($directoryId)
{
$directoryId = (int) $directoryId;
if ($directoryId < 1) {
return 0;
}
$usage = self::usageMap();
return isset($usage[$directoryId]) ? (int) $usage[$directoryId] : 0;
}
public static function usageMap()
{
$rows = DB::query(
'SELECT rubric_field_settings FROM ' . ContentTables::table('rubric_fields')
. " WHERE rubric_field_settings LIKE %ss",
'"option_source":"directory"'
)->getAll() ?: array();
$usage = array();
foreach ($rows as $row) {
$settings = Json::toArray((string) $row['rubric_field_settings'], array());
$id = isset($settings['directory_id']) ? (int) $settings['directory_id'] : 0;
if ($id > 0) {
$usage[$id] = isset($usage[$id]) ? $usage[$id] + 1 : 1;
}
}
return $usage;
}
public static function schemaReady()
{
static $ready = null;
if ($ready === null) {
$ready = DatabaseSchema::tableExists(self::table())
&& DatabaseSchema::tableExists(self::itemsTable());
}
return $ready;
}
public static function table()
{
return ContentTables::table('directories');
}
public static function itemsTable()
{
return ContentTables::table('directory_items');
}
public static function clear($directoryId = 0)
{
Cache::forgetTag(CacheKey::tag('directory'));
if ((int) $directoryId > 0) {
Cache::forgetTag(CacheKey::tag('directory', (int) $directoryId));
}
}
public static function normalizeDirectory($row)
{
$row = (array) $row;
foreach (array('id', 'is_active', 'created_at', 'updated_at', 'items_count', 'active_items_count') as $key) {
$row[$key] = isset($row[$key]) ? (int) $row[$key] : 0;
}
$row['name'] = isset($row['name']) ? (string) $row['name'] : '';
$row['code'] = isset($row['code']) ? (string) $row['code'] : '';
$row['description'] = isset($row['description']) ? (string) $row['description'] : '';
$row['is_active'] = $row['is_active'] === 1;
return $row;
}
public static function normalizeItem($row)
{
$row = (array) $row;
foreach (array('id', 'directory_id', 'sort_order', 'is_active', 'parent_id', 'created_at', 'updated_at') as $key) {
$row[$key] = isset($row[$key]) ? (int) $row[$key] : 0;
}
$row['item_key'] = isset($row['item_key']) ? (string) $row['item_key'] : '';
$row['label'] = isset($row['label']) ? (string) $row['label'] : '';
$row['data_json'] = isset($row['data_json']) ? (string) $row['data_json'] : '';
$row['is_active'] = $row['is_active'] === 1;
return $row;
}
protected static function normalizeData($raw)
{
$raw = trim((string) $raw);
if ($raw === '') {
return '';
}
$decoded = json_decode($raw, true);
if (!is_array($decoded)) {
throw new \InvalidArgumentException('Дополнительные данные должны быть корректным JSON-объектом');
}
return Json::encode($decoded, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
}