Кеширование в Cotonti
Что это и зачем
Кеш — сохранённая копия того, что сайт уже один раз посчитал или сгенерировал. Вместо того чтобы каждый раз заново собирать страницу из базы, шаблонов и PHP-кода, Cotonti достаёт готовый результат из хранилища. Это ускоряет сайт и снижает нагрузку на сервер и базу данных.
Какие уровни кеша есть в Cotonti
В движке четыре независимых хранилища:
- Дисковый кеш (File). Файлы в папке
cache_dir. Работает всегда, если папка доступна для записи. - Кеш в базе данных (MySQL). Таблица
cot_cacheс полями имя / область / срок жизни / значение. Используется по умолчанию для «средних» данных. - Кеш в памяти (Memcache / APC / XCache). Самый быстрый. Работает только если на сервере установлено соответствующее расширение PHP. При перезапуске сервиса данные из памяти пропадают.
- Статический кеш страниц (Page cache). Готовый HTML целой страницы, сжатый gzip, с заголовками Last-Modified и ETag. Отдаётся посетителю мгновенно, без обращения к PHP и БД.
Всеми четырьмя управляет одна общая надстройка Cache, у которой есть ссылки на каждый уровень отдельно: disk, db, mem, static.
Как это работает
Области (realm). Каждая запись принадлежит какой-то области: cot (по умолчанию), system, page, forums — обычно имя модуля или расширения. Область позволяет чистить кеш точечно — только то, что относится к конкретному разделу, не трогая остальное.
Срок жизни (TTL). У записи можно указать срок — сколько секунд она считается актуальной. По умолчанию для динамического кеша — 3600 секунд (1 час). У статического (File) срока нет, файл живёт до явного удаления.
Автоочистка (garbage collection). Для кеша в БД работает вероятностный сборщик: при создании объекта кеша примерно в 10% случаев происходит удаление записей с истёкшим сроком.
Статический кеш страниц. Перед выдачей страницы Cotonti проверяет, есть ли готовая копия. Если есть и она не устарела, посетителю уходят HTTP-заголовки 304 Not Modified либо сразу готовый gzip-html. Если копия отсутствует — генерируется новой.
Ограничение по размеру. Кеш в БД не принимает объекты больше ~16 МБ (предел типа MEDIUMTEXT). Более крупные значения в БД не сохраняются.
Где чистить кеш в админке
- Дисковый кеш. Админ-панель → Other → Disk cache. Показывается список областей, число файлов и общий размер. Кнопки: Обновить (перечитать состояние) и Очистить всё (полностью стереть содержимое папки кеша). Напротив каждой области — своя кнопка удаления.
- Кеш в БД и в памяти очищается программно — обычно через тот же административный раздел «Cache» (
admin.cache.disk.phpчистит только диск). Общая очистка всех уровней делается вызовом$cache->clear()из кода или через настройки движка (кнопка сброса кеша при изменении конфигурации). - Кеш шаблонов сбрасывается автоматически при их правке в админке.
Что защищено от удаления
При очистке дискового кеша не удаляются два файла в корне папки:
index.html— заглушка от просмотра содержимого папки в браузере..htaccess— правила доступа.
Они сохраняются всегда, в том числе при «Очистить всё».
Когда и зачем чистить дисковый кеш
Обязательно чистить:
- После обновления движка или модулей — старые файлы могут конфликтовать с новым кодом.
- После смены темы сайта — старая разметка может кешироваться и выдаваться вместо новой.
- После массовой правки статей, категорий, настроек — кеш хранит устаревшие версии.
- Если на сайте видны «призраки» — удалённый товар, изменённая цена, старый заголовок.
Полезно чистить:
- Перед важным обновлением контента — чтобы посетители сразу увидели новое.
- Если диск на сервере быстро заполняется — папка
cache_dirиногда разрастается до гигабайт.
Не нужно чистить:
- При каждом заходе в админку — кеш сам обновляется по срокам и событиям.
- Пока сайт работает нормально и данные свежие — очистка лишний раз только заставит сервер пересобрать всё заново, что на короткое время замедлит сайт.
Что происходит после очистки
Первые посетители после очистки получат страницы, собранные «с нуля» — это на секунды нагрузит сервер и БД. Затем кеш снова наполнится, и сайт вернётся к обычной скорости. Никакие данные при очистке не теряются — удаляется только копия, оригинал (статьи, товары, настройки) остаётся в базе.
Кеширование в Cotonti. Техническое руководство
Материал построен на анализе файлов: system/cache.php (класс Cache, драйверы), system/admin/admin.cache.disk.php, system/admin/tpl/admin.cache.disk.tpl. Никаких предположений — только то, что видно в исходниках.
1. Архитектура
Единая точка входа — класс Cache, который держит ссылки на четыре независимых драйвера:
public $disk; // File_cache — статический, файловый
public $db; // Db_cache_driver — временный, в MySQL
public $mem; // Temporary_cache_driver — в памяти (Memcache/APC/XCache)
public $static; // Page_cache — целые HTML-страницы
Управляется одной инициализацией Cache::init(), которая поднимает каждый драйвер по очереди. Если драйвер недоступен (папка не пишется, расширение PHP не установлено) — в $this->mem остаётся false, но остальные продолжают работать.
Всего четыре константы типа кеша для точечной очистки:
COT_CACHE_TYPE_ALL = 0
COT_CACHE_TYPE_DISK = 1
COT_CACHE_TYPE_DB = 2
COT_CACHE_TYPE_MEMORY = 3
COT_CACHE_TYPE_STATIC = 4
COT_CACHE_TYPE_DEFAULT = COT_CACHE_TYPE_DB
Константы COT_DEFAULT_REALM = 'cot' и COT_DEFAULT_TTL = 3600 (1 час) — значения по умолчанию для всех драйверов.
2. Иерархия классов драйверов
Абстрактные классы:
Cache_driver— базовый. Методыclear(),exists(),get(),remove().Static_cache_driver— добавляетstore($id, $data, $realm)без TTL. Живёт до явного удаления.Dynamic_cache_driver— добавляетstore($id, $data, $realm, $ttl = COT_DEFAULT_TTL). С TTL.Writeback_cache_driver(наследник Dynamic) — копит изменения в памяти и пишет их одним пакетом вflush()на__destruct. Даётremove_now()иstore_now()для немедленной записи в обход буфера.Db_cache_driver(наследник Writeback) — добавляетget_all($realm)для загрузки всех значений области в глобальную область видимости.Temporary_cache_driver(наследник Dynamic) — добавляетinc(),dec(),get_info()иget_ini_size().
Реальные драйверы:
| Класс | Родитель | Тип | Условие загрузки |
|---|---|---|---|
File_cache | Static_cache_driver | Диск | Всегда |
MySQL_cache | Db_cache_driver | БД | Всегда |
Memcache_driver | Temporary_cache_driver | Память | extension_loaded('memcache') |
APC_driver | Temporary_cache_driver | Память | extension_loaded('apc') |
Xcache_driver | Temporary_cache_driver | Память | extension_loaded('xcache') |
Класс Page_cacheне наследуется ни от чего — это самостоятельный контроллер статических страниц.
Список доступных драйверов ведётся в массиве $cot_cache_drivers при загрузке файла.
3. Область (realm)
Каждая запись кеша привязана к области. Это строковое имя — по умолчанию cot, но может быть любым: system, page, forums, market, имя плагина.
Область участвует в:
- Идентификации: ключ =
realm . '/' . id(для Memcache, APC, XCache). - Файловом пути:
cache_dir/<realm>/<id>(дляFile_cache). - SQL-условии:
WHERE c_realm = '...'(дляMySQL_cache). - Выборочной очистке:
clear_realm($realm, $type).
Автозагрузка при инициализации: Cache::init() формирует список областей $cot_cache_autoload, куда всегда попадают system и cot, плюс текущий Cot::$env['ext'], плюс значения, дописанные ранее. Все эти области загружаются из БД в глобальную область видимости через MySQL_cache::get_all().
Это значит: переменные, сохранённые в этих областях, доступны в PHP как $имя_переменной сразу после init(), без явного get().
4. Драйвер File_cache
Конструктор: принимает корневую папку (cache_dir по умолчанию). Если папки нет — создаёт с правами 0755. Если не пишется — бросает Exception. Папка обязательна.
Формат хранения: <cache_dir>/<realm>/<id>, содержимое — serialize($data). Без gzip.
exists($id, $realm, $ttl = 0): проверяет file_exists и, если TTL > 0, что time() - filemtime < $ttl.
get(): если запись существует и не истекла — unserialize(file_get_contents(...)), иначе NULL.
store(): создаёт папку области при необходимости, пишет serialize в файл.
remove(): удаляет файл, возвращает bool.
clear($realm = '', $exceptRealms = ['assets', 'static', 'htmlpurifier', 'templates']):
- Если указан
$realm— чистит только эту папку и удаляет её саму. - Если пусто — обходит все подпапки
cache_dir, кроме перечисленных в$exceptRealms. - Всегда сохраняет
index.htmlи.htaccessв корне кеша — их не удаляет ни при каких условиях. - Удаление рекурсивное: файлы, затем подпапки, затем саму папку.
Исключённые области не чистятся никогда при clear() без параметров. Это защита от потери статики сайта (ассеты темы), кеша HTMLPurifier (тяжело пересобирается) и кеша шаблонов.
5. Драйвер MySQL_cache
Работает через буфер в памяти и записывает всё пакетом. Таблица — Cot::$db->cache (обычно cot_cache).
Конструктор: с вероятностью 10% (условие mt_rand(1, 10) == 5) вызывает gc() — удаление записей с истёкшим TTL.
__destruct(): вызывает flush(). То есть все изменения уходят в БД в самом конце скрипта, одним запросом.
flush():
- Для
removed_dataформирует одинDELETE ... WHERE (c_name=... AND c_realm=...) OR .... - Для
writeback_dataформируетINSERT ... VALUES (...), (...), ... ON DUPLICATE KEY UPDATE c_value=VALUES(c_value), c_expire=VALUES(c_expire). c_expire = 0означает бессрочное хранение.
store(): проверяет длину serialize($data) — если больше 16 777 215 байт (предел MEDIUMTEXT), возвращает false без записи.
store_now() / remove_now(): пишут в БД немедленно, в обход буфера. Используются, когда данные нужны другим процессам прямо сейчас.
get_all($realm): выбирает значения из БД и через global ${$row['c_name']} создаёт/перезаписывает переменные в глобальной области. Возвращает количество загруженных записей.
exists(): после первого обращения к записи кладёт её в буфер $this->buffer[$realm][$id], повторные обращения идут из памяти.
6. Драйверы в памяти
Все три (Memcache, APC, XCache) наследуют Temporary_cache_driver и работают с ключом realm . '/' . id.
Memcache дополнительно префиксует ключ через createKey(): md5(Cot::$cfg['site_id'] . $key). Это позволяет нескольким сайтам использовать один Memcached-сервер без коллизий. clear() для Memcache не поддерживает очистку конкретной области — вызывает полный flush(), стирая всё.
APC при переполнении памяти (свободно < 20%) сам вызывает $this->clear() перед записью. Хранит serialize($data), читает с unserialize().
XCache при clear($realm) использует хак xcache_unset_by_prefix — обёртку над xcache_clear_cache(XC_TYPE_VAR, 0), которая по факту чистит всё, а не только по префиксу.
Все три получают информацию о памяти через get_info(). Если у драйвера нет данных (например, Memcache не отвечает) — возвращает массив с нулями или -1.
7. Выбор драйвера памяти
В Cache::init():
Cot::$cfg['cache_drv'] .= '_driver';
if (in_array(Cot::$cfg['cache_drv'], $cot_cache_drivers)) {
$selected = Cot::$cfg['cache_drv'];
}
То есть в конфиге $cfg['cache_drv'] хранится короткое имя (memcache, apc, xcache), а код дописывает суффикс _driver и ищет полученное имя в списке загруженных драйверов.
После создания объекта проверяется get_info()['max']. Если максимум ≤ 1024 байт (то есть расширение установлено, но кеш не работает), драйвер не принимается, $this->mem = false.
Если $cfg['cache_drv'] не совпадает ни с одним драйвером — $this->mem = false. Кеш в памяти просто не используется.
8. Контроллер Cache
Публичные методы:
init()— поднимает все четыре драйвера. Вызывается один раз при старте.bind($event, $id, $realm, $type)— привязывает событие к записи кеша. При срабатывании события запись автоматически удаляется.bind_array($bindings)— пакетная версияbind.unbind($realm, $id = '')— снять привязки. Без$id— все из области.trigger($event)— запустить событие, удалить все привязанные записи, вернуть число удалённых.clear($type = COT_CACHE_TYPE_ALL)— очистить всё или определённый тип.clear_realm($realm, $type = COT_CACHE_TYPE_ALL)— очистить конкретную область.get_info()— информация о памяти (от текущего memory-драйвера или пусто).
Магические свойства:
mem_available—true, если memory-драйвер работает.mem_driver— имя выбранного драйвера (Memcache_driverи т. п.).
Защита от повторной синхронизации: если в течение запроса были bind(), bind_array() или unbind(), ставится флаг resync_on_exit, и в __destruct() перечитываются привязки.
Важный нюанс. В исходниках функции resync_bindings() код закомментирован (строки SELECT/INSERT в БД). То есть механизм привязок декларирован, но в текущей версии не работает полностью: bind() пишет в таблицу cot_cache_bindings, а trigger() ищет привязки в пустом массиве $this->bindings и всегда возвращает 0.
Практический вывод: автоматическая инвалидация по событиям не работает. Очистку кеша нужно делать явно через $cache->clear_realm(...) или $cache->disk->remove(...).
9. Статический кеш страниц (Page_cache)
Хранит целый HTML страницы, сжатый gzip. Расположен в <cache_dir>/static/.
Инициализация:
$pageCache->init($path, $name, $exclude = [], $ext = '');
$path— путь-«папка» кеша внутриstatic/.$name— базовое имя файла.$exclude— GET-параметры, которые игнорируются при формировании имени.$ext— расширение файла (если нужно).
Имя файла = path/name + _ + md5(serialize(GET-args)) + sha1(...) + .ext. Параметры сортируются ksort перед хешированием.
initByUri($uri, ...)— вариант с автоматическим вычислением пути. Разбирает URI, берёт c и e из query string, формирует путь. Если установлена функция cot_staticCacheGetPathByUri (расширение), делегирует ей.
read()— если файл есть:
- Считает
ETag = md5(имя + размер + mtime). - Если пришёл заголовок
If-None-Matchсо совпадающим ETag иIf-Modified-Since >= mtime— отправляет304 Not Modifiedи выходит. - Иначе —
Last-Modified,ETag,Expires: Mon, 01 Apr 1974 00:00:00 GMT,Cache-Control: must-revalidate, proxy-revalidate. - Если клиент не принимает gzip —
readgzfile(), иначеContent-Encoding: gzip+ сырое содержимое. - Всегда завершается
exit— дальнейший код не выполняется.
write()— если кеш включён, создаёт папку пути и пишет gzencode(cot_outputFilters(ob_get_contents())). То есть сжатый gzip результат всех выходных фильтров.
clear($path, $withSubDirectories = false)— удаляет все файлы в папке (и подпапки, если указано).
disable()— отключает кеш для текущего запроса (например, если пришёл авторизованный пользователь).
Папка staticвсегда в списке $exceptRealms для File_cache::clear() — общая очистка дискового кеша её не тронет.
10. Управление дисковым кешем в админке
Файл system/admin/admin.cache.disk.php доступен только при $usr['isadmin'] == true. Хуки: admin.cache.disk.first, admin.cache.disk.loop, admin.cache.disk.tags.
Действия:
a=purge— вызываетcot_diskcache_clearall(), дополнительно удаляет из БД записьcot_rc_html(кеш консолидации ресурсов), редиректит обратно.a=delete&id=<имя>— удаляет одну папку или один файл. Значениеid=COT_DISKCACHE_ONLYFILES('*files*') означает «файлы в корне кеша, без подпапок».
Защита от подстановок: проверяется, что id не содержит / и \, не равен . или ...
Функции просмотра:
cot_diskcache_calc($dir, $do_subdirs = true)— рекурсивно считает файлы и размер папки.cot_diskcache_list()— возвращает массив «имя папки → [файлов, байт]». Плюс строка для корневых файлов под именем*files*.cot_diskcache_clear($dir, $do_subdirs = true, $rm_dir = false)— рекурсивное удаление.cot_diskcache_clearall()— чистит корень без подпапок, затем каждую подпапку с удалением.
Что не удаляется: ни при каком действии не трогаются <cache_dir>/index.html и <cache_dir>/.htaccess.
UI (admin.cache.disk.tpl): таблица «Item | Files | Size | Delete», кнопки «Обновить» и «Очистить всё», в подвале — общее число файлов и суммарный размер.
11. Конфигурационные параметры
Из кода используются:
$cfg['cache_dir']— корневая папка дискового кеша. Обязательна.$cfg['cache_drv']— имя memory-драйвера (memcache,apc,xcache). Пустая строка — memory-кеш не используется.$cfg['cache_drv_host']— хост для Memcache.$cfg['cache_drv_port']— порт для Memcache.$cfg['site_id']— используется Memcache для разделения ключей между сайтами на одном сервере.$cfg['dir_perms']— права на создаваемые папки.
Плюс константы в глобальной области:
$cot_cache_drivers— список загруженных memory-драйверов.$cot_cache_autoload— массив областей для автозагрузки.$cot_cache_bindings— устаревший массив; в текущей версии не заполняется.
12. Реальные примеры использования
Запись в БД-кеш со сроком жизни 5 минут:
Cot::$cache->db->store('my_data', $value, 'myrealm', 300);
Чтение:
$value = Cot::$cache->db->get('my_data', 'myrealm');
if ($value === null) {
// кеш промахнулся — считаем и сохраняем
$value = calc();
Cot::$cache->db->store('my_data', $value, 'myrealm', 300);
}
Запись немедленно (в обход буфера) — нужно, когда другой процесс должен увидеть данные сразу:
Cot::$cache->db->store_now('my_data', $value, 'myrealm', 300);
Запись в память, если доступна, с фолбэком на БД:
if (Cot::$cache->mem) {
Cot::$cache->mem->store('my_data', $value, 'myrealm', 300);
} else {
Cot::$cache->db->store('my_data', $value, 'myrealm', 300);
}
Очистка всей области во всех слоях:
Cot::$cache->clear_realm('myrealm', COT_CACHE_TYPE_ALL);
Точечное удаление одной записи во всех слоях:
$id = 'my_data';
$realm = 'myrealm';
Cot::$cache->disk->remove($id, $realm);
Cot::$cache->db->remove($id, $realm);
if (Cot::$cache->mem) {
Cot::$cache->mem->remove($id, $realm);
}
Автозагрузка переменной из БД-кеша в глобальную область:
Достаточно в Cache::init() расширить $cot_cache_autoload именем области. После этого переменные области будут доступны как глобальные.
13. Особенности и подводные камни
Буферизация записи в БД-драйвере. store() не пишет в базу сразу — только накапливает. Если процесс упадёт до __destruct(), данные не сохранятся. Для критичных случаев используйте store_now().
Проверка размера в БД. Значения больше ~16 МБ не запишутся, store() вернёт false без ошибки.
Очистка memory-кеша. У Memcache и XCache очистка конкретной области фактически сбрасывает всё хранилище. Это надо учитывать при совместном использовании сервера несколькими сайтами.
Page_cache завершает скрипт. Если read() нашёл страницу — вызывается exit. Весь оставшийся PHP не выполнится. Это значит, что динамические блоки (приветствие пользователя, корзина) в такую страницу не попадут.
Инвалидация по событиям не работает. Как указано выше, в текущей версии ядра trigger() ничего не удаляет — массив $this->bindings пуст. Придётся чистить вручную.
cache_drv в конфиге хранит короткое имя. Реальный класс называется <имя>_driver, суффикс дописывается кодом.
Корневые index.html и .htaccess неприкосновенны. Не пытайтесь удалить их скриптом — они всё равно не удалятся через штатные функции.
Примеры файлов с хуками для admin.cache.disk.php
Хуки в Cotonti регистрируются в заголовке каждого файла-обработчика, а не в setup-файле. Ниже — три отдельных файла, по одному на каждый хук этого документа.
1. admin.cache.disk.first
Вызывается в начале admin.cache.disk.php — до обработки purge и delete.
Файл plugins/cacheguard/cacheguard.admin.cache.disk.first.php:
<?php
/* ====================
[BEGIN_COT_EXT]
Hooks=admin.cache.disk.first
Order=10
[END_COT_EXT]
==================== */
/**
* Запрещает очистку дискового кеша в рабочее время (будни, 9:00–18:00).
* В остальное время очистка работает как обычно.
*
* @package cacheguard
* @version 1.0.0
* @author webitproff
* @copyright (c) 2026 webitproff
* @license BSD
*/
defined('COT_CODE') or die('Wrong URL.');
$hour = (int) date('G');
$dow = (int) date('N'); // 1 = Пн ... 7 = Вс
// Будни, рабочее время — блокируем очистку
if ($dow <= 5 && $hour >= 9 && $hour < 18) {
if (in_array($a, ['purge', 'delete'], true)) {
cot_message('Очистка кеша запрещена в рабочее время (9:00–18:00 по будням).');
cot_redirect(cot_url('admin', ['m' => 'cache', 's' => 'disk'], '', true));
}
}
Что делает: перехватывает запросы на purge и delete в рабочее время, разворачивает администратора обратно с сообщением.
2. admin.cache.disk.loop
Вызывается внутри цикла по областям кеша (foreach ($row as $i => $x)), для каждой строки таблицы.
Файл plugins/cacheguard/cacheguard.admin.cache.disk.loop.php:
<?php
/* ====================
[BEGIN_COT_EXT]
Hooks=admin.cache.disk.loop
Order=10
[END_COT_EXT]
==================== */
/**
* Добавляет в строку таблицы кеша пользовательскую заметку:
* для области "static" — сколько страниц закешировано,
* для остальных — пусто.
*
* @package cacheguard
* @version 1.0.0
* @author webitproff
* @copyright (c) 2026 webitproff
* @license BSD
*/
defined('COT_CODE') or die('Wrong URL.');
if ($i === 'static') {
// В области static кешируются целые страницы, файлы — обычные .gz
$t->assign(
'ADMIN_DISKCACHE_ITEM_NOTE',
'<span class="text-muted">Кеш страниц: ' . (int) $x[0] . ' файлов</span>'
);
} else {
$t->assign('ADMIN_DISKCACHE_ITEM_NOTE', '');
}
В шаблоне admin.cache.disk.tpl внутри блока ADMIN_DISKCACHE_ROW добавить колонку:
<td class="textcenter">{ADMIN_DISKCACHE_ITEM_NOTE}</td>
Что делает: для области static выводит отдельную заметку в строке таблицы. Для остальных областей — ничего.
3. admin.cache.disk.tags
Вызывается в самом конце admin.cache.disk.php — перед $t->parse('MAIN').
Файл plugins/cacheguard/cacheguard.admin.cache.disk.tags.php:
<?php
/* ====================
[BEGIN_COT_EXT]
Hooks=admin.cache.disk.tags
Order=10
[END_COT_EXT]
==================== */
/**
* Добавляет в шаблон страницы дискового кеша:
* — общий размер кеша в мегабайтах,
* — предупреждение, если размер превышает 500 МБ.
*
* @package cacheguard
* @version 1.0.0
* @author webitproff
* @copyright (c) 2026 webitproff
* @license BSD
*/
defined('COT_CODE') or die('Wrong URL.');
$mbTotal = round($cachesize / 1048576, 2);
$t->assign([
'ADMIN_DISKCACHE_TOTAL_MB' => $mbTotal,
'ADMIN_DISKCACHE_SIZE_WARNING' => ($mbTotal > 500)
? '<div class="alert alert-warning">Дисковый кеш превышает 500 МБ. Рекомендуется очистить.</div>'
: '',
]);
В admin.cache.disk.tpl добавить:
{ADMIN_DISKCACHE_SIZE_WARNING}
<p class="text-muted">Всего на диске: {ADMIN_DISKCACHE_TOTAL_MB} МБ</p>
Что делает: добавляет предупреждение при превышении 500 МБ и точный размер кеша в МБ.
Файл cacheguard.setup.php (без регистрации хуков)
В setup-файле никакой регистрации хуков быть не должно — она в заголовках выше. Setup-файл только описывает само расширение:
<?php
/* ====================
[BEGIN_COT_EXT]
Name=Cache Guard
Description=Защита и мониторинг дискового кеша
Version=1.0.0
Date=2026-01-01
Author=webitproff
Copyright=(c) 2026 webitproff
Notes=
Auth_guests=R
Lock_guests=W12345A
Auth_members=R
Lock_members=W12345A
[END_COT_EXT]
==================== */
defined('COT_CODE') or die('Wrong URL.');
Как это работает
- Cotonti при установке плагина сканирует все файлы
cacheguard.*.phpв папке плагина. - У каждого файла читает блок
[BEGIN_COT_EXT]из первых строк и смотрит значениеHooks=.... - Записывает пару «плагин → файл → хук» в таблицу
cot_plugins. - При вызове
cot_getextplugins('admin.cache.disk.first')ядро находит все плагины, зарегистрированные на этот хук, и подключает их файлы черезinclude. Orderв заголовке задаёт порядок вызова, если на один хук подписано несколько плагинов. По умолчанию — 10.
Один плагин может объявлять несколько хуков — по одному в каждом файле. Файл .setup.php для этого не нужен.