Подтверждение регистрации

Добавление статуса ожидания и активности пользователя. Реализация команды подтверждения регистрации по токену из ссылки в электронном письме.

  • 00:00:37 - Тест неподтверждённой регистрации
  • 00:01:17 - Скалярное значение статуса
  • 00:02:11 - Класс-словарь статусов
  • 00:02:49 - Объект-значение
  • 00:03:46 - Разделение ответственностей
  • 00:04:54 - Реализация класса для статусов
  • 00:06:10 - Сокрытие реализации
  • 00:07:18 - Юзкейс подтверждения регистрации
  • 00:09:35 - Тест для метода подтверждения
  • 00:11:17 - Построитель сущности для тестов
  • 00:16:16 - Метод confirmJoin
  • 00:16:34 - Инкапсуляция валидации токена
  • 00:19:04 - Проверка работы
  • 00:19:45 - Обзор результата
Скрытый контент (код, слайды, ...) для подписчиков. Открыть →
Дмитрий Елисеев
elisdn.ru
Комментарии (20)
Denis

Я немного не понял зачем в конструкторе по умолчанию ставить статус? А если мне потребуется из массива заполнить класс User данными? Получается, что статус будет не активный, но а пользователь на самом деле активен. Т.е. через конструктор я не смогу указать текущий статус? Поясните, пожалуйста, в чем дело.

Ответить
Дмитрий Елисеев

А если мне потребуется из массива заполнить класс User данными?

Конструктор используется по назначению только для первого создания объекта. Как и другие методы он содержит свою бизнес-логику, по которой может присваивать первоначальные значения, бросать исключения и генерировать события.

В случае же восстановления объекта по массиву из БД конструктор не используют. Вместо этого объект там создаётся без конструктора и заполняется данными прямо в приватные поля через рефлексию.

Например, пишем гидратор или находим любой готовый вроде такого:

class Hydrator
{
    public function hydrate(string $class, array $data): object
    {
        $reflection = new \ReflectionClass($class);
        $target = $reflection->newInstanceWithoutConstructor();
        foreach ($data as $name => $value) {
            $property = $reflection->getProperty($name);
            $property->setAccessible(true);
            $property->setValue($target, $value);
        }
        return $target;
    }
}

и с помощью него десериализуем объект текущими данными из БД:

$row = $stmt->fetch(PDO::FETCH_ASSOC);

$user = $this->hydrator->hydrate(User::class, [
    'id' => new Id($row['id']),
    'status' => new Status($row['status']),
]);

Если же нужно просто в разных юзкейсах создавать объект по-разному, то в этом случае можно сделать несколько именованных конструкторов, как мы делали в видео о конструкторах. В будущем эпизоде про регистрацию через соцсети мы сделаем именно так.

Ответить
Denis

Спасибо за ответ. Сегодня обязательно видео посмотрю.

Ответить
Alex

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

Ответить
Дмитрий Елисеев

В ответе как раз привел пример гидратора. Ещё про него рассказывал раньше здесь.

Ответить
Сергей

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

И вот у меня возникла потребность задекорировать все хендлеры, добавив логирование начала и конца работы каждого хендлера. Как-то это можно реализовать без костылей?

Ответить
Дмитрий Елисеев

В языках с дженериками можно указать общий интерфейс.

interface Handler<T> {
    public void handle(<T> command)
}

В PHP без шаблонов это пока не работает.

В любом языке можно сделать свой интерфейс CommandBus для запуска команд и декорировать уже шину. В PHP можно взять league/tactician и реализовать логирование через middleware в ней.

Ответить
Сергей

Не подскажете еще как быть с автовайрингом? Пытался внедрить симфонячий мессенджер. Указал в конфиге

MessageBusInterface::class => static function (ContainerInterface $container): MessageBusInterface {
    return new MessageBus([
        new HandleMessageMiddleware(new HandlersLocator([
            EmissionCommand::class => [$container->get(EmissionHandler::class)],
            TransferCommand::class => [$container->get(TransferHandler::class)],
        ])),
    ]);
}

Но если в хендлер внедряется MessageBusInterface, то получается Circular dependency. И вот что-то ума не приложу как эту беду победить.

Ответить
Дмитрий Елисеев

Переделать Locator на доставание хендлера только по требованию:

new HandlersLocator([
    EmissionCommand::class => EmissionHandler::class,
    TransferCommand::class => TransferHandler::class,
], $container)
Ответить
Сергей

Спасибо, Дмитрий. Пришлось переписать локатор и всё заработало.

Ответить
Евгений

Дмитрий, вы показали как вынести в отдельный класс операции со статусами.

Но есть ещё несколько часто используемых операций:

  • Сериализация статуса в код и обратно
  • Текстовое название статуса. Получение по коду статуса.
  • По названию получить код (для обработки форм с Тильды пришлось такое добавить)
  • Список доступных статусов (код => название)

Как бы вы решили такую задачу в рамках DDD ?

Ответить
Дмитрий Елисеев

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

Ответить
Ivan

Почему мы не используем mock в unit тестах? Когда вообще надо использовать мок? Нафига он нужен в принципе?

Ответить
Дмитрий Елисеев

Моки и стабы будут в 17-ом эпизоде, когда с их помощью нужно будет имитировать зависимости.

Ответить
Ivan
public function confirmJoin(Token $token)
{
    if (is_null($this->joinConfirmToken)) {
        throw new DomainException("User has already been confirmed!");
    }
    $this->joinConfirmationToken->validate($token);
    $this->status = Status::active();
    $this->joinConfirmationToken = null;
}

if (is_null($this->joinConfirmToken)) - такого не может быть ведь мы в Сommand ищем юзера по токену

Как мы дойдем до "User has already been confirmed!" если в этом случае токен null, а мы ищем юзера по токену?

Не является ли это логической ошибкой? Дмитрий

Ответить
Дмитрий Елисеев

Сейчас confirmJoin мы используем только в обработчике команды, поэтому проверка выглядит избыточно. Но скоро начнём его вызывать в UserBuilder и в фикстурах для заполнения БД демо-данными для разработки и для тестов. И тогда появятся проблемы, если такой проверки у нас не будет.

Поэтому любому методу должно быть всё равно, откуда и как его вызывают выше. Все проверки в нём должны быть всегда.

Ответить
Артем Астапов

Так себе идея - искать юзера по токену. Нужен дополнительный индекс ради одноразовой операции Если колонка не uuid, а хранится в бд как строка, то индекс ещё и большой будет. Почему-бы вместе с токеном в команду не пихнуть id юзера? Как вариант без id - хранить токены в отдельной таблице, но тогда до запрос или join придётся делать при обычном findById, что-бы все данные стянуть для модели.

Ответить
Дмитрий Елисеев

Если передавать id, то в ссылку подтверждения тоже придётся подставлять id и token.

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

Ответить
Артем Астапов

Еще можно использовать разряженный или условный индекс в pgsql/mongo, но придётся для любого токена его все-равно создавать.

Ответить
alex

забыли StatusTest показать

Ответить
Зарегистрируйтесь или войдите чтобы оставить комментарий

Или войти через:

Google
GitHub
Yandex
MailRu