symfony4 Form -- Форма с вложенной коллекцией форм. Создание, сохранение. Вложенные сущности, "документы", иерархия
Primary tabs
В Symfony существует компонент Forms, предназначенный для создания форм, основанных на сущностях (Entity). Если корректно написать класс формы, то можно максимально упростить рендеринг, сохранение и автозаполнение формы. Но чем сложнее поставленная задача, тем более неоднозначно решение.
Здесь, например, можно посмотреть, как сохранять non-mapped поля при автоматическом сохранении формы.
Задача
В данной статье мы рассмотрим, как АВТОМАТИЧЕСКИ (не каждое поле отдельно, а сущность целиком) вывести форму для создания сущности (Entity), а также вложенной в неё коллекции (ArrayCollection), и сохранить её.
Решение
Итак, у нас есть 2 связанные сущности: Product и ProductField. Связаны они Один-ко-Многим.
- Часть сущностей (нас интересуют в основном поля связи). Для синхронного сохранения вложенной сущности, добавим полю productFields сущности Product опцию cascade={"persist", "remove"}:
// Product.php: // ... здесь могут быть и другие поля /** * @var Collection * * @ORM\OneToMany(targetEntity="ProductField", mappedBy="product", cascade={"persist", "remove"}) */ protected $productFields; public function __construct() { $this->productFields = new ArrayCollection(); }
// ProductField.php: // ... здесь могут быть и другие поля /** * @var Product * * @ORM\ManyToOne(targetEntity="Product", inversedBy="productFields") * @ORM\JoinColumn(name="product_id", referencedColumnName="id", onDelete="CASCADE") */ protected $product;
И для сохранения внешнего ключа в таблице product_field, добавим установку связи с продуктом (обратная связь: ProductField->setProduct) при создании связи Product->addProductField.
Внимание! Это обязательно, иначе ProductField добавится в коллекцию, но не будет установлен внешний ключ в таблице БД!// Product.php(методы добавления, удаления и получения коллекции $productFields): /** * @param ProductField $productField * * @return self */ public function addProductField(ProductField $productField): self { if (!$this->productFields->contains($productField)) { $productField->setProduct($this); //вот эта строка важна $this->productFields[] = $productField; } return $this; } /** * @param ProductField $productField * * @return self */ public function removeProductField(ProductField $productField): self { if ($this->productFields->contains($productField)) { $this->productFields->removeElement($productField); } return $this; } /** * @return Collection | ProductField[] */ public function getProductFields(): Collection { return $this->productFields; }
- В контроллере будет всего лишь:
// ProductController.php // создаём форму $product = new Product(); $form = $this->createForm(ProductType::class, $product); $form->handleRequest($request); // если не нажата кнопка сохранить, печатаем форму на экран if (!$form->isSubmitted()) { return $this->renderProductUpdateView($request); } // если данные невалидны, бросаем исключение if (!$form->isValid()) { throw new ValidatorException('Введённые данные некорректны'); } // сохраняем сущность вместе со вложенной $em = $this->getDoctrine()->getManager(); $em->persist($product); $em->flush();
- Формы сущностей. Чтобы Symfony понимал, в какую сущность сохранять данные, добавляем метод configureOptions(). Чтобы поля для вложенной сущности подгружались в форму, добавляем атрибуты CollectionType-поля: by_reference, prototype, allow_add и allow_delete. Взято из оф. документации Симфони:
// ProductType.php: /** * @param FormBuilderInterface $builder * @param array $options */ public function buildForm(FormBuilderInterface $builder, array $options): void { $builder->add('name', TextType::class, [ 'label' => 'Название', 'constraints' => [ new Type([ 'type' => 'string', ]), ], ]) ->add('productFields', CollectionType::class, [ 'label' => false, 'entry_type' => ProductFieldType::class, 'by_reference' => false, 'prototype' => true, 'allow_add' => true, 'allow_delete' => true, ]) ->add('submit', SubmitType::class, [ 'label' => 'Сохранить изменения', ]); } /** * @param OptionsResolver $resolver */ public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'data_class' => Product::class, ]); }
// ProductFieldType.php: /** * @param FormBuilderInterface $builder * @param array $options */ public function buildForm(FormBuilderInterface $builder, array $options): void { $builder->add('name', TextType::class, [ 'label' => 'Свойство', 'constraints' => [ new Type([ 'type' => 'string', ]), ], ]) ->add('value', TextType::class, [ 'label' => 'Значение', 'constraints' => [ new Type([ 'type' => 'string', ]), ], ]); } /** * @param OptionsResolver $resolver */ public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'data_class' => ProductField::class, ]); }
- В шаблоне. Выводим вложенные сущности в виде списка, а также добавляем атрибуты для подгрузки неограниченного количества пунктов списка:
//product_update.html.twig: {{ form_start(productForm, {'attr': {'method': 'post'}}) }} {{ form_row(productForm.submit, {'attr': {'formnovalidate': 'true'}}) }} {{ form_row(productForm.name) }} <ul id="email-fields-list" data-prototype="{{ form_widget(productForm.productFields.vars.prototype)|e }}" data-widget-tags="{{ '<li></li>'|e }}"> {% for productField in productForm.productFields %} <li> {{ form_row(productField.name) }} {{ form_row(productField.value) }} </li> {% endfor %} </ul> <a href="#" class="add-another-collection-widget" data-list="#email-fields-list">Добавить свойство товара</a> {{ form_end(productForm) }}
- js-код для подгрузки неограниченного количества вложенных форм (не забудьте подключить его в своём шаблоне):
// add-collection-widget.js: jQuery(document).ready(function () { jQuery('.add-another-collection-widget').click(function (e) { e.preventDefault(); var list = jQuery(jQuery(this).attr('data-list')); // Try to find the counter of the list var counter = list.data('widget-counter') | list.children().length; // If the counter does not exist, use the length of the list if (!counter) { counter = list.children().length; } // grab the prototype template var newWidget = list.attr('data-prototype'); // replace the "__name__" used in the id and name of the prototype // with a number that's unique to your emails // end name attribute looks like name="contact[emails][2]" newWidget = newWidget.replace(/__name__/g, counter); // Increase the counter counter++; // And store it, the length cannot be used if deleting widgets is allowed list.data(' widget-counter', counter); // create a new list element and add it to the list var newElem = jQuery(list.attr('data-widget-tags')).html(newWidget); newElem.appendTo(list); }); });
Чтобы сделать из формы создания форму редактирования, просто передайте вторым аргументом в метод createForm() не пустой объект сущности, а тот, который надо изменить.
Вот и всё) Спасибо всем, кто писал хоть что-то на просторах интернета на эту тему) Без вас было бы слишком тяжело использовать "лёгкий" способ сохранения сущностей в Symfony :))
Источники
- Официальная документация Symfony. Лучшие практики по созданию форм.
- Официальная документация Symfony. Создание форм при наличии ассоциативных связей.
- Официальная документация Symfony. Как встроить коллекцию в форму.
- Вариант полуавтоматического сохранения коллекции
- Различные попытки (безуспешные) сохранить таки форму с формой внутри (описание на русском)
- Документация Doctrine. Работа с ассоциациями - как написать сущности.
- Пример cascade persist (и просто хороший сайт по Symfony)
- Log in to post comments
- 5080 reads