Symfony DDD сущности, репозитории и доктрина

MiksIr

miksir@home:~$
А подскажите, как лучше все это совместить.
Допустим есть сущность Domain\User\User.
Есть репозиторий (вернее его интерфейс) Domain\User\UserRepositoryInterface

И есть доктрина. Через симфони бандл реализую инфраструктуру. Как-то так
DomainBundle\Entity\User и DomainBundle\Repository\UserRepository

Последний extends EntityRepository implements Domain\User\UserRepositoryInterface
и в нем, допустим.. с, условно, таким кодом
PHP:
    public function findByEmail(string $email) : User
    {
        $user = $this->findOneBy(['email' => $email]);
        return new User(
            new UserId($user->getId()),
            $user->getFirstname(),
            $user->getLastname(),
            $user->getEmail(),
            $user->getPhone(),
            $user->getPassword()
        );
    }
Что-то мне странно как-то это переливание из User в User. Как лучше?
 

fixxxer

К.О.
Партнер клуба
Эээ, в чем проблема с

PHP:
    public function findByEmail(string $email) : User
    {
        return $this->findOneBy(['email' => $email]);
    }
?
 

Вурдалак

Продвинутый новичок
Видимо, он про UserId.

Насколько я знаю, там какая-то ограниченная поддержка VO для PK: https://groups.google.com/forum/#!topic/doctrine-user/0LJ4mS3x364

Вообще я как-то думал на тему того, что напрямую я бы вряд ли юзал Doctrine для маппинга domain model. Функционал Doctrine и требуемый не очень хорошо пересекаются. Из того, что сразу приходит на ум:
  • нет полноценной официальной поддержки того же VO для PK;
  • нет поддержки массивов VO;
  • есть навязываемый механизм проксирования для lazy loading целей, который требует отсутствие final (мне lazy loading нафиг не нужен);
  • встроенный механизм optimistic lock'а похоже подвержен RC.

Мне в последнее время нравится, как будет выглядеть модель при ES-подходе. И дело даже не в ES, который я пока не готов использовать, а в самой модели: модель сообщает об изменениях всегда в виде событий. Изменения в виде событий можно будет передавать в Doctrine-модель, которая будет чисто в роли persistence model. Как бонус, модель очень удобно будет покрываться unit-тестами: https://github.com/qandidate-labs/broadway/blob/master/examples/event-sourced-domain-with-tests/InvitesTest.php
 

fixxxer

К.О.
Партнер клуба
А так ли уж нужно, чтобы внутри модели было private $userId:UserId, а не private $userId:int?

Я, конечно, согласен, что я не должен вообще принимать такие решения под влиянием ограничений датамаппера, но тут вроде совсем небольшой компромисс.

А вообще - на уровне маппера должно быть можно все это определить, по идее. Доктрина довольно гибкая.
 

MiksIr

miksir@home:~$
Кроме VO, как я понимаю, в сущности доктрины должны быть сеттеры. Да и аннотации я использую. Т.е. как бы получается сущность с инфораструктурной завязкой.
А если это разные объекты, то и репозитория, видимо, нужно два делать, ибо банальный find не совпадет по сигнатуре.

Наверное можно делать описание схемы через yml, и в доктрине написать какую-то прослойку, которая будет создавать сущность домена через конструктор... и вроде как доктрина может работать с VO даже, если в нем __toString определить. Ну вот в общем и вопрос о каких-то best practices от форума. В гугле есть кое-что, но в общем тоже у всех свое.
 

Вурдалак

Продвинутый новичок
Кроме VO, как я понимаю, в сущности доктрины должны быть сеттеры.
Неа, там через рефлексию всё работает.

А что касается реализации репозитория, то я бы делал отдельный класс, в который бы инжектил EntityManager/Repository, т.к. интерфейс у Doctrine более общий. Если ты укажешь хотя бы find(int $id), то это уже будет несовпадением интерфейсов.
 

fixxxer

К.О.
Партнер клуба
@Вурдалак, какой-нибудь private getUserId() мне не видится такой уж огромной проблемой :) С коллекциями VO вот да, тут все сложно.
 

MiksIr

miksir@home:~$
Неа, там через рефлексию всё работает.
Через рефлексию ставятся значения приватных свойств? Если так, то, мне кажется, это еще хуже - теряется вообще хоть какая-то возможность контроля за приходом данных в сущность.
 

Вурдалак

Продвинутый новичок
Через рефлексию ставятся значения приватных свойств? Если так, то, мне кажется, это еще хуже - теряется вообще хоть какая-то возможность контроля за приходом данных в сущность.
Ты можешь как-то обосновать зачем тебе нужен «контроль за приходом данных» с точки зрения модели, которая ничего про persistence не знает?

Если у тебя будет какой-то InMemory-репозиторий, то там тоже не будет никакого «прихода данных».
 

MiksIr

miksir@home:~$
Т.е. ты хочешь сказать, что сущности домена совершенно наплевать - кто и каким образом (через конструктор или рефлексией) запихнул в нее данные?
 

Вурдалак

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

MiksIr

miksir@home:~$
Да, понял, сохраняем врнутреннее состояние объекта. Но в случае с доктриной получается, что сущность и структура базы должны всегда совпадать. Рефакторим сущность - рефакторим схему. Сущность домена === сущности доктрины?
 

Вурдалак

Продвинутый новичок
Что ты имеешь в виду? Если ты добавляешь свойство в модель или удаляешь, то скорее всего ты действительно захочешь соответствующих изменений в схеме.
 

MiksIr

miksir@home:~$
Может и на захочу. Но это, как я понял, решается созданием кастомного мапера.
Ну в общем стало чуточку понятнее, спасибо.
 

MiksIr

miksir@home:~$
Через конструктор данные приходят в момент, когда начинается жизненный цикл сущности: рега юзера, например. А получение сущности из базы — это не начало жизненного цикла, это детали реализации, про которые сама сущность знать не должна.
Если все же говорить о разделении сущности доктрины и домена, получив сущность доктрины нам нужно где-то (ну, видимо в репозитории) создать доменный объект. Конструктор тут категорически не? Писать с использованием рефлексии что-то для восстановления?
 

Вурдалак

Продвинутый новичок
Я делаю через рефлексию. Но можно в качестве компромисса
PHP:
final class User
{
    /**
     * @internal
     */
    public static function reconstituteFromDb(...) : User
    {
        // ...
    }
}
Конструктор сам по себе я привык делать приватным, мотивацию описывал тут: http://phpclub.ru/talk/threads/infrastructure-vs-domain-или-как-реализовать-правила-предметной-области.81226/#post-735763
 

MiksIr

miksir@home:~$
Я делаю через рефлексию.
А копируешь один в один, т.е. у тебя всегда свойства совпадают по наличию и названию? Как-то странно это.
Можешь этот кусок кода показать? А то у меня уже в голову всякие монстры конфигов мапинга (создания тех же VO) лезут, нужно остудить.
 

Вурдалак

Продвинутый новичок
А копируешь один в один, т.е. у тебя всегда свойства совпадают по наличию и названию?
Вручную :) Просто тупо два метода: 1) получение массива для сущности из persistence model 2) заполнение persistence model из массива сущности. «Массив сущности» получается с помощью рефлексии, тупо многомерный массив, соответствующий внутренней структуре модели.
 

fixxxer

К.О.
Партнер клуба
Если чем-то не нравится рефлексия, посмотри как сделан analogue orm. Вполне компромисс.
В нем, правда, несколько нехорошо захардкожены фабрики и все, что относится к hydration, так что, скажем, расширить возможности стандартных emdeddables без форка затруднительно. Но это несложно патчится :) Зато он достаточно небольшой, чтобы можно было разобраться в коде.
 
Сверху