loader
СГБ - Сологуб
Заказать услуги
Задать вопрос
Нажимая на кнопку, вы даете согласие на обработку своих персональных данных
Вчера в 09:25 Читать ~ 4 минуты 6
0ответов

Класс кэширования контента в файлы на PHP: мой рабочий вариант

Example blog post alt

Когда сайт на PHP начинает тормозить, первым делом смотрю на то, что там генерируется на каждый запрос. Меню, хлебные крошки, выгрузки из базы, тяжёлые виджеты с кучей запросов — всё это часто можно просто сохранить в файл и отдавать из кэша, а не пересчитывать заново для каждого посетителя. Делюсь своим классом для файлового кэширования, который я использую в проектах, и объясню, чем он отличается от простой версии из интернета.

Зачем вообще кэшировать в файлы

Идея простая: один раз сформировать контент, сохранить во временный файл и при следующих обращениях отдавать готовый результат, пока кэш не устарел или не сброшен вручную. Для блогов, каталогов и корпоративных сайтов без сложной реалтайм-логики это даёт ощутимый прирост скорости без подключения Redis или memcached. Файловый кэш не требует отдельного сервиса — просто папка на диске, которую можно почистить в любой момент.

Что я изменил в базовой реализации

Стандартный класс из туториалов работает, но слишком прямолинейно. Я доработал его под реальные задачи:

Добавил время жизни кэша — чтобы данные не лежали вечно и сами обновлялись по расписанию. Без этого любая правка на сайте рискует не отобразиться, пока кто-то вручную не снесёт папку.

Сделал параметр hard — жёсткое обновление. Иногда нужно принудительно пересобрать кэш прямо сейчас, даже если срок ещё не вышел, например, после импорта товаров. Раньше для этого приходилось лезть на сервер и удалять файлы руками.

Добавил корректное сохранение с атомарной записью, чтобы файл не побился в момент одновременных запросов. Это критично, когда два посетителя одновременно дёргают страницу и оба пытаются перезаписать один и тот же кэш.

Встроил автоматическую вложенность папок, чтобы при большом количестве ключей не создавать одну гигантскую директорию с тысячами файлов — это тоже замедляет файловую систему.

Сам класс

 <?php
/** * Класс для кэширования фрагментов контента или страниц целиком. * Сохраняет сгенерированный вывод в файлы с учётом времени жизни. */
class FileCache
{ /** @var string Директория для хранения файлов кэша */ private static string $dir = __DIR__ . '/cache'; /** @var int Время жизни кэша по умолчанию, в секундах */ private static int $ttl = 3600; /** @var array Стек активных буферов (для поддержки вложенных вызовов) */ private static array $stack = []; /** * Настроить директорию кэша. */ public static function setDir(string $dir): void { self::$dir = rtrim($dir, '/'); } /** * Установить время жизни кэша по умолчанию, в секундах. */ public static function setTtl(int $seconds): void { self::$ttl = max(1, $seconds); } /** * Начать кэширование фрагмента. * * @param string $key Уникальный ключ фрагмента. * @param int|null $ttl Время жизни кэша в секундах, либо null для значения по умолчанию. * @param bool $hard Принудительно пересобрать кэш, игнорируя актуальный файл. * @return bool true, если кэш отсутствует и нужно вывести контент; false, если контент отдан из файла. */ public static function begin(string $key, ?int $ttl = null, bool $hard = false): bool { $ttl = $ttl ?? self::$ttl; // Превращаем ключ в безопасное имя файла $hash = md5($key); $file = self::$dir . '/' . substr($hash, 0, 2) . '/' . $hash . '.cache'; // Если жёсткое обновление не требуется и файл ещё актуален — отдаём из кэша if (!$hard && is_file($file) && (time() - filemtime($file)) < $ttl) { echo file_get_contents($file); self::$stack[] = null; // держим стек сбалансированным return false; } // Открываем буфер и запускаем "тяжёлый" вывод ob_start(); self::$stack[] = [ 'file' => $file, 'key' => $key, ]; return true; } /** * Завершить кэширование и сохранить вывод в файл. */ public static function end(): void { $item = array_pop(self::$stack); if ($item === null) { // Кэш был отдан из файла, сохранять нечего return; } $content = ob_get_clean(); // Готовим директорию с подпапкой $subdir = dirname($item['file']); if (!is_dir($subdir)) { mkdir($subdir, 0755, true); } // Атомарная запись во временный файл с последующим переименованием $tmp = $item['file'] . '.' . uniqid('tmp', true); file_put_contents($tmp, $content, LOCK_EX); rename($tmp, $item['file']); echo $content; } /** * Удалить кэш по конкретному ключу. */ public static function forget(string $key): void { $hash = md5($key); $file = self::$dir . '/' . substr($hash, 0, 2) . '/' . $hash . '.cache'; if (is_file($file)) { unlink($file); } } /** * Полностью очистить весь кэш, опционально только истёкший. */ public static function clear(bool $onlyExpired = false): void { if (!is_dir(self::$dir)) { return; } $items = new RecursiveIteratorIterator( new RecursiveDirectoryIterator(self::$dir, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST ); foreach ($items as $item) { if ($item->isDir()) { continue; } $path = $item->getPathname(); if ($onlyExpired && (time() - $item->getMTime()) < self::$ttl) { continue; } if (substr($path, -6) === '.cache') { unlink($path); } } } /** * Очистить кэш, истёкший по времени жизни. */ public static function clearExpired(): void { self::clear(true); }
}

Как этим пользоваться

Подключаем класс и сразу кэшируем нужный фрагмент:

 require_once __DIR__ . '/FileCache.php';
FileCache::setDir(__DIR__ . '/cache');
FileCache::setTtl(3600); // час по умолчанию
if (FileCache::begin('main_menu')) { // Тяжёлый вывод: например, меню собранное из базы ?> <nav>...большое меню...</nav> <?php FileCache::end();
}

Теперь меню сгенерируется один раз, сохранится, и в течение часа будет отдаваться из файла. Через час класс сам пересоберёт кэш при первом обращении.

Принудительно сбросить конкретный фрагмент после обновления данных:

 FileCache::forget('main_menu');

Или жёстко пересобрать прямо в момент вывода, не трогая остальной кэш:

 if (FileCache::begin('main_menu', hard: true)) { ?> <nav>...свежее меню...</nav> <?php FileCache::end();
}

Для полной очистки, например, после импорта товаров, хватает одного вызова:

 FileCache::clear();

  А если нужно убрать только устаревшие файлы без затрагивания актуальных — clearExpired(). Это удобно ставить в крон раз в сутки, чтобы папка кэша не разрасталась бесконечно.  

Что важно учесть на практике

Папку с кэшем нельзя складывать в публичную директорию. Если файлы кэша окажутся в public_html без защиты, кто угодно сможет открыть их напрямую. Я всегда выношу кэш за пределы веб-рута или закрываю доступ через .htaccess.

Ключи лучше делать осмысленными и уникальными. main_menu, catalog_sidebar, footer — понятно, что внутри. Добавляйте в ключ язык или ID товара, если у проекта есть мультиязычность или много динамических сущностей, иначе получите один и тот же кэш для разных данных.

Время жизни подбирается под задачу. Меню можно кэшировать сутками, а блок с актуальными ценами — на минуты. Поэтому в моём классе ttl задаётся отдельно для каждого фрагмента, а не только глобально.

Для проектов на Битриксе и других CMS нужно аккуратно смотреть, в каком месте подключать такой кэш, чтобы не сломать встроенную систему кэширования самой платформы. Здесь лучше кэшировать точечно отдельные блоки, а не пытаться завернуть в файл всю страницу целиком.

Когда это реально оправдано

Файловый кэш — это не панацея под любую задачу. Он отлично работает для слабо меняющегося контента: меню, подвалов, списков категорий, «горячих» выгрузок с пагинацией. А вот корзину, личный кабинет, актуальные остатки и всё, что зависит от текущего пользователя, кэшировать так нельзя — получите чужие данные в чужой сессии.

Если сайт крупный и нагрузки большие, рано или поздно упираетесь в то, что файловая система на каждом запросе медленнее любых in-memory хранилищ. Тогда стоит смотреть в сторону Redis. Но для большинства проектов на PHP файловый кэш — простой и надёжный первый шаг, который не требует ничего, кроме включённого PHP.

0 Комментариев
Что-бы оставить свой комментарий Вы должны авторизоваться на сайте
Задать вопрос
Нажимая на кнопку, вы даете согласие на обработку своих персональных данных
x
Тема пуша
Сообщение пуша
Наверх
Отправить заявку
Нажимая на кнопку, вы даете согласие на обработку своих персональных данных