Skip to content

Гости: владение данными без учётной записи ​

Как дать неавторизованному посетителю владеть данными — корзиной, списком сравнения, закладками, черновиком формы — и не отдать их потом другому посетителю.

Приём простой: у гостя нет учётной записи, поэтому его строки подписывают случайным токеном, а токен кладут в cookie. Фреймворк держит cookie, формат токена и срок; чем именно гость владеет, остаётся вашему коду.

Выдать токен ​

php
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.

В контроллере — трейтом ​

php
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() отвечает на отдельный вопрос — смотрит ли админ сайт из той же вкладки.

Из bootstrap.php своего компонента, рядом с маршрутами и неймспейсом моделей:

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 — в том числе строки корзины — чистит партицию целиком. Гость, положивший товар, сам же и обнулил кэш всем.

© PageBlocks 2019-present