Как вбудувати колекцію форм
Дата оновлення перекладу 2024-05-27
Как вбудувати колекцію форм
Форми Symfony можуть вбудовувати колекцію множини іншиї форм, що корисно для
редагування пов'язаних сутностей в одній формі. В цій статті ви створите форму
для редагування класу Task
, і прямо всередині тієї ж форми ви зможете
редагувати, створювати та видаляти багато об'єктів Tag
, пов'язаних з цим
класом Task.
Давайте почнемо зі створення сутності Task
:
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
// src/Entity/Task.php
namespace App\Entity;
use Doctrine\Common\Collections\Collection;
class Task
{
protected string $description;
protected Collection $tags;
public function __construct()
{
$this->tags = new ArrayCollection();
}
public function getDescription(): string
{
return $this->description;
}
public function setDescription(string $description): void
{
$this->description = $description;
}
public function getTags(): Collection
{
return $this->tags;
}
}
Note
ArrayCollection відноситься до Doctrine і схоже на PHP-масив, але надає безліч утилітарних методів.
Тепер, створіть клас Tag
. Як ви бачили вище, Task
може мати багато
об'єктів Tag
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
// src/Entity/Tag.php
namespace App\Entity;
class Tag
{
private string $name;
public function getName(): string
{
return $this->name;
}
public function setName(string $name): void
{
$this->name = $name;
}
}
Далі, створіть клас форми так, щоб об'єкт Tag
міг бути змінений користувачем:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
// src/Form/TagType.php
namespace App\Form;
use App\Entity\Tag;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class TagType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('name');
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Tag::class,
]);
}
}
Далі, давайте створимо форму для сутності Task
, використовуючи поле
CollectionType форми TagType
.
Це дозволить нам модифікувати всі елементи Tag
нашого Task
прямо
всередині самої форми Завдання:
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
// src/Form/TaskType.php
namespace App\Form;
use App\Entity\Task;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class TaskType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('description');
$builder->add('tags', CollectionType::class, [
'entry_type' => TagType::class,
'entry_options' => ['label' => false],
]);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Task::class,
]);
}
}
У вашому контролері ви створите нову форму з TaskType
:
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
// src/Controller/TaskController.php
namespace App\Controller;
use App\Entity\Tag;
use App\Entity\Task;
use App\Form\TaskType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class TaskController extends AbstractController
{
public function new(Request $request): Response
{
$task = new Task();
// фіктивний код - він тут просто для того, щоб Task мав якісь
// інашке це не буде цікавим прикладом
$tag1 = new Tag();
$tag1->setName('tag1');
$task->getTags()->add($tag1);
$tag2 = new Tag();
$tag2->setName('tag2');
$task->getTags()->add($tag2);
// кінець фіктивного коду
$form = $this->createForm(TaskType::class, $task);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// ... проведіть обробку форми, на кшталт збереження сутностей Task і Tag
}
return $this->renderForm('task/new.html.twig', [
'form' => $form,
]);
}
}
Тепер у шаблоні ви можете ітерувати поверх існуючих форм TagType
для того, щоб їх відобразити:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
{# templates/task/new.html.twig #}
{# ... #}
{{ form_start(form) }}
{{ form_row(form.description) }}
<h3>Tags</h3>
<ul class="tags">
{% for tag in form.tags %}
<li>{{ form_row(tag.name) }}</li>
{% endfor %}
</ul>
{{ form_end(form) }}
{# ... #}
Коли користувач відправляє форму, відправлені дані для поля tags
використовуються
для створення ArrayCollection
об'єктів Tag
. Потім колекція встановлюється в
поле tag
Task
і до неї можна отримати доступ через $task->getTags()
.
Поки все працює чудово, але лише для редагування існуючих тегів. Ми ще не можемо додавати нові або видаляти вже існуючі теги.
Caution
Ви можете вбудувати вкладену колекцію на стільки рівнів нижче,
наскільки вам цього захочеться. Але якщо ви використовуєте Xdebug,
то ви можете отримати помилку
Досягнено максимального рівня функціонування вкладеності '100', переривання!
.
Щоб виправити це, збільшіть PHP-налаштування xdebug.max_nesting_level
, або
відобразіть кожне поле форми вручну, використовуючи form_row()
замість
відображення всієї форми одразу (наприклад, form_widget(form)
)
Дозвіл на "нові" теги за допомогою "прототипу"
Раніше ви додали два теги до вашого завдання в контролері. Тепер дозвольте користувача додавати стільки форм тегів, скільки їм необхідно, прямо у браузері. Це потребує трохи коду JavaScript.
Tip
Замість того, щоб писати необхідний код JavaScript самостійно, ви можете використовувати Symfony UX, щоб реалізувати цю функцію лише за допомогою коду PHP та Twig. Дивіться статтю Демонстрація колекцій форм Symfony UX.
Але спочатку вам потрібно дати колекції форми знати, що замість двох тегів вона
отримає невідому кількість тегів. Інашке ви побачите помилку
"Ця форма не має містити додаткових полів". Це робиться за допомогою опції
allow_add
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
// src/Form/TaskType.php
// ...
public function buildForm(FormBuilderInterface $builder, array $options): void
{
// ...
$builder->add('tags', CollectionType::class, [
'entry_type' => TagType::class,
'entry_options' => ['label' => false],
'allow_add' => true,
]);
}
Опція allow_add
також робить змінну prototype
доступною для вас. Цей "прототип"
- це невеликий шаблон, який містить весь HTML, необхідний для динамічного створення
будь-яких нових форм "tag" за допомогою JavaScript.
Давайте почнемо з простого JavaScript (Vanilla JS) – якщо ви використовуєте Stimulus, див. нижче.
Щоб відобразити прототип, додайте наступний атрибут data-prototype
до існуючого
<ul>
у вашому шаблоні:
1 2 3 4 5
{# атрибут data-index обовʼязковий для коду JavaScript нижче #}
<ul class="tags"
data-index="{{ form.tags|length > 0 ? form.tags|last.vars.name + 1 : 0 }}"
data-prototype="{{ form_widget(form.tags.vars.prototype)|e('html_attr') }}"
></ul>
На відображеній сторінці, результат виглядатиме якось так:
1 2 3 4
<ul class="tags"
data-index="0"
data-prototype="<div><label class=" required">__name__</label><div id="task_tags___name__"><div><label for="task_tags___name___name" class=" required">Name</label><input type="text" id="task_tags___name___name" name="task[tags][__name__][name]" required="required" maxlength="255" /></div></div></div>"
></ul>
Тепер додайте кнопку, щоб динамічно додавати новий тег:
1
<button type="button" class="add_item_link" data-collection-holder-class="tags">Add a tag</button>
See also
Якщо ви хочете налаштувати HTML-код в прототипі, дивіться .
Tip
form.tags.vars.prototype
- це елемент форми, який виглядає та веде себе так
само, як і індивідуальні елементи form_widget(tag)
всередині вашого циклу
for
. Це означає, що ви можете викликати form_widget()
, form_row()
або
form_label()
. Ви навіть можете обирати відображення лише одного з його полів
(наприклад, поля name
):
1
{{ form_widget(form.tags.vars.prototype.name)|e }}
Note
Якщо ви одразу відобразите всю суб-форму "tags" (наприклад, form_row(form.tags)
),
атрибут data-prototype
буде автоматично доданий до div
, і вам необхідно
відповідно налаштувати наступний JavaScript.
Тепер додайте трохи JavaScript, щоб прочитати цей атрибут та динамічно додати
нові форми тегів, коли користувач натискає на посилання "Add a tag". Додайте
тег <script>
десь на вашій сторінці, щоб включити обовʼязкову функціональність
для JavaScript:
1 2 3 4 5
document
.querySelectorAll('.add_item_link')
.forEach(btn => {
btn.addEventListener("click", addFormToCollection)
});
Роботою функції addFormToCollection()
буде використовувати атрибут data-prototype
,
щоб динамічно додавати нову форму, коли переходять за її посиланням. HTML
data-prototype
містить елемент введення тегу text
з іменем task[tags][__name__][name]
та id task_tags___name___name
. __name__
- це маленький "заповнювач", який
ви заміните унікальним числом, що збільшується (наприклад, task[tags][3][name]
).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
const addFormToCollection = (e) => {
const collectionHolder = document.querySelector('.' + e.currentTarget.dataset.collectionHolderClass);
const item = document.createElement('li');
item.innerHTML = collectionHolder
.dataset
.prototype
.replace(
/__name__/g,
collectionHolder.dataset.index
);
collectionHolder.appendChild(item);
collectionHolder.dataset.index++;
};
Тепер, кожний раз, коли користувач натискає на посилання Add a tag
, на сторінці
з'являтиметься нова підформа. Коли форма буде відправлена, будь-які нові форми тегів
будуть конвертовані у нові об'єкти Tag
і додані у властивість tags
об'єкта
Task
.
See also
Ви можете знайти робочий приклад тут - JSFiddle.
JavaScript з Stimulus
Якщо ви використовуєте Stimulus, огорніть все у <div>
:
1 2 3 4 5 6 7
<div {{ stimulus_controller('form-collection') }}
data-form-collection-index-value="{{ form.tags|length > 0 ? form.tags|last.vars.name + 1 : 0 }}"
data-form-collection-prototype-value="{{ form_widget(form.tags.vars.prototype)|e('html_attr') }}"
>
<ul {{ stimulus_target('form-collection', 'collectionContainer') }}></ul>
<button type="button" {{ stimulus_action('form-collection', 'addCollectionElement') }}>Add a tag</button>
</div>
Потім створіть контролер:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
// assets/controllers/form-collection_controller.js
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
static targets = ["collectionContainer"]
static values = {
index : Number,
prototype: String,
}
addCollectionElement(event)
{
const item = document.createElement('li');
item.innerHTML = this.prototypeValue.replace(/__name__/g, this.indexValue);
this.collectionContainerTarget.appendChild(item);
this.indexValue++;
}
}
Робота з новими тегами в PHP
Щоб полегшити управління новими тегами, додайте методи "adder" та "remover"
для тегів у класі Task
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
// src/Entity/Task.php
namespace App\Entity;
// ...
class Task
{
// ...
public function addTag(Tag $tag): void
{
$this->tags->add($tag);
}
public function removeTag(Tag $tag): void
{
// ...
}
}
Далі, додайте опцію by_reference
в поле tags
і встановіть її як false
:
1 2 3 4 5 6 7 8 9 10 11 12
// src/Form/TaskType.php
// ...
public function buildForm(FormBuilderInterface $builder, array $options): void
{
// ...
$builder->add('tags', CollectionType::class, [
// ...
'by_reference' => false,
]);
}
З цими двома змінами, при відправленні форми, кожний новий об'єкт Tag
буде
додаватися в клас Task
, шляхом виклику методу addTag()
. До цього вони
додавалися формою внутрішньо, шляхом виклику $task->getTags()->add($tag)
.
Це було нормально, використання методу "adder" робить керування новими об'єктами
Tag
легшим (особливо, якщо ви використовуєте Doctrine, про що ви дізнаєтеся далі!).
Caution
Вам потрібно створити обидва методи: addTag()
та removeTag()
, інакше
форма всеодно буде використовувати setTag()
навіть якщо by_reference
у
значенні false
. Ви дізнаєтеся більше про метод removeTag()
далі в цій статті.
Caution
Symfony може робити перетворення багато-до-одного (наприклад, із властивості
tags
у метод addTag()
) тільки для англійських слів. Код, написаний
будь-якою іншою мовою, не працюватиме так, як очікується.
Дозвіл на видалення тегів
Наступним кроком є дозвіл на видалення конкретного предмету в колекції. Рішення схоже на дозвіл на додавання тегів.
Почніть з додавання опції allow_delete
у форму Type:
1 2 3 4 5 6 7 8 9 10 11 12
// src/Form/TaskType.php
// ...
public function buildForm(FormBuilderInterface $builder, array $options): void
{
// ...
$builder->add('tags', CollectionType::class, [
// ...
'allow_delete' => true,
]);
}
Тепер вам потрібно розмістити деякий код в методі removeTag()
у Task
:
1 2 3 4 5 6 7 8 9 10 11 12
// src/Entity/Task.php
// ...
class Task
{
// ...
public function removeTag(Tag $tag): void
{
$this->tags->removeElement($tag);
}
}
Опція allow_delete
означає, що якщо предмет колекції не надіслано при
відправці, пов'язані дані видаляються з колекції на сервері. Для того, щоб
це працювало в HTML формі, вам необхідно видалити елемент DOM у видаленому
об'єкті колекції до відправки форми.
Спочатку додайте посилання "видалити цей тег" до кожного тегу форми:
1 2 3 4 5 6 7 8 9 10 11 12 13
const tags = document.querySelectorAll('ul.tags')
tags.forEach((tag) => {
addTagFormDeleteLink(tag)
})
// ... залишок блоку, описаного вище
function addFormToCollection() {
// ...
// додати посилання на видалення до нової форми
addTagFormDeleteLink(item);
}
Функція addTagFormDeleteLink()
виглядатиме приблизно так:
1 2 3 4 5 6 7 8 9 10 11 12 13
const addTagFormDeleteLink = (tagFormLi) => {
const removeFormButton = document.createElement('button')
removeFormButton.classList
removeFormButton.innerText = 'Delete this tag'
tagFormLi.append(removeFormButton);
removeFormButton.addEventListener('click', (e) => {
e.preventDefault()
// видалити li у формах тегів
tagFormLi.remove();
});
}
Коли форма тегів видалена з DOM і відправлена, видалений об'єкт Tag
не буде
додано в колекцію, передану в setTags()
. В залежності від вашого рівня зберігання,
це може бути (не)достатнім для видалення відносин між видаленим Tag
та об'єктом
Task
.
See also
Cпільнота Symfony створило деякі пакети JavaScript, які надають функціонал, необхідний для додавання, редагування та видалення елементів колекції. Розгляньте пакет @a2lix/symfony-collection для сучасних браузерів та пакет symfony-collection, заснований на jQuery, для решти браузерів.