зачем прокидывать? ты же новую создаешь?SiZE писал(а):А модель Subscription в сервис как прокинуть? И может ли сервис работать с несколькими наследниками модели Subscription? И где присваивать userId? =)zelenin писал(а):Код: Выделить всё
$subscriptionService->createFromForm(SubscriptionForm $form);
Сервисный слой, как правильно?
Re: Сервисный слой, как правильно?
Re: Сервисный слой, как правильно?
почему приватные методы? это же просто обертка над конструктором.slavcodev писал(а):Я сразу понял про что ты, я сам читаю статьи Mathias.zelenin писал(а):статических фабричных метода (так называемые именованные конструкторы (с) Верраес)
Но к этому подходу отношусь неуверенно. Я считаю что оно нарушает принципы ООП, а именно инкпасуляцию,
из-за того что устанавливаются приватные свойства.
а это уже вещь стандартная, и опять можно начинаться спорить. Но я чесговоря выдохся - пишем много, да большинство по кругу, обсуждая личные предпочтения.slavcodev писал(а):Небольшое уточнение:zelenin писал(а):создание объекта на основе одного id - вещь нетрадиционная и мной ни разу не встречаемая у известных авторов
Не одного ИД, а на основе данных без которых объект не может принять свое начальное состояние.
Re: Сервисный слой, как правильно?
так где ты видишь установку приватных свойств?slavcodev писал(а):Возможно конечно, но я предпочитаю так не делать.ElisDN писал(а):Ваш подход можно и со статическими конструкторами совместить
Причину написал выше, лично я считаю это обходом инкапсуляции через установку значений приватным свойствам не через конструктор или поведения сущности.
- slavcodev
- Сообщения: 3134
- Зарегистрирован: 2009.04.02, 21:42
- Откуда: Valencia
- Контактная информация:
Re: Сервисный слой, как правильно?
Код: Выделить всё
final class Time
{
private $hours, $minutes;
// We don't remove the empty constructor because it still needs to be private
private function __construct(){}
public static function fromValues($hours, $minutes)
{
$time = new Time;
$time->hours = $hours;
$time->minutes = $minutes;
return $time;
}
}
Это ИМХО, могу ошибаться, как говорится "Хотите верьте, хотите нет"
Жду Yii 3!
Re: Сервисный слой, как правильно?
этот пример я кстати упустил у верраеса - был уверен, что к приватным свойствам нельзя из статики обращаться.slavcodev писал(а):hours, minutes - являются приватными свойства объекта (конкретного экземпляра класса), а статический метод это поведение класса, т.е. всех экземпляров данного класса. Имхо статические классы могут только менять такие же статические свойства, но не могут менять приватные свойства инстанциированного объекта. Свойства объекта должны меняться через поведения, интерфейс объекта.Код: Выделить всё
final class Time { private $hours, $minutes; // We don't remove the empty constructor because it still needs to be private private function __construct(){} public static function fromValues($hours, $minutes) { $time = new Time; $time->hours = $hours; $time->minutes = $minutes; return $time; } }
Это ИМХО, могу ошибаться, как говорится "Хотите верьте, хотите нет"
Тут я согласен насчет хака, но сам хаки в виду не имел.
Re: Сервисный слой, как правильно?
Ну это да, некий хак для обхода отсутствия перегрузки конструкторов.slavcodev писал(а):hours, minutes - являются приватными свойства объекта (конкретного экземпляра класса), а статический метод это поведение класса, т.е. всех экземпляров данного класса. Имхо статические классы могут только менять такие же статические свойства, но не могут менять приватные свойства инстанциированного объекта. Свойства объекта должны меняться через поведения, интерфейс объекта.
Со статическими методами классический вариант с полным конструктором выглядит тоже не очень:
Код: Выделить всё
class User
{
private function __construct($email, ..., $role)
{
$this->email = $email;
...
$this->role = $role;
}
public static function draft($email, $password, $confirmationToken)
{
$user = new self($email, null, $password, null, null, 'pending-confirmation', $confirmationToken, 'User');
$user->recordEvent(new TestUserWasCreatedEvent($user));
return $user;
}
public static function register($email, $name, $password, $confirmationToken)
{
$user = new self($email, $name, $password, new DateTime(), new DateTime(), 'pending-confirmation', $confirmationToken, 'User');
$user->recordEvent(new NewUserWasRegisteredEvent($user));
return $user;
}
}
А ваш вариант с динамическими draft() и register() полностью избавляет от этих проблем.
Re: Сервисный слой, как правильно?
Как симбиоз статики и конструктора сейчас додумался до такого варианта:slavcodev писал(а):но не могут менять приватные свойства инстанциированного объекта. Свойства объекта должны меняться через поведения, интерфейс объекта.
Код: Выделить всё
class User
{
private $id;
private $email;
private $status;
private $role;
private function __construct($email)
{
$this->email = $email;
$this->status = 'inactive';
$this->role = 'User';
}
public static function createDraft($email, $password, $confirmationToken)
{
$user = new self($email);
$user->draft($password, $confirmationToken);
return $user;
}
private function draft($password, $confirmationToken)
{
$this->password = $password;
$this->confirmationToken = $confirmationToken;
$this->createdTime = new DateTime();
$this->status = 'pending-confirmation';
$this->recordEvent(new TestUserWasCreatedEvent($this));
}
public function getId() {return $this->email };
public function getStatus() {return $this->status };
public function getRole() {return $this->role };
}
Последний раз редактировалось ElisDN 2016.08.04, 00:46, всего редактировалось 1 раз.
Re: Сервисный слой, как правильно?
Высвобождать события в репозитории опасно, если несколько сохранении вызываются подряд в транзакции внутри блока try-catch.slavcodev писал(а):Код: Выделить всё
class UserRepository { function save(User $user) { // сохраняем пользователя в базу данных (имплементация упущена) // после чего обрабатываем события произошедшие с ним, // отсылаем емитеру чтоб оповестил подписчиков из других контектов foreach ($user->flushEvents() as $event) { $this->eventEmitter->emit($event); } } }
Надёжнее либо вызывать в хэндлере после всех сохранений:
Код: Выделить всё
public function handle (EmployeeRecruitCommand $command)
{
$employee = ...;
$contract = ...;
$this->employeeRepository->add($employee);
$this->contractRepository->add($contract);
$this->eventDispatcher->dispatch($employee->releaseEvents());
$this->eventDispatcher->dispatch($contract->releaseEvents());
}
Код: Выделить всё
class TransactionalCommandBus implements CommandBusInterface
{
private $next;
private $transactionManager;
private $eventDispatcher;
private $identityMap;
public function execute($command)
{
$transaction = $this->transactionManager->begin();
try {
$this->next->execute($command);
$transaction->commit();
$this->eventDispatcher->dispatch($this->identityMap->releaseAllEvents());
} catch (\Exception $e) {
$transaction->rollback();
throw $e;
}
}
}
Код: Выделить всё
class TransactionalCommandBus implements CommandBusInterface
{
private $next;
private $transactionManager;
private $eventEmitter;
public function execute($command)
{
$transaction = $this->transactionManager->begin();
try {
$this->next->execute($command);
$transaction->commit();
$this->eventEmitter->emitRecordedEvents();
} catch (\Exception $e) {
$transaction->rollback();
throw $e;
}
}
}
Последний раз редактировалось ElisDN 2016.07.25, 18:27, всего редактировалось 2 раза.
- slavcodev
- Сообщения: 3134
- Зарегистрирован: 2009.04.02, 21:42
- Откуда: Valencia
- Контактная информация:
Re: Сервисный слой, как правильно?
Может и опасно. А может опаснее обрабатывать события в разных хендлерах,ElisDN писал(а):Высвобождать события в репозитории опасно, если несколько сохранении вызываются подряд в транзакции внутри блока try-catch.slavcodev писал(а):Код: Выделить всё
class UserRepository { function save(User $user) { // сохраняем пользователя в базу данных (имплементация упущена) // после чего обрабатываем события произошедшие с ним, // отсылаем емитеру чтоб оповестил подписчиков из других контектов foreach ($user->flushEvents() as $event) { $this->eventEmitter->emit($event); } } }
Надёжнее либо вызывать в хэндлере после всех сохранений
можно забыть, прогер после вас просто не будет знать об этой особенности приложения.
И вообще советов на все случае жизни нет, иначе я и ты сидели бы без работы, все бы придумали до нас.
Мы же говорим о ДДД? Стоит ли думать тогда о тонкостях БД при проектировании?
Лично я думаю что события принадлежат сущности, и должны обрабатываться сразу после ее сохранения,
до сохранения второй сущности, тем более что эти события могут повлиять на сущность из другого контекста.
При проблеме с транзакцией сохранения в БД двух сущностей,
вполне возможно что границы между bounded contexts слишком слабы, спроектированы не достаточно закрыто и независимо.
Не знаю домена, можно предположить что $contract это не отдельный AR, а сущность связанная с $employee,
так что будет только сохранение $employee в сервисах.
В любом случае твой вариант не работает в системе над которой я работаю, т.к. сохранение в базу занимается хендлер ивент имитера (так называемые - проекции).
Жду Yii 3!
- slavcodev
- Сообщения: 3134
- Зарегистрирован: 2009.04.02, 21:42
- Откуда: Valencia
- Контактная информация:
Re: Сервисный слой, как правильно?
Это вариант практически ничем не лучше варианта Varraes, ты используешь приватные методы, он приватные свойства из публичного статического метода. Да ООП это позволяет, но как я узнал недавно, в народе это называют антипаттерном "Паблик Морозова".ElisDN писал(а):Как симбиоз статики и конструктора сейчас додумался до такого варианта:slavcodev писал(а):но не могут менять приватные свойства инстанциированного объекта. Свойства объекта должны меняться через поведения, интерфейс объекта.
Это придаёт атомарность действию регистрации (не даёт создать и сохранить пустого пользователя вне use case), не нарушает инкапсуляцию и избавляет от необходимости имевшихся там проверок и эксепшенов.Код: Выделить всё
class User { private $id; private $email; private $status; private $role; private function __construct($email) { $this->email = $email; $this->status = 'inactive'; $this->role = 'User'; } public static function createDraft($email, $password, $confirmationToken) { $user = new self($email); $user->draft($password, $confirmationToken); return $user; } private function draft($password, $confirmationToken) { $this->password = $password; $this->confirmationToken = $confirmationToken; $this->createdTime = new DateTime(); $this->status = 'pending-confirmation'; $this->recordEvent(new TestUserWasCreatedEvent($this)); } public function getId() {return $this->email }; public function getStatus() {return $this->status }; public function getRole() {return $this->role }; }
Поэтому мне лично не подходит.
Проблема с сохранением пустого юзера, решается тривиально, проверка наличия произошедших с этой сущностью событий.
Жду Yii 3!
Re: Сервисный слой, как правильно?
Я правильно понимаю, что просто начать с малого и внедрить сервисный слой в Й2 не получится?
Я зациклился на полученной информации. Просто с сервисами сразу хочется и ДТО, и события, и репозитории, но на все время нет. Как в классическом Й2 ограничится для начала ими?
Я зациклился на полученной информации. Просто с сервисами сразу хочется и ДТО, и события, и репозитории, но на все время нет. Как в классическом Й2 ограничится для начала ими?
Последний раз редактировалось SiZE 2016.07.26, 06:59, всего редактировалось 1 раз.
Re: Сервисный слой, как правильно?
Поразмышлял на эту тему пару дней. Поэкспериментирую тоже без статических методов.slavcodev писал(а):Поэтому мне лично не подходит.
И касательно эмиттера сразу не сообразил, что это могут быть сообщения ES для проекций, а не просто доменные события.
Re: Сервисный слой, как правильно?
Архитектура придумана для упрощения сложного кода, а не для усложнения простого. Если Вас всё устраивает, если не пишете тесты, не требуется подключать разные БД и в проекте нет сложных и неприятных мест, которые портят жизнь, то заморачиваться не нужно.SiZE писал(а):Я правильно понимаю, что просто начать с малого и внедрить сервисный слой в Й2 не получится?
Отличие подхода с проектированием от обычного лапшекода - необходимость много думать. Если есть рамки "некогда думать, быстрей фигачь в продакшн", то времени не будет никогда.SiZE писал(а):Я зациклился на полученной информации. Просто с репозиториями сразу хочется и ДТО, и события, и репозитории, но на все время нет.
Если всё же захочется, то перечитайте книги по ООП и рефакторингу и постепенно начинайте применять советы из них.SiZE писал(а):Как в классическом Й2 ограничится для начала ими?
- slavcodev
- Сообщения: 3134
- Зарегистрирован: 2009.04.02, 21:42
- Откуда: Valencia
- Контактная информация:
Re: Сервисный слой, как правильно?
"Сервисный слой" как раз не проблема вообще, проблема в правильно проектировании модели.SiZE писал(а):Я правильно понимаю, что просто начать с малого и внедрить сервисный слой в Й2 не получится?
Рецепт "Сервисный слой"
1. Создаем по классы на один use case (Варианты использования):
[*] Регистрация пользователя *
[*] Активация пользователя
[*] Просмотр профиля пользователя
[*] Удаление пользователя **
* - Не важно кто инициатор (какое приложение: веб форма, REST API, консоль), сервис ничего не должен о слое выше,
т.е. о том кто его вызвал, он получил данные и обработал. Важно бизнес логика, при выполнении. Например если из веб формы и REST API создание одинаково (не важно что формат данных разный) это один сервис, а из консоли можно создать только первого юзера (админа) - это уже совсем другой юз кейс, поэтому лучше сделать отдельным сервисом, чем меньше IF в коде, тем лучше.
** - Если есть такой юз кейс. Не путать с удалением модели из базы, это может быть действие внутренее. Другими словами по одному юз кейсу на каждое действие на вашу систему из вне (HTTP запрос, запуск консольной команды)
2. Проектирование сервисов:
[*] Зависимости сервиса - в конструктор (другие сервисы, компоненты приложения), настройки сервиса (какие-то общие свойства)
[*] Данные от контроллера - в метод (данные понятные сервису, контролер должен постараться и привести свои данные к этому формату)
[*] Результат - данные ожидаемые при успехе (при регистрации - пользователя, при активации - пользователя, при удалении - ничего).
Забудьте про возвращение булевого результата. Bool - это такой же тип данных как и другие, и используется для данных.
Есть такой принцип в ООП - Tell, don't ask (простите за еще один термин, за еще одну проблему в голове). Запустив команду ждите результат (то что команда вычислила, создала) либо исключение.
Бул используйте только при запросе каких-то данные (isValid(), isActive(), canUserDance())
3. Зона ответственности. Каждый слой знает только как работать с ниже стоящим:
[*] Контролер знает как вызвать инициализировать сервис и как запустить, с какими параметрами
[*] Сервис ничего не знает о контролере, или кто его там вызвал
[*] Сервис знает как работать с моделями либо с другими сервисами того же слоя либо из слоя ниже
[*] Модель ничего не знает о существовании сервиса или кто ее пользуется
В идеале контроллер ничего не знает о моделях (слой два уровня ниже) и какие методы можно вызвать,
т.е. сервис регистрации должен вернуть не саму модель пользователя, а данные пользователя, может быть DTO,
может быть и обычный массив, если не хочется иметь лишние классы. Это вполне допустимо, особенно если результат от сервиса тестируется. В этом случае можно быть уверенным что контроллер получит ожидаемый формат данных,
что является единственной необходимостью в DTO.
Кстати говоря, внешние экшены Yii, являются такими сервисами
Жду Yii 3!
Re: Сервисный слой, как правильно?
Спасибо.
Re: Сервисный слой, как правильно?
Уместно ли использовать внутри модели хелперы?
Код: Выделить всё
class UserDomainModel
{
private $login;
private $email;
private $password;
private $restoreСode;
private $confirmationСode;
private $type;
private $visible;
public static function createFromOrder(Order $order)
{
$model = new self($order->email, $order->email);
$model->generatePasswordHash();
$model->confirmation_code = Utils::generatePassword(32);
$model->visible = 0;
$model->type = 'user';
return $model;
}
public function __construct($login, $email, $password = null)
{
$this->login = $login;
$this->email = $email;
$this->password = $password;
$this->visible = 1;
}
public function generatePasswordHash()
{
$password = $this->password === null ? Utils::generatePassword() : $this->password;
$this->passwordРash = md5($this->login . $password . 'salrt');
}
public function activate()
{
$this->confirmationСode = Utils::generatePassword(32);
$this->visible = 1;
}
}
Re: Сервисный слой, как правильно?
в данном случае уместно назвать это domain service
Код: Выделить всё
public function generatePasswordHash(PasswordHashGeneratorInterface $hasher)
Re: Сервисный слой, как правильно?
Во-первых, наличие класса вроде Utils говорит о том, что в нём бессмысленно накидана большая куча несвязанных методов вроде:chesar писал(а):Уместно ли использовать внутри модели хелперы?
Код: Выделить всё
class Utils
{
public static function hashPassword(...)
public static function validatePassword(...)
public static function watermarkImage(...)
public static function showGravatar(...)
}
Код: Выделить всё
class PasswordHasher
{
public static function hash(...)
public static function validate(...)
}
class Image
{
public static function watermark(...)
}
class Gravatar
{
public static function show(...)
}
Код: Выделить всё
$this->confirmationСode = Utils::generatePassword();
Код: Выделить всё
$this->confirmationСode = Utils::generateСonfirmationСode();
В-третьих, статические вещи неподменяемы без переписывания при работе и при тестировании. Если в статическом методе используете, например, компонент Security фреймворка:
Код: Выделить всё
class PasswordHasher
{
public static function hash($password)
{
return Yii::$app->security->generatePasswordHash($password);
}
public static function validate($password, $hash)
{
return Yii::$app->security->validatePassword($password, $hash);
}
}
Код: Выделить всё
public function generatePassword($password)
{
$this->passwordРash = PasswordHasher::hash($password);
}
В-четвёртых, при использовании статики снаружи нам не понятно, куда наша модель сама лезет и зачем. Часто в Yii2 любят ещё и письма из модели посылать, и Yii::$app->user или Yii::$app->request дёргать. Такая самовольность весьма неприятна.
Как это победить? Если рассмотреть пример со статическими методами:
Код: Выделить всё
public function signup($email, $password)
{
$this->email = $email;
$this->passwordHash = PasswordHasher::hash($password);
$this->confirmationСode = TokenGenerator::generate();
$this->active = 0;
}
Простейший эффективный вариант - вынести генерацию из модели и передавать в модель уже готовые хеши и токены:
Код: Выделить всё
public function signup($email, $passwordHash, $confirmationСode)
{
$this->email = $email;
$this->passwordHash = $passwordHash;
$this->confirmationСode = $confirmationСode;
$this->active = 0;
}
А если не хочется делать хеши и токены доступными снаружи через геттеры, то есть третий подход с инъекцией зависимых компонетов в сам метод. Мы передаём нужные сервисы в метод, а сама модель вызывает из них то, что ей нужно:
Код: Выделить всё
public function signup(
$email,
$password,
PasswordHasherInterface $passwordHasher,
TokenGeneratorInterface $tokenGenerator
)
{
$this->email = $email;
$this->passwordHash = $passwordHasher->hash($password);
$this->confirmationСode = $tokenGenerator->generate();
$this->active = 0;
}
Для рабочего кода можем написать фреймворковский хешер:
Код: Выделить всё
class YiiPasswordHasher implements PasswordHasherInterface
{
public function hash($password)
{
return Yii::$app->security->generatePasswordHash($password);
}
public function validate($password, $hash)
{
return Yii::$app->security->validatePassword($password, $hash);
}
}
Код: Выделить всё
Yii::$container->set('app\services\PasswordHasherInterface', 'app\services\YiiPasswordHasher');
Код: Выделить всё
class TestPasswordHasher implements PasswordHasherInterface
{
public function hash($password)
{
return md5($password);
}
public function validate($password, $hash)
{
return md5($password) === $hash;
}
}
Код: Выделить всё
$passwordHasher = new TestPasswordHasher();
$tokenGenerator = new TestConfirmCodeGenerator();
$user->signup($email, $password, $passwordHasher, $tokenGenerator);
И, как бонус, на заглушках такой код можно будет тестировать без поднятия Yii::$app на голом PHPUnit_Framework_TestCase. Это ещё ускорит процесс.
Какова мораль? В статических хелперах можно оставить очень лёгкие, чистые и неизменяемые вещи, независимые от фреймворка. Например, обёртка StringHelper::toUpper($string) над ms_strtoupper. А зависимые от фреймворка, лезущие в сеть или в базу, либо слишком долгие вещи проще выносить в обычные классы, чтобы в любой момент можно было их подменить/перекомпоновать/закешировать и т.п., что со статическими методами нативно сделать никак не получится.
Re: Сервисный слой, как правильно?
Простой вопрос - связи (наши relations) где должны определятся\получаться? в модели либо в сервисе?
К примеру есть User у него есть связь Auth (которая belongs to User по user_id). Вот разделил я пользователя на
1) User (модель)
2) UserService (в сервисе есть методы add\delete\fethById\populate (это типа map(User user, array|UserDTO data) и прочие, только сервис знает, какой репозиторий использовать, PgSqlRepository или FileRepository и все в таком духе)
3) UserRepositoryPgSql
и вот появилась сущность Auth. вот имея объект user (объект модели User) хочу получить его Auth. В сервисе создать метод UserService.getAuth(user) ?
К примеру есть User у него есть связь Auth (которая belongs to User по user_id). Вот разделил я пользователя на
1) User (модель)
2) UserService (в сервисе есть методы add\delete\fethById\populate (это типа map(User user, array|UserDTO data) и прочие, только сервис знает, какой репозиторий использовать, PgSqlRepository или FileRepository и все в таком духе)
3) UserRepositoryPgSql
и вот появилась сущность Auth. вот имея объект user (объект модели User) хочу получить его Auth. В сервисе создать метод UserService.getAuth(user) ?
Re: Сервисный слой, как правильно?
Связи никуда не деваются. Auth так и будет в $this->auth у User.S c писал(а):Простой вопрос - связи (наши relations) где должны определятся\получаться? в модели либо в сервисе?
К примеру есть User у него есть связь Auth (которая belongs to User по user_id).
Последний раз редактировалось ElisDN 2016.08.04, 10:15, всего редактировалось 1 раз.