Melodic писал(а):Можно UserServiceInterface упразднить вообще и разбить его на хендлеры (UserRegistrationHandler,UserLoginHandler и т.д.)?
Да, так и делают. Вместо держания десятков методов в огромном UserService каждый операционный метод сервиса:
Код: Выделить всё
class UserSignupDto { ... }
class UserService
{
public function signup(UserSignupDto $dto) { ... }
public function update(UserUpdateDto $dto) { ... }
...
public function ban(UserBanDto $dto) { ... }
}
подключаемого в контроллере:
Код: Выделить всё
$this->userService->signup(new UserSignupDto($form->email, $form->password));
переезжает в отдельный класс:
Код: Выделить всё
class UserSignupCommand { ... }
class UserSignupHandler
{
public function handle(UserSignupCommand $command) { ... }
}
В итоге у нас убирается огромный UserService и десятками зависимостей в конструкторе и появляется набор примитивных классов на каждую операцию, где теперь невозможно заблудиться:
Код: Выделить всё
Command
User
Signup
UserSignupCommand.php
UserSignupHandler.php
UserSignupValidator.php
Ban
UserBanCommand.php
UserBanHandler.php
UserBanValidator.php
Теперь в контроллер подключается только общая на весь сайт шина и в неё кидаются все команды:
Код: Выделить всё
$this->commandBus->execute(new UserSignupCommand($form->email, $form->password));
А класс шины в своём методе execute будет автоматически конструировать и запускать нужный обработчик:
Код: Выделить всё
interface CommandBusInterface
{
public function execute($command);
}
Код: Выделить всё
class SimpleCommandBus implements CommandBusInterface
{
public function execute($command)
{
$handler = $this->resolveHandler($command);
call_user_func($handler, $command);
}
private function resolveHandler($command)
{
return [\Yii::createObject(substr(get_class($command), 0, -7) . 'Handler'), 'handle'];
}
}
А чтобы не вызывать валидаторы в самих хендлерах, можно обернуть эту шину в другую, которая будет аналогично находить и запускать нужный для команды валидатор:
Код: Выделить всё
class UserSignupValidator
{
private $userRepository;
public function __construct(UserRepositoryInterface $userRepository)
{
$this->userRepository = $userRepository;
}
public function validate(UserSignupCommand $command)
{
$errors = [];
...
if ($this->userRepository->existsByUsername($command->username)) {
$errors['username'][] = 'This username has already been taken.';
}
if ($this->userRepository->existsByEmail($command->email)) {
$errors['email'][] = 'This email has already been taken.';
}
return $errors;
}
}
Код: Выделить всё
class ValidationCommandBus implements CommandBusInterface
{
private $next;
public function __construct(CommandBusInterface $next) {
$this->next = $next;
}
public function execute($command) {
$validator = $this->resolveValidator($command);
if ($errors = call_user_func($validator, $command)) {
throw new ValidationException($errors);
}
$this->next->execute($command);
}
private function resolveValidator($command) {
return [\Yii::createObject(substr(get_class($command), 0, -7) . 'Validator'), 'validate'];
}
}
Аналогично можно cделать абсолютно любую обёртку. Например, для логов:
Код: Выделить всё
class LogCommandBus implements CommandBusInterface
{
private $next;
public function __construct(CommandBusInterface $next) {
$this->next = $next;
}
public function execute($command) {
Yii::info('Executing of ' . get_class($command), 'bus');
$this->next->execute($command);
}
}
И подключить обёртки в контейнере:
Код: Выделить всё
$container->setSingleton('app\commands\CommandBusInterface', function () {
return new LogCommandBus(new ValidationCommandBus(new SimpleCommandBus()));
});
При такой структуре имеем горсть полноценных, самодостаточных и легко тестируемых примитивных классов с простыми конструкторами (у хэндлеров) вместо одного монструозного конструктора UserService. Теперь UserService можно либо удалить, либо оставить в нём только геттеры вроде getNewUsers($limit) и т.п.
Но и от геттеров подобным образом можно избавиться, если рядом с CommandBus аналогично сделать QueryBus:
Код: Выделить всё
$newUsers = $this->queryBus->query(new NewUsersQuery(10));
и производить выборки в хэндлерах NewUsersHandler.
Вот, собственно, весь полноценный рабочий CommandBus мы здесь в одном комментарии и сочинили.