Можно дёрнуть айдишники SELECT product_id для array_diff перед сохранением.Melodic писал(а):И что бы сделать array_diff, нужно будет ещё раз загрузить Order?
Сервисный слой, как правильно?
Re: Сервисный слой, как правильно?
Re: Сервисный слой, как правильно?
Т.е. создавать новую сущность черезzelenin писал(а): Опять же вы не сможете создать сущность, не сгенерировав id в приложении (мы общались об этом в личке).
Код: Выделить всё
new User()
Re: Сервисный слой, как правильно?
сущность - это валидный объект с уникальной идентичностью (ID).Melodic писал(а):Т.е. создавать новую сущность черезzelenin писал(а): Опять же вы не сможете создать сущность, не сгенерировав id в приложении (мы общались об этом в личке).не правильно? В конструктор должен сразу id передаваться?Код: Выделить всё
new User()
пример: $coder = new User(); $project->addCoder($coder); $project->addCoder($coder); - второе добавление кодера должно выкинуть эксепшн, т.к. у нас не может быть в проекте один кодер два раза. А проверить его уникальность мы не можем, т.к. сущность Кодер на данный момент не обладает индентичностью. Поэтому создаем сущность мы уже полностью готовой.
Код: Выделить всё
$coder = new User($userRepo->nextIdentity(), $name);
$project->addCoder($coder);
Re: Сервисный слой, как правильно?
На сколько мне известно, из БД нельзя получить следующий Id, id известен только после вставки записи.
Да и как писал в личке, если сущности будут сохранятся не в БД, а где то через API, то следующий ID будет проблемно узнать.
Да и как писал в личке, если сущности будут сохранятся не в БД, а где то через API, то следующий ID будет проблемно узнать.
Re: Сервисный слой, как правильно?
http://stackoverflow.com/questions/6761 ... d-in-mysqlMelodic писал(а):На сколько мне известно, из БД нельзя получить следующий Id, id известен только после вставки записи.
в апи нет сущностей. Через апи мы кидаем DTO, а на стороне сервера в Application слое уже из DTO формируем сущность с ID.Melodic писал(а):Да и как писал в личке, если сущности будут сохранятся не в БД, а где то через API, то следующий ID будет проблемно узнать.
Но такой способ конечно крайне не надежный. Шанс получить один id для двух почти одновременно создаваемых сущностей очень велик.
Поэтому Uuid. https://github.com/zelenin/ddd-core/blo ... erator.php
Re: Сервисный слой, как правильно?
А не будет ли проблем с произодительностью базы данных из-за Uuid?zelenin писал(а):Но такой способ конечно крайне не надежный. Шанс получить один id для двух почти одновременно создаваемых сущностей очень велик.
Поэтому Uuid. https://github.com/zelenin/ddd-core/blo ... erator.php
Re: Сервисный слой, как правильно?
последовательные (есть такая оптимизация) uuid по скорости сравнимы с автоинкрементом, по размеру на жестком диске проигрывают 30%.nootropil писал(а):А не будет ли проблем с произодительностью базы данных из-за Uuid?zelenin писал(а):Но такой способ конечно крайне не надежный. Шанс получить один id для двух почти одновременно создаваемых сущностей очень велик.
Поэтому Uuid. https://github.com/zelenin/ddd-core/blo ... erator.php
Но это все это неважно на маленьком проекте, а на большом проекте для чтения юзаются read-движки (не sql) с моментальным доступом.
Re: Сервисный слой, как правильно?
Спасибо. В DDD мы можем работать с автоинкрементным идентификатором?zelenin писал(а): последовательные (есть такая оптимизация) uuid по скорости сравнимы с автоинкрементом, по размеру на жестком диске проигрывают 30%.
Но это все это неважно на маленьком проекте, а на большом проекте для чтения юзаются read-движки (не sql) с моментальным доступом.
А вообще, тема очень интересная и мне нужная.
Так как абстрактно всё сразу понять сложно, поробовал реализовать на примере регистрации и восстановления аккаунта.
Вороде на слои что-то разложить получлось, но уверенности что это DDD - нет.
Буду благодарен за адекватную критику:
PS Осторожно, много плагиата
https://bitbucket.org/nootropil/studyin ... ?at=master
Re: Сервисный слой, как правильно?
да. Генерацией id занимается хранилище (репозиторий). Метод nextIdentity может возвращать сегенерированный приложением uuid либо вытащенный из базы следующий id, но это race condition. Без генерации id в приложении уже некрасиво. Поэтому я бы использовал uuid и не извращался.nootropil писал(а):Спасибо. В DDD мы можем работать с автоинкрементным идентификатором?
это прикладная задача - лучше на нормальной доменной проблеме пробовать, например посты, теги, авторы - написание блога для практики думаю самое то.nootropil писал(а):Так как абстрактно всё сразу понять сложно, поробовал реализовать на примере регистрации и восстановления аккаунта.
Вороде на слои что-то разложить получлось, но уверенности что это DDD - нет.
https://bitbucket.org/nootropil/studyin ... ser.php-86nootropil писал(а):Буду благодарен за адекватную критику:
PS Осторожно, много плагиата
https://bitbucket.org/nootropil/studyin ... ?at=master
зачем скаляр возвращать? скаляр нужен только для хранения в базе.
https://bitbucket.org/nootropil/studyin ... ser.php-96
в ddd не используются тупые сеттеры. Методы должны быть названы более человекопонятно - rename например, changePassword, activize. Некоторые подобные методы могут быть сеттерами по сути, а могут объединять некое бизнес правило типа "Одобрить юзера" - проставить статус, изменить роль, увеличить рейтинг на один балл.
https://bitbucket.org/nootropil/studyin ... er.php-249
не уверен, что токены должны быть вообще частью домена. Активировать юзера мы можем разными способами - токен не обязательная часть юзера. Можно вынести в другой модуль типа Auth. Аналогично passwordResetToken и соответствующие методы в репозитории, и сервисы рассылок писем.
https://bitbucket.org/nootropil/studyin ... ew-default
команда - это просто сообщение, выполняемое command handler'ом, а не самой собой. Команды - это часть слоя Приложения - не домена.
Мы получили в контроллере какие-то данные (из формы или по api), сформировали из request команду и кинули ее куда-то на выполнение (в шину, сервис или хэндлер) - это уровень Приложения. А вот внутри хэндлера мы уже команды преобразовываем в домен на основе некоторой логики - извлекаем модель из репозитория, с помощью методов меняем ее, с помощью других сервисов рассылаем письма итд.
https://bitbucket.org/nootropil/studyin ... ew-default
репозиторий - это не сервис. Домен состоит из трех частей - модели (сущности и value objects), сервисы домена, репозитории.
https://bitbucket.org/nootropil/studyin ... ace.php-28
зачем add, если есть save? можно в save делать все проверки и на стороне базы уже либо инсертить либо апдейтить.
https://bitbucket.org/nootropil/studyin ... ?at=master
все команды у вас на самом деле сервисы слоя Приложение. Рекомендую вынести туда и разделить на Command и Command Handler/Application Service.
https://bitbucket.org/nootropil/studyin ... and.php-72
опять же сеттеры не юзаем, а юзаем например фабрики для создания юзера в контексте регистрации (User::signupUser(...)/UserFactory::signupUser(..)) - делаем домен более "говорящим".
https://bitbucket.org/nootropil/studyin ... ew-default
в домене у нас интерфейсы сервисов с одним методом, а реализации в инфраструктуре. Но опять же сам этот сервис - сервис приложения, который должен юзать сущности модулей User и Auth.
https://bitbucket.org/nootropil/studyin ... ry.php-161
логично всю логику по созданию модели вынести в UserFactory::create(..), чтобы не размазывать по всему приложению правила, по которым модель создается - инкапсулируем в фабрику.
Хороший старт!
Re: Сервисный слой, как правильно?
1. В сущности User у Вас практически только геттеры и сеттеры. Из-за этого сами работаете с ней процедурно, а не как с объектом. Сеттеров в сущности быть практически не должно.nootropil писал(а):Так как абстрактно всё сразу понять сложно, поробовал реализовать на примере регистрации и восстановления аккаунта.
Вороде на слои что-то разложить получлось, но уверенности что это DDD - нет. Буду благодарен за адекватную критику:
2. Вывернутая структура директорий. Удобнее наоборот Model\User.
3. Команды относятся к слою приложения. Так что их лучше переложить в Application\Command. И помещать внутрь них метод execute неудобно, так как приходится возиться с конструктором при их создании. Удобнее разбить на пустой класс-DTO с полями XxxCommand и обработчик XxxCommandHandler, чтобы шина команд их сопоставляла.
P.S. Опоздал с ответом
Re: Сервисный слой, как правильно?
zelenin писал(а): команда - это просто сообщение, выполняемое command handler'ом, а не самой собой. Команды - это часть слоя Приложения - не домена.
Мы получили в контроллере какие-то данные (из формы или по api), сформировали из request команду и кинули ее куда-то на выполнение (в шину, сервис или хэндлер) - это уровень Приложения. А вот внутри хэндлера мы уже команды преобразовываем в домен на основе некоторой логики - извлекаем модель из репозитория, с помощью методов меняем ее, с помощью других сервисов рассылаем письма итд.
Что то совсем с командами потерялся. Гугл предлагает обрабатывать всё в комманде методами или "execute" или "handle".ElisDN писал(а): 3. Команды относятся к слою приложения. Так что их лучше переложить в Application\Command. И помещать внутрь них метод execute неудобно, так как приходится возиться с конструктором при их создании. Удобнее разбить на пустой класс-DTO с полями XxxCommand и обработчик XxxCommandHandler, чтобы шина команд их сопоставляла.
Не совсем понятно как передавать зависимости в handler, такой вариант, как понимаю, не правильный (всё публично):
PS Пока без DTO
https://bitbucket.org/nootropil/studyin ... ew-default
https://bitbucket.org/nootropil/studyin ... ew-default
Re: Сервисный слой, как правильно?
Пример шины приводил здесь на четвёртой странице. Зависимости принимать через конструктор:nootropil писал(а):Не совсем понятно как передавать зависимости в handler
Код: Выделить всё
class SignUpCommand
{
public $username;
public $email;
public $password;
public function __construct($username, $email, $password)
{
$this->username = $username;
$this->email = $email;
$this->password = $password;
}
}
Код: Выделить всё
class SignUpHandler
{
private $userRepository;
private $confirmTokenRepository;
private $passwordHasher;
private $userNotificator;
public function __construct(
UserRepository $userRepository,
ConfirmTokenRepository $confirmTokenRepository,
PasswordHasherInterface $passwordHasher,
UserNotificationInterface $userNotificator,
)
{
$this->userRepository = $userRepository;
$this->confirmTokenRepository = $confirmTokenRepository;
$this->passwordHasher = $passwordHasher;
$this->userNotificator = $userNotificator;
}
public function handle(SignUpCommand $command)
{
$this->guardUsernameIsUnique($command->username);
$this->guardEmailIsUnique($command->email);
$userId = $this->userRepository->nextIdentity();
$user = User::singUp(
$userId,
$command->username,
$this->passwordHasher->hash($command->password),
$command->email
);
$this->userRepository->add($user);
$tokenId = $this->confirmTokenRepository->nextIdentity();
$token = ConfirmToken::create(
$tokenId,
$userId
);
$this->confirmTokenRepository->add($token);
$this->userNotificator->sendActivationEmail(
$user->getEmail(),
$user->getUsername(),
$token->getToken()
);
}
...
}
Код: Выделить всё
class PasswordHasher implements PasswordHasherInterface
{
public function hash($password)
{
return Yii::$app->security->generatePasswordHash($password);
}
public function validate($password, $hash)
{
return Yii::$app->security->validatePassword($password, $hash);
}
}
Последний раз редактировалось ElisDN 2016.07.25, 16:38, всего редактировалось 5 раз.
Re: Сервисный слой, как правильно?
Я не правильно вопрос задал, но ваш ответ всё равно натолкнул меня на решениеElisDN писал(а): Пример шины приводил здесь на четвёртой странице. Зависимости принимать через конструктор:
Примерно так должно быть в контроллере?
Код: Выделить всё
class SignUpCommand
public function actionXxx()
{
...
$signUpDto = new SignUpDto(
$username,
$email,
$password
);
$comBus = new CommandBus();
$comBus->execute(
new SignUpCommand($signUpDto),
new SignUpHandler(
new UserRepository(),
new UserReadRepository(),
new ConfirmTokenRepository(),
new PasswordHasherInterface(),
new UserNotificationInterface()
)
);
....
}
Re: Сервисный слой, как правильно?
типа того.
- class SignUpCommand читать как class SignUpController
- $comBus = new CommandBus(); логично ее откуда-то из di получить
- new SignUpHandler и соответственно шина внутри себя пусть инкапсулирует подбор хэндлера к команде
- а вообще DTO в данном случае ненужное промежуточное звено - сразу формируйте команду из данных из реквеста.
- class SignUpCommand читать как class SignUpController
- $comBus = new CommandBus(); логично ее откуда-то из di получить
- new SignUpHandler и соответственно шина внутри себя пусть инкапсулирует подбор хэндлера к команде
- а вообще DTO в данном случае ненужное промежуточное звено - сразу формируйте команду из данных из реквеста.
Re: Сервисный слой, как правильно?
это ларавельные псы искажают концепцию, скорее всего.nootropil писал(а):Что то совсем с командами потерялся. Гугл предлагает обрабатывать всё в комманде методами или "execute" или "handle".
Re: Сервисный слой, как правильно?
Команда - это и есть DTO. Формируем команду и кидаем в execute. А commandBus сама найдёт хэндлер по get_class($command) и выполнит $handler->handle($command).nootropil писал(а):Примерно так должно быть в контроллере?
Вот весь контроллер:
Код: Выделить всё
class UserController
{
private $commandBus;
public function __construct($id, $module, CommandBusInterface $commandBus, $config = [])
{
$this->commandBus = $commandBus;
parent::__construct($id, $module, $config);
}
public function actionSignup()
{
$form = Yii::createObject(SignupForm::className());
if ($form->load(Yii::$app->request->post()) && $form->validate()) {
$this->commandBus->execute(new SignUpCommand(
$form->username,
$form->email,
$form->password
));
Yii::$app->session->setFlash('success', 'Please confirm your Email.');
return $this->goHome();
}
return $this->render('signup', [
'signupForm' => $form,
]);
}
}
Re: Сервисный слой, как правильно?
Дмитрий, вы предлагали в view передавать dto, у меня получилось как то так:
Как то громоздко получилось или это нормально?
Код: Выделить всё
public function actionView($id)
{
$article = $this->loadArticle($id);
if ($article->isPublished()) {
$originals = [];
$thumbnails = [];
foreach ($article->images as $image) {
/**
* @var $image GalleryImage
*/
$thumbnails[] = $this->imageService->thumbnail($this->galleryService->path($image->file), $this->thumbnailWidth, $this->thumbnailHeight, Image::CROP);
$originals[] = $this->imageService->thumbnail($this->galleryService->path($image->file), $this->originalWidth, $this->originalHeight, Image::CROP);
}
$this->apiService->loadProfileAdditionalFields([$article->authorProfile]);
$this->articleService->view($article, \Yii::$app->request->userIP);
return $this->render('view', [
'dto' => new ArticleViewDto(
ProfileHelper::getFullName($article->authorProfile),
$article->authorProfile->avatar,
\Yii::$app->user->id === $article->user_id,
$article->id,
$article->header,
$article->text,
$article->approved_at,
$this->articleService->views($article),
$this->recommendService->countRecommends($article),
$this->commentService->countComments($article),
$thumbnails,
$originals,
$this->tagService->tags($article),
\Yii::$app->user->isGuest ? false : $this->favoriteService->isFavorite($article, \Yii::$app->user->identity),
\Yii::$app->user->isGuest ? false : $this->recommendService->isRecommended($article, \Yii::$app->user->identity)
)
]);
} else {
throw new NotFoundHttpException();
}
}
Re: Сервисный слой, как правильно?
громоздкость - это же не вопрос dto.
вообще суть такая:
все остальное - в сервисы.
вообще суть такая:
Код: Выделить всё
$dto = ArticleDto::createFromRequest(...);
$this->service->handleDto($dto); // тут ValidationException если что.
$this->render('...',['dto' => $dto]);
Re: Сервисный слой, как правильно?
Упростил задачу что бы сконцентироваться на том "как" делать. Попробовал максимально учесть накопленный опыт этого раздела. Реализован CRUD функционал сущности "Юзер". Пример рабочий.
Буду благодарен за адекватную критику:
https://bitbucket.org/nootropil/studyin ... ?at=master
Буду благодарен за адекватную критику:
https://bitbucket.org/nootropil/studyin ... ?at=master
Re: Сервисный слой, как правильно?
1. https://bitbucket.org/nootropil/studyin ... er.php-120nootropil писал(а):Упростил задачу что бы сконцентироваться на том "как" делать. Попробовал максимально учесть накопленный опыт этого раздела. Реализован CRUD функционал сущности "Юзер". Пример рабочий.
Буду благодарен за адекватную критику:
https://bitbucket.org/nootropil/studyin ... ?at=master
формат ни к чему. Можно сделать во время записи в базу.
2. https://bitbucket.org/nootropil/studyin ... ?at=master
шина - часть слоя Application (также query bus)
3. https://bitbucket.org/nootropil/studyin ... ew-default
лучше модели отдельно, репозитории отдельно - разделить директории
4. https://bitbucket.org/nootropil/studyin ... ew-default
гидрация - часть реализации хранилища (репозитория), т.е. инфраструктуры. для домена есть только абстрактное хранилище.
5. https://bitbucket.org/nootropil/studyin ... ew-default
протечка слоя фреймворка в приложение. Все слои ничего не должны знать о фреймворке, а тут явно у нас приложение на yii.
https://bitbucket.org/nootropil/studyin ... ory.php-47
тут казалось бы нету протечки, т.к. Query юзается в качестве библиотеки, но на самом деле Query внутри себя юзает опять же инстанс yii, получая доступ к db-коннекшну. Тоже протечка.
в прочем использование везде Yii::createObject оже протечка.
6. https://bitbucket.org/nootropil/studyin ... ew-default
дело вкуса, но я бы делал все-таки приватными поля и геттеры. либо публичные поля и констуктор в виде __construct(array $attributes)
а вообще я смотрю у вас и в хэндлерах поля публичные. Вообще публичность - это нарушение инкапсуляции, поэтому все поля по умолчанию приватные. Публичные поля нужны в редких случаях, например в DTO.
7. https://bitbucket.org/nootropil/studyin ... ler.php-47
вот эти все вызовы методов лучше вынести в фабрику или модель, чтобы они автоматически вызвались при регистрации (или какой у вас там кейс).
Смотрите, у нас модель богатая поведением. У нас метод activate() называется так не потому, что нужно просто переименовать setActive(), а потому что activate() у нас теперь обладает поведением, и не только может проставлять статус, но и заодно присваивать роль, проставлять дату итд в зависимости от вашего бизнес-кейса. Выносите максимум бизнес-задач в модель, делайте ее богатой.
8. https://bitbucket.org/nootropil/studyin ... ew-default
view - часть слоя Presentation
9. https://bitbucket.org/nootropil/studyin ... ?at=master
query (и command) - это часть парадигмы message driven design. Это сообщения императивного типа, т.е. приказ что-то выполнить. Поэтому назваться должны так: Создать юзера - CreateUser, Получить юзера - GetUser/GetUserById итд. ну а суффикс Command/Query - дело вкуса.