表單
對一個Web開發(fā)者來說,處理HTML表單是一個最為普通又極具挑戰(zhàn)的任務(wù)。Symfony整合了一個Form組件,讓處理表單變得容易起來。在本章,你將從零開始創(chuàng)建一個復(fù)雜的表單,學(xué)習(xí)表單類庫中的重要功能。
Symfony的Form組件是一個獨立的類庫,你可以在Symfony項目之外使用它。參考 Form組件文檔 以了解更多。
創(chuàng)建一個簡單的表單 ?
假設(shè)你正在構(gòu)建一個簡單的待辦事項列表,來顯示一些“任務(wù)”。你需要創(chuàng)建一個表單來讓你的用戶編輯和創(chuàng)建任務(wù)。在這之前,先來看看 Task
類,它可呈現(xiàn)和存儲一個單一任務(wù)的數(shù)據(jù)。
// 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
(任務(wù))之?dāng)?shù)據(jù)問題。當(dāng)然,在本章的最后,你將能夠通過HTML表單把數(shù)據(jù)提交到一個 Task
實例,驗證它的值,并把它持久化到數(shù)據(jù)庫。
構(gòu)建表單 ?
現(xiàn)在你已經(jīng)創(chuàng)建了一個 Task
類,下一步就是創(chuàng)建和渲染一個真正的html表單了。在Symfony中,這是通過構(gòu)建一個表單對象并將其渲染到模版來完成的?,F(xiàn)在,在控制器里即可完成所有這些:
// 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 // 創(chuàng)建一個task對象,賦一些例程中的假數(shù)據(jù)給它 $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(), )); }}
這個例子說明了如何直接在控制器中構(gòu)建你的form(表單)。后面的 創(chuàng)建表單類 中,你將使用一個獨立的類來構(gòu)建表單,這種方法被推薦,因為表單可以復(fù)用。
創(chuàng)建表單不需要很多代碼,因為Symfony的表單對象是通過一個“form builder(表單生成器)”來創(chuàng)建的。form builder的目的是讓你編寫簡單的表單創(chuàng)建“指令”,而真實創(chuàng)建表單時的全部“重載”任務(wù)則交由builder完成。
本例中,你已經(jīng)添加了兩個字段到表單,即 task
和 dueDate
。對應(yīng)的是 Task
類中的 task
和 dueDate
屬性。你已為它們分別指定了FQCN(Full Quilified Class Name/完整路徑類名)的“類型”(如 TextType
, DateType
),由類型決定為字段生成哪一種HTML表單標(biāo)簽(標(biāo)簽組)。
最后,你添加了一個帶有自定義label的提交按鈕以向服務(wù)器提交表單。
Symfony附帶了許多內(nèi)置類型,它們將被簡短地介紹(見下面的內(nèi)置表單類型)。
渲染表單 ?
表單創(chuàng)建之后,下一步就是渲染它。這是通過傳遞一個特定的表單“view”對象(注意上例控制器中的 $form->createView()
方法)到你的模板,并通過一系列的表單helper function(幫助函數(shù))來實現(xiàn)的。
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) ?>
本例假設(shè)你以"POST"請求提交表單,并且提交到和“表單顯示(頁面)”相同的URL。后面你將學(xué)習(xí)如何改變請求方法(request method)和表單提交后的目標(biāo)URL。
就是這樣!只需要三行就可以渲染出完整的form表單:
form_start(form)
- 渲染表單的開始標(biāo)簽,包括在使用文件上傳時的正確enctype屬性。
form_widget(form)
- 渲染出全部字段,包含字段元素本身,字段label以及字段驗證的任何錯誤信息。
form_end(form)
- 當(dāng)你手動生成每個字段時,它可以渲染表單結(jié)束標(biāo)簽以及表單中所有尚未渲染的字段。這在渲染隱藏字段以及利用自動的 CSRF Protection 保護(hù)機(jī)制時非常有用。
就是這么簡單,但不太靈活(暫時)。通常情況下,你希望單獨渲染出表單中的每一個字段,以便控制表單的樣式。你將在后面 如何去控制表單渲染 文章中掌握這種方法。
在繼續(xù)下去之前,請注意,為什么渲染出來的 task
輸入框中有一個來自 $task
對象的屬性值(即“Write a blog post”)。這是表單的第一個任務(wù):從一個對象中獲取數(shù)據(jù)并把它轉(zhuǎn)換成一種適當(dāng)?shù)母袷剑员阍贖TML表單中被渲染。
表單系統(tǒng)足夠智能,它們通過 getTask()
和 setTask()
方法來訪問 Task
類中受保護(hù)的 task
屬性。除非是public屬性,否則 必須 有一個 "getter" 和 "setter" 方法被定義,以便表單組件能從這些屬性中獲取和寫入數(shù)據(jù)。對于布爾型的屬性,你可以使用一個 "isser" 和 "hasser" 方法(如 isPublished()
和 hasReminder()
)來替代getter方法(getPublished()
和 getReminder()
)。
處理表單提交 ?
默認(rèn)時,表單會把POST請求,向“渲染它的同一個控制器”提交回去。
此處,表單的第二個任務(wù)就是把用戶提交的數(shù)據(jù)傳回到一個對象的屬性之中。要做到這一點,用戶提交的數(shù)據(jù)必須寫入表單對象才行。向控制器(Controller)中添加以下功能:
// ...use Symfony\Component\HttpFoundation\Request; public function newAction(Request $request){ // just setup a fresh $task object (remove the dummy data) // 直接設(shè)置一個全新$task對象(刪除了假數(shù)據(jù)) $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! // 一些操作,比如把任務(wù)存到數(shù)據(jù)庫中 // 例如,如果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()
方法應(yīng)該在 handleRequest
被調(diào)用 之后 再調(diào)用。否則,針對 *_SUBMIT
表單事件的修改,將不會應(yīng)用到視圖層(比如驗證時的錯誤信息)。
控制器(controller)在處理表單時遵循的是一個通用模式(common pattern),它有三個可能的途徑:
當(dāng)瀏覽器初始加載一個頁面時,表單被創(chuàng)建和渲染。
handleRequest()
意識到表單沒有被提交進(jìn)而什么都不做。如果表單未被提交,isSubmitted()
返回false;當(dāng)用戶提交表單時,
handleRequest()
會識別這個動作并立即將提交的數(shù)據(jù)寫入到$task
對象的task
anddueDate
屬性。然后該對象被驗證。如果它是無效的(驗證在下一章),isValid()
會返回false
,進(jìn)而表單被再次渲染,只是這次有驗證錯誤;當(dāng)用戶以合法數(shù)據(jù)提交表單的時,提交的數(shù)據(jù)會被再次寫入到表單,但這一次
isValid()
返回true
。在把用戶重定向到其他一些頁面之前(如一個“謝謝”或“成功”的頁面),你有機(jī)會用$task
對象來進(jìn)行某些操作(比如把它持久化到數(shù)據(jù)庫)。表單成功提交之后的重定向用戶,是為了防止用戶通過瀏覽器“刷新”按鈕重復(fù)提交數(shù)據(jù)。
如果你需要精確地控制何時表單被提交,或哪些數(shù)據(jù)被傳給表單,你可以使用 submit()。更多信息請參考 手動調(diào)用Form::submit()。
表單驗證 ?
在上一節(jié)中,你了解了附帶了有效或無效數(shù)據(jù)的表單是如何被提交的。在Symfony中,驗證環(huán)節(jié)是在底層對象中進(jìn)行的(例如 Task
)。換句話說,問題不在于“表單”是否有效,而是 $task
對象在“提交的數(shù)據(jù)應(yīng)用到表單”之后是否合法。調(diào)用 $form->isvalid()
是一個快捷方式,詢問底層 $task
對象是否獲得了合法數(shù)據(jù)。
驗證(validation)是通過把一組規(guī)則(稱之為“constraints/約束”)添加到一個類中來完成的。我們給 Task
類添加規(guī)則和約束,使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') ); }}
就是這樣!如果你現(xiàn)在重新以非法數(shù)據(jù)提交表單,你將會看到相應(yīng)的錯誤被輸出到表單。
驗證是Symfony一個非常強(qiáng)大的功能,它擁有自己的專屬章節(jié)。
內(nèi)置字段類型 ?
Symfony標(biāo)準(zhǔn)版內(nèi)含巨量字段類型,涵蓋了你所能遇到的全部常規(guī)表單字段和數(shù)據(jù)類型。
文本型字段 ?
- TextType
- TextareaType
- EmailType
- IntegerType
- MoneyType
- NumberType
- PasswordType
- PercentType
- SearchType
- UrlType
- RangeType
選擇型字段 ?
日期和時間字段 ?
其他類型字段 ?
字段群 ?
隱藏字段 ?
按鈕 ?
表單字段基類 ?
你也可以定義自己的字段類型。參考 如何創(chuàng)建一個自定義的表單字段類型。
字段類型選項 ?
每一種字段類型都有一定數(shù)量的選項用于配置。比如, dueDate
字段當(dāng)前被渲染成3個選擇框。而 DateType 日期字段可以被配置渲染成一個單一的文本框(用戶可以輸入字符串作為日期)。
1 | ->add('dueDate', DateType::class, array('widget' => 'single_text')) |
每一種字段類型都有一系列不同的選項用于傳入此類型。關(guān)于字段類型的細(xì)節(jié)都可以在每種類型的文檔中找到。
字段類型猜測 ?
現(xiàn)在你已經(jīng)添加了驗證元數(shù)據(jù)(譯注:即annotation)到 Task
類,Symfony對于你的字段已有所了解。如果你允許,Symfony可以“猜到”你的字段類型并幫你設(shè)置好。在下面的例子中,Symfony可以根據(jù)驗證規(guī)則猜測到 task
字段是一個標(biāo)準(zhǔn)的 TextType
字段, dueDate
是 DateType
字段。
public function newAction(){ $task = new Task(); $form = $this->createFormBuilder($task) ->add('task') ->add('dueDate', null, array('widget' => 'single_text')) ->add('save', SubmitType::class) ->getForm();}
當(dāng)你省略了 add()
方法的第二個參數(shù)(或者你輸入 null
)時,“猜測”會被激活。如果你輸入一個選項數(shù)組作為第三個參數(shù)(比如上面的 dueDate
),這些選項將應(yīng)用于被猜測的字段。
如果你的表單使用了一個特定的驗證組(validation group),猜測字段類型時仍將考慮 所有 驗證約束(包括不屬于這個“正在使用中”的驗證組的約束)。
對字段類型的選項進(jìn)行猜測 ?
除了猜測字段類型,Symfony還可嘗試猜出字段選項的正確值。
當(dāng)這些選項被設(shè)置時,字段將以特殊的HTML屬性進(jìn)行渲染,以用于HTML5的客戶端驗證。然而,它們不會在服務(wù)端生成相應(yīng)的驗證規(guī)則(如 Assert\Length
)。盡管你需要手動地添加這些服務(wù)器端的規(guī)則,這些字段類型的選項接下來可以根據(jù)這些規(guī)則被猜出來。
required
required
選項可以基于驗證規(guī)則 (如,該字段是否為NotBlank
或NotNull
) 或者是Doctrine的metadata元數(shù)據(jù) (如,該字段是否為nullable
) 而被猜出來。這非常有用,因為你的客戶端驗證將自動匹配到你的驗證規(guī)則。max_length
- 如果字段是某些列文本型字段,那么
max_length
選項可以基于驗證約束 (字段是否應(yīng)用了Length
或Range
) 或者是Doctrine元數(shù)據(jù) (通過該字段的長度) 而被猜出來。
這些字段選項 僅 在你使用Symfony進(jìn)行類型猜測時(即,忽略參數(shù),或傳入null
作為 add()
方法的第二個參數(shù))才會被猜測。
如果你希望改變某個被猜出來的(選項)值,可以在字段類型的選項數(shù)組中傳入此項進(jìn)行覆寫。
1 | ->add('task', null, array('attr' => array('maxlength' => 4))) |
創(chuàng)建表單類 ?
正如你看到的,表單可以直接在控制器中被創(chuàng)建和使用。然而,一個更好的做法,是在一個單獨的PHP類中創(chuàng)建表單。它能在你程序中的任何地方復(fù)用。創(chuàng)建一個持有“構(gòu)建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) ; }}
這個新類包含了創(chuàng)建task表單所需要的方方面面。它可用于在控制器中快速創(chuàng)建表單。
// src/AppBundle/Controller/DefaultController.phpuse AppBundle\Form\Type\TaskType; public function newAction(){ $task = ...; $form = $this->createForm(TaskType::class, $task); // ...}
把表單邏輯置于它自己的類中,可以讓表單很容易地在你的項目任何地方復(fù)用。這是創(chuàng)建表單最好的方式,但是決定權(quán)在你。
當(dāng)把表單映射成對象時,所有的字段都將被映射。表單中的任何字段如果在映射對象上“不存在”,都會拋出異常。
當(dāng)你需要在表單中使用附加字段(如,一個 “你是否同意這些聲明?”的復(fù)選框)而這個字段將不被映射到底層對象時,你需要設(shè)置 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) ;}
另外,若表單的任何字段未包含在提交過來的數(shù)據(jù)中,那么這些字段將被顯式設(shè)置為 null
。
在控制器中我們可以訪問字段的data(字段取值):
1 | $form->get('dueDate')->getData(); |
此外,未被映射的字段之?dāng)?shù)據(jù),也可直接修改:
1 | $form->get('dueDate')->setData(new \DateTime()); |
最后的思考 ?
構(gòu)建表單時,牢記首要目標(biāo)是把一個對象(task
)的數(shù)據(jù)轉(zhuǎn)換成一個HTML表單,以便用戶能夠修改(表單)取值。第二個目標(biāo)就是要取到用戶提交的數(shù)據(jù),并重新作用于該對象。
還有很多內(nèi)容需要掌握,F(xiàn)orm系統(tǒng)有大量 威力強(qiáng)大 的高級技巧。