Гости: владение данными без учётной записи
Как дать неавторизованному посетителю владеть данными — корзиной, списком сравнения, закладками, черновиком формы — и не отдать их потом другому посетителю.
Приём простой: у гостя нет учётной записи, поэтому его строки подписывают случайным токеном, а токен кладут в cookie. Фреймворк держит cookie, формат токена и срок; чем именно гость владеет, остаётся вашему коду.
Выдать токен
use Boshnik\PageBlocks\Support\GuestToken;
$guest = GuestToken::for('shop_cart'); // 60 дней по умолчанию
$guest = GuestToken::for('shop_wish', 180); // срок — вторым аргументом
$token = $guest->get(); // null — cookie нет, она погашена или не наша
$token = $guest->getOrIssue(); // выдать, если нет: гость кладёт первый товар
$guest->forget(); // гость вышел из гостейТокен — 64 шестнадцатеричных символа, то есть 32 случайных байта, и больше ничего: ни метки времени, ни угадываемой части. Cookie ставится с path=/, httponly, samesite=Lax и secure на HTTPS.
В контроллере — трейтом
use Boshnik\PageBlocks\Http\Controllers\Traits\ResolvesGuest;
class WishController
{
use ResolvesGuest;
public function toggle(Request $request): Response
{
// null — писать надо под учётной записью, а не под токеном.
$token = $this->guestWriteToken('shop_wish', 180);
return $this->items()->toggle($token, $this->currentUserId(), $id);
}
}Трейт даёт currentUserId(), isGuest(), guest() и guestWriteToken(). Последний несёт два правила, из-за которых это трейт, а не пара статических вызовов:
- Токен выдаётся на записи, не на чтении.
getOrIssue()на странице каталога завёл бы cookie каждому, кто прошёл мимо, — и раздробил бы кэш ответов на всём сайте. Поэтомуget()на чтении,getOrIssue()на действии. - Вошедшему токен не нужен вовсе. Его данные привязаны к учётной записи, и токен тут остался бы только затем, чтобы однажды показать ему чужое с того же браузера. Именно поэтому
guestWriteToken()возвращаетnullдля авторизованного.
«Авторизован» — это не $modx->user->id
Visitor::userId() спрашивает isAuthenticated($context). Идентификатор может быть равен 1 (администратор) и в веб-контексте, где человек не авторизован вовсе; Visitor::isManager() отвечает на отдельный вопрос — смотрит ли админ сайт из той же вкладки.
Имя cookie обязано быть зарегистрировано
Из bootstrap.php своего компонента, рядом с маршрутами и неймспейсом моделей:
use Boshnik\PageBlocks\Support\GuestCookies;
GuestCookies::register('pbShop', ['shop_cart', 'shop_compare', 'shop_wish']);Так кэш ответов узнаёт, что считать гостевым состоянием, и отличает личную страницу от общей, ничего не зная о том, что на свете бывают корзины.
Именно bootstrap, а не событие
_loadExtensionPackages() исполняется внутри modX::initialize() — до OnMODXInit, то есть до маршрутизации, а проверка кэша стоит первой в Route::dispatch(). Компонент, зарегистрировавшийся позже, не успел бы: кэш отдал бы чужую корзину ровно в том запросе, который должен был защитить.
GuestToken::for() на незарегистрированное имя пишет ошибку в лог MODX — по одной на имя за процесс. Молчать тут нельзя: незарегистрированная cookie означает, что кэш не отличит одного гостя от другого.
Что остаётся вашему коду
| Во фреймворке | У вас |
|---|---|
| имя cookie, её срок и флаги | какие cookie у компонента и сколько живут |
| формат токена и его выдача | колонка token на строке, уникальные индексы |
| «гость или вошедший» | слияние гостевых строк с пользовательскими при входе |
| реестр гостевых cookie и отпечаток для кэша | чистка брошенного |
Слияние при входе — ваше. Фреймворк не знает, одна у владельца строка или много, и переносить строки между владельцами вслепую дороже, чем написать это у себя.
Чем своя запись кэша на гостя не грозит
Выглядит как способ засыпать партицию кэша. Не засыпает по той же причине, по которой кэш вообще применим: любое сохранение модели PageBlocks — в том числе строки корзины — чистит партицию целиком. Гость, положивший товар, сам же и обнулил кэш всем.