Pull to refresh

Comments 94

Сеттеры в сущностях… репозитории не как сервисы… хотя для бложиков ок.

А куда сеттеры нужно убрать? И какая выгода будет от репозиториев как от сервисов? В теме symfony совсем новичек, так что извините если вопросы глупые.
UFO just landed and posted this here
В разрезе работы с доктриной, сущности таки меняются и имеют сеттеры. Это вопрос уже не к автору статьи, а скорей к доктрине.
В разрезе работы с доктриной, сущности таки меняются и имеют сеттеры.

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

Доктрина создаёт/меняет сущности без использования конструкторов и сеттеров. Они ей не нужны, она их не вызывает.
И какая выгода будет от репозиториев как от сервисов?

Да собственно такая же, как и от любых сервисов — слабая связанность c вытекающими.
Репозитории-сервисы легче инжектировать в другие сервисы, ну и зависимости указывать у самих репо. Другой вопрос, что это не всегда необходимо.
Другой вопрос, что это не всегда необходимо.

зависит от подходов. Я никогда не наследуюсь от доктриновских репозиториев и всегда делаю свои сервисы, в которые внедряется entity manager. А что бы было удобно — autowire все решает.

А куда сеттеры нужно убрать?

Для начала давайте различать сеттеры, которые сгенерированы вашей IDE или симфоневским генератором, и сеттеры которые реально нужны.


Вот вам простой пример. У вас есть требование "пользователь должен иметь возможность сменить пароль" и у вас появляется метод:


function changePassword(string $password, callable $hasher) : void 
{
     $this->password = $hash($password); // мы никогда не забудем захэшировать пароль
}

А если в рамках требований нужно сменить сразу 4 свойства — мы либо делаем метод с 4-мя аргументами, либо, что вероятнее, уберем эти 4 свойства в отдельный объект-значение (гуглить doctrine embeddable), и будем просто заменять оный.


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


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

Спасибо за развернутый ответ. Согласен, что на маленьких проектах можно некоторыми правилами пренебречь, но когда изучаешь новый инструмент хочется учиться применять его правильно изначально, а потом уже когда появится понимание как и что нужно делать — тогда уже можно и «похамить» в коде в угоду той самой скорости разработки.

Меня радует такой подход к изучению вещей) последовательный)


Суть в том что если вы хотите изучить доктрину — вам лучше не смотреть на ее использование в документации симфони. Так же чуть выше я дал ссылку на видео где преподносятся основные идеи когда и как использовать доктрину.

А если надо сразу все свойства изменить? Например, менеджер открыл заявку, позвонил пользователю, уточнил информацию, поменял некоторые поля и нажал «Сохранить». Как правильно сделать рендеринг формы с данными, прием запроса, валидацию и сохранение?
А если надо сразу все свойства изменить?

Я сомневаюсь что вам нужно менять ID сущности или поля вроде даты создания оной. С другой стороны вместо вызова десятка сеттеров можно вызвать один метод с комком данных а там уже внутри его разбирать. Или, что еще интереснее — запихнуть эти данные в имутабельный объект и заменять целиком.


Как правильно сделать рендеринг формы с данными, прием запроса, валидацию и сохранение?

формы не должны использовать сущности напрямую (по хорошему, конечно можно идти на компромис), и мы должны иметь какие-то промежуточные объекты, DTO. Их мы можем валидировать, в них можем записывать чушь. А сущности всегда должны быть валидны и не должны иметь возможности вызвать какой-то метод и сделать ее состояние невалидным.

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

В том то и дело. Сеттеры превращаются в обычные методы. Просто поведение внутри сущности, которое принимает решение менять состояние объекта или нет. Вывод — у нас нет сеттеров, хоть код этих методов часто и похож на обычные сеттеры.

Это не обычная функция, а функция, единственным назначением которой является изменение состояния объекта. Тупые сеттеры (просто назначение свойству аргумента) — частный случай таких функций. Но если мы добавляем в тупой сеттер какой-то код (а именно для этого сеттеры и создаются, чтобы добавлять какой-то код в объект при изменении свойства), то чёткой грани между умным сеттером и обычной функцией нет.

Интересный взгляд на сеттеры. Спасибо. Хотя с вашим решением не соглашусь.


  • требует передачу зависимостей через аргументы метода что не очень красиво
  • не гарантирует что пароль будет захеширован
  • при смене пароля может быть использован не тот же алгоритм хеширования что при проверки авторизации

ни кто не мешает в коде проекта не хешировать пароль


$user->changePassword('123', function($password) {
    return $password;
});

или использовать разные алгоритмы хеширования


$user->changePassword('123', function($password) {
    return password_hash($password, PASSWORD_DEFAULT);
});
$user->changePassword('123', 'md5');

Конечно за такое надо отрывать руки, но речь не об этом.
Лучше использовать классический сеттер и при сохранении сущности хэшировать пароль


function setPassword(string $password) : User
{
    $this->password = $password;
    $this->password_changed = true;

    return $this;
}

function isPasswordChanged() : bool
{
    return $this->password_changed;
}

и обработчик события


if ($user->isPasswordChanged()) {
    // изменяем через сеттер или напрямую пишем в свойство
    $user->setPassword($this->hasher->hash($user->getPassword()));
}

У этого подхода конечно тоже есть недостатки, но по крайней мере в базе все пароли будут единообразны и точка для хеширования одна


Ну и отказ от классических сеттеров может вызвать определённые проблемы в использовании стандартных пакетов.
Например SonataAdminBundle использует PropertyAccessor для изменения полей сущности.
То есть отказ от классических сеттеров потребует написания костылей для SonataAdminBundle или полный отказ от этого бандла и самостоятельное написание аналога с блекджеком и ...


SonataAdminBundle это только пример. Скорей всего это не единственный пакет использующий PropertyAccessor и отказ от готовых решений может выйти боком.
Не хочу сказать что сеттеры рулят и т.д., но нужно четкое понимание что мы выйграем отказавшись от классических сетторов и какой ценой, а это уже выходит за рамки простых проектов на подобии тех что в статье.

Лучше использовать классический сеттер и при сохранении сущности хэшировать пароль

это не явно и сильно повышает связанность системы, опять же обязанность отслеживания инвариантов объекта (о том что пароль всегда должен быть захэширован) перекладывается на другие объекты что явное нарушение инкапсуляции.


Если вам хочется что бы просто и удобно, делаем так:


class Password
{
    private $value;

    private function __constructor(string $password)
    {
          $this->value = $password;
    }

    public static function create(string $password, PasswordHasher $hasher)
    {
         return new self($hasher->hash($password));
    }

    public function __toString()
    {
         return $this->value;
    }
}

Собственно все. Вместо double-dispatch можно использовать просто фабрику… и тогда тоже все замечательно.


Например SonataAdminBundle использует PropertyAccessor для изменения полей сущности.

повторюсь, это нарушение инкапсуляции. Только объект владеющий состоянием должен следить за ним, а не наоборот. Это может и кажется простым занудством, но это дает огромное преимущество при дальнейшей поддержке системы и расширения функционала.

В таком случае сущность превращается в помойку.


  • Сущность хеширует пароль;
  • Сущность загружает файл;
  • Сущность изменяет свои загруженные файлы;
  • Сущность удаляет свои загруженные файлы;
  • Сущность сохраняет последнего пользователя редактирующего ее;
  • Сущность сохраняет редактора который заапрувил ее;
  • Сущность изменяет свои даты в соответствии с часовым поясом пользователя полученным из запроса, которого кстати нет в консолм;
  • Сущность привязывает к себе соседние сущности в цепочке сущностей.

Не многова ли зависимостей и ответственности у сущности? Может стоит делегировать часть задач?

UFO just landed and posted this here
В таком случае сущность превращается в помойку.

нет.


Сущность хеширует пароль;

нет, хэширует пароль тот кто создает инстанс Password, а это не сущность.


Сущность загружает файл;

нет, в сущность приходит уже готовый инстанс FileReference или что-нибудь такое.


Сущность изменяет свои загруженные файлы;

value object-ы имутабельные, сущность может только удалить те файлы которые к ней относятся (имеется в виду ссылки на файл а не физически лесть в файловую систему, это можно разрулить доменными событиями например). И то только если сущность об этих файлах должна знать изначально.


Сущность удаляет свои загруженные файлы;

смотри предыдущее.


Сущность сохраняет последнего пользователя редактирующего ее;

зависит от задачи. Иногда — да, иногда сущности столько информации давать не стоит и мы закрываем сверху все сервисом который запоминает + делигирует операцию сущности.


Сущность сохраняет редактора который заапрувил ее;

опять же возможно. Смотрим предыдущий пункт.


Сущность изменяет свои даты в соответствии с часовым поясом пользователя полученным из запроса, которого кстати нет в консолм;

эм… сущность получает готовый value object, не нужно ему ничего считать.


Сущность привязывает к себе соседние сущности в цепочке сущностей.

если эти сущности входят в агрегат сущностей для этой конкретной бизнес-трназакции.


Не многова ли зависимостей и ответственности у сущности? Может стоит делегировать часть задач?

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

Данные должны обрабатываться там, где для этого достаточно данных.

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


Такая же ситуация с файлами. Например картинка/обложка к CD это такое же свойство сущности как название альбома, группа, список песен и т. д. И исходя из вашей логики сущность должна инкапсулировать взаимодействие со своей обложкой, но в этом случае ей придется работать с фс, что означает очередные зависимости.


Приведу пример:
Есть 2 сущности с картинками:


  • обложка альбома
  • новость с картинкой на сайте группы.

Загружаемые картинки попадают во временную папку /upload/. После привязки к сущности они должны перемещаться каждая в свою папку:


  • обложка альбома — /image/album/{date}/cover/
  • новость с картинкой — /image/news/{date}/cover/

{date} это Y/m от даты создания сущности


Кто должен заниматься перемещением картинок? Напоминаю что только сущность знает путь загрузки картинки относительно корня проекта, но она не знает путь к корню проекта.


Идем дальше. Про удаление вы предлагаете удалять ссылки на файлы, а не сами файлы. То есть файлы остаются на диске и висят мертвым грузом потому что их никто не использует. Для решения этой задачи вы предлагаете использовать события, но опять вопрос. Кто должен бросать событие? Сущность которая удаляет файл? Ведь это ее уровень ответственности получается.


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


Суть моих изисканий в том что сущность это простой объект (не сервис) и она не должна использовать ни какие зависимости (может в доктрине и против сеттеров, но и сервисом они сущности не сделали).
Если появляется необходимость использовать зависимости, в той или иной форме, то нужно создавать еще один слой абстракции с требуемыми зависимостями, а не впихивать все в один класс.

Функции changePassword в вашей первой реализации не достаточно данных для того чтобы изменить пароль. Она вынуждена использовать зависимости для хеширования пароля.

И что в этом страшного? мы же передаем ее как аргумент и благодаря инверсии зависимостей сохраняем слабую связанность.


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

нет, в сущность приходит уже готовый линк на файл. и все. Просто value object. Сущность понятия не имеет что это такое и как оно было получено, ей и не надо.


о есть файлы остаются на диске и висят мертвым грузом потому что их никто не использует.

гуглить доменные ивенты. Тогда вопрос "кто что и как" отпадет. Пример: https://github.com/php-ddd/domain-event


Суть моих изисканий в том что сущность это простой объект (не сервис) и она не должна использовать ни какие зависимости

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

гуглить доменные ивенты

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


Почему мы не можем рассматривать пароль в сущности как всегда хешированный пароль (Просто value object)? Например бросать событие об изменении пароля с сущностью и plaintext паролем, а обработчик хеширует пароль и устанавливает его в сущность. Чем это отличается от загрузки файлов и установки в сущность пути к файлу?

мы можем, а… мы не можем.


Именно. Сущности не нужен побочный эффект в виде удаления старого файла, это кому-то другому нужно. А захешированный пароль нужен самой сущности. Как минимум зачем вводить оверхид да ещё с циклическими зависимостями, если можно сделать простой вызов функции? А ещё события могут логироваться, а там plaintext.
А захешированный пароль нужен самой сущности.

Да сущности захешированый пароль тоже не нужен. Ей нужен пароль. А хешировать этот самый пароль нужно кому-то другому. Например Symfony Security


Как минимум зачем вводить оверхид да ещё с циклическими зависимостями, если можно сделать простой вызов функции?

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


А ещё события могут логироваться, а там plaintext.

Запросы тоже могут логироваться, а там plaintext.

Не нужен сущности сам пароль. Зачем он ей?

Может я чего-то не понимаю, но вроде говорили про то, что сделать выбрасывания события на $user->setPassword(), обработчик которого будет хэшировать пароль и вызывать $user->setHashedPassowrd();

Запросы к сущности особого отношения не имеют, а вот самой передавать пароль плэйнтекстом всему миру через события — не хорошо.
Может я чего-то не понимаю, но вроде говорили про то, что сделать выбрасывания события на $user->setPassword(), обработчик которого будет хэшировать пароль и вызывать $user->setHashedPassowrd();

Это конечно вариант, но в этом случае метод setPassword() должен в зависимостях иметь Event Dispatcher. И это действительно получится ненужный оверхед. Я же имел в виду выбрасывание событие из контроллера.


$this->dispatcher->dispatch(
    StoreUserEvents::CHANGE_PASSWORD,
    new ChangeUserPassword($user, $password)
);

а в обработчике уже хешировать


public function onChangeUserPassword(ChangeUserPassword $event)
{
    $event->getUser()->setHashedPassowrd($this->hasher->hash($event->getPassword());
}

можно конечно и в контроллере все сделать


$user->setHashedPassowrd($this->hasher->hash($password);

Но на событие можно делать какие-то дополнительные действия. Например отправить пользователю на email отчет о том что его пароль был изменен. Естественно в письме пароль не указываем, а только уведомляем, что бы пользователь мог оперативно отреагировать на несанкционированную смену пароля в его профиле.

тут интереса ради заглянул под капот FOSUserBundle


при изменении сущности, по событию от Doctrine, выполняется обновление пароля
https://github.com/FriendsOfSymfony/FOSUserBundle/blob/master/Doctrine/UserListener.php#L97


при обновлении берется plain password (он не хранится в бд), хешируется и сохраняется как основной пароль через setPassword
https://github.com/FriendsOfSymfony/FOSUserBundle/blob/master/Model/UserManager.php#L195


как раз примерно такой вариант я и имел в виду, хотя я уже понял что вы не приверженец сеттеров и хеширования пароля вне пользователя

как раз примерно такой вариант я и имел в виду, хотя я уже понял что вы не приверженец сеттеров и хеширования пароля вне пользователя

Простите, но вот этот "кастыль" с листенерами и есть "вне пользователя и сеттеры". Вот как без них:


class UserBuilder
{
    private $email;
    private $password;
    private $plainPassword;
    private $firstName;
    // ...
    public function usingPassword(string $password, PasswordEncoder $encoder) 
    {
         $this->password = $encoder->encode($password);
         $this->plainPassword = $password; // может для каких email-ов

         return $this;
    }

    public function withEmail(string $email)
    {
         $this->email = $email;

         return $this;
    }
    // ...

    public function buildUser()
    {
          return new User($this);
    }
}

class User
{
    private $email;
    private $password;

    public function __construct(UserBuilder $builder)
    {
          // тут еще валидацию бы
          $this->email = $builder->getEmail();
          $this->password = $builder->getPassword();
          // ...
          // domain events, ну что б совсем упороться
          $this->remember(new UserRegisteredEvent($this, $builder));
    }

    public function changePassword(string $password, PasswordEncoder $encoder)
    {
         $this->password = $encoder->encode($password);
    }
}

$user = (new UserBuilder())
     ->withEmail($email)
     ->usingPassword($password, $passwordEncoder)
     // ...
     ->buildUser();

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

@Fesor Спасибо за конкретный пример. Я уже в общем понял что вы имели в виду, но за еще один пример спасибо. Я уже понял что обще признанные решения вы считаете недопустимыми.


Чем-то вы мне напоминаете нашего общего знакомого G-M-A-X. Вместо того чтоб использовать готовое решение делаем свое. Согласен, если мы разрабатываем ПО опираясь на парадигму Domain Driven Design, то готовые решения просто невозможно использовать. Я же предпочитаю опираться на Data Driven Design чтоб не усложнять себе жизнь и повышать уровень реиспользования кода. Да и не было у меня пока еще проектов с супер сложной бизнес логикой.


Сейчас решил углубится в тему DDD и был бы рад обмену опытом. Вы не думали написать статью на тему использования DDD в Symfony?


Размышления на тему


Как на счет сериализации сущностей для всяких API?


  • Условно, мы создаем сервис сериалайзер
  • Наследуемся от JsonSerializableNormalizer
  • В соответствии с форматом определяем формат нормализации объекта
  • Непосредственно нормализацию наверное выполняем все таки в сервисе, а не в сущности
  • На каждый формат для сущности я бы делал свой сервис чтоб не захламлять сериалайзер
  • А вот с денормализацией вопрос (эта задача хоть и не частая, но все равно задача)

Исходя из вашей логики денормализацией должна заниматься сущность.
Условно, пришел запрос от пользователя с id сущности и набором полей.


  • получаем сущность из бд
  • передаем сущность и данные в сериалайзер
  • сериалайзер передает данные в сущность
  • сущность заполняет свои поля на основе данных

Вроде все норм, если не считать что формат для нормализации мы определяем в одном месте, а формат денормализации используется в другом.
Можно задачу нормализации также возложить на сущность и тогда все будет в одном месте, но тогда в сущности будет куча пар методов нормализации и денормализации для каждого формата и контекста, и смысла в сервисе сериалайзере теряется.


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


Получаем минимум 4 дополнительных метода в сущности.
Так же не понятно как мы должны заполнять связи сущности при денормализации. Видимо туда же нужно передавать сериалайзер.


Мысль другая


Как идея. Разбить проект на 4 бандла и 4 окружения:


  • Frontend — веб часть проекта
  • Backend — админка
  • API — внешние сервисы
  • Core — для общего набора функций (не очень хорошая практика, но иногда иначе никак)

Идея в том чтобы весь набор сущностей сделать индивидуальным для каждого бандла / окружения. Копии сущностей и свой набор независимых репозиториев для каждого окружения. Не все бизнес процессы которые есть в API нудны на фронте, а задачи которые ставятся в админке не должны быть доступны остальным окружениям.


В таком случае фронтенд и api можно писать по DDD, а админку делать с тупым CRUD, сеттерами и на SonataAdminBundle. Это конечно если бизнес логика нам важна именно на внешнем интерфейсе, а не в админке, а так по идее и должно быть ибо админка все таки для администраторов.


Из минусов мы получаем дублирование некоторых функций (решается наследованием или трейтами)
Из плюсов нам не нужно писать админку с нуля под каждый проект отвечающую нашим бизнес задачам.

Чем-то вы мне напоминаете нашего общего знакомого G-M-A-X. Вместо того чтоб использовать готовое решение делаем свое.

Мне комфортнее когда мои бизнес сущности ничего не знают о фреймворках и т.д. Хотя сказать что я так делаю всегда и везде — нет конечно. У меня полно проектов где сеттеры в сущностях, и где толстые контроллеры бывают. Но это скорее как "компромис" когда нужно быстро и вроде как не страшно.


Вы не думали написать статью на тему использования DDD в Symfony?

DDD не про фреймворки. Можно скорее про DDD with Doctrine, но на эту тему можно просто сходить например к marco pivetta на воркшопы.


Я же предпочитаю опираться на Data Driven Design чтоб не усложнять себе жизнь и повышать уровень реиспользования кода.

это тип "спроектировать базу"? При таком раскладе весьма сложно бывает масштабироваться, да и слаб я как DBA, возможно потому и прокачиваюсь в подходах которые позволяют немного избегать проблем с этим.


Как на счет сериализации сущностей для всяких API?

Всякие API имеют такую штуку как версии. И вот у нас уже два способа сериализации одной и той же сущности. А еще есть нюансы кому что показывать можно а кому нет. И вот нам уже нужна прослойка, я в качестве оной использую fractal + symfony/serializer для всякой мелочи. все работает предсказуемо и лишнего кода нет.


Ну то есть идея та же что и у MVC как UI паттерна. Отделение ответственности преобразования представления данных из модели в то, что хочет видеть клиент.


Исходя из вашей логики денормализацией должна заниматься сущность.

нет, сущность ничего не должна знать об этом (так проще потом). Она может иметь метод который позволяет выплюнуть слепок состояния но это такое. Тут на самом деле я на 100% не могу сказать "как правильно", просто я уже пробовал "ваш" подход на протяжении 3-х лет и "мой" подход на протяжении последнего года и последний пока вызывает меньше проблем.


Вроде все норм, если не считать что формат для нормализации мы определяем в одном месте, а формат денормализации используется в другом.

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


Как идея. Разбить проект на 4 бандла и 4 окружения:

Так же пробовал, проигрыш. Даже symfony best practice говорит что это "так себе" идея. Они не дает никакого профита а все условное разделение и так можно сделать при помощи нэймспейсов.


Я допускаю использование бандлов если в будущем есть шанс разделить все на микросервисы, но в этом случае никаких CoreBundle-в (это как god object только бандл) и между бандлами не должно быть зависимостей на уровне сущностей (то есть данные одного бандла доступные только через сервисы, никаких сущностей, если что-то надо — данные дублируем). В таком случае мы потом сможем легко на микросервисы перейти. Но это нужно хорошо если 0.1% проектов.


Инфраструктуру выносить в бандлы — очень удобно. Например UploadBundle и т.д. Моя команда сейчас пытается это вообще в виде микросервисов отдельными докер контейнерами решать.


В таком случае фронтенд и api можно писать по DDD, а админку делать с тупым CRUD,

У меня как-то давно небыло проектов где админка это тупой CRUD. Да и для этого можно поднять какой-нибудь UI для прямого доступа в базу.


Из минусов мы получаем дублирование некоторых функций (решается наследованием или трейтами)

Короч вы предлагаете вместо "давайте разберемся что такое ООП, как проектировать системы, как использовать подходы и инструменты те что надо и там где это надо" фигачить все как тупой CRUD. Ну ок, я вас услышал.

слаб я как DBA

В дрогой статье про неправильный путь в PHP вы писали что сначала создаете объектную структуру в соответствии с бизнес логикой, описваете мапинг и диктрина сама билдит вам схему бд.
Вы не обращали внимание на то что схема генерируемая доктриной сильно не оптимальна?


я уже пробовал "ваш" подход на протяжении 3-х лет и "мой" подход на протяжении последнего года

Не удивлен. Потому я и считаю что каждый должен заниматся своей задачей. Имхо сериализация и десериализация не относятся к задачам модели.


Я допускаю использование бандлов если в будущем есть шанс разделить все на микросервисы

Я скорей имел в виду разделение на окружения и выделение отдельных фронт контроллеров. То есть получаются практически полностью разные приложения в одном проекте. При необходимомти их можно полностью разделить. Это позволяет нам разделить конфигурации окружений, разделить набор сервисов, разделить модели и репозитории.


Пример конкретной проблемы. Сейчас переписываю один проект с 0 и в репозиториях у меня получается каша. Там есть методы которые используются только в админке, есть методы которые есть только в консоли, методы которые используются только на фронте и методы которые используются для мигрирования данных со старого проекта на новый. Всё это разные методы и ни как не пересикаются.
И вот эта каша мне савсем не нравится. Есть у вас какие мысли на этот счёт?


никаких CoreBundle-в 
Инфраструктуру выносить в бандлы — очень удобно. Например UploadBundle и т.д. 

Полностью согласен. В одном из дакладов на framework day тоже говорили что CoreBundle это зло. Только я предпочитаю компоненты выносить не просто в бандлы, а в отдельные Composer пакеты.


Короч вы предлагаете вместо "давайте разберемся что такое ООП, как проектировать системы, как использовать подходы и инструменты те что надо и там где это надо" фигачить все как тупой CRUD.

Почти. Я "предлагаю" разбиратся в ООП и не использовать CRUD там где это действительно необходимо. На мой взгляд это действительно необходимо на фронте. В админке же работает ограниченный набор людей которые сидят рядом со мной и которым я лично могу надавать по шапке в случае чего.


Учитывая мощь Sonata, грешно не воспользоваться ею хотябы на первых этапах запуска проекта.

Вы не обращали внимание на то что схема генерируемая доктриной сильно не оптимальна?

А что есть "оптимально"? Схема там генерится так как описано, это все же не полностью все на магии.


Словом у меня пока небыло с этим проблем, но я часто в последнее время прибегаю к денормализации дабы все было ок в плане производительности.


Имхо сериализация и десериализация не относятся к задачам модели.

Согласен. И у меня за это отвечают отдельные штуки.


Я скорей имел в виду разделение на окружения и выделение отдельных фронт контроллеров. То есть получаются практически полностью разные приложения в одном проекте.

это то что я говорил про микросервисы. Если у вас есть CoreBundle — в этом нет смысла.


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

у меня репозиторий обычно содержит парочку методов (как правило add, и один-два метода на выборки). А если нужно делать много разных выборок и все может быть сложно — мне нравится паттерн спецификация. То есть за формирование запросов отвечают объекты спецификаци а не репозиторий. Это позволяет резко понизить сложность.


Полностью согласен. В одном из дакладов на framework day тоже говорили что CoreBundle это зло.

Если это было на тех выходных, то может даже я это и говорил)


Учитывая мощь Sonata, грешно не воспользоваться ею хотябы на первых этапах запуска проекта.

Возможно следует пояснить как выглядят мои проекты. У меня бэкэнд это чисто HTTP API, а админки, круды и т.д. у меня сделаны на ангулярах. На нем делать админки — просто в путь. И выходит очень гибко. А так как у меня всеравно будет HTTP API выходит так что нет лишних вещей.


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


Сонату года 4 не юзаю потому что "сейчас быстро наклепать" может и ок но потом просто слишком долго кастомизировать. Потому до перехода на ангуляр использовал стандартный CRUD генератор с кастомизированными шаблонами (нашел какие-то на гитхабе и чуть адаптировал под себя).

А что есть "оптимально"? Схема там генерится так как описано, это все же не полностью все на магии.

О, там много чего:


  • индексы нужно описывать в аннотациях. Мелочь, а не приятно
  • FOREIGN KEY доктрина не создает или я не нашел как это сделать
  • Хотябы на уровне таблицы нужно ставить кодировку. Не стоит расчитывать на настройки сервера.
  • кодировку для полей ставить тоже не очень удобно.
  • комментарии к полям таблицы и самой таблицы добавляются не очень красиво. В русскоязычных проектах я всегда добавляю комментарии, чтоб любой разработчик, не знакомый с проектом, мог по базе понять как всё устроено.
  • для интов тоже нужно указывать длинну. Если мы точно знаем что в конкретном случае число не будет больше 200, то нет необходимости использовать INT, тут больше подойдет TINYINT(2) UNSIGNED.
  • если в базе используется TINYINT (2) доктрина всё равно преобразует его в boolean при генерации маппинга.
  • для представления boolean в бд доктрина использует антипаттерн TINYINT(1) со значениями 1 и 0. Во первых в MySQL есть тип данных BOOLEAN который экономичней, но имеет значения TRUE и NULL, что не совсем логично (давно не заглядывал в доку) сейчас этого типа уже нет, а BOOL это синоним к TINYINT. В TINYINT(1) в действительности можно записать не только 0 и 1, а можно записать числа 0-9 и NULL, а если не стоит UNSIGNED то и отрицательные числа замечательно записываются. Это звучит смешно, но мне один раз достался проект в котором у пользователей в поле gender были значения 0, 1, 2, 3, 4 и NULL. Назначение некоторых значений не знали даже самые старые члены команды. И для каждого пола 10к+ пользователей. Поэтому я предпочитаю использовать тип ENUM. Он занимает столько же места что и TINYINT, но чётко ограничивает набор доступных значений и делает их более информативными. Согласитесь, status 0/1 выглядит менее информативно чем status enabled/disabled? И расширять такие статусы проще. Были у меня случаи когда при изменении требований boolean превращался в enum.

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


мне нравится паттерн спецификация

Согласе. Интересный паттерн. Хотя он решает только треть задач ставящихся перед репозиторием. Он позволяет управлять фильтрами, но запросы это не только выражение where, но и джойны, группировки, хевинги, кастомезированные селекты и т.д. Видимо нужно создавать классы для отдельных запросов, но что тогда делать с внешними зависимостями.


Если это было на тех выходных, то может даже я это и говорил)

Я смотрел доклад Олега Зинченко опубликованнный в 2014 году. На прошлой неделе что-то я таких замечаний не слышал.


У меня бэкэнд это чисто HTTP API, а админки, круды и т.д. у меня сделаны на ангулярах.

Это хорошо когда есть сильные фронтенд программисты. Я таким похвастаться не могу. Сам во фронтенд стараюсь не лезть.

индексы нужно описывать в аннотациях. Мелочь, а не приятно

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


FOREIGN KEY доктрина не создает или я не нашел как это сделать

создает же.


Хотябы на уровне таблицы нужно ставить кодировку. Не стоит расчитывать на настройки сервера.

эм… у меня это на уровне схемы делается, не нужно это делать на уровне таблицы.


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

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


для интов тоже нужно указывать длинну. Если мы точно знаем что в конкретном случае число не будет больше 200, то нет необходимости использовать INT, тут больше подойдет TINYINT(2) UNSIGNED.

ну так выставляйте, откуда доктрина то это узнает? Вы ее просите инты хранить — так храните. Доктрина не настолько волшебна что бы читать ваши мысли.


если в базе используется TINYINT (2) доктрина всё равно преобразует его в boolean при генерации маппинга.

Воу воу воу. Я правильно понимаю что вы используете доктрину… с готовой схемой базы данных? Ну так может в этом и проблема? Поскольку это не рекомендовано, доктрина это инструмент для быстрой разработки.


для представления boolean в бд доктрина использует антипаттерн TINYINT(1) со значениями 1 и 0.

Это нужно для совместимости. Вы можете спокойно это переопределить, вас никто ни в чем не ограничивает.


И имея на руках одну только базу можно легко написать новый проект на другом языке.

тогда вам не нужна доктрина. Об этом авторы доктрины твердят уже не первый год — если вам нравится ковыряться в базе данных и вы молитесь на 4-ую нормальную форму — доктрина не для вас.


Он позволяет управлять фильтрами, но запросы это не только выражение where, но и джойны, группировки, хевинги, кастомезированные селекты и т.д.

Шаблон спецификация не регламентирует то как вы выборки делаете. И я не про Criteria доктриновскую. У меня спецификации работают с query builder-ом.


Я таким похвастаться не могу. Сам во фронтенд стараюсь не лезть.

ну я фулстэк разработчик. Мы сделали отдельно отдел фронтэндщиков что бы похапэшники не лезли во фронтэнд. И я могу сказать что это весьма профитно.

Шаблон спецификация не регламентирует то как вы выборки делаете. И я не про Criteria доктриновскую. У меня спецификации работают с query builder-ом.

Я думал вы про шаблон проектирования, а тут выходит всё ещё хитрее. Не поделитесь примерчиком?


Мы сделали отдельно отдел фронтэндщиков что бы похапэшники не лезли во фронтэнд. И я могу сказать что это весьма профитно.

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

Библиотеку видел, но дальше readme не ушёл и про QueryModifier не прочитал. Спасибо

в репозиториях у меня получается каша. Там есть методы которые используются только в админке, есть методы которые есть только в консоли, методы которые используются только на фронте и методы которые используются для мигрирования данных со старого проекта на новый. Всё это разные методы и ни как не пересикаются.
И вот эта каша мне савсем не нравится. Есть у вас какие мысли на этот счёт?


Если стоит задача просто упорядочить код репозитория, разделить запросы фронта, бэка и т. п. для более удобной работы с ними, то можно просто использовать трейты. В основном классе репозитория только ссылки на все трейты и самые базовые методы, а специфичные методы в трейтах. Но более удобно в этом плане разделять методы на трейты не по «контроллеру», а по тому, что метод делает. Скажем, агрегирующие запросы в один трейт, плоские (ленивые) в другой и т. п. Но это сильно от проекта зависит.

Ещё вариант использовать наследование, но следует иметь в виду, что доктрина позволяет со стороны сущности ссылаться только на один репозиторий (если не устарела инфа) и чтобы использовать все прелести доктрины в бандлах со своими репозиториями придётся конфигугрировать доктрину отдельно.

В общем и в целом, выделение кода приложения в отдельные бандлы имеет смысл только если они не зависят друг от друга, в частности, если говорить про модель и БД, если сущности бандлов не ссылаются друг друга, вернее могут ссылаться по, например, айдишникам, но без поддержки объектной связи на уровне модели и без поддержки внешних ключей на уровне схемы. Практически микросервисная архитектура, но без потерь на межпроцессное и межсерверное взаимодействие. При необходимости можно создавать отдельные бандлы агрегаторы, которые будут собирать данные с других и отдавать клиенту, чтобы избавить клиента от необходимости делать 1+N запросов к серверу, просто делать эти 1+N запросов со стороны бандла агрегатора (вернее он будет делегировать запросы другим бандлам, сам агрегатор про базу ничего не знает). Как оптимизация агрегатор (хотя в принципе и клиент) может делать два запроса, скажем, сначала выбрать все договора по какому-то критерию обратившись к одному бандлу, а затем выбрать всех физлиц (сотрудников, клиентов, поручителей и т. п.), относящихся к пулу договоров одним запросом типа WHERE id IN (/* 100500 айдишников */). Если же хочется использовать в запросах джойны, то лучше все сущности, которые предполагается джойнить держать в одном бандле. Поддержка межбандловых связей на уровне ORM/СУБД очень быстро превращается в ад, особенно двухсторонних. В общем если возникает мысль вынести какой-то код связанный с ORM в бандл, я задаю себе вопрос «если я захочу вынести данные этого бандла в другую базу, может даже другую СУБД, а может вообще не в СУБД, а решу в файлах хранить, то мне надо будет переделывать остальное приложение?». И если ответ «да», то не выношу, выношу только то, что можно спрятать за простыми публичными контрактами, никак не касающихся системы хранения, из которых не торчат уши Доктрины. Бывают исключения, когда выношу в бандлы куски кода с контрактом из которого торчит доктрина, но исключительно для использования в разных несвязанных через базу проектах, когда нужно повторять одни и те же таблицы (пользователи, логирование и т. п.) в разных базах, но это именно исключение.
Если стоит задача просто упорядочить код репозитория, разделить запросы фронта, бэка и т. п. для более удобной работы с ними, то можно просто использовать трейты.

@Fesor уже предложил решение получше. Использовать спецификацию из DDD (пример)


В общем и в целом, выделение кода приложения в отдельные бандлы имеет смысл только если они не зависят друг от друга

Естественно речь шла о полном разделении и минимизации связей между бандлами. В одном из докладов, Олег Зинченко говорил о том что разделение приложения на несколько бандлов, керналов и фронт контроллеров позволит легко управлять отдельно каждой частью проекта независимо. Так же должна повысится читаемость и организованность кода по сравнению хранением всего проекта в одном бандле. И за счет разделения мы значительно уменьшим количество сервисов в каждой части приложения что тоже приведет к своим плюсам.

Про события я писал.

я вижу что вы пишите и потому предлагаю посмотреть конкретную реализацию. Суть доменных ивентов в том что их никто не выбрасывает. Они коллексятся в сущности и все, а кто-то сверху их потом просто забирает.


а бросить такое же событие об изменении сущности чтоб захешировать ее пароль мы не можем.

нет, поскольку до момента отработки пароля наша сущность является "не валидно" (пароль не задан или задан в открытом виде).


Почему мы не можем рассматривать пароль в сущности как всегда хешированный пароль

так я же привел пример именно такой. Когда нам в качестве аргумента приходит уже захэшированный пароль (инстанс типа Password). Правда мы тут себе чуть руки связываем конечно… Но это уже от задачи зависит.


$user = User::create($request->get('email'), Password::create($request->get('password'), $hasher);

Выглядит неплохо, но у этого подхода есть недостатки:


  • Как я уже говорил, этот подход полностью не совместим с SonataAdminBundle.
    Это означает необходимости написания своей админки с нуля.
    А это уже повлечет за собой дополнительные расходы ресурсов компании на что будет готова пойти далеко не каждая, даже крупная, компания.
  • Этот подход не позволяет использовать оригинальные сущности в формах.
    Это означает создание новых сущностей, почти полных клонов оригинальной сущности, для использования их в формах и последующей конвертации в оригинальны сущности доктирины.
    В этот подход неплохо укладывается Command Bus, но в результате мы плодим пустые сущности, дублирование кода и оверхед.
  • Отказ от стандартных компонентов усложняет проект и повышает цену сопровождения кода.

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

Это означает создание новых сущностей, почти полных клонов оригинальной сущности, для использования их в формах и последующей конвертации в оригинальны сущности доктирины.

Это не создание сущностей, а создание Data Transfer Object (обычно объект чисто с публичными свойствами и без методов, по сути сишная структура) с примерным циклом жизни в случае Create/Update запросов: создали DTO из запроса (с помощью формы или напрямую), провалидировали DTO (по сути провалидировали запрос), создали или обновили соответствующую сущность из DTO, удалили DTO.
этот подход полностью не совместим с SonataAdminBundle.

и это хорошо, потому как с Sonata доктрина нам как бы тоже не особо нужна. Нам нужно что-то тупое вроде active record.


Этот подход не позволяет использовать оригинальные сущности в формах.

именно! И это тоже хорошо! потому что сущности это не DTO.


Отказ от стандартных компонентов усложняет проект и повышает цену сопровождения кода.

никто не говорит об отказе от стандартных (кто сказал что саната стандарт?) компонентов, просто другие подходы. Я просто говорю что если у вас такие потребности — вам не нужна доктрина.

Интересный у вас взгляд на реиспользование кода и оптимизацию процесса разработки.
Доктрина слишком умная для сонаты — давайте напишем свою доктирину, но попроще.
Соната слишком универсальная для дактрины — давайте напишем свою сонату, но отвечающую нашим требованиям.
Слушайте, а симфони для вас не слишком универчальная? Может стоит написать свой фраймворк? Хотя что-то мне вспоминается что вы так и сделали.


Ладно. Хорошего понемножку. Прошу прощение за разведение холивара и за возможную грубость. Я понял вашу позицию. Я согласен что это интересный и наверное правильный подход. Если у меня будет ну очень много свободного времени, то я обязательно попробую ваш подход. Сейчас у меня даже на свой проект времени нет, а все задачи ставятся со сроком вчера.

Если Сонату легко прикрутить к приложению и проблем (например, приведение сущности в неконсистентное состояния руками администратора/менеджера) её использование не вызывает, значит возможности Доктрины и на 10% не используются в приложении, а сущности только лишь так называются, а по факту просто структуры данных для представления базы данных.
Напоминаю что только сущность знает путь загрузки картинки относительно корня проекта, но она не знает путь к корню проекта.


Это лишние знания для сущностей альбома и новости. Сущности должны хранить бизнес-знания и бизнес-логику, а в проекте типа сайта с альбомами и новостями, вещи типа путей, файлов и тем более манипуляции с ними — это инфраструктура. Сущность может хранить путь к файлу как неизменяемый ValueObject или строку, но это дело инфрастуктурных сервисов (например, контроллера) как-то создавать эти объекты и как-то синхронизировать их с файловой системой.
Сущность сохраняет последнего пользователя редактирующего ее;

Если бизнес-логика требует сохранения последнего пользователя, то метод типа aplpyPatchAs($patch, User $user) вполне нормален в сущности
Сущность сохраняет редактора который заапрувил ее;

Аналогично, метод approveAs(User $editor)
Сущность изменяет свои даты в соответствии с часовым поясом пользователя полученным из запроса, которого кстати нет в консолм;

Сущность не изменяет свои даты для показа пользователю, это ответственность представления. Сущность хранит даты (вернее датывремя?) либо с часовым поясом, либо в UTC, либо в каком-то «дефолт-сити», а представления уже форматируют дату (через тот же \DateTime::format()) как нужно пользователю.
Сущность привязывает к себе соседние сущности в цепочке сущностей.

Агрегаты не просто привязывает, а единственный кто их может создавать (через new или фабрику) и менять (иногда не хватает в PHP friendly модификатора доступа, приходится или создавать публичные модифайеры, которые может кто-то дернуть случайно, или, в особых случаях, заморачиваться с рефлексией).

Прямое (через свойство/сеттер) привязывание к себе именно соседних по иерархии классов сущностей обычно говорит о нарушении инкапсуляции. И доктрина обычно в этом не виновата.
Аналогично, метод approveAs(User $editor)

а чем это отличается от setApprovedUser(User $editor)? семантикой?


это ответственность представления

Я не упомянул что работаю с проектом у которого очень много логики завязано на часовых поясах. И изменение часового пояса это не только задача представления. В базе дата хранится в системном часовом поясе, а в коде проекта работа происходит с пользовательским часовым поясом. Это вполне удобно. Не буду утверждать что это идеальное решение, но пока нареканий не было.


Агрегаты не просто привязывает, а единственный кто их может создавать

А что если цепочка сущностей это целая таблица в которой 2кк записей? Агригатор тут не справится. Нужно как-то иначе связывать события.

а чем это отличается от setApprovedUser(User $editor)? семантикой?

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

И изменение часового пояса это не только задача представления

Изменение часового пояса в свойстве сущности, типа «а теперь это время будем считать по этому поясу» или использование часового пояса пользователя в расчётах, типа запроса к сущности «посчитай баланс счёта на 0:00:00 по московскому времени»?
А что если цепочка сущностей это целая таблица в которой 2кк записей? Агригатор тут не справится. Нужно как-то иначе связывать события.

Пример можно? А то, кажется, про разные вещи говорим.

Пример можно? А то, кажется, про разные вещи говорим

Телепрограмма. Если нам нужно вытащить следующее/предыдущее событие то можно использовать sql запрос с выборкой по дате, а можно использовать напрямую связанное следующее/предыдущее событие. Второй вариант будет быстрее при выборке

Так это корень агрегата должен устанавливать свойства при добавлении агрегата. Грубо что-то вроде:
public function addEvent(string $event, \DateTime $time) {
  $prevEvent = $this->events()->last();
  $newEvent = new Event($this, $event, $time, $prevEvent);
  $prevEvent->setNextEvent($newEvent);
  $this->events->add($newEvent);
}
Что мешает сделать так, не думаю что сеттеры вам особо усложнят жизнь, и на больших проектах наоборот могут помочь.

function changePassword(string $password, callable $hasher) : void 
{
     $this->setPassword($hash($password)); // мы никогда не забудем захэшировать пароль
}

1) сеттер будет приватным методом в этом случае, поскольку мы не хотим давать внешнему миру им пользоваться
2) у вас и так есть доступ к состоянию объекта и этот метод вполне себе способен его менять. Дополнительные методы не нужны.
3) на больших проектах с сеттерами, сущности доктрины превращаются в тупое хранилище данных, а вся бизнес логика вытекает в лучшем случае в сервисный слой. И в итоге толку тогда от доктрины? Да и даже ORM в этом случае нам вообще не нужен.

Это уже частные случаи, мы сейчас все же говорим относительно статьи.
В большинстве случае entity доктрины генерируются из бд через консоль автоматически и мы уже получаем класс с сеттерами, а уже все действия над ним лучше вынести в репозиторий или модель. На сколько они нужны, не нужны — вопрос второй, но даже в IDE вам будет проще с сеттерами с автокомплитом.
В большинстве случае entity доктрины генерируются из бд через консоль автоматически

Вот честно за 5 лет ни разу так не делал. Более того, разработчики доктрины не рекомендуют так делать.


Использование доктрины подразумевает что схема базы данных генерируется из сущностей, а не наоборот (хотя так и можно просто сложно и теряется профит). То есть мы сначала проектируем сущности (бизнес-объекты) со своей бизнес логикой, моделируем предметную область а уже потом занимаемся второстепенными вещами вроде "схему базы генерим".


То есть если мы работаем с доктриной и представляем сущности как тупую проэкцию рядов таблиц на объекты — вам не нужна доктрина. Вам хватит какого-нибудь active record.


а уже все действия над ним лучше вынести в репозиторий или модель.

Потому что у вас в сущностях сеттеры, а не потому что это просто лучше) Есть такой шаблон проектирования (или скорее даже принцип) под названием "информационный эксперт": информация должна обрабатываться там, где она есть. То есть в нашем случае пока бизнес логика находится в рамках одной сущности или мы можем легко выделить корень агрегата сущностей — лучше эту логику пихать в сущность. А уж если мы не можем выделить корень агрегата (например надо посчитать сумму всех заказов пользователя и сущность пользователя ничего не знает о заказах) то тогда да, выносим это дело в сервис. Но подсчет суммы одного заказа — дело самого заказа.

UFO just landed and posted this here

Если в gitter-е симфони пробегали, я там постоянно хэйчу на такое примитивное использование доктрины. Это слишком сложный инструмент что бы пользоваться только анемичными моделями.

этот спор из той же области, где выполнять больше действий в моделе или контроллере.
Вы можете выполнять действия в классе Entity или можете вынести эти функции в модель или репозиторий, где у вас будет тот же самый объект entity. Здесь скорее вопрос кому как нравится.
этот спор из той же области, где выполнять больше действий в моделе или контроллере.

тут нет никакого спора. Когда у вас больше действий в контроллере, у вас нет разделения бизнес логики и презентационной логики, и называется это SmartUI. Вполне себе годный вариант для CRUD-а например, и покрывает он подалвяющее большинство проектов. Это не плохо, просто на более сложных проектах с более сложной логикой вы проиграете. В лучшем случае все закончится нарушением DRY. В худшем — связанность системы будет настолько высока, что сопровождать проект будет болью (как для разработчика, так и для бизнеса)


Вы можете выполнять действия в классе Entity или можете вынести эти функции в модель или репозиторий

Просто приведите формулировку термина "модель". Что это у вас? Модель предметной области? Какая-то другая модель?


Здесь скорее вопрос кому как нравится.

Ну да, и принципы всякие вроде SOLID или GRASP придумали от скуки.

Вы понимаете что я могу ответить также что геттеры и сеттеры придумали не от скуки. Не важно нужны они вам или нет, это не значит что их, относительно данной статьи, не должно быть у других, и что это не правильный подход.
А на счет модели я думал вы знает для чего их придумали. В doctrine просто работу с данными можно вынести в repository, а так в модели.
Вы понимаете что я могу ответить также что геттеры и сеттеры придумали не от скуки.

Геттеры придумали в java что бы иметь возможность получать состояние. С ними все хорошо поскольку они не мутируют состояние объектов.


Сеттеры придумали потому что нужно было дать возможность извне менять состояние объектов. При этом мы нарушаем инкапсуляцию но это все же лучше чем публичные свойства и никакого контроля.


Не важно нужны они вам или нет, это не значит что их, относительно данной статьи, не должно быть у других, и что это не правильный подход.

Культ карго. Люди будут делать так как показали вне зависимости от размеров проекта. Без какого либо понимания что они делают и зачем.


А на счет модели я думал вы знает для чего их придумали.

Понимаете ли, это не имеет значения. Меня интересует ваша интерпритация что бы мы говорили на одном языке. Ибо для меня модель — это модель. А сущность — представляет собой модель бизнес объекта, одного. Сервис представляет модель взаимодействия нескольких несвязанных между собой сущностей (ибо если они связаны они сами разберутся).


В doctrine просто работу с данными можно вынести в repository, а так в модели.

Репозитории это вещи, которые отвечают за хранение. В них может находиться бизнес ограничения вроде "можем ли мы ложить туда эту сущность или нет". Но репозитории НЕ мутируют состояние сущностей. Это просто склад. Склад который меняет состояние объектов там хранящихся — плохой склад.

Ладно, все это хорошо, вы тогда нам поведайте как лучше в fixture переписать это для начинающих
$blog1 = new Blog();
$blog1->setTitle('A day in paradise - A day with Symfony2');
$blog1->setBlog('Lorem ipsum dolor sit d us imperdiet justo scelerisque. Nulla consectetur...');
$blog1->setImage('beach.jpg');
$blog1->setAuthor('dsyph3r');
$blog1->setTags('symfony2, php, paradise, symblog');
$blog1->setCreated(new \DateTime());
$blog1->setUpdated($this->getCreated());
$manager->persist($blog1);
UFO just landed and posted this here
ну вот видите, сложно сказать что ваш вариант в данном случае лучше,
я же по памяти не помню какие у меня поля, а смотреть в описании к Blog не очень удобно.
Я это просто к тому, что все хорошо при определённых условиях — в статье к созданию блога сеттеры вполне уместны, да и не только.
я же по памяти не помню какие у меня поля

Вы как бы в таком случае не знаете и какие там есть сеттеры. А так вашу проблему с памятью в обоих случаях решает автокомлит.

как мне должен помочь пример выше и ваш с автокомплитом?

ну автокомплит, все такое… подсказывает вам не только имена методов но и что вы должны в них передавать. Это та штука благодаря которой мы можем писать штуки вроде ProblemFactoryFactory вместо ProblemSolver.

А такой:
$blog = new Blog(
  'A day in paradise - A day with Symfony2',
  'Lorem ipsum dolor sit d us imperdiet justo scelerisque. Nulla consectetur...',
  'beach.jpg',
   'dsyph3r',
   ['symfony2', 'php', 'paradise', 'symblog'] 
);

?
Его плюсы:
— не забудем проинициализировать обязательные свойства
— внутреннюю логику объекта типа установки времени создания/обновления инкапсулируем в объекте
— упрощаем объект, как минимум упрощая его публичный интерфейс, убирая из него геттеры
Его минусы:
— если будут другие варианты создания, то надо будет думать, как их элегантно совместить

Вот только ArrayCollection вне сущностей создаваться не должен. Они живут только внутри сущностей, наружу тоже не вылазят.

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


Словом инкапсуляция.

$blog = Blog::fromArray([
    'title' => 'A day in paradise - A day with Symfony2',
    'description' => 'Lorem ipsum dolor sit d us imperdiet justo scelerisque. Nulla consectetur...',
    'image' => FileReference::local('beach.jpg'),
    'tags' => $this->tags(['symfony', 'php', 'paradise', 'symblog']) // получить референсы на тэги например,
]);

$blogRepository->add($blog); // никаких entity manager-ов.

Вообще удивительно что в вашем примере вы не сами вызываете setId. Ну и в целом фикстуры отстой, фабрики фикстур рулят.

Fesor Спасибо, очень познавательно. С удовольствием бы прочитал серию статей на тему best practice по Симфони и Доктрине от вас.
я бы даже добавил: о реальных best practice, а не о тех которые на оф сайте размещены, слишком упрощены и направлены на популяризацию фреймворка. местами там полный бред в примерах.

Я согласен с тем, что мануалы нужны для того чтобы учиться, но мануалы и best practice это разные вещи, imho
местами там полный бред в примерах.


А можно чуть подробнее?
Ну возможно словом «бред» я высказался слишком резко, просто меня очень расстроил тот факт, что они в best practices специально упрощают реализацию поставленных задач, те же формы которые создаются прямо в контроллерах. По моему мнению, best practices нужны именно тогда когда ты уже примерно понимаешь как готовить фреймворк, а best practices должны показывать как это делать красиво и правильно, а в тех доках что я успел прочитать получается, что авторы задачу решают красиво и правильно, а то что напрямую к вопросу задачи не относится — там код уже идет по проще и не всегда красивый.

А еще вот есть видео https://youtu.be/Fu9j7w2hbW8?t=49 в тему

А, вы о симфони best practice, там да, очень много упрощений.

У меня в этом плане проще намного. Я не использую твиг (почти), не использую формы (вообще)… симфони валидатор натравливаю на запросы, и сейчас вообще планирую выкинуть потому что он ужесно неудобен для валидации динамических структур данных (либо надо просто написать нормальный валидатор вместо Collection, но это я еще думать буду).


То есть сами понимаете что уже добрых две трети "бест практис" я могу выкинуть. Использую только app bundle, и то только потому что так рекомендовано, мне же нравится идея и AppBundle убрать оставив только кернел.


По доктрине же все бэст практис от разработчиков доктрины вполне себе достойны внимания.

А где тогда будет код если останется только кернел?
Кстати по поводу бандлов возник вопрос. В симфони к бандлам не принято делать миграции если что-то должно храниться в базе? Пытался найти хоть какой-то захудалый пример чтоб подсмотреть, но ни одного не нашел.
А где тогда будет код если останется только кернел?


в app/ или src/ на ваш вкус, но в одном из этих двух. Обе директории не особо нужны. Это ж просто код. Большая часть кода который я пишу вообще о симфони ничего не знает.
В симфони к бандлам не принято делать миграции если что-то должно храниться в базе?


Вы не должны напрямую использовать сущности. У вас должен быть ваш наследник что бы обезапасить себя хоть немного от изменений в бандле. Хотя лично мое мнение — я категорически против использования сущностей предоставляемых бандлами (за редкими исключениями). Мне больше нравятся embeddable объекты. Их проще поддерживать.
понял, поэтому и не нашел видимо примеров. Спасибо за ответ.
В симфони к бандлам не принято делать миграции если что-то должно храниться в базе?

Не принято. Миграции относятся к глобальному уровню приложения, а не бандла. Их генерируют после установки/обновления бандла.

ну по поводу fromArray — это еще не факт что это самый удобный способ. Есть на самом деле куча вариантов (билдеры например), но суть у них все та же — не должно быть промежуточного состояния у самой сущности. И все что можно делать внутри (то есть хватает данных) лучше делать внутри. И смак весь в том, что все это никакого отношения к доктрине не имеет.


Бэст практис по симфони от меня смысла не имеет слушать поскольку я использую очень урезанный вариант оного и сильно кастомизированный под себя. По доктрине — почти все есть у ocramius-а в его докладе на эту тему. Я практически полностью согласен с его тезисами и у него как одного из разработчиков доктрины намного больше опыта что бы подобное вещать)

Как раз на больших проектах сеттеры усложняют жизнь. В маленьких, «одноразовых» можно себе позволить держать в голове правила типа «не вызывать setPassword с plain text аргументами», но в больших в лучшем случае много времени будет тратиться на передачу подобных правил другим членам команды. А обычно даже сам начинаешь забываешь правила типа «эти два сеттера всегда нужно вызывать вместе, причём в строгой последовательности», а потом тратишь кучу времени на локализацию плавающих багов.
А почему не используется ParamConverter для преобразования id в объект?
Мелкие замечания:
— protected свойства по умолчанию — нарушение инкапсуляции
— protected $comments = array(); — лучше в конструкторе держать

Расширение Doctrine Fixtures не поставляется с Symfony2, мы должны вручную его установить. К счастью это простая задача. Откройте файл composer.json расположенный в корне проекта и вставьте следующее:

К счатью есть ещё более простое решение:
composer require doctrine/doctrine-fixtures-bundle

А даже если нравиться править compose.json руками, то после него нужно вызывать composer install, чтобы только установить новые/измененные пакеты, а не проводить вдобавок к установке ещё и глобальное обновление.
    public function __construct()
    {
        $this->setCreated(new \DateTime());
        $this->setUpdated(new \DateTime());
    }

Плохая практика создавать два разных инстанса \DateTime, когда их значение должно быть одинаковым. Как минимум:
    public function __construct()
    {
        $this->setCreated(new \DateTime());
        $this->setUpdated(clone $this->getCreated());
    }
Так же плохая практика использовать DateTime, лучше использовать DateTimeImmutable
Да, забыл как-то, что это новый проект, вероятно исключительно под новые версии PHP

5.5 уже далеко не новая версия. Но вообще с DateTimeImmutable есть свои нюансы в контексте доктрины.

Sign up to leave a comment.

Articles