Не используйте Сущности в формах Symfony. Используйте лучше объекты запросы.

symfony

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

ps. это мой вольный перевод статьи https://blog.martinhujer.cz/symfony-forms-with-request-objects/ просто потому, что мне захотелось. 

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

И я не думаю, что это хорошая идея!

Почему?

1. Сущность должна быть всегда валидной.

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

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

Так же не трудно представить ситуацию в которой можно получить серьезные проблемы. Если сущность уже добавлена в менеджер сущностей (т.к. это экшн updateAction) и если где-то в коде присутствует вызов $entityManager->flush(), то в конечном итоге невалидные данные будут сохранены в базу данных.

2. Изменения! Изменения! Изменения!

Мы можем быть уверены только в одной вещи в разработке — это изменения. Внезапно будет нужно поменять структуру или может разделить её на 2 шага. Тогда поля не будут на 100% соответствовать свойствам сущности.

3. Разделение слоев.

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

 

Что мы можем делать вместо использования сущностей в формах? Документация Symfony описывает подход с использованием массивов. Этот подход основан на переменной и имеет свои недостатки. Один из которых то, что разработчк не будет иметь автодополнение в IDE для данных формы. И будет трудно анализировать массив анализатором кода, таким как PHPStan.

Есть еще один подход, который я предпочитаю.

Собственные классы данных для победы.

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

Взглянем на этот пример

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

Следующий шаг — использовать этот объект в контроллере. Его можно применять так же, как сущность (следующий пример говорит сам за себя)

И для завершенности вот код для ArticleFormType

Класс называется CreateArticleRequest потому, что он представляет запрос на создание Article. Возможно так же понадобится класс UpdateArticleRequest для обновления с другим набором сущностей (в некоторых случаях эти запросы могут быть одинаковые и достаточно одного ArticleRequest класса).

Суффикс *Request может вызвать некий конфуз с классом Request, который представляет собой HTTP  запрос. Если это ваш случай, просто переименуйте суффикс на *Data и класс будет называться CreateArticleData.

А что на счет формы для обновления ?

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

Можно заметить, что поле $publishDate отсутствует. Но, что более важно, у нас есть новый метод fromArticle(Article $article);. Он позволяет получить данные из существуюшего источника.

Посмотрим на пример экшена updateAction() чтобы увидеть как это будет в контроллере

Можно подумать, что тут гораздо больше кода для написания. Я соглашусь, но все же уверен, что это стоит того в долгой перспективе. Если приложение содержит некую бизнес логику и это не просто CRUD, могут понадобится разные поля и наборы валидации для создания и обновления. И вот тут то и пригодится весь код, написанный заранее.

Заключение

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

И здесь две следующие основные мысли:

  1. Всегда сохраняйте разделение слоев
  2. Не стоит слепо следовать документации или за другими разработчиками

Если вы используете сущности в своих формах, то с какими проблемами вы сталкивались?
Так же вот ссылки на две статьи по теме: Avoiding Entities in Forms и Rethinking Form Development (Iltar van der Berg).

 

з.ы. от себя добавлю, что очень много проблем с тайпхинтами начиная с php 7.0. Как раз потому, что в сущности могут быть указаны типы параметров на сеттеры, а форма пытается положить string в int или null приходит там, где маппинг это запрещает. В 7.1 пришлось везде использовать ?string, ?int и т.д, чтобы только сущности работали в Symfony формах. Подход с CUSTOM DATA OBJECT полностью решит эту проблему, т.к. данные из CUSTOM DATA OBJECT будут передаваться в сущность уже в сервисе после валидации. Все это напомнило мне старый добрый подход с DTO. 

  • Denis Kazimirov

    Вместо статического метода UpdateArticleRequest::fromArticle на мой взгляд лучше использовать обычный конструктор, в который и передавать обязательным параметром Article.

    • Можно и так, главное не сохранять там ссылку на Article, а только извлечь поля.
      На самом деле тогда потеряется гибкость. Что, если мы захотим заполнять UpdateArticleRequest из другого источника? Можно добавить еще один фабричный метод fromArticleDTO, например. Если же вшить Article в конструктор, то придется сначала построить Article из DTO и потом уже только UpdateArticleRequest, что неудобно.

  • Вадим Бондаренко

    Что то я не сильно понимаю профита. Я понимаю что вместо использования изначального класса был создан класс «обёртка» который проверяется классом валидатором. Я не понимаю в чем разница если я к примеру буду использовать класса валидатор прямо в сущность, а формы создавать как класс и вызывать.

    /**
    * Цель 1
    * @var float
    *
    * @ORMColumn(type=»decimal», precision = 10, scale = 3, options={«default» = 0})
    * @AssertNotBlank()
    */
    protected $target1;

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

      • Вадим Бондаренко

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

        • Это описано в статье как раз. Если валидация выполняется на сущности, то все данные из формы сначала инжектятся в сущность и только потом валидируются.
          Именно так мы получаем в коде невалидную сущность.
          Т.е. валидация выполняется не до инжекта данных, а после. Это важно знать, что при вызове $form->isValid() заполняется сущность, где и прописаны правила валидации в аннотоациях.
          И если случайно далее по коду будет выполнен flush() то все эти невалидные данные могут быть записаны в БД.

  • Peter Gribanov

    Вы похоже не знаете что такое VO и чем VO отличается от DTO.
    Главное отличие DTO от VO в том что VO неизменяемые, а DTO не содержат никакой логики, только данные.
    Даже в оригинальной статье говорится про DTO.

    • Прекрасно знаю, ткните мне в место, где вам кажется, что не знаю? Надо же свои заявления обосновывать.

    • Прекрасно знаю, ткните мне в место, где вам кажется, что не знаю? Надо же свои заявления обосновывать.

      Кстати о DTO в оригинале не говорится напрямую, а о чем-то похожем на DTO.
      Его даже вот тут поправляют — https://blog.martinhujer.cz/symfony-forms-with-request-objects/#comment-3485859179
      А в ps я написал, что это похоже на DTO. Так в чем же ошибка?

      Возможно неудачный перевод для request object из статьи. Надо было назвать объект-запрос.

      • Peter Gribanov

        Ошибка у вас в том, что это не объект похожий на DTO, а это и есть DTO.

        В оригинальной статье, автор говорит об Data Objects, что как бы должно нас наводить на мысль что это Data Transfer Object. По сути и назначению это именно DTO.

        Автор оригинала не упоминал Value object в статье. Только в комментариях, что нас тоже должно наводить на мысль, что это не VO.

        > However, it can be solved by creating another Value Object which would be created from the request object

        Да, автор оригинала использует термин Request object, но это просто название DTO. Я в проектах использую CQRS подход и в контексте вашей статьи у меня будет Command object который является DTO.

        Рекомендую к прочтению:
        https://habrahabr.ru/post/268371/
        http://www.adam-bien.com/roller/abien/entry/value_object_vs_data_transfer
        https://en.wikipedia.org/wiki/Value_object

        Ну и примеры VO:
        https://github.com/moneyphp/money
        https://github.com/thephpleague/period
        https://github.com/marc-mabe/php-enum
        https://github.com/gpslab/interval

        • Я тоже использую CQRS. И DDD + Hexagonal architecture.
          И можно сделать выводы, что отлично знаю что есть VO, что DTO. Оба по сути POPO, но разное применение конечно. Не надо мне примеров и рекомендаций)) я могу своих дать столько же.

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

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

          Я потому и добавил в PS про DTO, что у автора про это ничего нет и я подумал то же самое, что и вы, что автор не знает понятий.

          Так что ваш адресат это автор статьи, не я. Теперь надеюсь понятно?

        • Вот перевод другой статьи, где автор прямо говорит про DTO.
          http://seyferseed.ru/ru/php/symfony-framework-2/izbeganie-sushhnostej-v-formah-symfony-pereosmyslenie-razrabotki-form-symfony.html

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

      Это чисто русский подход вот так в лоб говорить по сути «ты дурак». Пожив в Германии я от этого отучился и блог стараюсь перевести в англоязынчй сегмент, т.к. культура общения выше. ;)

  • Pingback: Избегание сущностей в формах Symfony и переосмысление разработки форм Symfony - Космонавт в лодке()

  • Vladimir Luchaninov

    Перевод официальной документации — https://symfony.com.ua/doc/current/forms.html