В типичном приложении на Laravel с развитой ролевой моделью Blade-шаблон страницы легко генерирует десятки вызовов @can, директив проверок прав или условий политик (Gate/Policy). Даже когда права пользователя закэшированы в Redis или Memcached через Cache::tags(), каждый вызов проверки обычно уходит отдельным запросом в хранилище кэша.
Кажется: ну Redis же работает по локальному сокету или в быстрой локалке, какие там задержки? Но на практике 50–100 обращений за один HTTP-запрос выливаются в ощутимый оверхед: сериализация, десериализация, чтение из сокета и переключение контекста. Очевидное решение — прочитать значение из внешнего кэша один раз за запрос и сложить в локальную память PHP. Начиная с релиза Laravel 13.33 фреймворк поддерживает для этого лаконичный синтаксис: Cache::memo()->tags(...).
В чем была проблема раньше
В Laravel уже существовал метод Cache::memo(), появившийся ранее для обычного (нетегированного) кэша. Он оборачивает репозиторий кэша во внутреннее хранилище MemoizedStore, которое сохраняет прочитанные ключи в оперативной памяти текущего процесса.
Однако при попытке вызвать Cache::memo()->tags(...) разработчик получал BadMethodCallException: класс MemoizedStore просто не реализовывал интерфейс тегирования. В течение некоторого времени функционал тегов в документации Laravel находился в подвешенном состоянии, но после их официального возвращения в документацию ветки 13.x возникла потребность вернуть единое поведение и для мемоизации.
До релиза 13.33 для решения этой проблемы приходилось выстраивать многослойный кэш вручную — поверх тегированного хранилища натягивали драйвер array:
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use App\Models\User;
public function getPermissions(User $user): Collection
{
$cacheKey = "permissions:{$user->id}";
// Приходилось вручную заворачивать теги в array-хранилище
return Cache::store('array')->tags('permission_cache')->rememberForever(
$cacheKey,
function () use ($user, $cacheKey) {
return Cache::tags($this->cacheTags($user))->remember(
$cacheKey,
config('auth.permissions.default_cache_time'),
fn () => $this->loadPermissionsFromDatabase($user)
);
}
);
}
Код получался избыточным, шумным и требовал помнить о двух уровнях ключей и синхронизации их инвалидации.
Как работает Cache::memo()->tags()
В PR #61593 автор Joost de Bruijn реализовал метод tags() непосредственно в MemoizedStore. Теперь связка работает «из коробки» ровно так, как от нее ждешь:
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use App\Models\User;
public function getPermissions(User $user): Collection
{
return Cache::memo()
->tags(['permissions', "user:{$user->id}"])
->remember(
"permissions:{$user->id}",
now()->addHour(),
fn () => $this->loadPermissionsFromDatabase($user)
);
}
Жизненный цикл чтения выглядит прозрачно:
- Первый вызов: Laravel обращается к основному хранилищу (например, Redis). Если там пусто, выполняется переданное замыкание и результат записывается и в Redis, и в локальную память PHP.
- Повторные вызовы в рамках того же запроса: значение отдается напрямую из массива в памяти PHP (O(1)), вообще не трогая сетевое соединение с Redis.
Что под капотом
Архитектурно реализация аккуратно вписана в существующие абстракции кэша Laravel:
- При вызове
$store->tags($names)создается (или достается из реестра по сериализованному списку тегов) экземпляр нового классаMemoizedTaggedCache, наследующего стандартныйTaggedCache. - Метод
get($key)вычисляет префикс ключа с учетом тегов ($this->itemKey($key)). Если ключ уже есть в локальном массиве$this->cache, он возвращается мгновенно. Если нет — дергается исходный тегированный репозиторий, и результат оседает в массиве. - Корректная инвалидация при записи: методы
put(),putMany(),add(),increment(),decrement()иforget()при выполнении вызываютunset($this->cache[$prefixedKey]). Это гарантирует, что если внутри одного запроса вы обновили или удалили ключ, мемоизированная копия не отдаст старые данные. - При вызове
flush()на уровне хранилища запускается очистка локальных копий всех зарегистрированных тегированных кэшей.
Поведение в Octane и фоновых воркерах
В классическом окружении PHP-FPM изолированность обеспечивается архитектурой самого PHP: по завершении HTTP-запроса вся память процесса сбрасывается, поэтому риск оставить «хвосты» нулевой.
Для долгоживущих процессов (Laravel Octane на базе FrankenPHP, RoadRunner или Swoole, а также очередей Horizon) критично, чтобы локальная память не протекала между разными запросами или задачами. В Laravel экземпляры MemoizedStore привязаны к жизненному циклу контейнера запроса и сбрасываются между итерациями через события фреймворка, поэтому эффект мемоизации строго изолирован одним запросом.
Когда использовать
Мемоизация тегированного кэша дает максимальный профит в следующих сценариях:
- Проверка прав и ролей: списки разрешений пользователя запрашиваются десятками компонентов интерфейса за один рендер.
- Настройки тенанта или глобальные конфигурации: данные, читаемые из БД/Redis во множестве сервисов и middleware.
- Флаги фич (Feature Flags): частые вызовы вида
Features::active('beta-ui')внутри циклов или Blade-условий.
При этом не стоит оборачивать в Cache::memo() огромные наборы данных (например, тяжелые отчеты или каталоги на десятки мегабайт): если такое значение читается всего один раз за запрос, локальное дублирование в памяти PHP создаст лишь лишнюю нагрузку на сборщик мусора.