2019年9月8日 星期日

Symfony4.2 基本應用【第一篇】表單(Form)



你更傾向影片教學?查看教學影片

對網頁開發來說,處理HTML表單是常見且具挑戰性的任務之一,Symfony結合元件Form來幫助你處理。在本文中,你將從簡單開始到複雜表單,逐步學習表單的最重要特性。


安裝

在使用之前請先執行以下指令安裝:
composer require symfony/form
Symfony Form這個元件是獨立的函式庫,因此可以在Symfony的專案以外使用,若想要了解更多資訊,請看Github上的Form component documentation


建立簡單的表單

假設你要建立一個簡單的備忘清單應用來顯示任務"tasks",因為使用者需要編輯和新增任務,所以你需要新增一個表格。但在建立表單之前,先建立一個Task的類(class)來儲存任務資料:
// src/Entity/Task.php
namespace App\Entity;

class Task
{
    protected $task;
    protected $dueDate;

    public function getTask()
    {
        return $this->task;
    }

    public function setTask($task)
    {
        $this->task = $task;
    }

    public function getDueDate()
    {
        return $this->dueDate;
    }

    public function setDueDate(\DateTime $dueDate = null)
    {
        $this->dueDate = $dueDate;
    }
}
這個類是個普通老式的PHP物件,因為到目前為止,它與Symfony或任何資源庫都無關,這個普通的PHP物件能夠直接解決你應用中的問題(i.e. 即這個應用裡代表任務的需求)。在本文結束時,你將能把數據提交到Task實例(透過HTML表單),驗證其數據並將此永久儲存到資料庫中。

創建表單
現在你已經有Task,下一步是新增實際的HTML表單,在Symfony中,實作的方式是產生一個表單的物件並給予模板顯示,現在這一切都可以在控制器內完成:
// src/Controller/TaskController.php
namespace App\Controller;

use App\Entity\Task;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\HttpFoundation\Request;

class TaskController extends AbstractController
{
    public function new(Request $request)
    {
        // creates a task and gives it some dummy data for this example
        $task = new Task();
        $task->setTask('Write a blog post');
        $task->setDueDate(new \DateTime('tomorrow'));

        $form = $this->createFormBuilder($task)
            ->add('task', TextType::class)
            ->add('dueDate', DateType::class)
            ->add('save', SubmitType::class, ['label' => 'Create Task'])
            ->getForm();

        return $this->render('task/new.html.twig', [
            'form' => $form->createView(),
        ]);
    }
}
這個範例事告訴你如何直接在控制器內建立表單,待會在"建立表單類(Creating Form Classes)"的章節裡,你會學到如何在一個獨立的類(a standlone class)裡建立表單,因為可以讓表單重複利用,所以比較推薦的方式。

新增一個表單需要相對短的程式碼,因為Symfony表單物件是由"form builder"所建立,這個"form builder"目的在方便你寫一個簡單的表單"食譜(recipes)",讓它完成實際構建表格的所有繁重工作。

在這個範例裡,你新增兩個欄位到表單 taskdueDate,相應於 Task 類(class)中的兩個屬性 taskdueDate,你也用完整的類名指定了每一個欄位的"類型(type)"(e.g. TextTypeDateType),除此之外,他還代表此欄位該顯示什麼HTML標籤。

最後,你需要增加一個提交(submit)的按鈕,並可以自訂顯示文字(label),來提交表單給伺服器。

Symfony提供許多類型,將稍後討論(請參考Built-in Field Types)

顯示表單(Rendering the Form)
現在表單已經被創建了,下一步就要顯示它,實作的方式是傳一個特殊的表單"視圖(view)"物件到你的模板(請看上面controller範例程式中寫道$form->createView()),接著使用 form helper functions:
{# templates/task/new.html.twig #}
{{ form(form) }}


就是這樣!form這個函式將會顯示所有欄位,夾在 <form> 開始和結束的tag。預設情況下,form method是 POST ,預設情況下目標路徑(target URL)和目前顯示表單的路徑相同,根據需求兩者都可以修改。

像這樣簡短的方式缺乏彈性,通常你需要針對整個或部分的欄位有更多調整的需求,Symfony提供一些方式來達成:
  • 如果你的應用程式有使用CSS框架如Bootstrap或Foundation,可以善用built-in form themes來讓你的表單達成一致的風格。
  • 如果你需要針對少數的欄位進行客製化,或者只是應用中少部分的表單,請閱讀文章How to Customize Form Rendering
  • 如果你希望所有的表單風格都一致,你可建立一個Symfony表單主題(Symfony form theme)(可以以任何內建的主題為基礎或是全部重新建立)。

往下進行下去之前,你會注意到如何顯示task輸入欄位,是因為$task物件有task的值,這是表單的第一個工作:從物件取出資料並轉換成適合在HTML表單中顯示的格式。
表單系統非常聰明,可以藉由Task類中兩個方法getTask()setTask()來取得task被保護屬性(protected property)的值,除非屬性是公開的(public),否則必須有"getter"和"setter"方法使得表單元件可以取得並將資料放入這個屬性,如果是Boolean性質,你可以用"isser"或"hasser"方法(例如:isPublished()hasReminder()來替代getter(例如:getPublished()getReminder()))


處理表單的提交
預設情況下,表格會透過POST的請求提交到與顯示表單同一個控制器。

接下來,第二個步驟是要解析使用者傳送資料的屬性,要達到這個目的,這些提交資料必須寫入Form物件內,請將下列程式碼加入你的控制器:
// ...
use Symfony\Component\HttpFoundation\Request;

public function new(Request $request)
{
    // just setup a fresh $task object (remove the dummy data)
    $task = new Task();

    $form = $this->createFormBuilder($task)
        ->add('task', TextType::class)
        ->add('dueDate', DateType::class)
        ->add('save', SubmitType::class, ['label' => 'Create Task'])
        ->getForm();

    $form->handleRequest($request);

    if ($form->isSubmitted() && $form->isValid()) {
        // $form->getData() holds the submitted values
        // but, the original `$task` variable has also been updated
        $task = $form->getData();

        // ... perform some action, such as saving the task to the database
        // for example, if Task is a Doctrine entity, save it!
        // $entityManager = $this->getDoctrine()->getManager();
        // $entityManager->persist($task);
        // $entityManager->flush();

        return $this->redirectToRoute('task_success');
    }

    return $this->render('task/new.html.twig', [
        'form' => $form->createView(),
    ]);
}
請注意!createView()方法必須在handleRequest()之後才能呼叫,否則*_SUBMIT事件所完成的變化將不會採用到view裡(像是驗證錯誤訊息)。

這個控制器是依照普遍的模式來處理表單,有三種可能的路徑:
  • 當瀏覽器初始載入頁面時,表單會被建立並顯示,handleRequest()知道表單尚未提交並不採取任何行動,若表單沒有被提交,則isSubmitted()回傳false
  • 當用戶提交表單時,handleRequest()會知道並立即把提交的資料寫入$task的屬性taskdueDate,接著對這個物件進行驗證,如果這個物件不合法(驗證將在下個章節說明),isValid()回傳false,並重新顯示表單,並附註驗證錯誤。
  • 當使用者提交合法資料的表單,被提交的資料又再次被寫入表單,但這一次isValid()會回傳true,在你重新導向到其他頁面之前(e.g. 一個"感謝"或"成功"頁面),你有機會用$task物件來執行一些動作(像是永久保存到資料庫)。

    成功地將表單提交後執行重新導向,可以避免使用者重新點擊"重新整理(refresh)"按鈕,導致重複送出資料。

如果你需要操控權,像是何時送出表單或是那些資料被傳輸,你可以使用方法submit(),詳情請看『自行呼叫Form::submit()』(Calling Form::submit() manually)。


表格驗證(Form Validation)

在前面的章節中,你已經知道表單送出的資料可能是合法的(valid)或不合法的(invalid),在Symfony中,驗證可以用在基礎物件(e.g. Task),換句話說,我們所要驗證的並非這個"表單form",而是表單被提交的這些資料所產生$task這個物件是否合法,呼叫$form->isValid()是個最方便的方法來確認$task物件是否為合法的輸入資料。

在使用驗證之前,請先執行下面指令讓你的應用支援這個功能:
composer require symfony/validator
所謂的驗證就是在class裡增加一些規定(稱為限制 constraints),為了展示這項功能,我們添加一些驗證限制使得task這個欄位不能是空的,另外dueDate欄位除了不能是空的,也必須是合法的Datetime物件。
// 省略程式碼
就是這樣!如果你用非合法的資料重新提交表單,你會看到相應的錯誤顯示在表單上。

驗證是Symfony非常強大的功能,可以參考這篇專門的介紹。(連結)

HTML5 Validation
多虧了HTML5,有需多瀏覽器可以在用戶端強制執行某些內建的驗證,最常見的驗證就是替必填的欄位加上required這個屬性,對於支援HTML5的瀏覽器,若使用這嘗試提交帶有空白欄位的表單,就會顯示瀏覽器內建的訊息。

建立帶有可被HTML偵測的屬性之表單將會充分發會這個新的特性,並觸發驗證。然而用戶端的驗證可以在formtag加上novalidate這個屬性,或是提交(submit)tag上加上formnovalidate這個屬性,來取消驗證。當你在測試伺服器端(server-side)的驗證限制時特別實用,但瀏覽器不會允許,例如,提交空白字段。
{# templates/task/new.html.twig #}
{{ form_start(form, {'attr': {'novalidate': 'novalidate'}}) }}
{{ form_widget(form) }}
{{ form_end(form) }}


內建欄位類型(Built-in Field Types)

Symfony囊括需多常見的表單欄位和資料類型:

Text Fields

Choice Fields

Date and Time Fields

Other Fields

Field Groups

Hidden Fields

Buttons

Base Fields

Field Type Options

每一個欄位類型都有一些選項可以設定,例如dueDat現在顯示3個選擇方框,然而DateType可以設定成顯示單一的文字方塊(使用者需要在方塊內輸入字串):
->add('dueDate', DateType::class, ['widget' => 'single_text'])


每一個欄位類型都有一些不一樣的選項可以以陣列的方式傳入,有許多欄位類型特有的選項,可以參閱各類型的文件了解細節。

必填選項
最常見的選項是required,可以廣泛地應用在任何欄位,預設情況required設定為true,表示HTML5會在用戶端採取這些驗證以防用戶未填寫欄位,如果這並不是你期望的行為,你可以選擇取消HTML5的驗證或將required設定成false
->add('dueDate', DateType::class, [
    'widget' => 'single_text',
    'required' => false
])
請注意required的設定並不會同時在伺服器端採用這些驗證,換句話說,如果用戶提交空值(假設是舊瀏覽器或網路服務),除非你使用SymfonyNotBlankNotNull的驗證條件,否則將被視為合法的值。

也就是說,雖然required很重要,但伺服器端的驗證還是隨時需要。

標籤選項
表單欄位的標籤可以透過label來設定,並可以應用在任何欄位:
->add('dueDate', DateType::class, [
    'widget' => 'single_text',
    'label'  => 'Due Date',
])
欄位的label也可以在模板的表單裡顯示,如果不需要你的輸入和label有關聯,你可以設定成false來取消關聯。
預設下,顯示必填欄位會在label加上required的CSS class,所以你可以應用下述的CSS style在必填欄位顯示一個*:
label.required:before {
    content: "*";
}


Field Type Options GuessingField Type Guessing

現在你為Task新增驗證metadata,Symfony對於你的欄位已經知道些許,如果你允許的話,Symfony可以"猜測guess"你欄位的類型並替你進行設定。例如,Symfony藉由驗證規則會揣測task是一個普通的TextType,而dueDate則是DateType
public function new()
{
    $task = new Task();

    $form = $this->createFormBuilder($task)
        ->add('task')
        ->add('dueDate', null, ['widget' => 'single_text'])
        ->add('save', SubmitType::class)
        ->getForm();
}
當你省略add()第二個參數時會啟動"guessing"(或傳入null),如果你傳入帶有選項的陣列作為第三個參數(如上述的dueDate),這些選項會被應用到需要被猜測的欄位。

如果表單使用特定的驗證組,當欄位類型guesser在猜測欄位類型時,會考慮所有的驗證限制(包含不屬於驗證組的一部份但有使用的限制)。


Field Type Options Guessing

為了猜測欄位的"類型",Symfony可以試圖猜出數字欄位的正確的數值。

如此設定後,這些欄位以特殊的HTML屬性提供HTML5在用戶端進行驗證,然而這不會相應地在伺服器端中產生這些限制(e.g. Assert\Length),雖然你必須在伺服器端手動加入驗證,這些欄位類型選項可以由這些資訊被預料的(guessed)。

required
required這個選項可以guessed,基於驗證的規則(i.e. 欄位是不是NotBlank或是NotNull)或是Doctrine metadata(i.e. 欄位是否nullable),這非常實用,用戶端驗證會自動與你的驗證規則相吻合。

maxlength
如果欄位是一種text field,那maxlength選項就可以guessed藉由驗證限制(若使用LengthRange)或藉由Doctrine metadata(藉由欄位長度)。

如果你使用Symfony猜測這些欄位類型,這些欄位選項只能是guessed。(i.e. 省略或傳入null作為add()第二個參數)

如果你想要改變其中一個guessed值,你可以傳入選項的陣列到非必填欄位來覆蓋。
->add('task', null, ['attr' => ['maxlength' => 4]])


建立表單類 Creating Form Classes

就像你看到的,表單可以直接在控制器裡建立並直接使用,然而更好的做法是建立獨立PHP class的表單,並可以在應用中隨處重複使用,建立新的類會包含構建任務表單的邏輯:
// src/Form/TaskType.php
namespace App\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;

class TaskType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('task')
            ->add('dueDate', null, ['widget' => 'single_text'])
            ->add('save', SubmitType::class)
        ;
    }
}
這個新的類(class)包含建立Task表單所有需要的動作,控制器需要建立表單物件時可以使用:
// src/Controller/TaskController.php
use App\Form\TaskType;

public function new()
{
    $task = ...;
    $form = $this->createForm(TaskType::class, $task);

    // ...
}
將表單邏輯放在它自有的類,表示表單可以在專案中重複使用,這是最好的建立表單方式,但選擇最後終究取決於你。

Setting the data_class
所有表單都必須知道保存資料的類名稱(e.g. App\Entity\Task)。大多guessed物件作為createForm()(i.e. $task)第二個參數傳入,等一下當你開始嵌入表單時,這樣就不再足夠了。因此,由於它並不是永遠必要的,如同下面範例,通常指定data_class選項為你的表單的type class是一個好的做法。
// src/Form/TaskType.php
use App\Entity\Task;
use Symfony\Component\OptionsResolver\OptionsResolver;

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

將表單對應到物件時,所有的欄位都必須被對應到,若表單中有任何欄位不存在於對應的物件,會促發一個例外事件(expection)。

因此如果你需要在表單中加一個額外的欄位(例如:一個核選方框checkbox"請問你同意這些條例嗎"),此欄位並不屬於對應的物件時,你需要將mapped這個選項設定成false
use Symfony\Component\Form\FormBuilderInterface;

public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder
        ->add('task')
        ->add('dueDate')
        ->add('agreeTerms', CheckboxType::class, ['mapped' => false])
        ->add('save', SubmitType::class)
    ;
}
此外,如果表單中有任何欄位不包含在提交的資料中,這些欄位必須明確地設定成null

欄位資料可以直接在控制器裡這樣存取:
$form->get('agreeTerms')->getData();
另外,非對應於物件的欄位資料可以直接被修改。
$form->get('agreeTerms')->setData(true);

表單名稱是由type class名稱自動產生而來,如果你想要修改,請使用方法createNamed()
// src/Controller/DefaultController.php
use App\Form\TaskType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

class DefaultController extends AbstractController
{
    public function newAction()
    {
        $task = ...;
        $form = $this->get('form.factory')->createNamed('name', TaskType::class, $task);

        // ...
    }
}
你也可以設定為空字串,直接完全地縮減名稱。


結語

當建立表單時,第一個要謹記於心的是如何將資料從物件(Task)轉換成HTML表格,讓使用者可以修改資料,表單的第二個目標就是讓使用者提交資料,並使這些更動重新儲存於物件。

表單系統還有須多可以學習,也還有很多強大的技巧。


了解更多

  • How to Change the Action and Method of a Form
  • Bootstrap 4 Form Theme
  • How to Choose Validation Groups Based on the Clicked Button
  • How to Create a Custom Form Field Type
  • How to Create a Form Type Extension
  • How to Choose Validation Groups Based on the Submitted Data
  • When and How to Use Data Mappers
  • How to Use Data Transformers
  • How to Use the submit() Function to Handle Form Submissions
  • How to Disable the Validation of Submitted Data
  • How to Dynamically Modify Forms Using Form Events
  • How to Embed Forms
  • Form Events
  • How to Embed a Collection of Forms
  • How to Customize Form Rendering
  • How to Access Services or Config from Inside a Form
  • How to Work with Form Themes
  • How to Reduce Code Duplication with "inherit_data"
  • How to Submit a Form with Multiple Buttons
  • Creating a custom Type Guesser
  • How to Unit Test your Forms
  • How to Configure empty Data for a Form Class
  • How to Dynamically Configure Form Validation Groups
  • How to Define the Validation Groups to Use
  • How to Use a Form without a Data Class
  • How to Upload Files
  • Form Types Reference
  • How to Implement CSRF Protection


資料翻譯自:
https://symfony.com/doc/4.2/forms.html

系列文章:

沒有留言:

張貼留言