Сервисный слой, как правильно?

Обсуждаем, как правильно строить приложения
zelenin
Сообщения: 9939
Зарегистрирован: 2013.04.20, 11:30

Re: Сервисный слой, как правильно?

Сообщение zelenin » 2016.05.12, 12:42

хотя вот такой вариант мне больше нравится:

Код: Выделить всё

public function actionTest()
{
$dto = UserRegistrationDto::createFromRequest(Yii::$app->request);

$response = new UserRegistrationResponse($dto);

try {
    $this->userRegistrationService->registrate($dto);
} catch (ValidationException $e) {
    $response->setErrors($e->getErrors());
}

return $this->render('test', ['response' => $response]);
}
 

Melodic
Сообщения: 79
Зарегистрирован: 2016.05.11, 17:43
Откуда: Луганск

Re: Сервисный слой, как правильно?

Сообщение Melodic » 2016.05.12, 13:01

zelenin писал(а):предлагаю так:

Код: Выделить всё

public function actionTest()
{
$dto = UserRegistrationDto::createFromRequest(Yii::$app->request);

if($this->userRegistrationValidator->validate($dto)) {
    $errors = new ValidationErrors();
    $this->userRegistrationService->registrate($dto);
} else {
    $errors = $this->userRegistrationValidator->getErrors();
}

return $this->render('test', ['response' => new UserRegistrationResponse($dto, $errors)]);
}
 
Правильным ли будет сделать через возможности Yii(не хотелось бы отказываться от ActiveForm) ? Т.е. так:

Код: Выделить всё

<?php
class RegistrationForm extends yii\base\Model{

    private $userService;
    public $login;
    public $password;
    public function __construct(UserServiceInterface $userService){
        $this->userService;
    }
    public function registrate(){
        if(!$this->hasErrors()){
            $dto = UserRegistrationDto::create($this->login,$this->password);
              $this->userService->registrate($dto);
              return true;
        }
        return false;
    }
    
    public function rules(){
        ......
        return $rules;
    }    
}

Код: Выделить всё

<?php
class TestController extends Controller{
    private $userService;
    public function __counstruct(UserServiceInterface $service){
        $this->userService = $service;    
    }
    
    public function actionTest(){
        $form = new RegistrationForm($this->userSerivce);
        if($form->load(Yii::$app->request->post) && $form->validate() && $form->registration()){
            //Успешная регистрация
        }else{
            
        }
    }
}
Т.е. вынести валидацию в саму форму.

zelenin
Сообщения: 9939
Зарегистрирован: 2013.04.20, 11:30

Re: Сервисный слой, как правильно?

Сообщение zelenin » 2016.05.12, 13:10

неверно. Еще раз напоминаю: забудьте о подобных йии-штучках - это все изначально неверно, и сделано, чтобы убыстрить разработку без мысли о дальнейшей поддержке. Пользуйтесь только базовыми фичами yii.
Один класс - одна ответственность. Форма в yii - это модель, валидация и вы туда еще добавляете сервис (так сделано везде в yii и всячески пропагандируется), тогда как правильным является вынести все эти три обязанности в отдельные сущности, соответственно dto, validator, service.
Но воспользоваться формой как формой с клиентской валидацией - ради бога.

Melodic
Сообщения: 79
Зарегистрирован: 2016.05.11, 17:43
Откуда: Луганск

Re: Сервисный слой, как правильно?

Сообщение Melodic » 2016.05.12, 13:59

С репозиториями не много не понятно.
По вашему совету, все методы для сохранения\добавления\обновления БД я вынес в репозиторий UserRepository.
У модели User есть поле $city, которое содержит модель City.
В репозитории UserRepository есть метод findById($id), который ищет пользователя по Id и заполняет модель User.
Проблема в том, что поле $city остаётся не заполненным. Джоинить таблицу `city` прям в методе UserRepository::findById($id) и там создавать модель City? Но это, вроде, не правильно, что UserRepository оперирует моделью City. Как быть в таком случае?

zelenin
Сообщения: 9939
Зарегистрирован: 2013.04.20, 11:30

Re: Сервисный слой, как правильно?

Сообщение zelenin » 2016.05.12, 14:12

Melodic писал(а):С репозиториями не много не понятно.
По вашему совету, все методы для сохранения\добавления\обновления БД я вынес в репозиторий UserRepository.
У модели User есть поле $city, которое содержит модель City.
В репозитории UserRepository есть метод findById($id), который ищет пользователя по Id и заполняет модель User.
Проблема в том, что поле $city остаётся не заполненным. Джоинить таблицу `city` прям в методе UserRepository::findById($id) и там создавать модель City? Но это, вроде, не правильно, что UserRepository оперирует моделью City. Как быть в таком случае?
это ок. в ddd есть понятие bounded context (сущность со всеми связями) - репозиторий создается именно для bounded context.

Melodic
Сообщения: 79
Зарегистрирован: 2016.05.11, 17:43
Откуда: Луганск

Re: Сервисный слой, как правильно?

Сообщение Melodic » 2016.05.12, 14:19

В репозитории UserRepository есть метод findAll(), который возвращает всех пользователей. Как правильно будет сделать, если нам нужно определённое кол-во записей? Создать метод findAllLimit($start,$end)? А если ещё кроме этого нужно будет искать по E-mail'y? Создать метод findByEmaillLimit($start,$end)?

zelenin
Сообщения: 9939
Зарегистрирован: 2013.04.20, 11:30

Re: Сервисный слой, как правильно?

Сообщение zelenin » 2016.05.12, 14:36

в репозитории может быть много методов, но пусть они все работают через один главный findByCondition, который на вход получает объект Condition (limit, orderBy, where итд). можно сделать что-то типа $conditionBuilder (как queryBuilder в yii). Этот объект обрабатывайте в этом методе и формируйте ваш query.

Код: Выделить всё

if($condition->limit) {
     $query->limit($condition->limit);
}
 
другие методы могут выглядеть так:

Код: Выделить всё

$condition = new Condition;
$condition->limit = 10;
$repository->findByEmail($email, $condition);

public function findByEmail($email, Condition $condition = null)
{
    $condition = Condition::createFromCondition($condition);
    $condition->orderBy = 'date desc';
    $condition->where = new Where('email', $email);
    return $this->findByCondition($condition);
}
 
Последний раз редактировалось zelenin 2016.05.12, 14:45, всего редактировалось 1 раз.

zelenin
Сообщения: 9939
Зарегистрирован: 2013.04.20, 11:30

Re: Сервисный слой, как правильно?

Сообщение zelenin » 2016.05.12, 14:39

!!! но date desc - это sql выражение, а репозиторий - абстрактное хранилище, поэтому: $condition->orderBy = new Sort('date', SORT_DESC);
SORT_DESC - встроенная в php константа (или можно заюзать свою Sort::DESC).

zelenin
Сообщения: 9939
Зарегистрирован: 2013.04.20, 11:30

Re: Сервисный слой, как правильно?

Сообщение zelenin » 2016.05.12, 14:46

UPD выше в примере забыл про email в condition - исправил.

Melodic
Сообщения: 79
Зарегистрирован: 2016.05.11, 17:43
Откуда: Луганск

Re: Сервисный слой, как правильно?

Сообщение Melodic » 2016.05.12, 15:54

zelenin писал(а):в репозитории может быть много методов, но пусть они все работают через один главный findByCondition, который на вход получает объект Condition (limit, orderBy, where итд). можно сделать что-то типа $conditionBuilder (как queryBuilder в yii). Этот объект обрабатывайте в этом методе и формируйте ваш query.

Код: Выделить всё

if($condition->limit) {
     $query->limit($condition->limit);
}
другие методы могут выглядеть так:

Код: Выделить всё

$condition = new Condition;
$condition->limit = 10;
$repository->findByEmail($email, $condition);

public function findByEmail($email, Condition $condition = null)
{
    $condition = Condition::createFromCondition($condition);
    $condition->orderBy = 'date desc';
    $condition->where = new Where('email', $email);
    return $this->findByCondition($condition);
}
findByCondition - по сути будет одинаков во всех репозиториях, которые будут использовать Query из Yii, в UserRepository и CityRepository в методе findByCondition() будет такое:

Код: Выделить всё

<?php
.......

public function findByCondition(Condition $condition){
    /* Этот код будет во всех репозиториях которые используют yii\db\Query */
     $query = new Query();
        if ($condition->where != null) {
            $query->where($condition->where->fields);
        }
        if ($condition->limit != null) {
            $query->limit = $condition->limit;
        }
        ..........
}
.......


 
Как избежать этого дублирования?

Создать отдельный сервис, который будет строить yii\db\Query из Condition?
Последний раз редактировалось Melodic 2016.05.12, 15:59, всего редактировалось 1 раз.

zelenin
Сообщения: 9939
Зарегистрирован: 2013.04.20, 11:30

Re: Сервисный слой, как правильно?

Сообщение zelenin » 2016.05.12, 15:59

а) ничего страшного
б) таблица будет разная
в) трейт (но см. а)
г) всегда в своем коде используйте строгую проверку на типы - === и !==. Нестрогая возможна только в соприкосновении с чужим или легаси-кодом, когда вы понимаете, что там могут быть сюрпризы.

Аватара пользователя
ElisDN
Сообщения: 4459
Зарегистрирован: 2012.10.07, 10:24
Контактная информация:

Re: Сервисный слой, как правильно?

Сообщение ElisDN » 2016.05.12, 16:00

Melodic писал(а):Правильным ли будет сделать через возможности Yii (не хотелось бы отказываться от ActiveForm)
Показывать красивые ошибки - дело UI и форм. Дело доменной модели - кидать исключения.

К валидации есть, по крайней мере, три подхода:

В первом мы предполагаем, что данные приходят в сервис и модель "нефильтрованными". Тогда в сервисе и сущностях везде пичкаем валидацию всего и вся. Сервисы и сущности при этом разбухают, выразительнось теряется за тоннами проверок. Получается "Любые данные -> Сервис с проверками -> Модель с проверками". Можно вынести валидации в отдельное место... но это уже третий вариант.

При втором предполагаем, что все данные, пришедшие в сервис или сущность, уже провалидированы формами фреймворка. Тогда в сервисах оставляем только важные логические проверки (например, на уникальность), а на остальное забиваем. Пользуемся формами и валидаторами фреймворка. Получается "Форма с валидацией -> Сервис -> Модель". Сервисы и модели чистые, но приходится реализовывать аналогичную фреймворковскую валидацию для API или консоли.

В третьем имеем ситуацию, когда у нас всё поступает в сервис через DTO (вроде Command). Тогда, как и предложили, валидировать можно целиком сам DTO. Получается "Любые данные -> DTO -> Валидатор DTO -> Сервис -> Модель". С CommandBus это решается элементарно через создание валидаторов для команд по аналогии с хендлерами. Валидация внедрена на уровне шины. Формы, консоль и API при этом можно даже вообще не валидировать, предавая хоть напрямую поля из $_POST.

В проектах без API склоняюсь к среднезатратному варанту: Для слоя UI пользуюсь контроллерами, представлениями и формами фреймворка (с их валидацией и прочими плюшками). А в модели делаю только логические проверки.

Оставляю Yii-шную форму, но эта форма используется только для принятия данных и базового "красивого" предварительного вывода ошибок валидации. Можно, при желании, внедрить проверки на уникальность. Но никакого метода signup в ней нет:

Код: Выделить всё

class SignupForm extends Model
{
    public $username;
    public $email;
    public $password;
    public $verifyCode;

    private $userRepository;

    public function __construct(UserRepositoryInterface $userRepository, $config = [])
    {
        $this->userRepository = $userRepository;
        parent::__construct($config);
    }

    public function rules()
    {
        return [
            [['username', 'email', 'password'], 'filter', 'filter' => 'trim'],
            [['username', 'email', 'password'], 'required'],            
            ['username', 'match', 'pattern' => '#^[\w_-]+$#i'],
            ['username', 'string', 'min' => 2, 'max' => 255],
            ['username', 'validateUsername'],
            ['email', 'email'],
            ['email', 'validateEmail'],
            ['password', 'string', 'min' => 6],
            ['verifyCode', 'captcha', 'captchaAction' => '/site/captcha'],
        ];
    }

    public function validateUsername($attribute)
    {
        if ($this->userRepository->existsByUsername($this->$attribute)) {
            $this->addError($attribute,  'This username has already been taken.');
        }
    }

    public function validateEmail($attribute)
    {
        if ($this->userRepository->existsByEmail($this->$attribute)) {
            $this->addError($attribute,  'This email has already been taken.');
        }
    }
} 
Дальше валидные данные из этой формы уже кидаются в сервис:

Код: Выделить всё

public function actionSignup()
{
    $form = Yii::createObject(SignupForm::className());

    if ($form->load(Yii::$app->request->post()) && $form->validate()) {
        $this->userService->requestSignup(
            $form->username,
            $form->email,
            $form->password
        );
        Yii::$app->session->setFlash('success', 'Please confirm your Email.');
        return $this->goHome();
    }
    
    return $this->render('signup', [
        'model' => $form,
    ]);
}

public function actionConfirmSignup($token)
{
    $this->userService->confirmSignup($token);
    Yii::$app->getSession()->setFlash('success', 'Email is confirmed successfully.');
    return $this->goHome();
} 
А в сервисе оставляем только критичные проверки:

Код: Выделить всё

class UserService
{
    ...

    public function requestSignup($username, $email, $password)
    {
        $this->guardUsernameIsUnique($username);
        $this->guardEmailIsUnique($email);
        $user = User::requestSignup(
            $username,
            $email,
            $password,
            $this->passwordHasher,
            $this->authTokenizer,
        );        
        $this->userRepository->add($user);
    }

    public function confirmSignup($token)
    {
        $user = $this->userRepository->findByEmailConfirmToken($token);
        $user->confirmSignup();
        $this->userRepository->save($user);
    }

    ...

    private function guardUsernameIsUnique($username, $exceptId = null)
    {
        if ($this->userRepository->existsByUsername($username, $exceptId)) {
            throw new \DomainException('Username already exists');
        }
    }

    private function guardEmailIsUnique($email, $exceptId = null)
    {
        if ($this->userRepository->existsByEmail($email, $exceptId)) {
            throw new \DomainException('Email already exists');
        }
    }
} 
В этом примере не заморачиваюсь с DTO, а передаю просто аргументами.

В итоге спокойно используем ActiveForm, валидации фреймворка и прочие вещи.
Последний раз редактировалось ElisDN 2016.08.06, 01:58, всего редактировалось 13 раз.
Не забудьте пройти мастер-класс по Yii2.

zelenin
Сообщения: 9939
Зарегистрирован: 2013.04.20, 11:30

Re: Сервисный слой, как правильно?

Сообщение zelenin » 2016.05.12, 16:02

Melodic писал(а):Создать отдельный сервис, который будет строить yii\db\Query из Condition?
а, в этом смысле...
ну да, либо вспомогательный класс либо трейт ConditionParserForYiiQuery с методом parseCondition($query, Condition $condition);

zelenin
Сообщения: 9939
Зарегистрирован: 2013.04.20, 11:30

Re: Сервисный слой, как правильно?

Сообщение zelenin » 2016.05.12, 16:13

ElisDN писал(а):И спокойно используем ActiveForm.
ну я собственно и написал, что в кач-ве клиентской валидации (ui-валидации) можем заюзать встроенную форму. Собственно в любых решениях так и делается (на основе форм-билдеров симфони или зенда). Но не в качестве единственной или главной валидации. Данные мы можем получить и не из формы, а из запроса клиента нашего rest api. Поэтому у нас в обоих случаях флоу должен быть одинаков: получили данные, сформировали dto, кинули в сервис, отловили исключения валидации, вернули один и тот же response. Мы не должны зашиваться непосредственно на ui - ui только подсветить юзеру поля.

Melodic
Сообщения: 79
Зарегистрирован: 2016.05.11, 17:43
Откуда: Луганск

Re: Сервисный слой, как правильно?

Сообщение Melodic » 2016.05.12, 20:32

А что делать при такой слоёной архитектуре со входом, т.е. с Yii::$app->user->login()? На какой класс вешать IdentityInterface?
Создать отдельный сервис UserAuthService?

zelenin
Сообщения: 9939
Зарегистрирован: 2013.04.20, 11:30

Re: Сервисный слой, как правильно?

Сообщение zelenin » 2016.05.12, 20:36

Melodic писал(а):А что делать при такой слоёной архитектуре со входом, т.е. с Yii::$app->user->login()? На какой класс вешать IdentityInterface?
Создать отдельный сервис UserAuthService?
навскидку проблема не понятна, но вы всегда можете сделать адаптер, реализующий IdentityInterface, заполняющийся необходимыми данными вашей настоящей модели.

Melodic
Сообщения: 79
Зарегистрирован: 2016.05.11, 17:43
Откуда: Луганск

Re: Сервисный слой, как правильно?

Сообщение Melodic » 2016.05.13, 11:14

Насчёт Condition для репозитория. Как правильно будет сделать если нужно будет найти пользователей по определённому городу?
В Where передавать саму модель City или передавать её id?

Если модели пользователь будет много связанных моделей, у которых в свою очередь тоже будут какие то связи, правильным ли будет грузить из БД сразу всё? Или в Condition добавить параметр With, аналог ActiveQuery::with() ?

Melodic
Сообщения: 79
Зарегистрирован: 2016.05.11, 17:43
Откуда: Луганск

Re: Сервисный слой, как правильно?

Сообщение Melodic » 2016.05.13, 11:22

ElisDN, у вас в коде UserService есть код

Код: Выделить всё

  $user = User::requestSignup(
            $username,
            $email,
            $password,
            $this->passwordHasher,
            $this->authTokenizer,
        ); 
 
Нельзя код этого метода было вынести в сам UserService::registrationRequest()? Почему сделали именно так?

zelenin
Сообщения: 9939
Зарегистрирован: 2013.04.20, 11:30

Re: Сервисный слой, как правильно?

Сообщение zelenin » 2016.05.13, 13:32

Melodic писал(а):Насчёт Condition для репозитория. Как правильно будет сделать если нужно будет найти пользователей по определённому городу?
В Where передавать саму модель City или передавать её id?
id. тут все таки уже речь о хранилище, и репозиторий знает что выборка идет по id
Melodic писал(а):Если модели пользователь будет много связанных моделей, у которых в свою очередь тоже будут какие то связи, правильным ли будет грузить из БД сразу всё? Или в Condition добавить параметр With, аналог ActiveQuery::with() ?
вопрос неоднозначный. Советовать что-то не буду - пробуйте сами. Надо как-то lazy делать.

Аватара пользователя
ElisDN
Сообщения: 4459
Зарегистрирован: 2012.10.07, 10:24
Контактная информация:

Re: Сервисный слой, как правильно?

Сообщение ElisDN » 2016.05.13, 21:12

Melodic писал(а):Нельзя код этого метода было вынести в сам UserService::registrationRequest()? Почему сделали именно так?
Потому что это ООП.
Не забудьте пройти мастер-класс по Yii2.

Закрыто