SymfonyWorld Online 2020
100% online
30+ talks + workshops
Live + Replay watch talks later

Pas 14: Formulare pentru recenzii

5.0 version
Maintained

Formulare pentru recenzii

E timpul să-i lăsăm pe participanții noștri să-și dea părerea cu privire la conferințe. Vor contribui cu comentarii printr-un formular HTML.

Generarea unui tip de formular

Folosește pachetul Maker pentru a genera o clasă de formular:

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

Clasa App\Form\CommentFormType definește un formular pentru entitatea 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,
        ]);
    }
}

Un tip de formular descrie câmpurile formularului legat de un model. Face conversia datelor între datele transmise și proprietățile clasei de model. În mod implicit, Symfony folosește metadate de la entitatea Comentariu, cum ar fi metadatele Doctrine, pentru a ghici configurația fiecărui câmp. De exemplu, câmpul text se redă ca textarea deoarece folosește o coloană mai mare în baza de date.

Afișarea unui formular

Pentru a afișa formularul utilizatorilor, creează formularul în controler și transmite-l șablonului:

patch_file
 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;
@@ -35,6 +37,9 @@ class ConferenceController extends AbstractController
      */
     public function show(Request $request, Conference $conference, CommentRepository $commentRepository)
     {
+        $comment = new Comment();
+        $form = $this->createForm(CommentFormType::class, $comment);
+
         $offset = max(0, $request->query->getInt('offset', 0));
         $paginator = $commentRepository->getCommentPaginator($conference, $offset);

@@ -43,6 +48,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(),
         ]));
     }
 }

Niciodată nu ar trebui să inițiezi direct tipul formularului. În schimb, utilizează metoda createForm(). Această metodă face parte din AbstractController și ușurează crearea formularelor.

Când transmiți un formular la un șablon, utilizează createView() pentru a converti datele într-un format potrivit pentru șabloane.

Afișarea formularului în șablon se poate face prin funcția Twig form:

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

Când actualizezi o pagină de conferință în browser, reține că fiecare câmp de formular arată widgetul HTML corect (tipul de date este derivat din model):

Funcția form() generează formularul HTML pe baza tuturor informațiilor definite în tipul Form. De asemenea, adaugă enctype=multipart/form-data pe eticheta <form>, așa cum este necesar pentru câmpul de încărcare a fișierului. Mai mult, are grijă să afișeze mesaje de eroare atunci când trimiterea are unele erori. Totul poate fi personalizat prin suprascrierea șabloanelor implicite, dar nu vom avea nevoie de el pentru acest proiect.

Personalizarea unui tip de formular

Chiar dacă câmpurile de formular sunt configurate pe baza omologului lor de model, poți personaliza direct configurația implicită din clasa formularului:

patch_file
 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)
     {
         $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)
         ;
     }

Rețineți că am adăugat un buton de expediere (care ne permite să continuăm folosind expresia simplă {{ form(comment_form) }} din șablon).

Unele câmpuri nu pot fi configurate automat, cum ar fi photoFilename. Entitatea Comment trebuie doar să salveze numele de fișier foto, dar formularul trebuie să se ocupe de încărcarea fișierului. Pentru a rezolva acest aspect, am adăugat un câmp numit photo drept câmp non-mapped: acesta nu va fi mapat în nicio proprietate din Comment. Îl vom gestiona manual pentru a implementa o logică specifică (cum ar fi stocarea fotografiei încărcate pe disc).

Ca exemplu de personalizare am modificat și eticheta implicită pentru unele câmpuri.

Validarea modelelor

Form Type configurează redarea formularului pe frontend (prin unele validări HTML5). Iată formularul HTML generat:

 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>

Formularul utilizează câmpul email pentru e-mailul de comentariu și face cele mai multe câmpuri required. Reține că formularul conține și un câmp ascuns `` _token`` pentru a proteja formularul de atacurile CSRF.

Dar dacă trimiterea formularului ocolește validarea HTML (prin utilizarea unui client HTTP care nu aplică aceste reguli de validare, cum ar fi cURL), datele nevalide pot ajunge la server.

De asemenea, trebuie să adăugăm câteva restricții de validare la modelul de date Comment:

patch_file
 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
--- a/src/Entity/Comment.php
+++ b/src/Entity/Comment.php
@@ -3,6 +3,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)
@@ -19,16 +20,20 @@ class Comment

     /**
      * @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;

Gestionarea unui formular

Codul pe care l-am scris până acum este suficient pentru a afișa formularul.

Acum ar trebui să ne ocupăm de expedierea formularului și persistența informațiilor sale în baza de date din controler:

patch_file
 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;
     }

     /**
@@ -39,6 +42,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);

Când formularul este expediat, obiectul Comment este actualizat în conformitate cu datele transmise.

Conferința este obligată să fie aceeași cu cea de pe URL (am eliminat-o din formular).

Dacă formularul nu este valid, afișăm pagina, dar formularul va conține acum valorile trimise și mesajele de eroare, astfel încât acestea să poată fi afișate înapoi utilizatorului.

Încearcă formularul. Ar trebui să funcționeze bine, iar datele ar trebui să fie stocate în baza de date (verifică-le în backend-ul admin). Există însă o problemă: fotografii. Ele nu funcționează deoarece nu le-am gestionat încă în controler.

Încărcarea fișierelor

Fotografiile încărcate trebuie stocate pe discul local, undeva accesibil de frontend, astfel încât să le putem afișa pe pagina de conferință. Le vom stoca în directorul public/upload/photos:

patch_file
 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
--- a/src/Controller/ConferenceController.php
+++ b/src/Controller/ConferenceController.php
@@ -10,6 +10,7 @@ use App\Repository\ConferenceRepository;
 use Doctrine\ORM\EntityManagerInterface;
 use Doctrine\ORM\Tools\Pagination\Paginator;
 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;
@@ -37,7 +38,7 @@ class ConferenceController extends AbstractController
     /**
      * @Route("/conference/{slug}", name="conference")
      */
-    public function show(Request $request, Conference $conference, CommentRepository $commentRepository)
+    public function show(Request $request, Conference $conference, CommentRepository $commentRepository, string $photoDir)
     {
         $comment = new Comment();
         $form = $this->createForm(CommentFormType::class, $comment);
@@ -45,6 +46,15 @@ class ConferenceController extends AbstractController
         $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();

Pentru a gestiona încărcările de fotografii, creăm un nume aleatoriu pentru fișier. Apoi, mutăm fișierul încărcat în locația sa finală (directorul foto). În cele din urmă, stocăm numele fișierului în obiectul Comment.

Ai observat noul argument a metodei show()? $photoDir este un șir și nu un serviciu. Cum poate Symfony să știe ce să injecteze aici? Symfony Container este capabil să stocheze parametrii * pe lângă servicii. Parametrii sunt scalari care ajută la configurarea serviciilor. Acești parametri pot fi injectați în mod explicit în servicii sau pot fi *legați prin nume:

patch_file
 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:
+            $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

Setarea bind permite lui Symfony să injecteze valoarea ori de câte ori un serviciu are un argument `` $photoDir``.

Încearcă să încărci un fișier PDF în loc de fotografie. Ar trebui să vezi mesajele de eroare în acțiune. Designul este destul de urât în acest moment, dar nu îți fă griji, totul va deveni frumos în câțiva pași când vom lucra la designul site-ului. Pentru formulare, vom schimba o linie de configurare în stilul tuturor elementelor de formular.

Depanarea formularelor

Când un formular este expediat și ceva nu funcționează destul de bine, utilizează opțiunea „Form” al depanatorului Symfony. Îți oferă informații despre formular, toate opțiunile sale, datele transmise și modul în care acestea sunt convertite în interior. Dacă formularul conține erori, acestea vor fi afișate și ele.

Fluxul de lucru tipic este urmatorul:

  • Formularul este afișat pe o pagină;
  • Utilizatorul trimite formularul printr-o solicitare POST;
  • Serverul redirecționează utilizatorul către o altă pagină sau aceeași pagină.

Dar cum poți accesa depanatorul pentru a expedia o cerere cu succes? Deoarece pagina este redirecționată imediat, nu vom vedea niciodată bara de instrumente de depanare web pentru solicitarea POST. Nicio problemă: pe pagina redirecționată, treci peste partea verde „200” din stânga. Ar trebui să vezi redirecționarea „302” cu un link către profil (în paranteză).

Fă clic pe el pentru a accesa profilul solicitării POST și accesează panoul „Form”:

1
$ rm -rf var/cache

Afișarea fotografiilor încărcate în Backendul Admin

Backend-ul de administrare afișează în prezent numele fișierului foto, dar vrem să vedem fotografia reală:

patch_file
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
--- a/config/packages/easy_admin.yaml
+++ b/config/packages/easy_admin.yaml
@@ -8,6 +8,7 @@ easy_admin:
                 fields:
                     - author
                     - { property: 'email', type: 'email' }
+                    - { property: 'photoFilename', type: 'image', 'base_path': "/uploads/photos", label: 'Photo' }
                     - { property: 'createdAt', type: 'datetime' }
             edit:
                 fields:

Excluzând fotografiile încărcate din Git

Nu salva încă! Nu dorim să stocăm imagini încărcate în repozitoriul Git. Adăugă directorul /public/uploads în fișierul .gitignore:

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

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

Stocarea fișierelor încărcate pe serverele de producție

Ultimul pas este stocarea fișierelor încărcate pe serverele de producție. De ce ar trebui să facem ceva special? Deoarece majoritatea platformelor cloud moderne folosesc containere doar în regim de citire din diverse motive. SymfonyCloud nu face excepție.

Nu totul are permisiuni de citire într-un proiect Symfony. Încercăm din răsputeri să generăm cât mai multă memorie cache atunci când construim containerul (în faza de încălzire a cache-ului). Dar Symfony trebuie să poată scrie undeva pentru cache-ul utilizatorului, jurnalele, sesiunile și alte elemente stocate în sistemul de fișiere.

Aruncă o privire la .symfony.cloud.yaml, există deja o montare în regim de scriere pentru directorul var/. Directorul var/ este singurul director în care Symfony scrie (cache, jurnale, …).

Să creăm un nouă cale de montare pentru fotografiile încărcate:

patch_file
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
--- a/.symfony.cloud.yaml
+++ b/.symfony.cloud.yaml
@@ -26,6 +26,7 @@ disk: 512

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

 hooks:
     build: |

Acum poți lansa codul și fotografiile vor fi stocate în directorul public/upload/, similar versiunii noastre locale.


  • « Previous Pas 13: Gestionarea ciclului de viață al obiectelor Doctrine
  • Next » Pas 15: Securizarea Backend-ului Admin

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