Skip to content

Отримання відгуків за допомогою форм

Час дозволити нашим відвідувачам залишати відгуки про конференції. Вони зможуть додавати свої коментарі за допомогою HTML-форми.

Генерування типу форми

Використовуйте бандл Maker для генерування класу форми:

1
$ symfony console make:form CommentFormType Comment
1
2
3
4
5
6
7
8
created: src/Form/CommentFormType.php


 Success!


Next: Add fields to your form and start using it.
Find the documentation at https://symfony.com/doc/current/forms.html

Клас App\Form\CommentFormType визначає форму для сутності App\Entity\Comment:

src/App/Form/CommentFormType.php
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
namespace App\Form;

use App\Entity\Comment;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class CommentFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('author')
            ->add('text')
            ->add('email')
            ->add('createdAt')
            ->add('photoFilename')
            ->add('conference')
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => Comment::class,
        ]);
    }
}

Тип форми описує поля форми, пов'язані з моделлю. Він виконує перетворення даних між представленими даними й властивостями класу моделі. За замовчуванням Symfony використовує метадані з сутності Comment (як-от метадані Doctrine) для визначення конфігурації кожного поля. Наприклад, поле text відмальовується як textarea, оскільки воно використовує більший стовпчик у базі даних.

Відображення форми

Щоб відобразити форму користувачеві, створіть форму в контролері та передайте її до шаблону:

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
--- a/src/Controller/ConferenceController.php
+++ b/src/Controller/ConferenceController.php
@@ -2,7 +2,9 @@

 namespace App\Controller;

+use App\Entity\Comment;
 use App\Entity\Conference;
+use App\Form\CommentFormType;
 use App\Repository\CommentRepository;
 use App\Repository\ConferenceRepository;
 use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@@ -31,6 +33,9 @@ class ConferenceController extends AbstractController
     #[Route('/conference/{slug}', name: 'conference')]
     public function show(Request $request, Conference $conference, CommentRepository $commentRepository): Response
     {
+        $comment = new Comment();
+        $form = $this->createForm(CommentFormType::class, $comment);
+
         $offset = max(0, $request->query->getInt('offset', 0));
         $paginator = $commentRepository->getCommentPaginator($conference, $offset);

@@ -39,6 +44,7 @@ class ConferenceController extends AbstractController
             'comments' => $paginator,
             'previous' => $offset - CommentRepository::PAGINATOR_PER_PAGE,
             'next' => min(count($paginator), $offset + CommentRepository::PAGINATOR_PER_PAGE),
+            'comment_form' => $form->createView(),
         ]));
     }
 }

Ніколи не слід створювати екземпляр типу форми безпосередньо. Натомість використовуйте метод createForm(). Цей метод є частиною AbstractController і полегшує створення форм.

Передаючи форму в шаблон, використовуйте метод createView() для перетворення даних у формат, прийнятний для шаблонів.

Відображення форми в шаблоні можна здійснити за допомогою функції Twig form:

1
2
3
4
5
6
7
8
9
10
11
--- a/templates/conference/show.html.twig
+++ b/templates/conference/show.html.twig
@@ -30,4 +30,8 @@
     {% else %}
         <div>No comments have been posted yet for this conference.</div>
     {% endif %}
+
+    <h2>Add your own feedback</h2>
+
+    {{ form(comment_form) }}
 {% endblock %}

Після оновлення сторінки конференції в браузері, зверніть увагу, що кожне поле форми використовує правильний HTML-елемент (тип даних є похідним від моделі)

/conference/amsterdam-2019

Функція form() генерує HTML-форму на основі інформації, що визначена в типі форми. Вона також додає атрибут enctype=multipart/form-data до тегу <form>, як цього вимагає поле для завантаження файлу. Навіть більше, вона піклується про відображення повідомлень, коли відправлені дані містять помилки. Все можна налаштувати, перевизначивши шаблони за замовчуванням, але для цього проекту вони нам не знадобляться.

Кастомізація типу форми

Не зважаючи на те, що поля форми налаштовані на основі моделі-аналога, ви можете налаштувати конфігурацію за замовчуванням безпосередньо в класі типу форми:

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
--- a/src/Form/CommentFormType.php
+++ b/src/Form/CommentFormType.php
@@ -4,20 +4,31 @@ namespace App\Form;

 use App\Entity\Comment;
 use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\Extension\Core\Type\EmailType;
+use Symfony\Component\Form\Extension\Core\Type\FileType;
+use Symfony\Component\Form\Extension\Core\Type\SubmitType;
 use Symfony\Component\Form\FormBuilderInterface;
 use Symfony\Component\OptionsResolver\OptionsResolver;
+use Symfony\Component\Validator\Constraints\Image;

 class CommentFormType extends AbstractType
 {
     public function buildForm(FormBuilderInterface $builder, array $options): void
     {
         $builder
-            ->add('author')
+            ->add('author', null, [
+                'label' => 'Your name',
+            ])
             ->add('text')
-            ->add('email')
-            ->add('createdAt')
-            ->add('photoFilename')
-            ->add('conference')
+            ->add('email', EmailType::class)
+            ->add('photo', FileType::class, [
+                'required' => false,
+                'mapped' => false,
+                'constraints' => [
+                    new Image(['maxSize' => '1024k'])
+                ],
+            ])
+            ->add('submit', SubmitType::class)
         ;
     }

Зверніть увагу, що ми додали кнопку відправки форми (це дозволяє нам використовувати простий вираз {{ form(comment_form) }} у шаблоні).

Деякі поля не можна налаштувати автоматично, як-от photoFilename. Сутність Comment має зберегти лише ім'я файлу фотографії, але форма має також подбати про завантаження файлу. Щоб впоратися з цим завданням, ми додали поле з назвою photo як поле з відключеною опцією mapped: воно не буде зіставлено ні з однією властивістю сутності Comment. Ми будемо керувати ним вручну, щоб реалізувати певну логіку (як-от збереження завантаженої фотографії на диск).

Як приклад кастомізації, ми також змінили мітку за замовчуванням, для деяких полів.

/conference/amsterdam-2019

Валідація моделей

Тип форми налаштовує зовнішню відмальовку форми (використовуючи валідацію HTML5). Ось згенерована HTML форма:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<form name="comment_form" method="post" enctype="multipart/form-data">
    <div id="comment_form">
        <div >
            <label for="comment_form_author" class="required">Your name</label>
            <input type="text" id="comment_form_author" name="comment_form[author]" required="required" maxlength="255" />
        </div>
        <div >
            <label for="comment_form_text" class="required">Text</label>
            <textarea id="comment_form_text" name="comment_form[text]" required="required"></textarea>
        </div>
        <div >
            <label for="comment_form_email" class="required">Email</label>
            <input type="email" id="comment_form_email" name="comment_form[email]" required="required" />
        </div>
        <div >
            <label for="comment_form_photo">Photo</label>
            <input type="file" id="comment_form_photo" name="comment_form[photo]" />
        </div>
        <div >
            <button type="submit" id="comment_form_submit" name="comment_form[submit]">Submit</button>
        </div>
        <input type="hidden" id="comment_form__token" name="comment_form[_token]" value="DwqsEanxc48jofxsqbGBVLQBqlVJ_Tg4u9-BL1Hjgac" />
    </div>
</form>

Форма використовує поле з типом email для введення електронної пошти й додає атрибут required для більшості полів. Зверніть увагу, що форма також містить приховане поле _token, що використовується для захисту форми від CSRF-атак.

Але якщо відправка форми проходить без HTML-валідації (за допомогою HTTP-клієнта, який не застосовує правила перевірки, як-от cURL), невалідні дані можуть потрапити на сервер.

Нам також потрібно додати деякі обмеження (правила) валідації до моделі даних Comment':

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
--- a/src/Entity/Comment.php
+++ b/src/Entity/Comment.php
@@ -4,6 +4,7 @@ namespace App\Entity;

 use App\Repository\CommentRepository;
 use Doctrine\ORM\Mapping as ORM;
+use Symfony\Component\Validator\Constraints as Assert;

 #[ORM\Entity(repositoryClass: CommentRepository::class)]
 #[ORM\HasLifecycleCallbacks]
@@ -15,12 +16,16 @@ class Comment
     private $id;

     #[ORM\Column(type: 'string', length: 255)]
+    #[Assert\NotBlank]
     private $author;

     #[ORM\Column(type: 'text')]
+    #[Assert\NotBlank]
     private $text;

     #[ORM\Column(type: 'string', length: 255)]
+    #[Assert\NotBlank]
+    #[Assert\Email]
     private $email;

     #[ORM\Column(type: 'datetime_immutable')]

Обробка форми

Коду, що ми написали, достатньо для відображення форми.

Тепер нам необхідно реалізувати обробку даних форми та зберегти інформацію до бази даних у контролері:

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
--- a/src/Controller/ConferenceController.php
+++ b/src/Controller/ConferenceController.php
@@ -7,6 +7,7 @@ use App\Entity\Conference;
 use App\Form\CommentFormType;
 use App\Repository\CommentRepository;
 use App\Repository\ConferenceRepository;
+use Doctrine\ORM\EntityManagerInterface;
 use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
@@ -16,10 +17,12 @@ use Twig\Environment;
 class ConferenceController extends AbstractController
 {
     private $twig;
+    private $entityManager;

-    public function __construct(Environment $twig)
+    public function __construct(Environment $twig, EntityManagerInterface $entityManager)
     {
         $this->twig = $twig;
+        $this->entityManager = $entityManager;
     }

     #[Route('/', name: 'homepage')]
@@ -35,6 +38,15 @@ class ConferenceController extends AbstractController
     {
         $comment = new Comment();
         $form = $this->createForm(CommentFormType::class, $comment);
+        $form->handleRequest($request);
+        if ($form->isSubmitted() && $form->isValid()) {
+            $comment->setConference($conference);
+
+            $this->entityManager->persist($comment);
+            $this->entityManager->flush();
+
+            return $this->redirectToRoute('conference', ['slug' => $conference->getSlug()]);
+        }

         $offset = max(0, $request->query->getInt('offset', 0));
         $paginator = $commentRepository->getCommentPaginator($conference, $offset);

Після відправки форми, сутність Comment оновлюється відповідно до поданих даних.

Конференція має бути такою ж, як і та, що вказана в URL-адресі (ми видалили її з форми).

Якщо форма невалідна, ми відображаємо сторінку, але тепер форма буде містити подані значення і повідомлення про помилки, щоб їх можна було відобразити користувачеві.

Спробуйте заповнити й відправити форму. Вона має працювати правильно, а дані мають зберігатися в базі даних (перевірте це в панелі керування). Однак є одна проблема: фотографії. Вони не зберігаються, оскільки ми ще не обробляли їх у контролері.

Завантаження файлів

Завантажені фотографії слід зберігати на локальному диску, в місці до якого є доступ з фронтенду, щоб ми могли відображати їх на сторінці конференції. Ми будемо зберігати їх у каталозі public/uploads/photos:

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
--- a/src/Controller/ConferenceController.php
+++ b/src/Controller/ConferenceController.php
@@ -9,6 +9,7 @@ use App\Repository\CommentRepository;
 use App\Repository\ConferenceRepository;
 use Doctrine\ORM\EntityManagerInterface;
 use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+use Symfony\Component\HttpFoundation\File\Exception\FileException;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\Routing\Annotation\Route;
@@ -34,13 +35,22 @@ class ConferenceController extends AbstractController
     }

     #[Route('/conference/{slug}', name: 'conference')]
-    public function show(Request $request, Conference $conference, CommentRepository $commentRepository): Response
+    public function show(Request $request, Conference $conference, CommentRepository $commentRepository, string $photoDir): Response
     {
         $comment = new Comment();
         $form = $this->createForm(CommentFormType::class, $comment);
         $form->handleRequest($request);
         if ($form->isSubmitted() && $form->isValid()) {
             $comment->setConference($conference);
+            if ($photo = $form['photo']->getData()) {
+                $filename = bin2hex(random_bytes(6)).'.'.$photo->guessExtension();
+                try {
+                    $photo->move($photoDir, $filename);
+                } catch (FileException $e) {
+                    // unable to upload the photo, give up
+                }
+                $comment->setPhotoFilename($filename);
+            }

             $this->entityManager->persist($comment);
             $this->entityManager->flush();

Щоб керувати завантаженням фотографій, ми створюємо випадкове ім'я для файлу. Потім переміщуємо завантажений файл у його кінцеве місце розташування (каталог фотографій). Нарешті, ми зберігаємо ім'я файлу в об'єкті Comment.

Зверніть увагу на новий аргумент у методі show()? $photoDir — це текстовий рядок, а не сервіс. Як Symfony може знати, що тут впроваджувати? Контейнер Symfony, окрім сервісів, здатний зберігати параметри. Параметри — це скалярні значення, які допомагають налаштовувати сервіси. Ці параметри можуть бути впроваджені в сервіси явно, або можуть бути прив'язані за іменем:

1
2
3
4
5
6
7
8
9
10
11
--- a/config/services.yaml
+++ b/config/services.yaml
@@ -10,6 +10,8 @@ services:
     _defaults:
         autowire: true      # Automatically injects dependencies in your services.
         autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
+        bind:
+            string $photoDir: "%kernel.project_dir%/public/uploads/photos"

     # makes classes in src/ available to be used as services
     # this creates a service per class whose id is the fully-qualified class name

Параметр bind дозволяє Symfony впроваджувати значення щоразу, коли сервіс має аргумент $photoDir.

Спробуйте завантажити PDF-файл замість фотографії. Ви маєте побачити повідомлення про помилки в дії. Дизайн на даний момент досить потворний, але не хвилюйтеся, все стане красивим за кілька кроків, коли ми будемо працювати над дизайном веб-сайту. Для форм, ми змінимо всього один рядок конфігурації, щоб стилізувати всі елементи.

Налагодження форм

Коли форма відправлена й щось працює не так, як потрібно, використовуйте панель "Form" Symfony Profiler. Він надає вам інформацію про форму, всі її параметри, відправлені дані й про те, як вони перетворюються всередині. Якщо форма містить якісь помилки, вони також будуть відображені.

Порядок взаємодії з формою, як правило, виглядає наступним чином:

  • Форма відображається на сторінці;
  • Користувач відправляє форму за допомогою POST-запиту;
  • Сервер перенаправляє користувача на іншу або ту саму сторінку.

Але як ви можете отримати доступ до профілювальника в разі успішної відправки запиту? Оскільки сторінка негайно перенаправляється, ми ніколи не побачимо панель інструментів веб-налагодження для POST-запиту. Немає проблем: на перенаправленій сторінці наведіть курсор на зелену область зліва, з надписом "200". Ви маєте побачити перенаправлення "302" із посиланням на профіль (у дужках).

/conference/amsterdam-2019

Натисніть на нього, щоб відкрити профіль POST-запиту, та перейдіть до панелі "Form":

1
$ rm -rf var/cache
/_profiler/450aa5

Відображення завантажених фотографій у панелі керування

Наразі в панелі керування відображається ім’я файлу, але ми хочемо бачити саме фотографію:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
--- a/src/Controller/Admin/CommentCrudController.php
+++ b/src/Controller/Admin/CommentCrudController.php
@@ -9,6 +9,7 @@ use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
 use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField;
 use EasyCorp\Bundle\EasyAdminBundle\Field\DateTimeField;
 use EasyCorp\Bundle\EasyAdminBundle\Field\EmailField;
+use EasyCorp\Bundle\EasyAdminBundle\Field\ImageField;
 use EasyCorp\Bundle\EasyAdminBundle\Field\TextareaField;
 use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
 use EasyCorp\Bundle\EasyAdminBundle\Filter\EntityFilter;
@@ -45,7 +46,9 @@ class CommentCrudController extends AbstractCrudController
         yield TextareaField::new('text')
             ->hideOnIndex()
         ;
-        yield TextField::new('photoFilename')
+        yield ImageField::new('photoFilename')
+            ->setBasePath('/uploads/photos')
+            ->setLabel('Photo')
             ->onlyOnIndex()
         ;

Виключення завантажених фотографій з Git

Не фіксуйте зміни! Ми не хочемо зберігати завантажені зображення у Git-репозиторії. Додайте каталог /public/uploads у файл .gitignore:

1
2
3
4
5
6
7
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
+/public/uploads

 ###> symfony/framework-bundle ###
 /.env.local

Зберігання завантажених файлів на продакшн-серверах

Останнім кроком є зберігання завантажених файлів на продакшн-серверах. Чому необхідно робити щось особливе? Тому що більшість сучасних хмарних платформ використовують контейнери тільки для читання, з різних причин. Platform.sh не є винятком.

В проекті Symfony не всі ресурси доступні лише для читання. Ми намагаємося згенерувати якомога більше кешу при створенні контейнера (під час фази розігрівання кешу), але Symfony все одно має мати можливість записувати кеш користувачів, журнали, сеанси, якщо вони зберігаються в файловій системі тощо.

Погляньте на файл .platform.app.yaml, там вже є доступна для запису точка монтування для каталогу var/. Каталог var/ є єдиним каталогом, у який Symfony записує дані (кеші, журнали, ...).

Створімо нову точку монтування для завантажених фотографій:

1
2
3
4
5
6
7
8
9
10
--- a/.platform.app.yaml
+++ b/.platform.app.yaml
@@ -35,6 +35,7 @@ web:

 mounts:
     "/var": { source: local, source_path: var }
+    "/public/uploads": { source: local, source_path: uploads }

 relationships:
     database: "database:postgresql"

Тепер ви можете розгорнути код, і фотографії зберігатимуться у каталозі public/uploads/, як у нашій локальній версії.

This work, including the code samples, is licensed under a Creative Commons BY-NC-SA 4.0 license.
TOC
    Version