Добавление статуса ожидания и активности пользователя. Реализация команды подтверждения регистрации по токену из ссылки в электронном письме.
Скрытый контент
Чтобы не пропускать новые эпизоды подпишитесь на наш канал @deworkerpro в Telegram
Комментарии (17)
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
Спасибо за ответ. Сегодня обязательно видео посмотрю.
Сергей
Всем привет. Дмитрий, добрый день.
Вот у меня такой вопрос возник. У нас хендлеры не могут быть описаны каким-то единым интерфейсом, т.к. на вход принимают разные команды. Точнее можно сделать команды , реализующие некий интерфейс или унаследованные от какой-то абстрактной команды, но тут уже будет какой-то костыль.
И вот у меня возникла потребность задекорировать все хендлеры, добавив логирование начала и конца работы каждого хендлера. Как-то это можно реализовать без костылей?
Дмитрий Елисеев
В языках с дженериками можно указать общий интерфейс.
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, но придётся для любого токена его все-равно создавать.
Я немного не понял зачем в конструкторе по умолчанию ставить статус? А если мне потребуется из массива заполнить класс User данными? Получается, что статус будет не активный, но а пользователь на самом деле активен. Т.е. через конструктор я не смогу указать текущий статус? Поясните, пожалуйста, в чем дело.
Конструктор используется по назначению только для первого создания объекта. Как и другие методы он содержит свою бизнес-логику, по которой может присваивать первоначальные значения, бросать исключения и генерировать события.
В случае же восстановления объекта по массиву из БД конструктор не используют. Вместо этого объект там создаётся без конструктора и заполняется данными прямо в приватные поля через рефлексию.
Например, пишем гидратор или находим любой готовый вроде такого:
и с помощью него десериализуем объект текущими данными из БД:
Если же нужно просто в разных юзкейсах создавать объект по-разному, то в этом случае можно сделать несколько именованных конструкторов, как мы делали в видео о конструкторах. В будущем эпизоде про регистрацию через соцсети мы сделаем именно так.
Спасибо за ответ. Сегодня обязательно видео посмотрю.
Всем привет. Дмитрий, добрый день. Вот у меня такой вопрос возник. У нас хендлеры не могут быть описаны каким-то единым интерфейсом, т.к. на вход принимают разные команды. Точнее можно сделать команды , реализующие некий интерфейс или унаследованные от какой-то абстрактной команды, но тут уже будет какой-то костыль.
И вот у меня возникла потребность задекорировать все хендлеры, добавив логирование начала и конца работы каждого хендлера. Как-то это можно реализовать без костылей?
В языках с дженериками можно указать общий интерфейс.
В PHP без шаблонов это пока не работает.
В любом языке можно сделать свой интерфейс CommandBus для запуска команд и декорировать уже шину. В PHP можно взять league/tactician и реализовать логирование через middleware в ней.
Не подскажете еще как быть с автовайрингом? Пытался внедрить симфонячий мессенджер. Указал в конфиге
Но если в хендлер внедряется MessageBusInterface, то получается Circular dependency. И вот что-то ума не приложу как эту беду победить.
Переделать Locator на доставание хендлера только по требованию:
Спасибо, Дмитрий. Пришлось переписать локатор и всё заработало.
Дмитрий, вы показали как вынести в отдельный класс операции со статусами.
Но есть ещё несколько часто используемых операций:
Как бы вы решили такую задачу в рамках DDD ?
Обычно название статуса нужно только для отображения на фронтенде. Домену интересен только код. Поэтому все эти преобразования будут только снаружи в контроллерах или в файлах переводов. Поэтому если для сериализации используется пакет вроде symfony/serializer, то для статуса легко пишется кастомный сериализатор.
Почему мы не используем mock в unit тестах? Когда вообще надо использовать мок? Нафига он нужен в принципе?
Моки и стабы будут в 17-ом эпизоде, когда с их помощью нужно будет имитировать зависимости.
if (is_null($this->joinConfirmToken))
- такого не может быть ведь мы в Сommand ищем юзера по токенуКак мы дойдем до "User has already been confirmed!" если в этом случае токен null, а мы ищем юзера по токену?
Не является ли это логической ошибкой? Дмитрий
Сейчас
confirmJoin
мы используем только в обработчике команды, поэтому проверка выглядит избыточно. Но скоро начнём его вызывать вUserBuilder
и в фикстурах для заполнения БД демо-данными для разработки и для тестов. И тогда появятся проблемы, если такой проверки у нас не будет.Поэтому любому методу должно быть всё равно, откуда и как его вызывают выше. Все проверки в нём должны быть всегда.
Так себе идея - искать юзера по токену. Нужен дополнительный индекс ради одноразовой операции Если колонка не uuid, а хранится в бд как строка, то индекс ещё и большой будет. Почему-бы вместе с токеном в команду не пихнуть id юзера? Как вариант без id - хранить токены в отдельной таблице, но тогда до запрос или join придётся делать при обычном findById, что-бы все данные стянуть для модели.
Если передавать id, то в ссылку подтверждения тоже придётся подставлять id и token.
Да, можно сохранить в другой таблице и токен привязать через связь, если есть смысл в такой экономии.
Еще можно использовать разряженный или условный индекс в pgsql/mongo, но придётся для любого токена его все-равно создавать.
Или войти через: