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