You are browsing the book for Symfony 5.0 which is not maintained anymore. Code examples might not work anymore, even in a project using Symfony 5.0.
Consider reading the book for Symfony 5.2 instead.
Pas 14: Formulare pentru recenzii
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
:
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:
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): Response
{
+ $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
:
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 %}
|
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:
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
:
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/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)
@@ -20,16 +21,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:
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
:
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;
@@ -38,13 +39,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();
|
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:
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ă:
1 2 3 4 5 6 7 8 9 10 | --- a/config/packages/easy_admin.yaml
+++ b/config/packages/easy_admin.yaml
@@ -17,6 +17,7 @@ easy_admin:
fields:
- author
- { property: 'email', type: 'email' }
+ - { property: 'photoFilename', type: 'image', 'base_path': "/uploads/photos", label: 'Photo' }
- { property: 'createdAt', type: 'datetime' }
sort: ['createdAt', 'ASC']
filters: ['conference']
|
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
:
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:
1 2 3 4 5 6 7 8 9 10 | --- a/.symfony.cloud.yaml
+++ b/.symfony.cloud.yaml
@@ -38,6 +38,7 @@ web:
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.