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

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

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

Сообщение zelenin »

ElisDN писал(а):
Melodic писал(а):Нельзя код этого метода было вынести в сам UserService::registrationRequest()? Почему сделали именно так?
Потому что это ООП.
Он имеет в виду почему сервисные вещи типа хэширования делаются внутри модели, а не внутри сервиса.
Потому что это DDD, в котором богатые поведениями сущности с методами, работающими с доменными сервисами. UserService это сервис приложения.
Аватара пользователя
ElisDN
Сообщения: 5845
Зарегистрирован: 2012.10.07, 10:24
Контактная информация:

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

Сообщение ElisDN »

zelenin писал(а):Он имеет в виду почему сервисные вещи типа хэширования делаются внутри модели, а не внутри сервиса.
Хеширование и генерация токенов - это низкоуровневые вещи. Если это делать всё в UserService, то будет примерно так:

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

$user->changePassword($this->passwordHasher->hash($password));
...
$this->passwordHasher->validate($password, $user->getPasswordHash()); 
Получается много нюансов в UserService. И приходится нарушать инкапсуляцию, делая публичный геттер getPasswordHash() у $user. Да и другой программист может в changePassword голый пароль случайно закинуть вместо хеша.

В идеале, удобнее всё инкапсулировать наглухо:

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

$user->changePassword($password);
...
$user->validatePassword($password); 
Так и хеши остаются приватными, и ошибиться нельзя.

Но сущности мы через DI не создаём и статический Yii::$app->passwordHasher не используем. Вместо этого просто закидываем хешер в наш метод ещё одним параметром:

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

$user->changePassword($password, $this->passwordHasher);
...
$user->validatePassword($password, $this->passwordHasher); 
Получаем идеальную инкапсуляцию, невозможность ошибиться и чистый UserService.
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

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

Сообщение zelenin »

ElisDN писал(а):
zelenin писал(а):Он имеет в виду почему сервисные вещи типа хэширования делаются внутри модели, а не внутри сервиса.
Хеширование и генерация токенов - это низкоуровневые вещи. Если это делать всё в UserService, то будет примерно так:

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

$user->changePassword($this->passwordHasher->hash($password));
...
$this->passwordHasher->validate($password, $user->getPasswordHash());  
И это ок. Либо хэширование неотъемлимая часть доменного слоя и должна производиться в нем, либо хэширование - это сервисная фича и должна производиться в сервисе.
ElisDN писал(а):Получается много нюансов в UserService.
потому что валидации/генерации должны быть частью AuthService c генераторами/хэшерами внутри.
Melodic
Сообщения: 87
Зарегистрирован: 2016.05.11, 17:43
Откуда: Луганск

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

Сообщение Melodic »

ElisDN,zelenin вы встречали на github'e yii проекты со слоёной архитектурой, что бы можно было посмотреть и быстрее понять?
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

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

Сообщение zelenin »

Melodic писал(а):ElisDN,zelenin вы встречали на github'e yii проекты со слоёной архитектурой, что бы можно было посмотреть и быстрее понять?
нет, но слоеная архитектура создана именно для того, чтобы не зависеть от фреймворка, поэтому можно любой проект смотреть, созданный по ddd. https://github.com/codeliner/php-ddd-cargo-sample
Melodic
Сообщения: 87
Зарегистрирован: 2016.05.11, 17:43
Откуда: Луганск

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

Сообщение Melodic »

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

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

public function getCity(){
    if($this->city===null){
        $this->dispatcher->event(User::CITY_NOT_LOADED,$this); // обработчик обращается к CityRepository и загружает модель
    }
    return $this->city;
}
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

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

Сообщение zelenin »

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

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

public function getCity(){
    if($this->city===null){
        $this->dispatcher->event(User::CITY_NOT_LOADED,$this); // обработчик обращается к CityRepository и загружает модель
    }
    return $this->city;
}
в доктрине для этого фоново создаются прокси-сущности, в которых есть проверка на null и дозагрузку связей - для разработчика это выглядит как будто прокси нет. Но это сложный метод, требующий инжиниринга.
С событиями это не то... это должно решаться вне сущности, т.к. сущность знать о лейзи лоаде знать не должна.
Есть еще варианты?
Melodic
Сообщения: 87
Зарегистрирован: 2016.05.11, 17:43
Откуда: Луганск

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

Сообщение Melodic »

Сделать класс который будет отвечать за хранение связанной модели. Т.е.

class Value{
private $value;
public function getValue(){
if($this->value===null){
//здесь бросаейэм евент и загружаем value
}
return $this->value;
}

public function User::getCity(){
return $this->city->getValue();
}
Тему надо бы переименовать в что-то типа "архитектура без active record ".
Пишу с телефона, могут быть ошибки)
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

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

Сообщение zelenin »

Melodic писал(а):Сделать класс который будет отвечать за хранение связанной модели. Т.е.

class Value{
private $value;
public function getValue(){
if($this->value===null){
//здесь бросаейэм евент и загружаем value
}
return $this->value;
}

public function User::getCity(){
return $this->city->getValue();
}
Тему надо бы переименовать в что-то типа "архитектура без active record ".
Пишу с телефона, могут быть ошибки)
в презентации ocramius (по-моему член команды доктрины) по прокси такой способ проксирования назван самым плохим)

надо все-таки комплексно подойти к вопросу, т.к. костылить в сущности, которая ничего не знает о лейзи лоад и хранилище, мы не имеем права. Значит надо проксировать где-то в репозитории.
https://github.com/Ocramius/ProxyManager
Аватара пользователя
ElisDN
Сообщения: 5845
Зарегистрирован: 2012.10.07, 10:24
Контактная информация:

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

Сообщение ElisDN »

Melodic писал(а):вы встречали на github'e yii проекты со слоёной архитектурой?
Я и с обычным-то ООП или с тестами на Yii2 ничего не встречал, не то что с какой-либо архитектурой. Ни открытых на гитхабе, ни чужих коммерческих на фрилансе.
Аватара пользователя
ElisDN
Сообщения: 5845
Зарегистрирован: 2012.10.07, 10:24
Контактная информация:

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

Сообщение ElisDN »

zelenin писал(а):Но не в качестве единственной или главной валидации. Данные мы можем получить и не из формы, а из запроса клиента нашего rest api.
Да, кстати. Если перевести проект на тройки Command (DTO) + Validator + Handler с CommandBus, декорированной в ValidationCommandBus, то проблема больших сервисов, валидации и guard* отпадает.
Melodic
Сообщения: 87
Зарегистрирован: 2016.05.11, 17:43
Откуда: Луганск

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

Сообщение Melodic »

zelenin писал(а):
Melodic писал(а):Сделать класс который будет отвечать за хранение связанной модели. Т.е.

class Value{
private $value;
public function getValue(){
if($this->value===null){
//здесь бросаейэм евент и загружаем value
}
return $this->value;
}

public function User::getCity(){
return $this->city->getValue();
}
Тему надо бы переименовать в что-то типа "архитектура без active record ".
Пишу с телефона, могут быть ошибки)
в презентации ocramius (по-моему член команды доктрины) по прокси такой способ проксирования назван самым плохим)

надо все-таки комплексно подойти к вопросу, т.к. костылить в сущности, которая ничего не знает о лейзи лоад и хранилище, мы не имеем права. Значит надо проксировать где-то в репозитории.
https://github.com/Ocramius/ProxyManager
Т.е. по сути, репозиторий будет возвращать не сам объект модели, а объект прокси для этой модели?
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

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

Сообщение zelenin »

Melodic писал(а):
zelenin писал(а):
Melodic писал(а):Сделать класс который будет отвечать за хранение связанной модели. Т.е.

class Value{
private $value;
public function getValue(){
if($this->value===null){
//здесь бросаейэм евент и загружаем value
}
return $this->value;
}

public function User::getCity(){
return $this->city->getValue();
}
Тему надо бы переименовать в что-то типа "архитектура без active record ".
Пишу с телефона, могут быть ошибки)
в презентации ocramius (по-моему член команды доктрины) по прокси такой способ проксирования назван самым плохим)

надо все-таки комплексно подойти к вопросу, т.к. костылить в сущности, которая ничего не знает о лейзи лоад и хранилище, мы не имеем права. Значит надо проксировать где-то в репозитории.
https://github.com/Ocramius/ProxyManager
Т.е. по сути, репозиторий будет возвращать не сам объект модели, а объект прокси для этой модели?
да
Melodic
Сообщения: 87
Зарегистрирован: 2016.05.11, 17:43
Откуда: Луганск

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

Сообщение Melodic »

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

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

<?php
namespace app\core\application\adapters;

use app\core\domain\models\User;
use yii\web\IdentityInterface;
use app\core\domain\condition\Condition;
use app\core\domain\condition\Where;
use app\core\domain\repositories\UserRepositoryInterface;

class IdentityInterfaceAdapter implements IdentityInterface
{

    /**
     * @var UserRepositoryInterface $userRepository
     */
    private $userRepository;

    /**
     * @var User $model
     */
    private $model;

    public function __construct($id, UserRepositoryInterface $userRepository)
    {
        $this->userRepository = $userRepository;
        $this->model = $this->userRepository->findById($id);

    }

    public static function findIdentity($id)
    {

        $self = \Yii::createObject('app\core\application\adapters\IdentityInterfaceAdapter',
            [$id]
        );
        if (!$self->getModel() !== null) {
            return $self;
        }
        return null;
    }

    public static function findIdentityByAccessToken($token, $type = null)
    {
        // TODO: Implement findIdentityByAccessToken() method.
    }

    public function getId()
    {
        return $this->model->getId();
    }

    public function getAuthKey()
    {
        // TODO: Implement getAuthKey() method.
    }
    public function validateAuthKey($authKey)
    {
        // TODO: Implement validateAuthKey() method.
    }

    public function getModel()
    {
        return $this->model;
    }
}
 
Сервис для авторизации:

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

<?php

namespace app\core\application\services;


use app\core\application\adapters\IdentityInterfaceAdapter;
use app\core\domain\repositories\UserRepositoryInterface;

class AuthService implements AuthServiceInterface
{

    private $userRepository;

    public function __construct(UserRepositoryInterface $userRepository)
    {
        $this->userRepository = $userRepository;
    }

    public function login($login, $password)
    {
        $user = $this->userRepository->findByEmail($login);
        if ($user !== null && \Yii::$app->security->validatePassword($password, $user->getPasswordHash())) {
            $adapter = IdentityInterfaceAdapter::findIdentity($user->getId());
            return \Yii::$app->user->login($adapter);
        }
        return false;
    }
} 

Но как то мне не нравится подобная реализация, костыльно, что ли. Возможно у вас есть какие то советы?
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

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

Сообщение zelenin »

как раз наоборот - по всем канонам. вы просто привыкли к сильной связанности, и слабая связанность кажется костылями, хотя все ровно наоборот.
Melodic
Сообщения: 87
Зарегистрирован: 2016.05.11, 17:43
Откуда: Луганск

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

Сообщение Melodic »

DTO к какому слою относится? Domain,App,Infrastructure?

Т.к. UserServiceInterface относится к Domain, то и DTO относится к Domain (т.к. метод UserServiceInterface::registration($dto) - зависит от DTO UserRegistrationDto)? Или всё же в сервисах не использовать DTO(использовать обычные параметры UserServiceInterface::registration($login,$password) и прикрутить CommandBus( такую например https://github.com/thephpleague/tactician ), которая уже будет оперировать этими DTO(командами по сути), вызывая хендлеры ,которые в свою очередь уже будут дёргать UserServiceInterface::registration($login,$password)?
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

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

Сообщение zelenin »

по lazy load. Есть 3 способа:

1. Сделать стандартный lazy в сущности.

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

public function getPosts()
{
    if($this->posts === null) {
        $this->posts = $this->postRepository->findForUser($this->id);
    }
    return $this->posts;
 
вариант плохой - хранилище протекает в доменный слой.

2. Прокси

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

class UserProxy extends User
{
    public function getPosts()
    {
        $posts = parent::getPosts();
        if($posts === null) {
            $posts = $this->postRepository->findForUser($this->id);
            $this->setPosts($posts);
        }
        return $posts;
    }
}
 
Плюсы: успешный instanceOf User
Минусы: нельзя заэкстендить final class User, а я люблю final.

2.1. а) генерировать прокси автоматически б) инджектить в $this->posts через рефлексию
Плюсы: не нужен сеттер
Минусы: видимо тот же, что и в п.2

3. Domain Dependency Resolver

В месте, где будут обрабатываться связанные модели, ресолвить их.

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

$this->userDependencyResolver->resolve($user, ['posts']);
$posts = $user->getPosts();
 

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

public function resolve($entity, array $strategies)
{
    foreach ($strategies as $strategy) {
        $this->resolveStrategy($entity, $strategy);
    }
}

private function resolveStrategy($entity, $strategy)
{
    switch $strategy {
        case 'posts' :
            $this->resolvePosts($entity);
        break;
    }
}

private function resolvePosts($entity)
{
    $posts = $entity->getPosts();
    if($posts === null) {
        $posts = $this->postRepository->findForUser($entity->id);
        $entity->setPosts($posts);
    }
}
 
плюс: самый лучший вариант
минус: нет магии, нужно вручную ресолвить, но в Application-слое, поэтому вроде и ок.
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

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

Сообщение zelenin »

Melodic писал(а):DTO к какому слою относится? Domain,App,Infrastructure?
- domain - сущности, доменные сервисы, репозитории
- infrastructure - реализации репозиториев
- application - сервисы приложений, клей между presentation и domain. От presentation получаем dto, в domain передаем entity.
dto относится к app.
Melodic писал(а):Т.к. UserServiceInterface относится к Domain, то и DTO относится к Domain (т.к. метод UserServiceInterface::registration($dto) - зависит от DTO UserRegistrationDto)? Или всё же в сервисах не использовать DTO(использовать обычные параметры UserServiceInterface::registration($login,$password) и прикрутить CommandBus( такую например https://github.com/thephpleague/tactician ), которая уже будет оперировать этими DTO(командами по сути), вызывая хендлеры ,которые в свою очередь уже будут дёргать UserServiceInterface::registration($login,$password)?
UserServiceInterface относится к Application. см. выше.
CommandBus - вариация сервисного слоя с немного другой парадигмой. Мне больше нравится.
Советую самому написать, чтобы разобраться. https://github.com/zelenin/cqrs
Melodic
Сообщения: 87
Зарегистрирован: 2016.05.11, 17:43
Откуда: Луганск

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

Сообщение Melodic »

zelenin писал(а):
Melodic писал(а):DTO к какому слою относится? Domain,App,Infrastructure?
- domain - сущности, доменные сервисы, репозитории
- infrastructure - реализации репозиториев
- application - сервисы приложений, клей между presentation и domain. От presentation получаем dto, в domain передаем entity.
dto относится к app.
Melodic писал(а):Т.к. UserServiceInterface относится к Domain, то и DTO относится к Domain (т.к. метод UserServiceInterface::registration($dto) - зависит от DTO UserRegistrationDto)? Или всё же в сервисах не использовать DTO(использовать обычные параметры UserServiceInterface::registration($login,$password) и прикрутить CommandBus( такую например https://github.com/thephpleague/tactician ), которая уже будет оперировать этими DTO(командами по сути), вызывая хендлеры ,которые в свою очередь уже будут дёргать UserServiceInterface::registration($login,$password)?
UserServiceInterface относится к Application. см. выше.
CommandBus - вариация сервисного слоя с немного другой парадигмой. Мне больше нравится.
Советую самому написать, чтобы разобраться. https://github.com/zelenin/cqrs
Если UserServiceInterface относится к Application, то что тогда может являться доменным сервисом на примере пользователя?

По CommandBus. Т.е. хендлеры по сути являются сервисами слоя Application?
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

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

Сообщение zelenin »

Melodic писал(а):Если UserServiceInterface относится к Application, то что тогда может являться доменным сервисом на примере пользователя?
сервис, имеющий значение для домена - генератор id, хэшер паролей, расчет комиссии - то, о чем знает домен, и что является непосредственной частью домена.
UserServiceInterface - это сервис, который данные из реквеста, преобразует в данные для домена с использованием некоей логики - другой слой. Реквест не является частью домена.
Melodic писал(а):По CommandBus. Т.е. хендлеры по сути являются сервисами слоя Application?
верно. dto превращаются в Command, а сервисы в хэндлеры.
Закрыто