Files
ave-cms/adminx/modules/PublicSite/Model.php
T
2026-07-30 11:56:32 +03:00

499 lines
17 KiB
PHP

<?php
/*
|--------------------------------------------------------------------------------------
| AVE.cms
|--------------------------------------------------------------------------------------
| @package AVE.cms
| @file adminx/modules/PublicSite/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\PublicSite;
defined('BASEPATH') || die('Direct access to this location is not allowed.');
use App\Common\DatabaseSchema;
use App\Content\CatalogTables;
use App\Content\ContentPlacementMap;
use App\Content\ContentTables;
use DB;
class Model
{
public static function stats()
{
return array(
'rubrics' => self::count(ContentTables::table('rubrics')),
'documents' => self::count(ContentTables::table('documents'), "document_deleted!='1'"),
'requests' => self::count(ContentTables::table('request')),
'components' => self::count(ContentTables::table('sysblocks'))
+ self::count(ContentTables::table('navigation')),
'catalogs' => self::count(CatalogTables::table('module_catalog_settings')),
'presentations' => self::count(CatalogTables::table('catalog_card_templates'))
+ self::count(CatalogTables::table('catalog_filter_templates')),
);
}
public static function structure($query = '')
{
$query = trim((string) $query);
$sql = 'SELECT r.Id id,r.rubric_title title,r.rubric_alias alias,r.rubric_template_id template_id,'
. ' r.rubric_docs_active documents_enabled,CHAR_LENGTH(TRIM(r.rubric_template)) template_length,'
. ' COALESCE(t.template_title,\'\') template_title'
. ' FROM ' . ContentTables::table('rubrics') . ' r'
. ' LEFT JOIN ' . ContentTables::table('templates') . ' t ON t.Id=r.rubric_template_id';
$args = array();
if ($query !== '') {
$sql .= ' WHERE r.rubric_title LIKE %s OR r.rubric_alias LIKE %s OR r.Id=%i';
$args[] = '%' . $query . '%';
$args[] = '%' . $query . '%';
$args[] = (int) $query;
}
$sql .= ' ORDER BY r.rubric_position,r.rubric_title,r.Id';
$rows = self::queryAll($sql, $args);
$documents = self::documentCounts();
$fields = self::groupedCount(ContentTables::table('rubric_fields'), 'rubric_id');
$requests = self::groupedCount(ContentTables::table('request'), 'rubric_id');
$rubricTemplates = self::groupedCount(ContentTables::table('rubric_templates'), 'rubric_id');
$catalogs = self::groupedCount(CatalogTables::table('module_catalog_settings'), 'rubric_id');
foreach ($rows as &$row) {
$id = (int) $row['id'];
$row['id'] = $id;
$row['title'] = self::decode($row['title']);
$row['alias'] = (string) $row['alias'];
$row['template_id'] = (int) $row['template_id'];
$row['template_title'] = self::decode($row['template_title']);
$row['documents_enabled'] = (int) $row['documents_enabled'] === 1;
$row['documents_count'] = isset($documents[$id]) ? (int) $documents[$id]['total'] : 0;
$row['published_count'] = isset($documents[$id]) ? (int) $documents[$id]['published'] : 0;
$row['fields_count'] = isset($fields[$id]) ? (int) $fields[$id] : 0;
$row['requests_count'] = isset($requests[$id]) ? (int) $requests[$id] : 0;
$row['rubric_templates_count'] = isset($rubricTemplates[$id]) ? (int) $rubricTemplates[$id] : 0;
$row['catalogs_count'] = isset($catalogs[$id]) ? (int) $catalogs[$id] : 0;
$row['issues'] = self::rubricIssues($row);
}
unset($row);
return $rows;
}
public static function diagnostics(array $structure)
{
$items = array();
foreach ($structure as $rubric) {
foreach ($rubric['issues'] as $issue) {
$items[] = array(
'level' => $issue['level'],
'title' => $rubric['title'],
'message' => $issue['message'],
'url' => '/rubrics?q=' . rawurlencode($rubric['title']),
);
}
}
$requestTable = ContentTables::table('request');
if (self::available($requestTable)) {
$rows = DB::query(
'SELECT Id,request_title,request_alias,request_template_item FROM ' . $requestTable
. " WHERE TRIM(request_template_item)=''"
. ' ORDER BY request_title,Id LIMIT 50'
)->getAll() ?: array();
foreach ($rows as $row) {
$items[] = array(
'level' => 'warning',
'title' => self::decode($row['request_title']),
'message' => 'У подборки не заполнен шаблон элемента',
'url' => '/requests',
);
}
}
return array(
'items' => $items,
'errors' => count(array_filter($items, function ($item) { return $item['level'] === 'error'; })),
'warnings' => count(array_filter($items, function ($item) { return $item['level'] === 'warning'; })),
);
}
public static function placements(array $filters = array(), $refresh = false)
{
$all = ContentPlacementMap::all((bool) $refresh);
$query = self::lower(isset($filters['q']) ? trim((string) $filters['q']) : '');
$type = isset($filters['type']) ? trim((string) $filters['type']) : '';
$state = isset($filters['state']) ? trim((string) $filters['state']) : '';
$area = isset($filters['area']) ? trim((string) $filters['area']) : '';
$view = isset($filters['view']) && $filters['view'] === 'usage' ? 'usage' : 'placements';
$page = isset($filters['page']) ? max(1, (int) $filters['page']) : 1;
$rows = array_values(array_filter($all, function ($item) use ($query, $type, $state, $area) {
if ($type !== '' && $item['component_type'] !== $type) {
return false;
}
if ($state !== '' && $item['component_status'] !== $state) {
return false;
}
if ($area !== '' && $item['area'] !== $area) {
return false;
}
if ($query === '') {
return true;
}
$haystack = self::lower(implode(' ', array(
$item['source_title'],
$item['source_type_title'],
$item['source_part_title'],
$item['component_title'],
$item['component_alias'],
$item['component_key'],
$item['tag'],
)));
return strpos($haystack, $query) !== false;
}));
$filteredCount = count($rows);
$usage = self::placementUsage($rows);
$pagination = self::paginate($view === 'usage' ? $usage : $rows, $page, 25);
if ($view === 'usage') {
$usage = $pagination['items'];
} else {
$rows = $pagination['items'];
}
return array(
'rows' => $rows,
'usage' => $usage,
'stats' => self::placementStats($all, $filteredCount),
'options' => self::placementOptions($all),
'pagination' => $pagination,
);
}
public static function siteMap($query = '')
{
$query = self::lower(trim((string) $query));
$navigationTable = ContentTables::table('navigation');
$itemsTable = ContentTables::table('navigation_items');
$documentsTable = ContentTables::table('documents');
$menus = self::available($navigationTable)
? DB::query('SELECT navigation_id id,title,alias FROM ' . $navigationTable . ' ORDER BY title,navigation_id')->getAll() ?: array()
: array();
$rows = self::available($itemsTable)
? DB::query(
'SELECT i.navigation_item_id id,i.navigation_id,i.parent_id,i.level,i.position,i.status,i.title,i.alias,i.document_id,'
. ' d.document_title,d.document_alias,d.document_status,d.document_deleted'
. ' FROM ' . $itemsTable . ' i LEFT JOIN ' . $documentsTable . ' d ON d.Id=i.document_id'
. ' ORDER BY i.navigation_id,i.parent_id,i.position,i.navigation_item_id'
)->getAll() ?: array()
: array();
$itemsByMenu = array();
$linkedDocuments = array();
foreach ($rows as $row) {
$row = self::siteMapItem($row);
if ($query !== '' && strpos(self::lower($row['title'] . ' ' . $row['alias'] . ' ' . $row['document_title']), $query) === false) {
$row['query_match'] = false;
} else {
$row['query_match'] = true;
}
$itemsByMenu[$row['navigation_id']][] = $row;
if ($row['document_id'] > 0) { $linkedDocuments[$row['document_id']] = true; }
}
$totalItems = 0;
foreach ($menus as &$menu) {
$menu['id'] = (int) $menu['id'];
$menu['title'] = self::decode($menu['title']);
$menu['alias'] = (string) $menu['alias'];
$menuItems = isset($itemsByMenu[$menu['id']]) ? $itemsByMenu[$menu['id']] : array();
$menu['items_count'] = count($menuItems);
$menu['tree'] = self::siteMapTree($menuItems, 0, $query !== '');
$totalItems += $menu['items_count'];
}
unset($menu);
$orphanSql = 'SELECT d.Id id,d.document_title title,d.document_alias alias,d.document_status status,r.rubric_title rubric_title'
. ' FROM ' . $documentsTable . ' d'
. ' LEFT JOIN ' . ContentTables::table('rubrics') . ' r ON r.Id=d.rubric_id'
. " WHERE d.document_deleted!='1'";
$args = array();
if ($linkedDocuments) {
$orphanSql .= ' AND d.Id NOT IN %li';
$args[] = array_keys($linkedDocuments);
}
if ($query !== '') {
$orphanSql .= ' AND (LOWER(d.document_title) LIKE %s OR LOWER(d.document_alias) LIKE %s)';
$args[] = '%' . $query . '%';
$args[] = '%' . $query . '%';
}
$orphanSql .= ' ORDER BY d.document_status DESC,d.document_title,d.Id LIMIT 100';
$orphans = self::queryAll($orphanSql, $args);
foreach ($orphans as &$orphan) {
$orphan['id'] = (int) $orphan['id'];
$orphan['title'] = self::decode($orphan['title']);
$orphan['rubric_title'] = self::decode($orphan['rubric_title']);
$orphan['active'] = (string) $orphan['status'] === '1';
}
unset($orphan);
return array(
'menus' => $menus,
'orphans' => $orphans,
'stats' => array(
'menus' => count($menus),
'items' => $totalItems,
'linked_documents' => count($linkedDocuments),
'orphans' => count($orphans),
),
);
}
protected static function siteMapItem(array $row)
{
return array(
'id' => (int) $row['id'],
'navigation_id' => (int) $row['navigation_id'],
'parent_id' => (int) $row['parent_id'],
'level' => (int) $row['level'],
'position' => (int) $row['position'],
'active' => (string) $row['status'] === '1',
'title' => self::decode($row['title']),
'alias' => (string) $row['alias'],
'document_id' => isset($row['document_id']) ? (int) $row['document_id'] : 0,
'document_title' => self::decode(isset($row['document_title']) ? $row['document_title'] : ''),
'document_alias' => isset($row['document_alias']) ? (string) $row['document_alias'] : '',
'document_active' => isset($row['document_status']) && (string) $row['document_status'] === '1' && (string) $row['document_deleted'] !== '1',
);
}
protected static function siteMapTree(array $items, $parentId, $filtering, array $visited = array())
{
$tree = array();
foreach ($items as $item) {
if ((int) $item['parent_id'] !== (int) $parentId || isset($visited[$item['id']])) { continue; }
$nextVisited = $visited;
$nextVisited[$item['id']] = true;
$item['children'] = self::siteMapTree($items, $item['id'], $filtering, $nextVisited);
if (!$filtering || !empty($item['query_match']) || !empty($item['children'])) {
$tree[] = $item;
}
}
return $tree;
}
protected static function rubricIssues(array $row)
{
$issues = array();
if ((int) $row['template_id'] <= 0 || trim((string) $row['template_title']) === '') {
$issues[] = array('level' => 'error', 'message' => 'Не назначен существующий шаблон сайта');
}
if ((int) $row['template_length'] <= 0) {
$issues[] = array('level' => 'warning', 'message' => 'Пустой основной шаблон рубрики');
}
if ((int) $row['fields_count'] <= 0) {
$issues[] = array('level' => 'warning', 'message' => 'У типа контента нет полей');
}
return $issues;
}
protected static function placementStats(array $rows, $filtered)
{
$sources = array();
$total = 0;
$broken = 0;
$modules = 0;
foreach ($rows as $row) {
$sources[$row['source_type'] . ':' . $row['source_id']] = true;
$total += max(1, (int) $row['occurrences']);
$broken += $row['component_status'] === 'broken' ? 1 : 0;
$modules += $row['component_type'] === 'module' ? 1 : 0;
}
return array(
'total' => $total,
'rows' => count($rows),
'filtered' => (int) $filtered,
'sources' => count($sources),
'broken' => $broken,
'modules' => $modules,
);
}
protected static function placementUsage(array $rows)
{
$usage = array();
foreach ($rows as $row) {
$key = $row['component_type'] . ':' . strtolower((string) $row['component_key']);
if (!isset($usage[$key])) {
$usage[$key] = array(
'component_type' => $row['component_type'],
'component_title' => $row['component_title'],
'component_alias' => $row['component_alias'],
'component_key' => $row['component_key'],
'component_status' => $row['component_status'],
'component_status_title' => $row['component_status_title'],
'component_url' => $row['component_url'],
'placements' => 0,
'sources' => array(),
);
}
$usage[$key]['placements'] += max(1, (int) $row['occurrences']);
$usage[$key]['sources'][$row['source_type'] . ':' . $row['source_id']] = true;
if ($row['component_status'] === 'broken') {
$usage[$key]['component_status'] = 'broken';
$usage[$key]['component_status_title'] = $row['component_status_title'];
}
}
foreach ($usage as &$item) {
$item['sources'] = count($item['sources']);
}
unset($item);
$usage = array_values($usage);
usort($usage, function ($left, $right) {
return array(
$left['component_status'] === 'broken' ? 0 : 1,
-$left['placements'],
$left['component_title'],
) <=> array(
$right['component_status'] === 'broken' ? 0 : 1,
-$right['placements'],
$right['component_title'],
);
});
return $usage;
}
protected static function paginate(array $items, $page, $limit)
{
$total = count($items);
$limit = max(1, (int) $limit);
$pages = max(1, (int) ceil($total / $limit));
$page = min(max(1, (int) $page), $pages);
$offset = ($page - 1) * $limit;
return array(
'items' => array_slice($items, $offset, $limit),
'page' => $page,
'pages' => $pages,
'total' => $total,
'from' => $total > 0 ? $offset + 1 : 0,
'to' => min($total, $offset + $limit),
);
}
protected static function placementOptions(array $rows)
{
$options = array(
'types' => array(),
'states' => array(),
'areas' => array(),
);
$typeTitles = array(
'block' => 'Блоки',
'request' => 'Подборки',
'navigation' => 'Навигации',
'module' => 'Модульные теги',
);
foreach ($rows as $row) {
$options['types'][$row['component_type']] = isset($typeTitles[$row['component_type']])
? $typeTitles[$row['component_type']]
: $row['component_type'];
$options['states'][$row['component_status']] = $row['component_status_title'];
$options['areas'][$row['area']] = $row['area_title'];
}
foreach ($options as &$values) {
asort($values, SORT_NATURAL | SORT_FLAG_CASE);
}
unset($values);
return $options;
}
protected static function documentCounts()
{
$table = ContentTables::table('documents');
if (!self::available($table)) { return array(); }
$rows = DB::query(
'SELECT rubric_id,COUNT(*) total,'
. " SUM(CASE WHEN document_status='1' AND document_deleted!='1' THEN 1 ELSE 0 END) published"
. ' FROM ' . $table . " WHERE document_deleted!='1' GROUP BY rubric_id"
)->getAll() ?: array();
$counts = array();
foreach ($rows as $row) {
$counts[(int) $row['rubric_id']] = array(
'total' => (int) $row['total'],
'published' => (int) $row['published'],
);
}
return $counts;
}
protected static function groupedCount($table, $column)
{
if (!self::available($table) || !preg_match('/^[a-z0-9_]+$/', (string) $column)) {
return array();
}
$rows = DB::query(
'SELECT `' . $column . '` group_id,COUNT(*) amount FROM ' . $table
. ' GROUP BY `' . $column . '`'
)->getAll() ?: array();
$counts = array();
foreach ($rows as $row) { $counts[(int) $row['group_id']] = (int) $row['amount']; }
return $counts;
}
protected static function count($table, $where = '')
{
if (!self::available($table)) { return 0; }
return (int) DB::query('SELECT COUNT(*) FROM ' . $table . ($where !== '' ? ' WHERE ' . $where : ''))->getValue();
}
protected static function queryAll($sql, array $args)
{
return call_user_func_array(array('DB', 'query'), array_merge(array($sql), $args))->getAll() ?: array();
}
protected static function available($table)
{
try {
return DatabaseSchema::tableExists($table);
} catch (\Throwable $e) {
return false;
}
}
protected static function decode($value)
{
return html_entity_decode((string) $value, ENT_QUOTES, 'UTF-8');
}
protected static function lower($value)
{
$value = (string) $value;
return function_exists('mb_strtolower') ? mb_strtolower($value, 'UTF-8') : strtolower($value);
}
}