Как реализуете репозиторий (Doctrine, Analogue, ручками, etc)

Вурдалак

Продвинутый новичок
Особой разницы тогда не будет дернуть через dispatch, других подписчиков быть не должно
Как это — не должно?
ну, или завести отдельный тип событий именно для персистентности
В этом и смысл ES, что они являются domain events, это бизнес-события и не должно быть деления на какие-то технические и нетехнические события.
Только потребуется исполнять события в том порядке, в котором они появились, иначе неконсистентность можно словить.
А commit транзакции ты где будешь делать?
 

Вурдалак

Продвинутый новичок

Юрий Быков

Новичок
Отдельных заводить событий не нужно, верно.
Dispatch сделать для репозитория $this->user_repo->dispatch($events), а потом для EventDispatcher вызвать его dispatch для подписчиков.
Можно CommandBus завернуть в декоратор TransactionalCommanBus и коммит делать по завершении вызова innerBus->method(), все изменения должны приняться.
 

Вурдалак

Продвинутый новичок
Только не нужно забывать, что в транзакцию не должны попасть подписчики.
 

Юрий Быков

Новичок
В CommandHandler/ServiceMethod нужно в одной транзакции коммитить все обращения в репозитории. EventDispatcher вынести EVCommandBus, но придётся сохранить массив ивентов в контексте хендлера.
 

Вурдалак

Продвинутый новичок
Да, можно их запушить в какую-то очередь, потом в EVCommandBus её разгрести.
 

Юрий Быков

Новичок
@Вурдалак, столкнулся с таким моментом. Есть некая сущность, на неё ссылаются другие сущности, она на них не ссылается. При удалении этой сущности нужно проверить, если на неё ссылки и если есть, то не удалять. Сам алгоритм проверки на возможность удаления отлично ложится на доменный сервис. Возникает вопрос, а само удаление делать из прикладного сервиса или перенести эту логику в доменный сервис по удалению.

Прикладной, если проверка на возможность удаления в нём:
can_delete_vendor_service - invoke domain service
Код:
$vendor = $this->vendor_repo->get($command->id);
if ($this->can_delete_vendor_service($vendor)) {
     $this->vendor_repo->remove($vendor);
} else {
     throw new UsedResourceDeleteDisallowException($command->id);
}
или полностью удаление передать в доменный слой и прикладной сервис-метод сократится до
Код:
$vendor = $this->vendor_repo->get($command->id);
$this->delete_vendor_service($vendor);
Как поступаешь в таком случае? Стоит или не стоит увеличивать сложность?
 

Вурдалак

Продвинутый новичок
Да без разницы как. Я бы, вероятно, запилил бы соответствующий сервис в application, который бы тупо проверял через query service и удалял бы через отправку команды.

Если тебе это нужно железно, то, к примеру, foreign key и выбрасывать исключение в случае попытки удалить из базы.

Это не та бизнес-логика, о которой бы я сильно парился.
 

Вурдалак

Продвинутый новичок
Я не рассматриваю подобные constraint'ы как что-то серьёзное, потому что это не суть продукта. Не эта логика делает продукт уникальным и не благодаря ей продукт приносит прибыль.

«Более важной» бизнес-логикой я считаю, соответственно, модели и связанные с ними основные бизнес-процессы.
 

Юрий Быков

Новичок
Мне тоже было бы больше по душе сразу возвращать, но что-то мне помешало это элегантно реализовать.
Покажи потом, если получится, интересно.
В продолжение темы:
Скорее всего помешало то, что из конструктора ничего вернуть нельзя не приколотив объект, а раз так, то внутри сущности (агрегатора) накапливаем события и по запросу их релизим.

UPD (для истории):
--------------------------------------------------
В некоторых случаях события можно вытащить из конструктора, если использовать статичный фабричный метод.
--------------------------------------------------

Небольшой оффтоп пример похожей ситации:
Столкнулся с подобной проблемой только по работе с ошибками. Надо вернуть их массив и оставить объект живым, по-другому это не сделать. Либо отходить от насыщенной модели в сторону анемичной, обрабатывать валидатором и получать список ошибок из него.
 
Последнее редактирование:

Вурдалак

Продвинутый новичок
Столкнулся с подобной проблемой только по работе с ошибками. Надо вернуть их массив и оставить объект живым, по-другому это не сделать. Либо отходить от насыщенной модели в сторону анемичной, обрабатывать валидатором и получать список ошибок из него.
Тут можно только с примерами обсуждать. Скорее всего тебе не нужно возвращать ошибки.
 

Юрий Быков

Новичок
Пример:
Было задача реализовать CRUD для некоторой сущности. Сущность имеет несколько зашитых валидаторов, если возникает ошибка, пользователю показывается сообщение, которое формируется через исключение. Получалось итеративная модель поведения пока не будут устранены все ошибки. Т.к. выбрасывалось одно исключение, то за раз выдавалось сообщение об одной ошибке (реализовано такое поведение, пока нареканий не было).

Поступила новая задача. Внести массовые изменения через xls-файл, где каждая строка забита свойствами одного типа сущности. Если возникают ошибки, нужно сформировать ответный файл, где в ошибочных ячейках добавить комментарий с описанием ошибки. Просто так исключения уже не покидаешь, их надо ловить в каждом методе где есть валидатор, либо накапливать массив ошибок. В таком случае исключение нужно обрабатывать в сервисе (чтобы не дублировать код в сервисе импорта; сервис импорта вызывает сервис обновления одной сущности), который отвечает за обновление, получается ужасно неэлегантный код из try/catch-ей, либо if`ов и потом релизить их в сервис импорта данных для дальнейшей обработки ответа.

Собственно вопрос, как работать с ошибками элегантно и для одной сущности, и для массовых операций?
 

Вурдалак

Продвинутый новичок
Это техническое описание проблемы, я подразумевал конкретный пример сущности и её бизнес-логики.

Для пользователей валидация обычно выполняется снаружи. Без исключений, просто возвращается массив ошибок на $validator->validate($command), например.
В сущности же стоит жёсткая валидация (исключения), которая сигнализирует о неправильном использовании модели (это подразумевает, что виноват программист, а не пользователь).
Да, эти правила валидации могут повторяться.
Это не нарушает DRY.

Пользовательская валидация в сущности — это смешение контекстов. Чистая бизнес-логика смешивается с логикой валидации отдельно взятого источника данных. Эти правила могут совпадать до поры до времени, но в общем случае это ошибка.
 

Юрий Быков

Новичок
Пример. Есть сущность устройство, у него имеются порты. Порт имеет имя. Имя должно быть уникально в пределах устройства и соответствовать некому regexp, который пользователь задаёт отдельно в настройках. При задании/смене имени порта делается проверка в сущности порта:
Код:
class Port
{
     public function __construct(Unit $unit, string $name, PortDirection $direction, BusType     $bus_type, int $number = null, int $root_id = null)
    {
        $this->unit = $unit;
        $this->direction = $direction;
        $this->bus_type = $bus_type;
        $this->changeName($name);
        $this->root_id = $root_id;

        !empty($number) ? $this->changeNumber($number) : $this->setDefaultPortNumber();

        $this->pins = new ArrayCollection();
        $this->port_filling_references = new ArrayCollection();

        $unit->addPort($this);
    }

    public function changeName(string $name)
    {
        if ($name === $this->name) {
            return;
        }

        if (!$this->unit->isUniquePortName($name)) {
            throw new PortNameNotUniqueException($this, $name);
        }

        $validator = new PortMainLangNameValidator($this->unit->settings_schema);
        if ($validator->isValid($name)) {
            $this->name = $name;
        } else {
            throw new SettingsSchemaConstraintViolationException('name', $validator->getPattern(), $name);
        }
    }
}
На данный момент в сущности порта происходят обе проверки. Есть понимание, что декомпозиция валидации будет приводить к анемичной модели, но также есть понимание, что валидация бывает разной. Сущность может быть валидной относительно своих свойств, но невалидная относительно коллекции и т.п. Т.е. валидация м.б. внутри и снаружи сущности, причём та часть, что снаружи может в принципе не подходить внутреннему местоположению.

Если проверить комманду в сервисной шине набором валидаторов, то можно пользователю вернуть красивые сообщения, но тогда зачем делать валидацию в сущности и часть валидации в доменных сервисах (кросссущностная валидация)? Валидация уже в 3 местах.
 

Вурдалак

Продвинутый новичок
Этот пример сфокусирован не на действиях, а на состоянии (собственно, в 99% случаев около-DDD-шные темы почему-то крутятся вокруг такой фигни). Имя порта, возможно, в бизнес-логике вообще никакой роли не играет, но по какой-то причине этому уделяется достаточно большое внимание.

Имя должно ... соответствовать некому regexp, который пользователь задаёт отдельно в настройках.
На это можно посмотреть с такой стороны: возможно, это правило не относится к «чистой» сущности порта. Это лишь некая надстройка, которая нужна для решения каких-то визуальных/эстетических проблем. То есть, это может быть bounded context'ом «над» контекстом с устройствами/портами. В таком случае, валидация имени вообще может быть отдельным классом в отдельной папке, который будет проверяться до вызова команды, дублирования не будет вообще.

Сыграет ли какую-то роль цвет и материал компаса в вопросе выигрыша в соревновании, где требуется ориентация на местности? Я сомневаюсь. В контексте соревнования, это бессмысленная и нерелевантная информация. В данном случае от модели требуется чёткое выполнение вполне конкретной задачи и отсутствие информации о цвете никак не делает компас «анемичным». «Анемичный» — это когда явные бизнес-глаголы не отражены в модели, а происходит фокусировка на свойствах.

В контексте продаж компасов же, вероятно, это вполне может играть какую-то роль. Здесь же мы можем определить наиболее популярные цвета и материалы, и после очередной продажи, вероятно, выбросить событие, которое будет сигнализировать о недостаточном количестве популярных моделей, по которому мы сделаем запрос на пополнение со склада.

Какую конечную роль играют эти устройства/порты, в чём состоит набор действий?

Отдельного внимания заслуживает вопрос почему «устройство» у себя внутри не может проверить уникальность имени порта? Сам по себе порт за свою уникальность отвечать не может и на этом фоне некий unit->isUniquePortName() выглядит странно. Также вызывает вопрос почему добавление порта в устройство внутри самого порта. Как компас может отвечать за ассортимент в магазине в целом?
 

Юрий Быков

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

При создании порта в конструктор порта передаём объект устройства, тем самым мы уже привязываем порт к устройству, почему не сделать следующий шаг и не вызвать метод $unit->addPort($this) там же, мне кажется это логичным (хотя перед принятием данного способа пробовал в коде оба варианта).

Метод проверки уникальности находится в устройстве, но вызов, действительно, из порта. Изначально проверка была в устройстве, но потом перенесена в порт. Также, чтобы переименовать порт был метод $unit->changePortName($port, $new_name), но теперь это внутри порта, чтобы имя порта нельзя было сделать невалидным никак. Например, сделать $port->changeName(); $port_repo->flush(); изменения ушли в БД уже не получится, валидатор будет ругаться.
Да, это кусок ответственность программиста, но они все разные и могут забыть сделать предварительную проверку команды, а при batch, видимо придётся отдельно писать проверки, либо вызывать оригинальный сервис изменения n-раз, чтобы не дублировать код.

Устройство не м.б. агрегатом, т.к. ссылки в других частях системы идут на сам порт.
 
Последнее редактирование:

WMix

герр M:)ller
Партнер клуба
При создании порта в конструктор порта передаём объект устройства,
а не наборот ли? в обьект "устройство" добавить порт
PHP:
function addPort( Port $port ){
  $this->ports[] = $port;
  // ...
}
можно дополнить метод именем, мне кажется логичнее назначать имена при добавлении, а самому порту знание его имени в устройстве необязательно
PHP:
function addPort( string $name, Port $port ){
  if(!in_array($name, array_keys($this->ports) ){
    $this->ports[$name] = $port;
    // ...
  }
  else throw new Exception(...)
}
 

Юрий Быков

Новичок
Первый метод есть в устройстве. Он вызывается из конструктора порта при создании порта и сделано это для простоты использования. Свойство "устройство" порта обязательно при создании.

Второй метод, конечно, красивый, но нам нужно имя в порт всё равно прописать, т.е. будет какой-то метод changeName(), иначе Doctrine2 некорректно сохранит сущность в БД.
 
Сверху