Моделирование юзкейса запроса регистрации по email. Декомпозиция объектов. Абстрактные типы данных. Проработка сущностей, агрегатов и сервисов через Unit-тесты подходом Test First. Написание и тестирование доменных сервисов.
- 00:00:36 Команда запроса регистрации
- 00:01:17 Обработчик команды
- 00:02:46 Реализация юзкейса
- 00:05:04 Идентификатор пользователя
- 00:06:56 Инкремент через секвенцию
- 00:08:51 Использование UUID идентификаторов
- 00:12:36 Библиотека генерации UUID
- 00:13:32 Сортировка по дате
- 00:17:01 Сохранение пользователя
- 00:18:25 Проверка уникальности email
- 00:20:27 Неочевидность кода
- 00:22:39 Проверки на корректность
- 00:24:11 Объекты-значения вместо примитивов
- 00:27:22 Объект-значение Id
- 00:28:36 Инкапсуляция проверок
- 00:30:10 Вспомогательные конструкторы
- 00:30:58 Вспомогательные сервисы
- 00:31:45 Токен с истекающим временем
- 00:32:52 Сервис генерации токенов
- 00:33:57 Самодокументирующийся код
- 00:35:05 Вызов Unit Of Work
- 00:38:08 Отправление письма подтверждения
- 00:39:27 Придумывание методов в тестах
- 00:40:12 Тест для Email
- 00:41:19 Тест для Id
- 00:42:25 Библиотека Assert для проверок
- 00:44:14 Тест для создания пользователя
- 00:45:21 Реализация сущности и значений
- 00:46:54 Интерфейсы сервисов
- 00:48:38 Написание токенизера через тесты
- 00:51:41 Класс PasswordHasher
- 00:54:20 Медленные алгоритмы хэширования
- 00:56:46 Ускорение хэширования в тестах
- 00:58:45 Подведение итогов
- 01:00:24 Отличие Test First от TDD
Скрытый контент (код, слайды, ...) для подписчиков.
Открыть →Чтобы не пропускать новые эпизоды подпишитесь на наш канал @deworkerpro в Telegram
спасибо
Дмитрий, спасибо! Не планируете ли рассмотреть подход с валидацией всех данных сущности отложено, чтобы, к примеру, дать более информативный ответ пользователю о том, какие данные были введены неверно. К примеру М. Фаулер писал о неком Шаблоне Увеломления ( Notification Pattern ) ( https://martinfowler.com/articles/replaceThrowWithNotification.html, https://martinfowler.com/eaaDev/Notification.html, . В текущем подходе, при модификации данных сущности, исключение будет выброшено при обнаружении первого невалидного аргумента. До остальных проверок не дойдет и пользователь не узнает, что он где-то еще мог допустить ошибку при вводе данных.
Первичную валидацию (на обязательность и пр.) данных можно сделать в экшене или command через Assert::lazy(), а дальше не вижу ничего страшного выводить по одной ошибки при уже логических ошибках
Это зависит от требований к сценариям. Несомненно, бывают ситуации, когда можно ограничиться первой ошибкой при модификации агрегата, но также бывают, когда нужна полная информация о валидации агрегата.
Не является ли использование флашера отдаленной привязкой к Doctrine, ведь не все библиотеки для работы с БД работают по такой схеме? И является ли использование флашера вообще правильным, ведь мы отправляем в БД сразу вообще все изменения, о которых наш Handler даже может и не знать. Мне кажется, что сохранять в БД нужно уже в репозитории, причем весь агрегат вместе с дочерними сущностями (например user, user_profile) и только их и внутри транзакции, а не всё, что накопил EM. Что думаете?
Если инфраструктура может предоставить Api именно для таких взаимодействий: $this->users->add($user), $this->flusher->flush(), то тут никуда не деться. В прикладном слое некоторые детали взаимодействия с инфраструктурой знать нормально, но опосредовано. Что касается гибкости, мне кажется, что тут тоже особо нет проблем. Если перейти на стиль взаимодействия с бд, когда при вызове метода $this->users->add($user) данные будут сразу вставляться в бд, можно, к примеру, инжектить заглушку Flusher'a ( но лучше, конечно, хранить код в чистоте и вообще не инжектить, если перешли на другую ORM ).
Да и сложно представить ситуацию, когда используешь ORM с UoW и переходишь на более простой вариант ORM.
Дмитрий, Вы выбрали использовать простые типы данных в командах, преобразуя их в DTO уже в Handler. Я видел еще 2 варианта, кто-то сразу в команду передает DTO, кто-то передает в команду простые типы и в конструкторе делает DTO. Никак не могу понять, есть ли какая-то разница кроме вкусовщины?
Вы имеете ввиду VO, не DTO. И тут правильнее сказать, что VO инициализируются в хэндлере, не преобразуются. Вне прикладного слоя инициализировать не норм. Во-первых, внешним слоям программы ( контроллеры, cli-команды ) не следует знать о доменном слое. А VO - это элемент модели, Во-вторых, если HttpController'e вызывает некий Handler, а вы захотите вызывать Handler еще из Cli, вам придется дублировать логику инициализации VO.
Да-да, я про VO, прошу прощения)) В таком случае можно инициализировать в команде, это разгрузит код хэндлера
Считаю, что в прикладной команде тоже не норм инициализировать VO. Это просто объект для переноса данных в прикладной слой.
А может быть уже есть библиотека, в которой определены сущности Entity? Email и т.п.
Да, есть готовые наборы https://packagist.org/?query=value-objects
Здравствуйте.
В ваших роликах, при проектировании, сущности всегда встречал повторяющиеся Value Objects: ID, Status, Role, Name, Email.
Каждый раз, при проектировании какой-либо сущности, вы копируйте ID, который является UUID. И таким образом мы получаем копипасту, в тестах и сущностях.
Если в наших проектах Status, Role, Name как-то может поменяться, то ID и Email никогда не меняется. Таким образом его можно вынести в отдельную папку Value Object и использовать от туда. Но вы этого не делаете. Почему?) вместо этого у вас ID на каждую сущность. Можете пояснить по этому вопросу?
И ещё один вопрос «является ли status, role» VO?
Да, это всё Value Objects.
Наличие своего Id у каждой сущности избавляет от путаницы при типизации. Если есть метод:
то в него никто вместо
member->id
случайно не передастposition->id
илиcompany->id
.Если же будет один общий класс Id или Uuid на всю систему, то может быть путаница.
Можно вынести общий абстрактный класс Id и наследовать все User\Id уже от него. Но будут неудобства, если некоторые идентификаторы вдруг понадобится сделать числовыми инкрементными. Тогда придётся делать ещё один отдельный базовый класс.
В данном случае можно безболезненно вынести Email. Но и уже в этом можем заметить, что агрегат User использует его метод isEqualTo, а другим сущностям из другого модуля этот метод может быть не нужен.
Неудобство обобщений в том, что временем в таких общих классах (простых или абстрактных) накапливается куча лишних методов, которые обычно нужны всего одному или двум модулям, но не нужны остальным.
В итоге таким выносом мы вроде избавляемся от небольшой копипасты, но это сразу привносит лишние зависимости, лишнее наследование и путаницу в методах.
Так что выносить или нет - вопрос не тривиальный.
Теперь понял))) "Шубка выделки не стоит") Благодарю!
А что произойдет если мы в какой то момент захотим изменить алгоритм хэширования пароля? Как мы будем проверять пароль? придется проверять все использованные ранее алгоритмы? или просто пользователи будут вынуждены восстатавливать свои пароли?
Если поменяем алгоритм в password_hash, то ничего не сломается, так как password_verify продолжит работать со всеми возможными алгоритмами.
А не лучше бы было оставить все же интерфейсы и работать с ними - Tokenizer, PasswordHasher и т.д. а сами реализации вынести в отдельную инфраструктуру?
Да, можно. Тогда можно будет включить более строгое правило объявлять все классы как
abstract
илиfinal
.Очень похоже на ваш доклад с PhpRussia 2019 )
Да. Доклад как раз был по коду прошлогоднего проекта на Symfony. И сюда многое оттуда перешло.
Раньше на сайте был доступен ещё 1 проект?
Это на личном сайте https://elisdn.ru/products
Кто-нибудь желает добавить к видео субтитры? Потому что я не понимаю по-русски.
Здравствуйте! К некоторым видео уже добавлены субтитры. Например, к 45, 46, 47 и 51 эпизодам.
А если бы мы хешировали пароль по старинке с помощью соли, то нужно было бы ее передавать вместе с $passwordHash в requestJoinByEmail и везде добавлять проверки $salt !== null, там где есть $passwordHash !== null? Или можно как-то более интереснее поступить?
Да, тогда можно сделать объект-значение
new Password($hash, $salt)
а я что-то не понял момент, почему в классе Token мы приводим euid в нижний регистр (хотя он вроде и так всегда в нижнем), но вопрос в том почему в юнит тесте этого класса мы проверяем обычный euid, не приведен в нижний регистр?
Регистр проверяется в отдельном тестовом методе:
Не подскажите, какую версию Uuid , чтоб генерировались более менее отсортированные Uuid. (У меня миллионы записей, поэтому мне интересно) Чуть странно, что ваши курсы рассчитана на аудиторию выше среднего, а тут взяли и сказали , что у нас будет маленькое приложение :)
За урок отдельное спасибо.
Как раз недавно добавили v6.
Спасибо. Я уже было подумал, что всё заброшено и никто не читает комменты.
А почему UUID передается "извне". Почему бы его в конструкторе не создавать?
Чтобы указывать нужный ID в тестах и фикстурах.
Ещё если нужно будет вернуть идентификатор из контроллера, то может понадобиться создавать идентификатор прямо в контроллере и оттуда передавать в команду.
Дмитрий, а почему
находится в usecase, если это правило доменной сущности? То есть если в системе появится место, где еще раз используется requestJoinByEmail, то про такую проверку разработчик может в теории забыть, хотя эта проверка очевидно должна быть обязательной для всех операций, связанных с запросом на регастрацию по email
или я что то не так понял?
Это не правило самой сущности. Сущность следит только за своими внутренностями. Она не имеет доступа к БД и не знает, есть ли там кто-то ещё.
На классе Email я кайфанул. Спасибо за инсайт
по sender-у на емайл в JoinByEmail/Request/ а не лучше Event сделать ?
Да, можно сделать. Особенно если использовать очередь, чтобы отправлять письма в фоне.
Дмитрий, Скажите пожалуйста: А где лучше делать обработку исключений, например на не корректную строку ? Например если я вызываю команду сервиса - то как мне правильно обработать исключения?
В контроллере?
В контроллере обычно добавляют дополнительную валидацию для полей запроса. Там можно продублировать правило, чтобы оно не пропускало некорректные данные.
Благодарю Дмитрия за его труд. И хочу высказать свое мнение по поводу передачи в конструктор класса большого числа переменных. Но ведь можно пойти и другим путем. Просто использовать сеттеры. Иначе зачем их тогда прописывать. И тогда отпадает необходимость все или многие параметры оборачивать в объекты. Лично у меня возникает дискомфорт при создании дополнительных сущностей без которых можно обойтись. А потом для этих сущностей приходится писать конвертеры для доктрины. Это только мое лично мнение, и не критика подхода Дмитрия.
Не просто. С сеттерами объект никак не контролирует инвариант при создании.
Дима, ребята, привет.
Мне кажется такой подход:
Нас сильно привязывает к инфраструктуре.
Допустим у нас есть другой компонент, комментариев. Там будет тоже самое.
И везде будет то же самое.
Почему бы нам не инкапсулировать необходимость вызова ->flush(); в Отдельный объект что-то вроде:
И потом вызывать его методах ->add(...) в репозиториях:
где это нужно...
Удалить $this->flusher->flush(); в командах..
Затем в Middleware только в одном месте получить FlushFlag через DI в конструкторе и выполнить:
Тем самым мы уберем зависимость от инфраструктуры (в данном случае от выбранной ORM) и инкапсулируем ее особенность в той же инфраструктуре в виде Middleware...
И, например, если мы захотим хранить юзеров в файлах, нам нужно будем менять только репозиторий (UserRepository), инфраструктуру, не трогая бизнес логику (не надо удалять везде где используется UserRepository строку $this->flusher->flush(); ) + уберем зависимость от Flusher в наших Command.
Это сработает только для
add()
иremove()
, но не сработает в командах редактирования, где мы вызываем тольеоget()
.А чтобы отвязаться от инфраструктуры мы можем выполнять команды в шине CommandBus и уже в ней делать
flush()
.Спасибо за ответ. Жду урок по CommandBus. Хорошего дня!
Столкнулся с такой проблемой, сделал свой тип поля для доктрины Status как у вас и в методе convertToPHPValue мы возвращаем return !empty($value) ? new Status($value) : null;
Но статусы могут быть не только у User, но и у других сущностей, и они могут иметь как общие статусы, типа active, inactive, так и для разных сущностей специфические
UserStatus active inactive
HouseStatus active inactive send new
WorkStatus active trainee fired
создавать под статусы разных сущностей отдельные типы полей в доктрине показалось too match, в то же время в методе convertToPHPValue у нас нет никакого понимания, какой тип статуса вернуть, какие есть варианты решения этой проблемы?
Добрый вечер. По поводу класса Id. В нём используется __toString(). Решил немного побаловаться rector-ом. При проверке класса Id получил такую рекомендацию.
В документации написано следующее
Нужно ли следовать этой рекомендации?
Там сразу в следующм абзаце:
Если в коде будут функции или методы с таким типом параметра, то придётся добавить. А с остальными классами можно не торопиться это делать. Но лучше добавить везде по аналогии с
Serializable
и подобными интерфейсами.Как раз в классе id есть метод getValue(): string.
Получается надо добавлять.
Как я понял, этот интерфейс обязывает добавлять метод __toString() в класс, дабы избежать проблем?
Это надо понимать так, что из-за наличия метода __toString() в классе
Или войти через: