گام 14: پذیرش بازخوردها از طریق فرم

5.0 version
Maintained

پذیرش بازخوردها از طریق فرم

زمان آن رسیده که به شرکت‌کننده‌های کنفرانس اجازه بازخورد دادن بدهیم. آن‌ها کامنت‌هایشان را از طریق یک فرم 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,
        ]);
    }
}

یک form type، فیلدهای فرم مرتبط با یک مدل را توصیف می‌کند و کار تبدیل داده بین اطلاعات وارد شده و ویژگی‌های کلاس مدل را انجام می‌دهد. به صورت پیشفرض،‌ سیمفونی از فراداده‌های موجودیت Comment - همچون فراداده‌های Doctrine - استفاده می‌کند، تا بتواند پیکربندی هر فیلد را حدس بزند. مثلاً، یک فیلد متنی (text) به شکل textarea، render می‌شود، چون در پایگاه‌داده، ستون مربط به آن، فضای بیشتری را اشغال می‌کند.

نمایش یک فرم

برای نمایش فرم به کاربر، فرم را در کنترلر ایجاد کرده و به قالب بدهید:

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(),
         ]));
     }
 }

هرگز نباید فرم را مستقیماً ایجاد کنید. به جای این کار، از متد ()createForm استفاده کنید. این متد بخشی از AbstractController است و ساخت فرم‌ها را ساده می‌کند.

هنگام دادن یک فرم به قالب، از ()createView استفاده کنید تا اطلاعات به فرمت مناسب برای قالب تبدیل شود.

نمایش فرم در قالب، می‌تواند از طریق تابع form در Twig انجام شود:

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 %}

هنگام تازه‌سازی صفحه‌ی کنفرانس در مرورگر، توجه داشته باشید که هر فیلد ویجت HTML صحیح خود را نشان می‌دهد (نوع داده از مدل گرفته شده است):

تابع ()form بر اساس اطلاعات تعریف‌شده در نوع فرم (Form type)، یک فرم HTML تولید می‌کند. همچنین به همان شکلی که در فیلد بارگذاری فایل لازم است، روی تگ <form>، یک enctype=multipart/form-data اضافه می‌کند. علاوه بر این، نمایش پیغام‌های خطا در صورت بروز خطا هنگام ثبت فرم را نیز به عهده می‌گیرد. همه چیز می‌تواند با بازنویسی قالب پیش‌فرض شخصی‌سازی شود، ولی ما برای این پروژه به آن نیاز نخواهیم داشت.

سفارشی‌سازی یک فرم

حتی اگر فیلدهای فرم با مدل معادلش تطبیق داشته باشد، می‌توانید تنظیمات پیش‌فرض را در کلاس فرم مستقیماً شخصی‌سازی کنید:

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

توجه داشته باشید که ما یک دکمه سابمیت هم اضافه کردیم (این به ما امکان استفاده ساده از عبارت {{ form(comment_form) }} را در تمپلیت می‌دهد).

برخی فیلدها مانند photoFilename، امکان پیکربندی خودکار را ندارند. موجودیت Comment تنها نیاز به ذخیره نام عکس دارد، ولی فرم باید بارگذاری فایل را نیز به عهده بگیرد. به منظور رسیدگی به این مسئله، ما یک فیلد به نام photo و به صورت تصویر نشده (un-mapped) اضافه کرده‌ایم: این ویژگی به هیچ یک از ویژگی‌های Comment تصویر نخواهد شد. ما جهت پیاده‌سازی برخی اعمال و منطق‌های ویژه، این قسمت را به صورت دستی مدیریت می‌کنیم (مثل ذخیره‌سازی فایل بارگذاری شده روی دیسک).

به عنوان مثالی از سفارشی‌سازی، ما برخی برچسب‌های (label) پیش‌فرض فیلدها را نیز تغییر داده و اصلاح می‌کنیم.

اعتبارسنجی مدل‌ها

نوع فرم (Form Type)، نحوه‌ی renderشدن فرم در جلوی صحنه را پیکربندی می‌کند. (به کمک برخی ویژگی‌های اعتبارسنجی 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 را دور بزند (با استفاده از یک ابزار مانند cURL که اعتبارسنجی‌های HTML را تحمیل نمی‌کند)، آنگاه داده‌های نامعتبر می‌توانند به سرور برسند.

ما علاوه بر این نیاز داریم که برخی محدودیت‌های اعتبارسنجی را روی مدل 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;

رسیدگی به یک فرم

کدی که تا به اینجا نوشته‌ایم برای نمایش فرم کافیست.

حالا باید در درون کنترلر، به دریافت فرم و ثبت اطلاعات آن در پایگاه‌داده را رسیدگی کنیم:

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);

وقتی فرم (از طرف کاربر) ثبت می‌گردد، شیء``Comment`` با توجه به اطلاعات ارسال شده به روزرسانی می‌شود.

کنفرانس باید منطبق با همان کنفرانس اشاره‌شده در URL باشد. (آن را از فرم حذف کردیم).

اگر فرم معتبر نباشد، صفحه را نمایش می‌دهیم، ولی اکنون فرم حاوی اطلاعات ارسال‌شده‌ و خطاهاست تا بتوان آن‌ها را به کاربر نمایش داد.

فرم را امتحان کنید. فرم باید درست کار کند و اطلاعات می‌بایست در پایگاه‌داده ذخیره شود (این موضوع را از طریق پشت صحنه‌ی مدیریت بررسی کنید). ولی همچنان یک مشکل وجود دارد: تصاویر. آن‌ها درست کار نمی‌کنند چون هنوز در کنترلر به آن‌ها رسیدگی نکرده‌ایم.

بارگذاری فایل‌ها

تصاویر بارگذاری‌شده باید بر روی دیسک محلی و جایی در دسترس جلو صحنه (frontend) ذخیره شوند تا بتوان آن‌ها را در صفحه‌ی کنفرانس نمایش داد. ما تصاویر را در پوشه‌ی public/uploads/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();

برای مدیریت تصاویر بارگذاری‌شده، برای فایل‌ یک نام تصادفی ایجاد می‌کنیم. پس از آن، تصویر را به محل نهایی آن (پوشه‌ی تصاویر) انتقال می‌دهیم. در نهایت، نام فایل را در شیء Comment ذخیره می‌کنیم.

به آرگمان جدید در متد show() توجه کردید؟ آرگمان $photoDir، یک رشته (string) است و سرویس نیست. سیمفونی چگونه می‌داند که چه چیزی را در اینجا تزریق کند؟ کانتینر سرویس، می‌تواند علاوه بر سرویس‌ها، پارامترها را نیز ذخیره کند. پارامترها، مقادیری هستند که سرویس‌ها را پیکربندی می‌کنند. این پارامترها می‌توانند صریحاً به سرویس‌ها تزریق شوند یا اینکه به صورت مقید به نام (bound by name) باشند.

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

تنظیم bind این امکان را به سیمفونی می‌دهد تا هر زمان که سرویس یک آرگمان $photoDir داشت، تزریق را انجام دهد.

سعی کنید که یک فایل PDF به جای تصویر بارگذاری کنید. باید پیغام‌های خطا ببینید. در حال حاظر طراحی‌کاملاً زشت است، اما نگران نباشید، همه چیز در چند گام دیگر هنگامی که بر روی طراحی وب‌سایت کار کنیم، زیبا خواهد شد. برای فرم‌ها، ما یک خط از پیکربندی را تغییر خواهیم داد تا به تمام المان‌های فرم یک سبک ببخشیم.

عیب‌یابی فرم‌ها

زمانی که فرمی ارسال شده است و یک چیزی درست کار نمی‌کند، از پنل «Form» در نمایه‌ساز سیمفونی استفاده کنید. این بخش به شما در رابطه با فرم اطلاعات می‌دهد، تمام گزینه‌ها، داده‌های ارسالی و اینکه داده‌ها به صورت داخلی چگونه تبدیل شده‌اند. اگر فرم حاوی هر گونه خطایی باشد، آن‌ها نیز لیست خواهند شد.

یک جریان‌کار معمول فرم، به صورت زیر است:

  • فرم در یک صفحه، نمایش داده شده است؛
  • کاربر فرم را از طریق یک درخواست POST ارسال می‌کند؛
  • سرور کاربر را به یک صفحه‌ی دیگر یا همان صفحه هدایت می‌کند.

اما برای یک درخواست موفقیت‌آمیز، چگونه به نمایه‌ساز دسترسی پیدا کنیم؟ به دلیل اینکه صفحه بلافاصله بازهدایت (redirect) می‌شود، ما هرگز برای درخواست POST، نوارابزار اشکا‌ل‌زدایی وب را نمی‌بینیم. مشکلی نیست: در صفحه‌ی بازهدایت‌شده، نشانگر موس را بر روی قسمت سبز رنگ «200» در سمت چپ نوارابزار قرار دهید (کلیک نکنید). باید یک بازهدایت «302» به همراه پیوندی به نمایه‌ساز را مشاهده کنید (در داخل پرانتز).

بر روی آن کلیک کنید تا به پروفایل درخواست POST دسترسی پیدا کنید و سپس به پنل «Form» بروید:

1
$ rm -rf var/cache

نمایش تصاویر بارگذاری‌شده در پشت صحنه‌ی مدیریتی

در حال حاضر پشت صحنه‌ی مدیریتی نام فایل را نمایش می‌دهد، اما ما می‌خواهیم که تصویر واقعی را ببینیم:

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:

مستثنی کردن تصاویر بارگذاری‌شده از Git

الان commit نکنید! ما نمی‌خواهیم که تصاویر بارگذاری‌شده در مخزن ‌Git ذخیره شوند. پوشه‌ی /public/uploads را به فایل .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

ذخیره‌ی فایل‌های بارگذاری‌شده در سرورهای عمل‌آوری

آخرین گام، ذخیره‌ی فایل‌های بارگذاری‌شده در سرورهای عمل‌آوری است. چرا مجبوریم یک سری کار خاص انجام دهیم؟ زیرا اکثر سکوهای ابری مدرن به دلایل مختلف از کانتینرهای فقط‌خواندنی استفاده می‌کنند. SymfonyCloud نیز از این موضوع مستثنی نیست.

تمام بخش‌ها در پروژه‌ی سیمفونی فقط‌خواندنی نیستند. ما به سختی تلاش می‌کنیم تا جایی که ممکن است در هنگام تولید کانتینر، cache تولید کنیم (در طول فاز cache warmup). اما سیمفونی هنوز هم به جایی برای نوشتن cache‌های کاربر، لاگ‌ها، نشست‌ها (اگر در filesystem ذخیره می‌شوند) و ... نیاز دارد.

نگاهی به فایل .symfony.cloud.yaml بیاندازید، یک mount قابل نوشتن برای پوشه‌ی var/ وجود دارد. پوشه‌ی var/ تنها پوشه‌ای است که سیمفونی در آن می‌نویسد (cacheها، لاگ‌ها و غیره).

بیایید یک mount جدید برای تصاویر بارگذاری‌شده ایجاد کنیم:

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: |

حالا می‌توانید کد را مستقر کنید و تصاویر همچون نسخه‌ی محلی، در پوشه‌ی public/uploads/ ذخیره خواهند شد.


  • « Previous گام 13: مدیریت چرخه‌حیات اشیاء Doctrine
  • Next » گام 15: امن‌سازی پشت صحنه‌ی مدیریتی

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