Перейти к содержимому
Поиск
Разработка

Symfony Cache: кеширование в PHP — мастерская и 9 практических опытов

Разбираем кеширование PHP на практике: интерактивная модель, девять настоящих опытов, CLI-карточка товара, TTL, теги, обновление и обслуживание на VPS.

Александр Некрасов
Symfony Cache — кеширование данных PHP, TTL и обновление

Symfony Cache без фреймворка: ключи, TTL, callback, теги, namespace, ошибки и prune. Интерактивная модель, девять настоящих PHP-опытов и готовые исходники для Windows, Linux и VPS.

Что такое Symfony Cache и что получит ваш PHP-проект

Одна и та же карточка товара запрашивается десятки раз, отчёт постоянно пересчитывает одинаковую сводку, а внешний API отвечает медленнее самого сайта. Если результат некоторое время остаётся одинаковым, приложение может сохранить его и повторно использовать. Symfony Cache предоставляет для этого отдельный PHP-компонент: ключи, сроки жизни, разные хранилища и удобное получение значения через callback.

Компонент работает и без полного Symfony Framework. Его можно подключить к обычному PHP-сервису, административной панели, интеграции или консольному заданию. При первом обращении загрузчик вычисляет результат, а последующие обращения получают сохранённое значение. После истечения срока или инвалидации загрузчик вызывается снова. Именно правила обновления, а не только скорость чтения, определяют полезность такого решения.

Здесь собрана практика на Symfony Cache 7.4.23: девять настоящих PHP-опытов, интерактивная модель жизни записи, отдельная CLI-карточка товара и исходники для Windows, Linux и VPS. Вы сможете увидеть устаревшую цену, обновить её после удаления ключа, сравнить пространства имён и проверить, почему null тоже может быть корректным попаданием в кеш.

Где применять: каталог, кабинет, отчёты и внешние API

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

Например, список городов меняется редко, поэтому его срок жизни может быть большим. Остатки товара и доступность услуги требуют более осторожной политики. У персонального кабинета результат зависит от клиента и прав доступа: одну сохранённую сводку нельзя раздавать всем пользователям под общим ключом. На странице мастерской namespace A и B помогает увидеть это разделение на простом наборе.

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

Кеш, база и HTTP-кеширование решают разные задачи

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

HTTP-кеширование на уровне браузера, CDN или reverse proxy относится к ответам HTTP и их заголовкам. Symfony Cache в наших примерах сохраняет PHP-данные внутри приложения. Эти уровни могут работать вместе, но удаление ключа PHP не обязательно обновит уже сохранённый ответ CDN. Если проект использует несколько уровней, для каждого нужны свои ключи, сроки и правила инвалидации.

Также не стоит заменять кешем проверку прав, транзакцию или ограничение уникальности. Даже свежая карточка интерфейса может устареть между показом и подтверждением заказа. Финальная операция должна снова проверить существенные условия в источнике истины. Ускорение чтения не отменяет корректность записи.

Почему в примере Symfony 7.4 LTS и PHP 8.2+

Для проекта закреплена версия symfony/cache 7.4.23 из поддерживаемой ветки 7.4 LTS. Она совместима с PHP 8.2+, поэтому подходит и для серверов, которые ещё не перешли на более новую основную ветку PHP. Текущий статус ветки и требования опубликованы на официальной странице Symfony 7.4. Это выбор совместимости для конкретного учебного проекта, а не утверждение, что все приложения обязаны использовать одну версию.

В composer.json закреплена точная версия компонента, а composer.lock фиксирует весь разрешённый набор зависимостей. Для установки скачанного проекта используйте install. Команда update решает зависимости заново и может изменить результат. После обновления библиотеки повторите реальные опыты и проверку собственного приложения, особенно если у вас другой backend.

Полный Framework не требуется. Мы самостоятельно создаём адаптер и передаём путь к кешу. Команды FrameworkBundle вроде очистки именованного пула не появляются автоматически после установки одного компонента. В исходниках для обслуживания предусмотрен отдельный bin/prune.php с ограниченной областью действия.

Мини-мастерская: измените источник и проследите судьбу записи

В первой части мастерской нажмите «Прочитать товар» дважды. Первый запрос даст MISS и вызовет загрузчик модели, второй — HIT. Затем увеличьте цену в источнике на сто рублей и прочитайте товар ещё раз. Вы увидите прежнюю сохранённую цену. Продвиньте учебное время или удалите выбранный ключ: следующее чтение получит новое значение. Так удобно разобраться в причине устаревших данных.

Попробуйте товар 43 и инвалидацию категории, переключите клиента A на B, выберите отсутствующий товар 404. В модели есть виртуальные часы: реальное ожидание не меняет время. Это JavaScript-объяснение правил, а не выполнение PHP. Ни один её переключатель не обращается к вашей базе или файловой системе.

Во второй части находятся девять записей настоящего PHP-прогона: конкретные результаты, количество вызовов callback, проверки и фрагменты подхода. Исходный раннер действительно создаёт filesystem-кеш, ждёт истечения TTL и выполняет prune. JSON модели и JSON реального опыта скачиваются отдельно с явными обозначениями. На телефоне удобнее открыть мастерскую целиком; исходники доступны сразу под описанием.

Установка через Composer и устройство исходников

Если управление PHP-зависимостями пока непривычно, начните с нашего руководства по Composer. Для готового архива достаточно установить зависимости из lock и проверить платформу. Компонент подключается обычным vendor/autoload.php. Веб-сервер, Redis и база данных для лаборатории не требуются.

composer install --no-interaction --no-plugins --no-scripts --prefer-dist
composer check-platform-reqs
composer test
composer verify

Класс Laboratory содержит девять исполняемых опытов. ApplicationCache создаёт прикладной пул, каталог fixtures хранит искусственный источник товаров, а public содержит готовую страницу и запись результатов. Временные данные лежат в runtime вне публичной папки. Установленные зависимости и результаты прошлых запусков не включены в исходный ZIP.

Команда build осознанно обновляет опубликованную запись; verify повторяет настоящие операции и сравнивает результат без изменения public. Отдельный каталог нового прогона создаётся автоматически. Это позволяет проверить свой PHP и сохранить доказательство результата, не стирая предыдущий опыт.

Опыт 1. Get с callback: загрузить один раз, прочитать дважды

Первый опыт получает карточку товара дважды. Callback возвращает массив с ценой 12500 и сроком жизни 300 секунд. Счётчик загрузчика увеличивается только один раз, оба результата совпадают, а isHit подтверждает существующую запись. Такой контракт удобен: код получения данных остаётся рядом с правилом их сохранения.

use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Symfony\Contracts\Cache\ItemInterface;

$cache = new FilesystemAdapter('catalog_v1', 0, $cacheDirectory);
$product = $cache->get('product.42', function (ItemInterface $item): array {
    $item->expiresAfter(300);
    return ['id' => 42, 'price' => 12500];
}, 0);

Последний аргумент здесь равен нулю: мы отключили вероятностный ранний пересчёт именно для воспроизводимости маленького опыта. Позже разберём beta отдельно. Callback должен возвращать данные, которые допустимо вычислить повторно. Отправлять внутри него письмо или проводить платёж нельзя только на основании надежды, что кеш вызовет его единственный раз.

В рабочем приложении вместо массива будет репозиторий, API или вычисление. Начинайте с небольшого результата понятной структуры. Сложные объекты с соединением к базе, открытым файлом или состоянием сервиса плохо подходят в качестве переносимой кешированной карточки.

Ключи: какие параметры влияют на результат

Ключ — часть контракта приложения. Если карточка отличается по языку, валюте, клиенту или набору полей, эти различия должны отражаться в ключе либо namespace. Иначе успешный HIT вернёт корректное значение для другого запроса. Такие ошибки труднее заметить, чем обычное исключение: страница работает, но показывает чужую или неподходящую информацию.

$key = sprintf('product.v2.%d.%s.%s', $productId, $locale, $currency);
// productId, locale and currency must already be validated by the application.
$value = $cache->get($key, $loader);

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

В тестах есть недопустимый ключ с двоеточием: Symfony его отклоняет. Для совместимости придерживайтесь простого набора букв, цифр, точки и подчёркивания. Зарезервированные символы и требования к ключам описаны в PSR-6. Версия представления в ключе помогает не читать старую структуру после изменения формата данных.

Опыт 2. TTL: значение перестало быть пригодным

Во втором опыте запись живёт одну секунду. PHP действительно ждёт две секунды, проверяет промах и снова вызывает загрузчик. Ревизия результата меняется с первой на вторую. Ожидание существует только в консольной лаборатории; задержку sleep не нужно переносить в обработчик веб-запроса ради демонстрации истечения срока.

$item->expiresAfter(60); // Relative lifetime in seconds.
// Another option for a known deadline:
$item->expiresAt(new DateTimeImmutable('tomorrow 00:00:00 UTC'));

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

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

Опыт 3. Источник изменился: почему нужна инвалидация

Третья сцена сохраняет цену 12500, затем меняет источник на 14000. Повторное get ещё возвращает 12500. Только после delete следующий запрос вызывает callback и получает новую цену. Это типичный сценарий административной панели: менеджер сохранил товар, а посетитель некоторое время видит прежнюю карточку.

$repository->updatePrice(42, 14000); // Commit the source change first.
$cache->delete('product.42');
// A following get() loads the new value on demand.

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

Удаление одного ключа удобно, когда зависимость проста. Но изменение товара может затронуть карточку, список категории, поисковую подсказку и сводку. Разрозненные вызовы delete быстро становятся трудными для сопровождения. Для группы связанных результатов полезны теги и заранее описанная карта зависимостей.

Опыт 4. Теги: обновить связанные результаты вместе

В опыте с тегами два объекта относятся к category.7, третий — к category.9. После invalidateTags первой категории загрузчики первых двух объектов вызываются повторно, а третий остаётся попаданием. В записи видны итоговые счётчики 2, 2 и 1. Это показывает не просто удаление строки по имени, а зависимость нескольких результатов от одного изменения.

use Symfony\Component\Cache\Adapter\TagAwareAdapter;

$cache = new TagAwareAdapter(new FilesystemAdapter('catalog_v1', 0, $cacheDirectory));
$product = $cache->get('product.42', function (ItemInterface $item) {
    $item->expiresAfter(300);
    $item->tag(['product.42', 'category.7']);
    return loadProduct(42);
}, 0);
$cache->invalidateTags(['category.7']);

Список категории тоже можно пометить её тегом. Тогда изменение товара инвалидирует и карточку, и зависимый список. При этом слишком широкий тег вроде all превращает почти каждое редактирование в массовый пересчёт. Выбирайте область по реальным зависимостям и измеряйте, сколько значений затрагивает одно событие.

В мастерской категория действует в текущем namespace клиента. Это помогает не смешивать независимые наборы. В реальной архитектуре важно согласовать, где хранятся элементы и версии тегов и какие процессы видят их обновление. Схема одного локального процесса не доказывает корректность распределённого размещения.

Опыт 5. Namespace: одинаковые ключи разных клиентов

Пятая сцена создаёт два filesystem-пула с пространствами tenant_a_v1 и tenant_b_v1. Оба используют ключ summary, но возвращают разные данные. Очистка первого пула делает его запись промахом и оставляет запись второго пригодной. Этот приём помогает разделять приложения, окружения, версии представления и независимые наборы данных.

$tenantA = new FilesystemAdapter('tenant_a_v1', 0, $cacheDirectory);
$tenantB = new FilesystemAdapter('tenant_b_v1', 0, $cacheDirectory);
$tenantA->clear(); // Only this pool's namespace.

Namespace не проверяет, имеет ли пользователь право видеть клиента A. Выбор клиента должен происходить после авторизации и проверки принадлежности запроса. Нельзя просто принять любое имя пространства из query-параметра и считать разделение хранилища защитой кабинета.

При смене версии namespace новые запросы получают отдельный набор и не читают старую структуру. Но старые файлы не исчезают от переименования автоматически. Планируйте срок хранения и очистку предыдущих версий, иначе серия релизов постепенно увеличит занимаемое место. Изменение пространства также создаёт холодный кеш и нагрузку на источник.

Опыт 6. Negative caching: null не обязательно означает промах

Запрос несуществующего товара тоже может быть дорогим. Если отсутствие подтверждено источником, его можно ненадолго сохранить. В шестом опыте callback возвращает null, два чтения вызывают его один раз, а isHit остаётся true. Отдельный тест показывает аналогичное поведение для false. Значение результата и факт попадания — разные признаки.

$product = $cache->get('product.404', function (ItemInterface $item): ?array {
    $item->expiresAfter(30);
    return $repository->find(404);
}, 0);

Короткое отрицательное кеширование полезно для повторяющихся обращений к отсутствующим объектам и справочникам. Однако после создания такого объекта прежний null может оставаться до истечения TTL. В обработчике создания предусмотрите удаление соответствующего ключа либо примите эту задержку как явное правило продукта.

Не смешивайте отсутствие с ошибкой. Timeout API, недоступная база или запрет доступа не доказывают, что товара нет. Если превращать любой сбой в null и сохранять надолго, временная проблема станет устойчивой ложной информацией. Структура ответа загрузчика должна различать эти состояния до решения о кешировании.

Опыт 7. Исключение callback и следующая попытка

В седьмой сцене загрузчик выбрасывает учебное исключение. Запись не становится успешным кешированным значением, а следующая исправная попытка вызывает callback и возвращает нормальный результат. В записи мастерской показаны ошибка, отсутствие попадания и счётчик двух вызовов. Сбой не маскируется значением ноль или пустым массивом.

try {
    $rates = $cache->get('rates', function (ItemInterface $item) use ($api) {
        $item->expiresAfter(60);
        return $api->fetchRates();
    });
} catch (RuntimeException $error) {
    // Return an explicit unavailable state and record a safe diagnostic event.
}

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

Также различайте ошибку источника и проблемы хранилища кеша. Переполненный диск, неверные права и отказ Redis требуют своей диагностики. Приложение должно сохранять бизнес-корректность при потере ускоряющего слоя, но конкретный способ деградации зависит от стоимости исходной операции и требований сервиса.

Опыт 8. Beta: управляемый ранний пересчёт

Если все пользователи ждут ровно до истечения срока, затем множество запросов одновременно обращается к дорогому источнику. В Cache Contracts есть вероятностный ранний пересчёт. В восьмой сцене мы проверяем только детерминированные крайние режимы: beta=0 сохраняет обычное попадание, beta=INF принудительно пересчитывает значение. Ревизия меняется с первой на вторую без ожидания полного TTL.

$value = $cache->get('popular', $loader, 0);
$fresh = $cache->get('popular', $loader, INF);
// Without the third argument, default beta is 1.0.

Число beta не является количеством секунд и не задаёт постоянный таймер обновления. При обычном значении поведение вероятностное и зависит от сохранённых метаданных вычисления. Поэтому мастерская не рисует вымышленный прогноз точного момента пересчёта и не выдаёт одиночный последовательный опыт за нагрузочный тест.

Вероятностный ранний пересчёт опирается на метаданные времени вычисления и срока жизни. INF принудительно пересчитывает значение и без TTL. Принудительный INF полезен для понимания механизма, но ставить его во все запросы означает лишиться ожидаемой экономии вычислений. Подробное описание раннего пересчёта и его связи с Cache Contracts находится в официальной документации.

Cache stampede: почему callback должен быть повторяемым

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

Один PHP-процесс, несколько FPM-воркеров на одном сервере и несколько узлов с общим Redis — разные условия. Локальная координация не должна автоматически считаться общей блокировкой всех машин. Проведите отдельный тест параллельных запросов, считайте реальные обращения к источнику и проверяйте поведение при ошибке загрузчика. Девять последовательных опытов из архива эту проверку не заменяют.

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

Опыт 9. Prune: просроченные файлы и место на диске

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

$cache = new FilesystemAdapter('catalog_v1', 0, $cacheDirectory);
$success = $cache->prune();

В исходниках bin/prune.php обслуживает только прикладной пул этого проекта. Он не очищает исходные данные и не удаляет каталоги других приложений. Планировщик запускается отдельным пользователем с нужными правами. Не заменяйте понимание области очистки рекурсивным удалением случайного общего каталога на сервере.

Для filesystem-кеша контролируйте не только занятые байты, но и количество файлов, свободные inode и время обхода. Подходящий интервал обслуживания зависит от объёма ключей и их срока жизни. Особенности локального файлового backend описаны в разделе Filesystem Cache Adapter.

Cache Contracts и PSR-6: два способа работы

В основных примерах используется удобное get с callback: компонент получает значение или вызывает загрузчик. PSR-6 предоставляет более явную работу с item: получить элемент, проверить isHit, установить данные, срок и сохранить. Выбор интерфейса влияет на то, какую часть поведения нужно организовать самостоятельно.

$item = $cache->getItem('summary');
if (!$item->isHit()) {
    $item->set(loadSummary());
    $item->expiresAfter(120);
    $cache->save($item);
}
$summary = $item->get();

Нельзя определять промах только проверкой на null или false: оба значения могут быть сохранены корректно, что подтверждают наши опыты. В PSR-6 используйте isHit. Явная последовательность удобна для интеграции с библиотеками, ожидающими стандартный пул, но не добавляет автоматически защиту от конкурентного пересчёта, которую вы могли ожидать от другого API.

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

Как выбрать Filesystem, APCu или Redis

Filesystem удобно начать без отдельного сервера кеша: данные лежат в выделенном каталоге. Это делает лабораторию воспроизводимой на Windows и Linux. При большом количестве объектов появляются стоимость файловых операций, права и обслуживание диска. Расположение каталога и тип файловой системы тоже влияют на результат; наш опыт не сравнивает скорость разных вариантов.

APCu хранит данные в памяти PHP-окружения и требует установленного расширения. Существенная практическая деталь: CLI и FPM не следует считать одним общим хранилищем APCu. Команда, запущенная в терминале, не обязана очистить данные веб-процессов. Это ограничение прямо отмечено в документации APCu Adapter.

Redis или Valkey позволяют вынести хранение из отдельного PHP-процесса. Появляются сетевое соединение, клиент, права, ограничения памяти и обслуживание отдельного сервиса. Для нескольких приложений важны разделение ключей и область инвалидации. Подключение и совместимые клиенты описаны в официальном разделе Redis Adapter. Рабочие DSN и облачные аккаунты в архив не включены.

Практическая CLI-карточка: запустить и увидеть настоящий HIT

Помимо лабораторных записей в архиве есть bin/catalog.php. Он читает учебный fixtures/catalog.json и сохраняет карточку в runtime/application-cache. Первый вызов показывает sourceLoaded=true, повторный — false. Это уже два независимых процесса PHP, использующих один локальный пул, а не только повтор внутри одного метода.

php bin/catalog.php 42
php bin/catalog.php 42
php bin/catalog.php 42 --invalidate
php bin/catalog.php 404
php bin/prune.php

Измените цену товара 42 в исходном JSON и снова прочитайте карточку. До истечения срока или явного invalidate сохранённый результат останется прежним. Флаг удаляет только ключ выбранного товара и сразу выполняет чтение заново. Для отсутствующего товара 404 возвращается null с коротким сроком жизни.

У CLI нет параметра произвольного каталога назначения, а идентификатор проверяется до работы. Это ограниченный учебный интерфейс для повторения опыта. В настоящем приложении доступ к товарам, проверка JSON-схемы и правила изменения источника должны принадлежать вашему прикладному слою. Не выставляйте эту консольную команду как публичное выполнение пользовательской строки на сайте.

Windows: установка, проверка и локальная мастерская

Распакуйте проект и откройте PowerShell в cache-project. Убедитесь, что PHP в PATH соответствует нужной версии, затем установите зависимости и выполните тесты. Команды composer.bat используют Windows-обёртку Composer. Если интерпретаторов несколько, проверьте также PHP, которым пользуется сам Composer, иначе CLI и установленный набор зависимостей могут оказаться несогласованными.

php --version
composer.bat install --no-interaction --no-plugins --no-scripts --prefer-dist
composer.bat check-platform-reqs
composer.bat test
composer.bat verify
php -S 127.0.0.1:8174 -t public

Откройте http://127.0.0.1:8174. Через file:// страница не предназначена для работы: модули и запись JSON должны загружаться по HTTP. Встроенный сервер раздаёт только public, поэтому файлы runtime не становятся доступными посетителю. Его остановка через Ctrl+C не очищает созданный кеш.

Используйте личный рабочий каталог с подходящими правами доступа. Права Linux 0700 не превращаются автоматически в аналогичную Windows ACL-политику. Если задача запускается Планировщиком под другой учётной записью, отдельно проверьте чтение проекта и запись её runtime, не выдавая административные права без необходимости.

Linux: те же исходники и отдельное окружение

На Linux последовательность аналогична: PHP CLI 8.2+, Composer, установка из lock, тест и verify. Для Ubuntu 24.04 можно использовать штатный PHP 8.3; на другом дистрибутиве уточните доступные пакеты. Не переключайте версию PHP работающего сайта только ради примера. Отдельный каталог и отдельный CLI-процесс позволяют сначала проверить совместимость.

sudo apt update
sudo apt install php8.3-cli unzip
php --version
composer install --no-interaction --no-plugins --no-scripts --prefer-dist
composer check-platform-reqs
composer test
composer verify
php bin/catalog.php 42

Composer 2 устанавливается отдельно по официальной инструкции с проверкой подписи установщика. Запускайте его обычным пользователем проекта. Runtime должен быть доступен на запись тому процессу, который действительно выполняет вычисление, а исходный код и vendor не требуют общей записи всем пользователям сервера.

При работе по SSH браузер вашего компьютера не видит удалённый localhost напрямую. Используйте туннель из README либо опубликуйте готовую статику через существующий HTTPS-сайт. Для обычного чтения мастерской PHP на публичном сервере вообще не нужен: реальные результаты уже записаны в JSON.

VPS: кеш вне public и расписание prune

На сервере разделите исходники, публичные файлы и изменяемые данные. В нашем примере код расположен в /opt/aen-cache, а runtime остаётся внутри проекта, но вне document root. Пользователь aen-cache должен существовать заранее. Сначала проверьте запуск от его имени, затем устанавливайте подготовленные systemd-шаблоны.

sudo install -d -m 0700 -o aen-cache -g aen-cache /opt/aen-cache/runtime
sudo -u aen-cache /usr/bin/php /opt/aen-cache/bin/catalog.php 42
sudo -u aen-cache /usr/bin/php /opt/aen-cache/bin/prune.php
sudo install -m 0644 deploy/aen-cache-prune.service /etc/systemd/system/
sudo install -m 0644 deploy/aen-cache-prune.timer /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl start aen-cache-prune.service
sudo journalctl -u aen-cache-prune.service -n 40 --no-pager
sudo systemctl enable --now aen-cache-prune.timer

Шаблон запускает только prune этого прикладного пула. Расписание использует часовой пояс сервера, а Persistent помогает выполнить пропущенное срабатывание после включения. Это не очередь отдельного задания за каждый час простоя. Полная последовательность проверки unit-файлов и отключения таймера находится в README.

Если приложение обслуживают CLI и FPM, согласуйте их доступ к одному filesystem-пулу. В Windows аналогичная проверка относится к учётной записи Планировщика. В архиве регистрация Windows-задания закомментирована: просмотр или скачивание шаблона не меняет расписание компьютера.

Наблюдение: попадания, загрузчик и возраст результата

После внедрения важно понять, приносит ли кеш пользу. Считайте обращения к источнику, долю попаданий и длительность callback. Сравнивайте одинаковые периоды до и после изменения. Высокий процент HIT сам по себе не является успехом, если пользователи получают слишком старые данные или ключи неправильно объединяют разные запросы.

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

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

Релиз и прогрев: как не создать резкий всплеск нагрузки

Новая версия ключей или пустое хранилище означает холодный старт. Если одновременно придут обычные пользователи, дорогое вычисление потребуется многим запросам. Для известных популярных значений можно подготовить отдельный прогрев, но он должен использовать те же правила и права, что рабочее приложение, и не создавать чрезмерную нагрузку заранее.

Не прогревайте весь теоретический диапазон параметров без оценки. Комбинации языка, клиента, фильтра и страницы способны породить огромное количество ключей, большинство из которых никто не прочитает. Начните с действительно популярных результатов и ограничьте объём задания. Проверяйте отсутствие ошибок источника, а не просто завершение процесса.

Смена структуры сохранённого массива требует версии ключа или контролируемой инвалидации. Во время постепенного развёртывания старая и новая версии приложения могут работать одновременно. Их договорённость о формате данных должна быть явной; общая папка или общий Redis сами по себе не решают совместимость релизов.

Что проверяют тесты и как изменить пример

PHP-тесты выполняют настоящие операции: попадание после callback, истечение TTL, удаление, теги, namespace, кеширование null и false, обработку исключения, beta, prune и отклонение недопустимого ключа. Всего в проекте четырнадцать assertions. Затем результат сравнивается с поставленной записью. Успешная проверка подтверждает конкретное поведение файлового backend на этом наборе.

composer test
composer verify
# Optional browser-model checks, Node.js 24+:
node tests/model.test.mjs
# Intentionally regenerate the published recording:
composer build

У JavaScript-модели собственные четырнадцать проверок: они контролируют объясняющие правила интерфейса и не подменяют PHP-тесты. Мобильная проверка относится к управлению мастерской, а не к производительности серверного кеша. Нагрузку, распределённые блокировки и внешний Redis здесь не измеряли.

Для изменения примера начните с одного сценария или учебной карточки. Задайте ожидаемое поведение, поменяйте код и повторите проверку. Если намеренно изменяется контракт результата, обновите ожидания с объяснением. Не убирайте assertions только ради зелёного вывода: они нужны, чтобы читатель мог отличить случайное выполнение от подтверждённого результата.

Частые ошибки: устаревшая цена, неверный клиент и общий clear

Если новая цена не появилась, проверьте, когда был заполнен ключ, какой у него TTL и выполнялась ли инвалидация после commit. Если проблема возникает только у части пользователей, сравните namespace, язык и параметры ключа. Общая кнопка clear иногда скрывает ошибку на несколько минут, но не исправляет отсутствующую зависимость.

Если CLI сообщает, что кеш очищен, а веб-приложение не изменилось, сравните backend, путь, namespace, пользователя процесса и SAPI. Для APCu это особенно существенно. Если после каждого обращения callback вызывается снова, проверьте права записи и сообщения backend, прежде чем сокращать TTL или менять библиотеку.

Не кешируйте полную страницу личного кабинета под общим именем, не используйте кеш как единственную память о выполненном платеже и не удаляйте чужой runtime ради исправления своего пула. Практический порядок внедрения прост: один дорогой запрос, понятный ключ, измеримый срок, проверенное обновление, наблюдение и сценарий восстановления после потери кеша.

Исходники и официальные материалы для продолжения

Проект можно забрать целиком и использовать собственный код под MIT. В архиве есть lock, учебные данные, CLI, тесты, мастерская и шаблоны обслуживания. Зависимости сохраняют свои лицензии; полные уведомления приложены отдельно. На опубликованной странице нет записи в серверный кеш посетителя и запуска произвольных команд.

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