Кэширование аутентифицированных пользователей в Laravel
Я покажу, как это сделать, но это не быстрое решение; придётся подумать, что произойдёт, когда пользователь будет обновлён или удалён.
Давайте закэшируем.
Зачем кэшировать аутентифицированных пользователей
Для каждой аутентифицированной страницы или запроса API в приложении Laravel извлекает пользователя из базы данных с помощью запроса, аналогичного этому (в зависимости от ID, конечно):
select * from `users` where `id` = 1 limit 1
На текущий момент не существует способа автоматического кэширования этого пользовательского объекта. Таким образом, пока пользователь аутентифицирован, этот запрос к базе данных будет выполняться при каждом запросе пользователя.
Поскольку пользователь вряд ли будет часто меняться между запросами, имеет смысл кэшировать данные, пока ничего не изменится, особенно для приложений с высокой посещаемостью.
Как работают провайдеры авторизации в Laravel
Для начала необходимо разобраться с механизмом провайдера Auth
в Laravel.
По умолчанию Laravel использует EloquentUserProvider
для управления аутентифицированными пользователями. Этот класс содержит множество полезных методов, таких как retrieveById
, rehashPasswordIfRequired
и validateCredentials
. В общем, всё, что нужно для получения и обновления пользователя в отношении аутентификации.
Можно добавить нового провайдера используя метод Auth::provider
следующим образом:
Auth::provider('someCustomProvider', function (Application $app, array $config) {
// здесь возвращается кастомный провайдер
});
После добавления провайдера во время выполнения можно подключить его в файле конфигурации config/auth.php
:
'providers' => [
'users' => [
'driver' => 'someCustomProvider',
'model' => env('AUTH_MODEL', App\Models\User::class),
],
Создание собственного провайдера аутентификации
Теперь мы немного лучше понимаем, что такое провайдеры авторизации, создадим собственный!
Начнём с создания класса провайдера, CachedEloquentUserProvider
. Его можно назвать как угодно и поместить в любое место в приложении:
namespace App\Auth\Providers;
use Illuminate\Auth\EloquentUserProvider;
class CachedEloquentUserProvider extends EloquentUserProvider
{
public function retrieveById($identifier)
{
//
}
}
Он расширяет базовый EloquentUserProvider
, обеспечивая всю дополнительную функциональность, необходимую нам. Нас интересует только переопределение retrieveById
для выбора способа получения пользователя.
Сейчас мы ничего не делаем для получения пользователя (об этом речь пойдёт дальше), но давайте свяжем его с нашим провайдером в методе boot
AppServerProvider
'а:
public function boot(): void
{
Auth::provider('cachedEloquent', function (Application $app, array $config) {
return new CachedEloquentUserProvider(
$app['hash'],
$config['model']
);
});
}
В конструкторе оригинального EloquentUserProvider
необходимо передать текущий хэшер (отвечающий за хэширование паролей и т.д.), а также пространство имён модели из конфига, представляющее User
(обычно App\Models\User
).
Вот поэтому мы и передали эти две вещи выше.
Теперь заменим драйвер в config/auth.php
на cachedEloquent
(или как вы его назвали).
'providers' => [
'users' => [
'driver' => 'cachedEloquent',
'model' => env('AUTH_MODEL', App\Models\User::class),
],
На данный момент приложение не сможет получать данные об аутентифицированных пользователях, поскольку новый метод retrieveById
пока пуст.
Возвращение кэшированного объекта пользователя
Пришло время заполнить метод retrieveById
кэшированной версией пользователя.
Вариантов множество, но есть один, с которого можно начать:
public function retrieveById($identifier)
{
return cache()->remember('user_' . $identifier, now()->addHours(2), function () use ($identifier) {
return parent::retrieveById($identifier);
});
}
$identifier
— просто идентификатор пользователя, поэтому передаём его родительскому методу retrieveById
, чтобы он сделал своё дело. Но, конечно, с помощью cache()->remember
кэшируем и возвращаем результат.
Вот более короткий способ получить тот же результат с помощью стрелочной функции PHP:
public function retrieveById($identifier)
{
return cache()->remember(
'user_' . $identifier,
now()->addHours(2),
fn () => parent::retrieveById($identifier)
);
}
Любой из этих методов работает хорошо; всё зависит от того, много ли других действий вы выполняете внутри замыкания, в этом случае лучше выбрать стандартную функцию обратного вызова.
Смените драйвер кэша
По умолчанию Laravel использует базу данных в качестве драйвера кэша. Необходимо изменить это, иначе вернёмся к получению кэшированной версии пользователя из базы данных… снова.
CACHE_STORE=redis
Как только его измените, можно будет войти в приложение и увидеть первичный запрос к базе данных для пользователя. Однако при обновлении данные пользователя извлекаются из нашего (в данном случае Redis) кэша!
Сбрасывайте кэш, при изменениях
Кэширование — это здорово, но необходимо позаботиться о сбросе (инвалидации) кэша при изменениях.
Если пользователь прямо сейчас обновит свои данные в приложении, он не увидит немедленного отражения изменений и будет вынужден ждать, пока истечёт срок действия кэша. Не лучший вариант.
Чтобы избежать этого, достаточно наблюдать за изменениями User
и сбрасывать кэш вручную.
Создадим UserObserver
:
php artisan make:observer UserObserver
Откроем его и зарегистрируем события для обновления и удаления.
class UserObserver
{
public function updated(User $user)
{
cache()->forget('user_' . $user->id);
}
public function deleted(User $user)
{
cache()->forget('user_' . $user->id);
}
}
Ключ кэша был установлен на user_[id]
, поэтому просто сбрасываем этот ключ.
Регистрируем наблюдателя в модели User
, и всё готово:
use App\Observers\UserObserver;
#[ObservedBy(UserObserver::class)]
class User extends Authenticatable
{
//...
}
Когда пользователи изменяются или удаляются, кэш становится неактуальным, и в итоге кэшируются свежие данные (или полностью удаляются, при удалении пользователя).
Кэширование — это сложно
Важно отметить, что это очень упрощённый взгляд на сброс кэшированных данных пользователя с помощью двух событий Eloquent. В реальности, по мере усложнения приложения, могут возникать граничные случаи, когда: либо не нужно сбрасывать кэш, либо кэш не сбрасывается в нужный момент.
Например:
- Если для обновления пользователя используется фасад
DB
, кэш не будет сброшен, потому что событие Eloquent не будет вызвано. - Если данные в базе данных обновляются вручную, кэш не будет сброшен.
- Если непубличный столбец пользователя (например,
email_verified_at
) будет обновлён, это вызовет сброс кэша, но скорее всего, вы этого не хотите. - Если есть задание в очереди (например), регулярно обновляющее пользователя (например, когда он последний раз делал запрос), оно будет постоянно сбрасывать кэш.
Как говорилось в начале статьи, это не быстрое и лёгкое решение, не требующее обдумывания.
Да, это ускорит работу приложения. Но придётся приложить немного больше усилий, чтобы убедиться, что кэш не возвращает устаревшие данные или что кэш не сбрасывается слишком часто, делая аутентифицированное кэширование бесполезным.