loader
СГБ - Сологуб
Заказать услуги

Материалы с меткой: php

24.09.2026 в 05:51 Читать ~ 4 минуты 23
0ответов
Example blog post alt

Есть в разработке под заказ один момент, о котором не пишут в учебниках, но который знает каждый фрилансер и агентство: момент передачи кода клиенту, который ещё не заплатил последний транш.

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

Знакомо? Мне тоже. Поэтому расскажу, как перестать зависеть от честности слова и договорной дисциплины и переложить эту функцию на код.

Почему «просто добавить проверку лицензии» не работает

Первая мысль у всех одинаковая: сделать функцию checkLicense(), которая стучится на ваш сервер, и вызывать её где-нибудь в начале index.php. Красиво, просто, работает ровно до тех пор, пока клиент (или его «знакомый программист за пиво») не откроет файл и не увидит подозрительный вызов с незнакомым доменом.

Дальше сценарий немного предсказуем:

  • вызов находится за пять минут поиском по слову curl или file_get_contents
  • строчка удаляется
  • сайт продолжает работать как ни в чём не бывало
  • защита была скорее психотерапией для вас, чем реальным препятствием

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

Идея: не проверка, а «печать»

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

То есть в файле клиента вместо нормального кода лежит нечто вроде:

PHPКопировать код

 $criticalCode = "U2FsdGVkX19q...длинная нечитаемая строка...";

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

Разница принципиальная: раньше вырезалась проверка, а теперь вырезается ключ к собственному коду сайта. Удалить вызов к вашему серверу — это не «убрать защиту», это «выстрелить себе в само сердце функционала». Специально захочешь — не сделаешь так удачно.

Что происходит по шагам

Если разложить механику без магии:

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

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

Немного здорового цинизма

Честно: это не сейф Форт-Нокса. Если у клиента есть неограниченный доступ к серверу, время и желание, теоретически можно вытащить расшифрованный код прямо из памяти процесса в момент выполнения. Абсолютной защиты от человека с root-доступом и упорством не существует — и любой, кто продаёт вам такую защиту, немного вас обманывает.

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

Плюс это всё равно не отменяет договор, акты и человеческую беседу «слушайте, оплатите, пожалуйста». Код — это подстраховка на случай, если беседа не сработала, а не замена ей.

Итог

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

28.08.2026 в 09:25 Читать ~ 4 минуты 52
0ответов
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.

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