Symfony: фильтр связанных полей в формах

Symfony: опция query_builder для форм

Задача: вывести в форме только те связанные объекты OneToOne/OneToMany, которые ещё не были связаны с сущностями данного типа.
Например: при создании нового счёта вывести только те заказы, которые ещё не прикреплены ни к каким счетам (связь один-к-одному: счёт может относиться только к одному заказу). А при редактировании счета выводить все незанятые заказы, плюс тот, к которому счёт уже прикреплён.

Я взял код из рабочего проекта, немного упростил его и переименовал сущности. Подробнее (и понятнее) ниже.

Связи

Редактируются файлы:

  • MyBundle\Entity\Bill — сущность Счёт (Entity)
  • MyBundle\Form\BillType — класс формы счёта (FormView)

Сущность Bill (счёт) содержит связь с сущностью Order (заказ):

Если для этой связи в типе формы (метод buildForm) просто указать:

то для выбора будут выводиться все существующие заказы. Чтобы как-то отфильтровать этот список, нужно использовать параметр query_builder (3-й аргумент метода add), в которой доступен QueryBuilder (по-русски, наверное, подойдёт «построитель запроса»), с помощью которого можно изменять запрос, выбирающий связанные объекты.

Решение

  • Добавить для формы опции, указывающие на режим работы: редактирование или создание;
  • Создавать форму в контроллере с этими опциями;
  • В зависимости от режима, фильтровать список заказов:
    • Если счёт создаётся, то выводить все свободные заказы: все существующие минус те, которые уже были связаны с другими счетами;
    • Если счёт редактируется, то выводить все свободные заказы плюс тот, который уже связан с данным счётом;
    • Предусмотреть режим, в котором заказы никак не фильтруются.

Настройка формы

В методе setDefaultOptions объявлены опции form_mode и parent_id, которые будут доступны при создании формы:

Объявим константы для режимов в контроллере BillController (хорошей практикой будет объявить их в базовом контроллере вашего бандла, который наследуют все остальные контроллеры):

Создание формы в контроллере:

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

Внутри query_builder построитель запроса доступен только для сущности заказа, поэтому выборка происходит из него, и связывается «наоборот» с родительским, по сути, счётом.

SQL-вариант DQL-запроса из query_builder: обычное соединение с выборкой записей из левой таблицы, для которых нет связанных строк в правой таблице (LEFT JOIN ... WHERE b.order_id IS NULL)