表單


對一個Web開發者來說,處理HTML表單是一個最普通又極具挑戰的任務。 Symfony整合了一個Form元件,讓處理表單變得容易。在本章,你將從零開始建立一個複雜的表單,學習表單類別庫中的重要功能。

Symfony的Form元件是一個獨立的類別庫,你可以在Symfony專案之外使用它。參考 Form元件文件 以了解更多。

建立一個簡單的表單 

#假設你正在建立一個簡單的待辦事項列表,來顯示一些「任務」。你需要建立一個表單來讓你的使用者編輯和建立任務。在這之前,先來看看 Task 類,它可呈現並儲存一個單一任務的資料。

// src/AppBundle/Entity/Task.phpnamespace AppBundle\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物件類,直接解決了 程式中的 task (任務)之資料問題。當然,在本章的最後,你將能夠透過HTML表單把資料提交到一個 Task 實例,驗證它的值,並將它持久化到資料庫。

建立表單 

現在你已經建立了一個Task 類,下一步就是建立並渲染一個真正的html表單了。在Symfony中,這是透過建立表單物件並將其渲染到模版來完成的。現在,在控制器裡即可完成所有這些:

// src/AppBundle/Controller/DefaultController.phpnamespace AppBundle\Controller; use AppBundle\Entity\Task;use Symfony\Bundle\FrameworkBundle\Controller\Controller;use Symfony\Component\HttpFoundation\Request;use Symfony\Component\Form\Extension\Core\Type\TextType;use Symfony\Component\Form\Extension\Core\Type\DateType;use Symfony\Component\Form\Extension\Core\Type\SubmitType; class DefaultController extends Controller{
    public function newAction(Request $request)
    {
        // create a task and give it some dummy data for this example
        // 创建一个task对象,赋一些例程中的假数据给它
        $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, array('label' => 'Create Task'))
            ->getForm();         return $this->render('default/new.html.twig', array(
            'form' => $form->createView(),
        ));
    }}

這個範例說明如何直接在控制器中建立你的form(表單)。在後面的 建立表單類別 中,你將使用一個獨立的類別來建立表單,這種方法被推薦,因為表單可以重複使用。


建立表單不需要很多程式碼,因為Symfony的表單物件是透過一個「form builder(表單產生器)」來建立的。 form builder的目的是讓你編寫簡單的表單創建“指令”,而真實創建表單時的全部“重載”任務則交由builder完成。

在本例中,你已經加入了兩個欄位到表單,即 taskdueDate 。對應的是 Task 類別中的 taskdueDate 屬性。你已為它們分別指定了FQCN(Full Quilified Class Name/完整路徑類別名稱)的「類型」(如TextTypeDateType ),由類型決定為欄位產生哪一種HTML表單標籤(標籤組)。

最後,你新增了一個帶有自訂label的提交按鈕以向伺服器提交表單。

Symfony附帶了許多內建類型,它們將簡短地介紹(請參閱下面的內建表單類型)。

渲染表單 

表單建立之後,下一步就是渲染它。這是透過傳遞一個特定的表單「view」物件(注意上例控制器中的 $form->createView() 方法)到你的模板,並透過一系列的表單helper function(幫助函數)來實現的。

TWIG:{# app/Resources/views/default/new.html.twig #}
{{ form_start(form) }}
{{ form_widget(form) }}
{{ form_end(form) }}
PHP:<!-- app/Resources/views/default/new.html.php -->
<?php echo $view['form']->start($form) ?>
<?php echo $view['form']->widget($form) ?>
<?php echo $view['form']->end($form) ?>

1465202253_36066_5344_form-simple.png

本例假設你以"POST"請求提交表單,並且提交到和「表單顯示(頁)」相同的URL。後面你將學習如何改變請求方法(request method)和表單提交後的目標URL。

就是這樣!只需要三行就可以渲染完整的form表單:

  • form_start(form)
  • 渲染表單的開始標籤,包含在使用檔案上傳時的正確enctype屬性。
  • form_widget(form)
  • 渲染出全部字段,包含字段元素本身,字段label以及字段驗證的任何錯誤訊息。
  • form_end(form)
  • 當你手動產生每個欄位時,它可以渲染表單結束標籤以及表單中所有尚未渲染的欄位。這在渲染隱藏字段以及利用自動的 CSRF Protection 保護機制時非常有用。

就是這麼簡單,但不太靈活(暫時)。通常情況下,你希望單獨渲染出表單中的每一個字段,以便控製表單的樣式。你將在後面 如何去控​​製表單渲染 文章中掌握這種方法。

#

在繼續下去之前,請注意,為什麼渲染出來的 task 輸入框中有一個來自 $task 物件的屬性值(即「Write a blog post」)。這是表單的第一個任務:從一個物件中取得資料並把它轉換成適當的格式,以便在HTML表單中被渲染。

表單系統夠智能,它們透過getTask()setTask() 方法來存取Task 類別中受保護的task 屬性。除非是public屬性,否則 必須 有一個 "getter" 和 "setter" 方法被定義,以便表單元件能從這些屬性中取得和寫入資料。對於布林型的屬性,你可以使用一個"isser" 和"hasser" 方法(如 isPublished()hasReminder() )來取代getter方法(getPublished( )getReminder())。

處理表單提交 

預設時,表單會把POST請求,向「渲染它的同一個控制器」提交回去。

此處,表單的第二個任務就是把使用者提交的資料傳回一個物件的屬性之中。要做到這一點,使用者提交的資料必須寫入表單物件才行。在控制器(Controller)中新增以下功能:

// ...use Symfony\Component\HttpFoundation\Request; public function newAction(Request $request){
    // just setup a fresh $task object (remove the dummy data)
    // 直接设置一个全新$task对象(删除了假数据)
    $task = new Task();     $form = $this->createFormBuilder($task)
        ->add('task', TextType::class)
        ->add('dueDate', DateType::class)
        ->add('save', SubmitType::class, array('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
        //  $form->getData() 持有提交过来的值
        // 但是,原始的 `$task` 变量也已被更新了
        $task = $form->getData();         // ... perform some action, such as saving the task to the database
        // for example, if Task is a Doctrine entity, save it!
        // 一些操作,比如把任务存到数据库中
        // 例如,如果Tast对象是一个Doctrine entity,存下它!
        // $em = $this->getDoctrine()->getManager();
        // $em->persist($task);
        // $em->flush();         return $this->redirectToRoute('task_success');
    }     return $this->render('default/new.html.twig', array(
        'form' => $form->createView(),
    ));}

注意createView() 方法應該在handleRequest 被呼叫之後 再呼叫。否則,針對 *_SUBMIT 表單事件的修改,將不會套用到視圖層(例如驗證時的錯誤訊息)。


控制器(controller)在處理表單時遵循的是一個通用模式(common pattern),它有三個可能的途徑:

  1. # #當瀏覽器初始載入一個頁面時,表單被建立和渲染。

    handleRequest() 意識到表單沒有被提交進而什麼都不做。如果表單未被提交,isSubmitted() 傳回false;

  2. 當使用者提交表單時,

    handleRequest( ) 會辨識這個動作並立即將提交的資料寫入到$task 物件的task and dueDate 屬性。然後該物件被驗證。如果它是無效的(驗證在下一章),isValid() 會傳回false,進而再次渲染,只是這次有驗證錯誤;

  3. 當使用者以合法資料提交表單的時,提交的資料會再次寫入表單,但這次

    isValid() 回傳true。在把使用者重新導向到其他一些頁面之前(如一個「謝謝」或「成功」的頁面),你有機會用$task 物件來進行某些操作(例如把它持久化到資料庫)。

    表單成功提交之後的重定向用戶,是為了防止用戶透過瀏覽器「刷新」按鈕重複提交資料。

    #

如果你需要精確地控制何時表單被提交,或哪些資料傳給表單,你可以使用 submit()。更多資訊請參考 手動呼叫Form::submit()

表單驗證 

在上一節中,你了解了附帶了有效或無效資料的表單是如何被提交的。在Symfony中,驗證環節是在底層物件中進行的(例如 Task)。換句話說,問題不在於「表單」是否有效,而是 $task 物件在「提交的資料應用到表單」之後是否合法。呼叫 $form->isvalid()  是一個快捷方式,詢問底層  $task 物件是否獲得了合法資料。

驗證(validation)是透過將一組規則(稱為「constraints/約束」)加入到一個類別中來完成的。我們為 Task 類別新增規則和約束,使task屬性不能為空,duDate 欄位不空且必須是有效的DateTime物件。

Annotations:// src/AppBundle/Entity/Task.phpnamespace AppBundle\Entity; use Symfony\Component\Validator\Constraints as Assert; class Task{
    /**
     * @Assert\NotBlank()
     */
    public $task;     /**
     * @Assert\NotBlank()
     * @Assert\Type("\DateTime")
     */
    protected $dueDate;}
YAML:# src/AppBundle/Resources/config/validation.ymlAppBundle\Entity\Task:
    properties:
        task:
            - NotBlank: ~
        dueDate:
            - NotBlank: ~
            - Type: \DateTime
XML:<!-- src/AppBundle/Resources/config/validation.xml --><?xml version="1.0" encoding="UTF-8"?><constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"    xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping        http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">     <class name="AppBundle\Entity\Task">
        <property name="task">
            <constraint name="NotBlank" />
        </property>
        <property name="dueDate">
            <constraint name="NotBlank" />
            <constraint name="Type">\DateTime</constraint>
        </property>
    </class></constraint-mapping>
PHP:// src/AppBundle/Entity/Task.phpuse Symfony\Component\Validator\Mapping\ClassMetadata;use Symfony\Component\Validator\Constraints\NotBlank;use Symfony\Component\Validator\Constraints\Type; class Task{
    // ...     public static function loadValidatorMetadata(ClassMetadata $metadata)
    {
        $metadata->addPropertyConstraint('task', new NotBlank());         $metadata->addPropertyConstraint('dueDate', new NotBlank());
        $metadata->addPropertyConstraint(
            'dueDate',
            new Type('\DateTime')
        );
    }}

就是這樣!如果你現在重新以非法資料提交表單,你將會看到對應的錯誤被輸出到表單。

驗證是Symfony非常強大的功能,它擁有自己的專屬章節

html5驗證

HTML5以來,許多瀏覽器都原生支援了客戶端的驗證約束。最常用的驗證之激活方式,是在一個必填字段上渲染一個required 屬性(譯註:文檔中的“渲染”二字,對應英文rendering,可以理解為“輸出”。在Symfony中,把從程式底層或控制中向視圖層顯示內容的過程,稱為render)。對於支援HTML5的瀏覽器來說,如果使用者嘗試提交一個空白欄位到表單時,會有一條瀏覽器原生資訊顯示出來。

產生出來的表單充分利用了這個新功能,透過加入一些有意義的HTML屬性來觸發驗證。用戶端驗證,也可透過將 novalidate 屬性加入 form 標籤,或是把 formnovalidate 加入到submit標籤來關閉之。這在你想要測試伺服器端的驗證規則(validation constraints)卻被瀏覽器端阻止,例如,在提交空白欄位時,就非常有用。

PHP:<!-- app/Resources/views/default/new.html.php --><?php echo $view['form']->form($form, array(
    'attr' => array('novalidate' => 'novalidate'),)) ?>


Twig:{# app/Resources/views/default/new.html.twig #}
{{ form(form, {'attr': {'novalidate': 'novalidate'}}) }}
#

內建欄位類型 

Symfony標準版內含巨量欄位類型,涵蓋了你所能遇到的全部常規表單欄位和資料型別。

文字型欄位 

#選擇型欄位 

  • ##TimezoneType
  • ###############CurrencyType###############日期和時間欄位 ###¶#### ###############DateType###################DateTimeType############## ###TimeType##################BirthdayType################其他類型欄位 ###¶#################################################################################### #############CheckboxType###################FileType################################FileType################# #RadioType###############字段群 ###¶##################CollectionType############################################################ ##########RepeatedType##########

隱藏欄位 

按鈕 

## ¶

  • ButtonType
#ResetType

############################################################################################################################################### # #######SubmitType###############表單欄位基底類別 ###¶###################FormType ###############你也可以定義自己的欄位類型。參考 ###如何建立一個自訂的表單欄位類型###。 ######欄位類型選項 ###¶#########每種欄位類型都有一定數量的選項用於設定。例如, ###dueDate### 欄位目前被渲染成3個選擇框。而 ###DateType### 日期欄位可以被設定渲染成單一的文字方塊(使用者可以輸入字串作為日期)。 ########################
1
#############
->add('dueDate', DateType::class, array('widget' => 'single_text'))
############ ###

-simple2.png

每種欄位類型都有一系列不同的選項用於傳入此類型。關於欄位類型的細節都可以在每種類型的文件中找到。

required選項

最常用的是 required 選項,它可以套用到任何欄位。預設情況下它被設定為 true 。這就意味著支援HTML5的瀏覽器會使用客戶端驗證來判斷欄位是否為空。如果你不想需要這種行為,要嘛 關閉HTML5驗證,要嘛把欄位的 required 選項設為 false

->add('dueDate', DateType::class, array(
    'widget' => 'single_text',
    'required' => false))

要注意設定 requiredtrue 並且 表示伺服器端驗證會被使用。換句話說,如果使用者提交一個空值(blank)到該欄位(例如在舊瀏覽器中,或使用web service時),這個空值當被當作有效值來採納,除非你使用了Symfony的NotBlankNotNull 驗證約束。

也就是說, required 選項是很 "nice",但是服務端驗證卻應該 總是 使用。

label選項

#表單欄位可以使用label選項來設定表單欄位的label ,它適用於任何欄位:

->add('dueDate', DateType::class, array(
    'widget' => 'single_text',
    'label'  => 'Due Date',))

欄位的label也可以在模版渲染表單時進行設置,請參見下文。如果你不需要把label關聯到你的input(標籤),你可以設定選項值為 false

欄位類型猜測 

#現在你已經加入了驗證元資料(翻譯:即annotation)到Task 類,Symfony對於你的字段已有所了解。如果你允許,Symfony可以「猜測」你的欄位類型並幫你設定好。在下面的範例中,Symfony可以根據驗證規則猜測到task 字段是一個標準的TextType 字段,dueDateDateType 字段。

public function newAction(){
    $task = new Task();     $form = $this->createFormBuilder($task)
        ->add('task')
        ->add('dueDate', null, array('widget' => 'single_text'))
        ->add('save', SubmitType::class)
        ->getForm();}

當你省略了 add() 方法的第二個參數(或你輸入 null )時,「猜測」會被啟動。如果你輸入一個選項陣列作為第三個參數(例如上面的 dueDate),這些選項將應用於被猜測的欄位。

如果你的表單使用了一個特定的驗證群組(validation group),猜測欄位類型時仍將考慮所有 驗證約束(包括不屬於這個「正在使用中」的驗證群組的約束)。

#

對字段類型的選項進行猜測 

除了猜測字段類型,Symfony還可嘗試猜出字段選項的正確值。

當這些選項被設定時,欄位將以特殊的HTML屬性進行渲染,以用於HTML5的用戶端驗證。然而,它們不會在服務端產生相應的驗證規則(如 Assert\Length )。儘管你需要手動地新增這些伺服器端的規則,這些欄位類型的選項接下來可以根據這些規則被猜出來。

  • required
  • #required 選項可以基於驗證規則(如,該欄位是否為 NotBlankNotNull) 或是Doctrine的metadata元資料(如,該欄位是否為nullable) 而被猜出來。這非常有用,因為你的客戶端驗證將自動匹配到你的驗證規則。
  • max_length
  • 如果字段是某些列文字型字段,那麼max_length 選項可以基於驗證約束(字段是否應用了 LengthRange) 或是Doctrine元資料(透過該欄位的長度) 而被猜出來。

這些欄位選項 在你使用Symfony進行類型猜測時(即,忽略參數,或傳入null 作為add() 方法的第二個參數)才會被猜測。

如果你希望改變某個被猜出來的(選項)值,可以在欄位類型的選項陣列中傳入此項目進行覆寫。

1
#
->add('task', null, array('attr' => array('maxlength' => 4)))

建立表單類別 

如你所看到的,表單可以直接在控制器中被建立和使用。然而,一個更好的做法,是在一個單獨的PHP類別中建立表單。它能在你程式中的任何地方復用。建立一個持有「建立task表單」所需邏輯的新類別:

// src/AppBundle/Form/Type/TaskType.phpnamespace AppBundle\Form\Type; use Symfony\Component\Form\AbstractType;use Symfony\Component\Form\FormBuilderInterface;use Symfony\Component\Form\Extension\Core\Type\SubmitType; class TaskType extends AbstractType{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('task')
            ->add('dueDate', null, array('widget' => 'single_text'))
            ->add('save', SubmitType::class)
        ;
    }}

這個新類別包含了建立task表單所需的各個方面。它可用於在控制器中快速建立表單。

// src/AppBundle/Controller/DefaultController.phpuse AppBundle\Form\Type\TaskType; public function newAction(){
    $task = ...;
    $form = $this->createForm(TaskType::class, $task);     // ...}

把表單邏輯放在它自己的類別中,可以讓表單很容易地在你的專案任何地方重複使用。這是創建表單最好的方式,但是決定權在你。

設定data_class

每個表單都需要知道「持有底層資料的類別」的名稱(如 AppBundle\Entity\Task )。通常情況下,這是根據傳入 createForm 方法的第二個參數來猜測的(例如 $task )。以後,當你開始嵌入表單時,這便不再夠用。因此,雖然不是絕對必須,但透過添加下面程式碼到你的表單類型類別中,以明確地指定 data_class 選項是一個好方法。

use Symfony\Component\OptionsResolver\OptionsResolver; public function configureOptions(OptionsResolver $resolver){
    $resolver->setDefaults(array(
        'data_class' => 'AppBundle\Entity\Task',
    ));}

當表單對應成物件時,所有的欄位都會被對應。表單中的任何欄位如果在映射物件上“不存在”,都會拋出異常。

當你需要在表單中使用附加欄位(如,一個「你是否同意這些宣告?」的複選框)而這個欄位將不會被映射到底層物件時,你需要設定 mapped 選項為false

use Symfony\Component\Form\FormBuilderInterface; public function buildForm(FormBuilderInterface $builder, array $options){
    $builder
        ->add('task')
        ->add('dueDate', null, array('mapped' => false))
        ->add('save', SubmitType::class)
    ;}

另外,若表單的任何欄位未包含在提交過來的資料中,那麼這些欄位將會被明確地設定為null

在控制器中我們可以存取字段的data(字段取值):

1
$form->get('dueDate')->getData();

#此外,未被映射的欄位之數據,也可直接修改:

##

最後的思考 

#建構表單時,牢記首要目標是把一個物件(task )的資料轉換成一個HTML表單,以便使用者能夠修改(表單)取值。第二個目標就是要取到使用者提交的數據,並重新作用於該物件。

還有很多內容要掌握,Form系統有大量 威力強大 的高階技巧。

1
$form->get('dueDate')->setData(new \DateTime());