Задача: вывести в форме только те связанные объекты OneToOne/OneToMany, которые ещё не были связаны с сущностями данного типа.
Например: при создании нового счёта вывести только те заказы, которые ещё не прикреплены ни к каким счетам (связь один-к-одному: счёт может относиться только к одному заказу). А при редактировании счета выводить все незанятые заказы, плюс тот, к которому счёт уже прикреплён.
Я взял код из рабочего проекта, немного упростил его и переименовал сущности. Подробнее (и понятнее) ниже.
Связи
Редактируются файлы:
MyBundle\Entity\Bill
— сущность Счёт (Entity
)MyBundle\Form\BillType
— класс формы счёта (FormView
)
Сущность Bill
(счёт) содержит связь с сущностью Order
(заказ):
1 2 3 4 5 6 | // MyBundle\Entity\Bill /** * @ORM\OneToOne(targetEntity="MyBundle\Entity\Order", cascade={"persist"}) * @var Order */ private $order; |
Если для этой связи в типе формы (метод buildForm
) просто указать:
1 | $formBuilder->add('order', EntityType::class) |
то для выбора будут выводиться все существующие заказы. Чтобы как-то отфильтровать этот список, нужно использовать параметр query_builder
(3-й аргумент метода add
), в которой доступен QueryBuilder
(по-русски, наверное, подойдёт «построитель запроса»), с помощью которого можно изменять запрос, выбирающий связанные объекты.
Решение
- Добавить для формы опции, указывающие на режим работы: редактирование или создание;
- Создавать форму в контроллере с этими опциями;
- В зависимости от режима, фильтровать список заказов:
- Если счёт создаётся, то выводить все свободные заказы: все существующие минус те, которые уже были связаны с другими счетами;
- Если счёт редактируется, то выводить все свободные заказы плюс тот, который уже связан с данным счётом;
- Предусмотреть режим, в котором заказы никак не фильтруются.
Настройка формы
В методе setDefaultOptions
объявлены опции form_mode
и parent_id
, которые будут доступны при создании формы:
1 2 3 4 5 6 | // MyBundle\Form\BillType $resolver->setDefaults([ 'data_class' => 'MyBundle\Entity\Bill', 'form_mode' => null, 'parent_id' => null, ]); |
Объявим константы для режимов в контроллере BillController
(хорошей практикой будет объявить их в базовом контроллере вашего бандла, который наследуют все остальные контроллеры):
1 2 3 4 5 | // MyBundle\Controller\BillController const MODE_EDIT = 'edit'; const MODE_SHOW = 'show'; const MODE_CREATE = 'create'; const MODE_RESET = 'reset'; |
Создание формы в контроллере:
1 2 3 4 5 6 | // MyBundle\Controller\BillController $form = $this->createForm($formType, $entity, [ 'form_mode' => BillController::MODE_EDIT, // используется только в MODE_EDIT 'parent_id' => $entity->getId(), ]); |
В классе формы для счёта, в опции query_builder
мы проверяем переданные параметры, и возвращаем нужный QueryBuilder
с изменённым запросом на выборку заказов.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | class BillType extends AbstractType { /** * {@inheritdoc} */ public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('date') // ... ->add('order', EntityType::class, [ 'class' => 'MyBundle\Entity\Order', 'query_builder' => function (EntityRepository $repository) use ($options) { $qb = $repository->createQueryBuilder('e'); switch ($options['form_mode']) { // Редактировать case BillController::MODE_EDIT: // Все незанятые + текущий id // id счёта выбирается как HIDDEN для того, чтобы можно было сравнивать // это значение с переданным parent_id $qb->select('o, b.id AS HIDDEN') ->leftJoin('MyBundle\Entity\Bill', 'b', 'WITH', 'b.pipe=o') ->where('o.closed = 0') ->andWhere($qb->expr()->isNull('b.pipe')) ->orWhere('b.id = :id') ->orderBy('o.id', 'DESC'); $qb->setParameter(':id', $options['parent_id']); break; // Ничего не менять case BillController::MODE_RESET: break; // Создать case BillController::MODE_CREATE: default: // Все незанятые $qb->leftJoin('MyBundle\Entity\Bill', 'b', 'WITH', 'b.order=o') ->where('o.closed = 0') ->andWhere($qb->expr()->isNull('b.pipe')) ->orderBy('o.id', 'DESC'); break; } return $qb; }, ]); } // Остальные методы } |
Внутри query_builder
построитель запроса доступен только для сущности заказа, поэтому выборка происходит из него, и связывается «наоборот» с родительским, по сути, счётом.
SQL-вариант DQL-запроса из query_builder
: обычное соединение с выборкой записей из левой таблицы, для которых нет связанных строк в правой таблице (LEFT JOIN ... WHERE b.order_id IS NULL
)
1 2 3 4 5 | SELECT b.id bill_id, o.id order_id FROM order o LEFT JOIN bill b ON b.order_id=o.id WHERE b.order_id IS NULL OR b.id=2 -- parent_id |