對于一個Web開發者來說,處理HTML表單是一個最為普通又具挑戰的任務。Symfony2內建了一個Form元件,讓處理表單變的容易起來。在這一節裡,我們将
從基礎開始建立一個複雜的表單,學習表單類庫中最重要的内容。
Symfony2 的Form元件是一個獨立的類庫,你可以在Symfony2項目之外使用它。
建立一個簡單的表單:
假設你要建立一個應用程式的todo清單,需要顯示一些任務。因為你的使用者需要編輯和建立任務,是以你需要建立一個表單。在你開始之前,首先來看通用的Task類,用來表示和存儲一個單一任務的資料:
// src/Acme/TaskBundle/Entity/Task.php
namespace Acme\TaskBundle\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;
}
}
如果你是按照我們提供的示例編碼,那麼你需要先建立一個AcmeTaskBundle:
$ php app/console generate:bundle --namespace=Acme/TaskBundle
該類是一個普通的PHP對象類,因為他們沒有任何Symfony或者其它類庫引用。非常簡單的一個PHP對象類,它直接解決的是你程式中表現task的資料。當然,在本節的最後,你将能夠通過HTML表單送出一個Task執行個體資料,校驗它的數值,并把它持久化到資料庫。
建立一個Form
現在已經建立了一個Task類,下一步就是建立和渲染一個真正的HTML表單了。在symfony2中,它是通過建立一個表單對象并渲染到模闆的。現在,可以從controller内部處理form。
//src/Acme/TaskBundle/Controller/DefaultController.php
namespace Acme\TaskBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Acme\TaskBundle\Entity\Task;
class DefaultController extends Controller
{
//建立一個任務并給它一些假資料作為示例
$task = new Task();
$task->setTask('Write a blog post');
$task->setDueDate(new \DateTime('tomorrow'));
$form = $this->createFormBuilder($task)
->add('task','text')
->add('dueDate','date')
->getForm();
return $this->render('AcmeTaskBundle:Default:new.html.twig',array(
'form' =>$form->createView(),
));
}
上面的示例顯示了如何直接在Controller中建立一個表單,為了可以讓表單重用你完全可以在一個單獨的類檔案中建立表單。
因為Symfony2通過一個表單生成器“form builder"來建立表單對象,是以你可以使用很少的代碼就能完成建立表單任務。表單生成器的目的是讓你能編寫簡單的表單建立方法,讓它來負責繁重的建立任務。
在這個示例中,你已經添加了兩個字段到你的表單,一個是task一個是dueDate。它們關聯到Task類的task和dueDate屬性。你已經為它們分别指定了類型(比如,text,date等),由這些類型來決定為這些字段生成什麼樣的HTML表單标簽。
Symfony2 擁有許多内建的類型,接下來我們将簡單的介紹。
渲染一個表單
表單建立以後,下一步就是渲染它。這是通過傳遞一個特定的表單”view"對象(就是上例中的 $form->createView()傳回的view對象)到你的模闆并通過一些列的表單幫助函數來實作的。
Twig格式:
{# src/Acme/TaskBundle/Resources/views/Default/new.html.twig #}
<form action="{{ path('task_new') }}" method ="post" {{ form_enctype(form) }}>
{{ form_widget(form) }}
<input type="submit" />
</form>
PHP代碼格式:
<!-- src/Acme/TaskBundle/Resources/views/Default/new.html.php -->
<form action="<?php echo $view['router']->generate('task_new') ?>" method="post" <?php echo $view['form']->enctype($form) ?> >
<?php echo $view['form']->widget($form) ?>
<input type="submit" />
</form>
在這裡假設你已經建立了一個名叫task_new的路由指向AcmeTaskBundle:Default:new Controller。
就是這些了,通過列印form_widget(form),表單中的每個字段都會被渲染出來。同時還有一個文本标簽和錯誤資訊。是不是很簡單,不過現在它還不夠靈活。通常情況下,我們渴望單獨渲染表單中的每一個字段,這樣我們可以更好的控制表單的樣式。我們會在在模闆中渲染表單一節介紹。
在繼續下去之前,我們注意到,為什麼我們渲染出來的task輸入框中有一個來自$task對象的屬性值“Write a blog post"。這是表單的第一個工作:從一個對象中擷取資料并把它轉換為合适的格式渲染到一個HTML表單中。
注意,表單系統已經足夠聰明,它們能夠通過像getTask()和setTask()方法來通路Task類中受保護的屬性task。除非一個是公共屬性,否則必須有一個getter和setter方法被定義來用于表單元件從這些屬性中擷取和保持資料。對于布爾型的屬性,你可以使用一個”isser"方法(比如 isPublished())替代getter方法(getPublished())。
處理表單送出
表單系統的第二個任務就是傳遞使用者送出的資料回到一個對象的屬性中。要做到這一點,使用者送出的資料必須綁定到表單才行。添加如下代碼到你的Controller類:
//...
public function newAction(Request $request)
{
//隻是建立一個新的$task對象(不需要假資料)
$task = new Task();
$form= $this->createFormBuilder($task)
->add('task','text')
->add('dueDate','date')
->getForm();
if($request->getMethod() == "POST"){
$form->bindRequest($request);
if($form->isValid()){
//執行一些行為,比如保持task到資料庫
return $this->redirect($this->generateUrl('task_success'));
}
}
//...
}
現在,當表單被送出時,Controller可以綁定被送出的資料到表單,表單會把資料傳回$task對象的task和dueDate屬性。這些都在bindRequest()方法中完成。隻要bindRequest()方法被調用,送出的資料就會立刻被傳輸到底層對象。不管資料是否被真正的校驗通過。
controller一般會遵循一個通用的模式來處理表單,它有三個可能的途徑:
1.當在浏覽器初始加載一個頁面時,請求方法是GET,表單處理僅僅是建立和渲染。
2.當使用者送出帶有不合法資料的表單(方法為POST)時,表單會并綁定然後渲染,這時候顯示所有校驗錯誤。
3.當使用者送出的表單帶有的資料均合法時,表單綁定并且在頁面跳轉之前你有機會去使用資料去執行一些業務邏輯活動,比如持久化它到資料庫)。
表單校驗
在前面我們提到了,如何送出一個帶有合法資料和非法資料的表單。在Symfony2中,校驗是在底層對象上進行的。換句話說,form表單合法與否不重要,主要看在表單送出資料以後,底層對象比如$task對象是否合法。調用$form->isvalid() 是一個詢問底層對象是否獲得合法資料的快捷方式。
校驗是通過添加一些列規則(限制)到一個類來完成的。我們給Task類添加規則和限制,使它的task屬性不能為空,duDate字段不能空并且是一個合法的DateTime對象。
YAML格式:
# Acme/TaskBundle/Resources/config/validation.yml
Acme\TaskBundle\Entity\Task:
properties:
task:
- NotBlank: ~
dueDate:
- NotBlank: ~
- Type: \DateTime
在Task類中聲明格式:
// Acme/TaskBundle/Entity/Task.php
use Symfony\Component\Validator\Constraints as Assert;
class Task
{
/**
* @Assert\NotBlank()
*/
public $task;
/**
* @Assert\NotBlank()
* @Assert\Type("\DateTime")
*/
protected $dueDate;
}
XML格式:
<!-- Acme/TaskBundle/Resources/config/validation.xml -->
<class name="Acme\TaskBundle\Entity\Task">
<property name="task">
<constraint name="NotBlank" />
</property>
<property name="dueDate">
<constraint name="NotBlank" />
<constraint name="Type">
<value>\DateTime</value>
</constraint>
</property>
</class>
PHP代碼格式:
// Acme/TaskBundle/Entity/Task.php
use 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'));
}
}
就是這樣了,如果你現在再送出包含非法資料的表單,你将會看到相應的錯誤被列印在表單上。
HTML5 校驗
作為HTML5,許多浏覽器都加強了用戶端某些校驗限制。最常用的校驗活動是在一個必須的字段上渲染一個required屬性。對于支援HTML5的浏覽器來說,如果使用者此時送出一個空字段到表單時,浏覽器會顯示提示資訊。生成的表單廣泛吸收了這些新内容的優點,通過添加一些HTML屬性來監控校驗。用戶端校驗可以通過添加novalidate屬性到form标簽或者formnovalidate 到送出标簽而關閉。這對你想檢查服務端校驗規則時非常有用。
校驗分組
如果你的對象想從校驗組中受益,你需要指定你的表單使用哪個校驗組。
$form = $this->createFormBuilder($users, array(
'validation_groups' => array('registration'),
))->add(...)
;
如果你建立表單類,你需要添加羨慕的getDefaultOptions()方法:
public function getDefaultOptions(array $options)
{
return array(
'validation_groups' => array('registration')
);
}
在這兩種情況下,隻有registration 校驗組将被用于校驗底層對象。
内建字段類型
Symfony标準版含有大量的字段類型,它們幾乎涵蓋了所有通用表單的字段和資料類型。
文本字段:
text
textarea
integer
money
number
password
percent
search
url
選擇字段:
choice
entity
country
language
locale
timezone
日期和時間字段:
date
datetime
time
birthday
其它字段:
checkbox
file
radio
字段組:
collection
repeated
隐藏字段:
hidden
csrf
基礎字段:
field
form
當然,你也可以定義自己的字段類型。
字段類型選項
每一個字段類型都有一定數量的選項用于配置。比如,dueDate字段目前被渲染成3個選擇框。而日期字段可以被配置渲染成一個單一的文本框,使用者可以輸入字元串作為日期。
->add('dueData','data', array('widget' = 'single_text'))
required選項:
最常用到的選項是required選項,它可以應用于任何字段。預設情況下它被設定為true。這就意味着支援HTML5的浏覽器會使用用戶端校驗來判斷字段是否為空。如果你不想讓它發生,或者把在你的字段上把required選項設定為false,或者關閉HTML5校驗。設定required為true并不意味着服務端校驗被應用。換句話說,如果使用者送出一個空數值到該字段,它将接受這個控制除非你使用Symfony的NotBlank或者NotNull校驗限制。也就是說,required選項是很好,但是服務端校驗還是要繼續用。
label選項:
表單字段可以使用label選項設定顯示字元标簽,可以應用于任何字段:
->add('dueDate', 'date',array(
'widget' =>'single_text',
'label' => 'Due Date',
))
字段類型猜測:
現在你已經添加了校驗中繼資料到Task類,Symfony早已經了解一點關于你的字段了。如果你允許,Symfony可以猜到你的字段資料類型并為你設定它。在下面的例子中,Symfony可以根據校驗規則猜測到task字段是一個标準的text字段,dueDate是date字段。
public function newAction()
{
$task = new Task();
$form = $this->createFormBuilder($task)
->add('task')
->add('dueDate', null, array('widget' => 'single_text'))
->getForm();
}
當你省略了add方法的第二個參數(或者你輸入null)時,Symfony的猜測能力就起作用了。如果你輸入一個選項數組作為第三個參數(比如上面的dueDate),那麼這些選項會成為Symfony猜測的依據。如果你的表單使用了指定的校驗數組,字段類型猜測器将還是要考慮所有的校驗規則來綜合猜測你的字段類型。
字段類型可選項猜測
除了猜測字段類型,Symfony還能是這猜測一些可選項字段值。當這些可選項被設定時,字段将會被渲染到特定HTML屬性中,讓HTML5用戶端來提供校驗。
然而,它們不會在服務端生成相應的校驗規則。盡管你需要手動的在服務端添加這些規則,但是這些字段類型選項還是能根據這些資訊猜測到。
required: required規則可以在校驗規則或者Doctrine中繼資料的基礎上猜測到。這當你的用戶端校驗将自動比對你的校驗規則時很有用。
max_length: 如果字段是一些列文本字段,那麼max_length選項可以從校驗規則或者Doctrine中繼資料中猜到。
如果你喜歡改變一個猜到的數值,你可以通過在可選項數組中傳遞該選項來重寫它。
->add('task',null, array('max_length'=>4))
在模闆中渲染表單
到目前為止,我們已經看了一個完整的表單是如何通過一行代碼被渲染的。當然,你通常需要更加靈活的渲染方式:
Twig格式:
{# src/Acme/TaskBundle/Resources/views/Default/new.html.twig #}
<form action="{{ path('task_new') }}" method="post" {{ form_enctype(form) }}>
{{ form_errors(form) }}
{{ form_row(form.task) }}
{{ form_row(form.dueDate) }}
{{ form_rest(form) }}
<input type="submit" />
</form>
PHP代碼格式:
<!-- // src/Acme/TaskBundle/Resources/views/Default/newAction.html.php -->
<form action="<?php echo $view['router']->generate('task_new') ?>" method="post" <?php echo $view['form']->enctype($form) ?>>
<?php echo $view['form']->errors($form) ?>
<?php echo $view['form']->row($form['task']) ?>
<?php echo $view['form']->row($form['dueDate']) ?>
<?php echo $view['form']->rest($form) ?>
<input type="submit" />
</form>
讓我們看看這組代碼的詳細:
form_enctype(form) 隻要有一個字段是檔案上傳,那麼它就會義務的設定為 enctype="multipart/form-data";
form_errors(form) 渲染任何整個form的任何錯誤資訊(特定字段的錯誤,會顯示在每個字段的下面一行)。
form_row(form.dueDate) 預設情況下,為給定的字段在一個div中渲染一個文本标簽,任何錯誤,和HTML表單部件。
form_rest(form) 渲染沒有指出的其餘任何字段,通常在表單的末尾調用它防止遺忘或者渲染一些你不願意手動設定的隐藏字段。它同時還能為我們提供CSRF保護。
大部分工作是由form_row幫助方法類完成的,它預設在一個div中為每個字段渲染顯示标簽,錯誤資訊和HTML表單部件。
注意,你可以通過form.vars.value 來通路你目前是表當資料:
Twig格式:
{{ form.vars.value.task }}
PHP代碼格式:
<?php echo $view['form']->get('value')->getTask() ?>
手工渲染每一個表單字段
form_row幫助器能讓你很快的渲染你表單中的每一個字段,并且每一行可以被自定義化。但是生活不總是那麼簡單的,你也可能要手動的渲染每一個字段。
Twig格式:
{{ form_errors(form) }}
<div>
{{ form_label(form.task) }}
{{ form_errors(form.task) }}
{{ form_widget(form.task) }}
</div>
<div>
{{ form_label(form.dueDate) }}
{{ form_errors(form.dueDate) }}
{{ form_widget(form.dueDate) }}
</div>
{{ form_rest(form) }}
PHP代碼格式:
<?php echo $view['form']->errors($form) ?>
<div>
<?php echo $view['form']->label($form['task']) ?>
<?php echo $view['form']->errors($form['task']) ?>
<?php echo $view['form']->widget($form['task']) ?>
</div>
<div>
<?php echo $view['form']->label($form['dueDate']) ?>
<?php echo $view['form']->errors($form['dueDate']) ?>
<?php echo $view['form']->widget($form['dueDate']) ?>
</div>
<?php echo $view['form']->rest($form) ?>
如果自動生成顯示标簽不準确,那麼你可以顯式的指定它:
Twig格式:
{{ form_label(form.task, 'Task Description') }}
PHP代碼格式:
<?php echo $view['form']->label($form['task'], 'Task Description') ?>
一些字段類型有一些額外的渲染選項可以傳入widget,一個常用的選項為attr,它允許你修改表單元素的屬性。下面的示例将添加task_field class到渲染的文本輸入字段:
Twig格式:
{{ form_widget(form.task, {'attr': {'class':'task_field'} }) }}
PHP代碼格式:
<?php echo $view['form']->widget($form['task'], array(
'attr' => array('class' => 'task_field'),
)) ?>
如果你想手工渲染表單字段,你可以單獨通路每個字段的值,比如id,name和label,這裡我們擷取id
Twig格式:
{{ form.task.vars.id }}
PHP代碼格式:
<?php echo $form['task']->get('id') ?>
需要擷取表單字段名稱屬性你需要使用full_name值:
Twig格式:
{{ form.task.vars.full_name }}
PHP代碼格式:
<?php echo $form['task']->get('full_name') ?>
建立表單類
正如你看到的,一個表單可以直接在controller類中被建立和使用。然而,一個更好的做法是在一個單獨的PHP類中建立表單。它可以被重用到你應用程式的任何地方。建立一個新類來儲存生成task表單的邏輯:
// src/Acme/TaskBundle/Form/Type/TaskType.php
namespace Acme\TaskBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
class TaskType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('task');
$builder->add('dueDate', null, array('widget' => 'single_text'));
}
public function getName()
{
return 'task';
}
}
這個新類包含了所有建立一個task表單所需要的内容,注意getName()方法将傳回一個該表單類型的唯一辨別,用于快速建立該表單。
// src/Acme/TaskBundle/Controller/DefaultController.php
// 在類上添加這個新的引用語句
use Acme\TaskBundle\Form\Type\TaskType;
public function newAction()
{
$task = // ...
$form = $this->createForm(new TaskType(), $task);
// ...
}
設定data_class
每個表單都需要知道它底層儲存資料的類名稱,(比如Acme\TaskBundle\Entity\Task)。通常情況下,是根據createForm方法的第二個參數來猜測的。以後,當你開始嵌入表單時,這個可能就不怎麼充分了,是以,通常一個好的方法是通過添加下面代碼到你的表單類型類來顯式的指定data_class 選項。
public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'Acme\TaskBundle\Entity\Task',
);
}
當然,這種做法也不總是必須的。
當你映射表單到一個對象是,所有的字段都被映射。 表單的任何字段如果在映射的對象上不存在那麼就會造成抛出異常。在這種情況下,你需要在表單中擷取字段(比如,一個“你同意這些說法嗎?”複選框)将不能映射到底層對象,那麼你需要設定property_path為false以避免抛出異常。
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('task');
$builder->add('dueDate', null, array('property_path' => false));
}
另外,如果有任何的表單字段沒有被包含着送出的資料中,那麼這些字段需要顯式的設定為null。
在controller類中我們可以通路字段資料:
$form->get('dueDate')->getData();
Forms和Doctrine
表單的目的是把資料從一個底層對象傳遞給一個HTML表單然後把使用者送出的資料傳回到原先的底層對象。是以,底層對象把資料持久化到資料庫就跟表單沒有任何的關系了。但是,如果你已經配置了底層類是通過Doctrine來持久化,(你已經定義了映射中繼資料在底層類),接下來當表單送出資料後,當表單合法後就可以持久化它了。
if ($form->isValid()) {
$em = $this->getDoctrine()->getEntityManager();
$em->persist($task);
$em->flush();
return $this->redirect($this->generateUrl('task_success'));
}
如果處于某種原因,你不想通路原有的$task對象,你可以從表單中直接擷取資料:
$task = $form->getData();
在這裡,關鍵要了解當表單跟底層對象綁定後,使用者送出的資料會立刻傳遞給底層對象。如果你想持久化這些資料,你隻需要持久化對象本身即可。
嵌入式表單:(Embedded Forms)
通常,你可能想生成一個表單,它包含來自不同對象的字段。比如,一個系統資料庫單可能包含屬于User對象和Address對象的字段。幸運的是,這些對于form元件來說都是很容易很自然的事。嵌入一個單獨對象:假設每個Task屬于一個Category對象,首先建立這個Category對象:
// src/Acme/TaskBundle/Entity/Category.php
namespace Acme\TaskBundle\Entity;
use Symfony\Component\Validator\Constraints as Assert;
class Category
{
/**
* @Assert\NotBlank()
*/
public $name;
}
接下來,添加一個新的category屬性到Task類:
// ...
class Task
{
// ...
/**
* @Assert\Type(type="Acme\TaskBundle\Entity\Category")
*/
protected $category;
// ...
public function getCategory()
{
return $this->category;
}
public function setCategory(Category $category = null)
{
$this->category = $category;
}
}
現在我們來相應我們應用程式的一個新需求,需要建立一個 表單可以讓使用者修改Category對象。
// src/Acme/TaskBundle/Form/Type/CategoryType.php
namespace Acme\TaskBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
class CategoryType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('name');
}
public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'Acme\TaskBundle\Entity\Category',
);
}
public function getName()
{
return 'category';
}
}
我們的最終目的是能夠讓使用者在Task表單中修改Category對象,是以,我們需要添加一個類型為CategoryType表單類的category字段到TaskType 表單類。
public function buildForm(FormBuilder $builder, array $options)
{
// ...
$builder->add('category', new CategoryType());
}
這時我們可以在TaskType類字段渲染的旁邊渲染CategoryType類的字段了:
Twig格式:
{# ... #}
<h3>Category</h3>
<div class="category">
{{ form_row(form.category.name) }}
</div>
{{ form_rest(form) }}
{# ... #}
PHP代碼格式:
<!-- ... -->
<h3>Category</h3>
<div class="category">
<?php echo $view['form']->row($form['category']['name']) ?>
</div>
<?php echo $view['form']->rest($form) ?>
<!-- ... -->
當使用者送出表單時,送出的Category字段資料被用于建立一個Category執行個體,然後被設定到Task執行個體的category字段。該Category執行個體可以通過Task執行個體來通路,同時也能被持久化到資料或者用作它用。
$task->getCategory()
嵌入一個表單集合
你也可以将一個表單集合嵌入到一個表單(想象一個Category 表單和許多Product子表單)。它是通過一個字段類型集合類實作的。
表單主題化
表單的每一部分渲染都是可以被自定義個性化的。你可以自由的改變每一個表單行的渲染,改變渲染錯誤的标志,更或者是textarea标簽應該怎樣顯示等。沒有任何限制,不同的個性化設定能用到不同的區域。
Symfony使用模闆渲染每一個或者部分表單,比如label标簽,input标簽,錯誤資訊以及任何其它内容。在Twig中,每個表單片段會被一個Twig block來渲染。要個性化渲染表單,你隻需要重寫相應的block即可。在PHP模闆中,它是通過單獨的模闆檔案來渲染表單片段的,是以你需要通過編寫新的模闆來替代舊的模闆即可。在了解了它們是怎麼工作的之後,讓我們來個性化form_row片段并添加一個class屬性到包裹每一表單行的div元素。首先建立一個新模闆檔案用于存放新的标志:
Twig格式:
{# src/Acme/TaskBundle/Resources/views/Form/fields.html.twig #}
{% block field_row %}
{% spaceless %}
<div class="form_row">
{{ form_label(form) }}
{{ form_errors(form) }}
{{ form_widget(form) }}
</div>
{% endspaceless %}
{% endblock field_row %}
PHP代碼格式:
<!-- src/Acme/TaskBundle/Resources/views/Form/field_row.html.php -->
<div class="form_row">
<?php echo $view['form']->label($form, $label) ?>
<?php echo $view['form']->errors($form) ?>
<?php echo $view['form']->widget($form, $parameters) ?>
</div>
field_row表單片段會在通過form_row函數渲染大部分的表單字段時使用。 要告訴你的表單元件使用你的新的field_row片段,需要添加下面的内容到你渲染該表單的模闆頂部:
Twig格式:
{# src/Acme/TaskBundle/Resources/views/Default/new.html.twig #}
{% form_theme form 'AcmeTaskBundle:Form:fields.html.twig' %}
{% form_theme form 'AcmeTaskBundle:Form:fields.html.twig' 'AcmeTaskBundle:Form:fields2.html.twig' %}
<form ...>
PHP代碼格式:
<!-- src/Acme/TaskBundle/Resources/views/Default/new.html.php -->
<?php $view['form']->setTheme($form, array('AcmeTaskBundle:Form')) ?>
<?php $view['form']->setTheme($form, array('AcmeTaskBundle:Form', 'AcmeTaskBundle:Form')) ?>
<form ...>
其中的form_theme 标簽導入前面定義的片段。換句話說,當form_row函數在模闆中被調用後,它将從你的自定義主題中使用field_row 塊(替代Symfony已有的field_row block)。你的個性化主題不必重寫所有的塊。當渲染一個你沒有重寫過的塊時,主題引起會找全局的主題(定義在bundle級的主題)使用。
在擁有多個個性化主題的情況下,它會在使用全局主題之前查找定制清單。要個性化你表單的任何部分,你隻需要重寫相關的片段即可。
表單片段命名
在symfony中,表單的每一部分都會被渲染,HTML表單元素,錯誤消息,顯示标簽等這些都是被定義在基礎主題裡的。它組成了一個Twig的塊集合和一個PHP模闆集合。
在Twig中,每個需要的塊都被定義到一個單獨的模闆檔案中(form_dive_layout.html.twig),它們被儲存在Twig Bridge裡。在這個檔案中,你可以看到渲染一個表單多需要的每一個block和預設的字段類型。
在PHP模闆中,片段是單獨的模闆檔案。 預設情況下它們位于架構bundle的Resources/views/Form 目錄下。每個偏度名稱都遵循相同的基本模式,用一個下劃線(_)分為兩部分,比如:
field_row 用于form_row渲染大部分的字段
textarea_widget 用于form_widget渲染一個textarea字段類型
field_errors 用于form_errors渲染一個字段的錯誤資訊
每個片段都命名都遵循:type_part 模式。type部分對應被渲染的字段類型(比如textarea,checkbox,date等),而part部分對應着是什麼被渲染(比如label,widget,errors等)
預設情況下,有4種可能的表單part被用來渲染:
label 渲染字段的标簽 如field_label
widget 渲染字段的HTML表示 如field_widget
errors 渲染字段的錯誤資訊 如field_errors
row 渲染字段的整個行(包括label,widget和errors) 如 filed_row
還有其它3個part類型,分别是rows,rest和enctype,不過這三個一般不會用到。
通過知道字段類型(比如:textarea)和你想渲染那一部分(比如:widget),你可以建立一個你需要重寫的片段名稱(比如:textarea_widget).
模闆片段繼承
在某些情況下,你個性化的片段可能會丢失。比如,在Symfony提供的預設主題中沒有提供textarea_errors片段。那麼如何來渲染一個textarea字段的錯誤資訊呢?
答案是通過field_errors片段。當Symfony渲染一個textarea類型的錯誤時,它首先查找一個textarea_errors片段,如果沒有找到則會回到field_errors片段。
每個field類型有一個parenttype(textarea的父類型為field),Symfony如果沒有發現本身的片段,就會轉而使用父類片段。
是以,要重寫textarea字段的errors,拷貝field_errors片段,重命名為textarea_errors并個性化它們。為所有字段重寫預設的error渲染,則需要直接拷貝和個性化field_errors片段。
全局表單主題
在上面的示例中,我們使用了form_theme helper來導入自定義個的表單片段到表單。你也可以告訴Symfony在全項目中導入自定義的form。
Twig
為了從所有之前建立的fileds.html.twig模闆中自動包含個性化的block,修改你的應用程式配置檔案:
YAML格式:
# app/config/config.yml
twig:
form:
resources:
- 'AcmeTaskBundle:Form:fields.html.twig'
# ...
XML格式:
<!-- app/config/config.xml -->
<twig:config ...>
<twig:form>
<resource>AcmeTaskBundle:Form:fields.html.twig</resource>
</twig:form>
<!-- ... -->
</twig:config>
PHP代碼格式:
// app/config/config.php
$container->loadFromExtension('twig', array(
'form' => array('resources' => array(
'AcmeTaskBundle:Form:fields.html.twig',
))
// ...
));
現在在fields.html.twig模闆中的任何塊都可以被廣泛的使用來定義表單輸出了。
自定義表單輸出到一個單一的Twig檔案中
在Twig中,你也可以個性化一個表單塊在模闆中
{% extends '::base.html.twig'%}
{# 導入"_self" 作為一個表單主題 #}
{% form_theme form _self %}
{# 個性化表單片段 #}
{% block field_row %}
{# 自定義字段行輸出 #}
{% endblock field_row %}
{% block content %}
{# ... #}
{{ form_row(form.task) }}
{% endblock %}
這裡{% form_theme form _self %}标簽允許表單塊在使用那些自動化内容的模闆中被直接自定義化。使用這個方法來快速的生成個性化輸出。
注意,{% form_theme form _self %}的功能隻有在繼承自其它模闆時才能起作用,如果不是繼承自其它模闆,則需要指出form_theme 到單獨模闆中。
PHP
從以前在所有模闆中建立的Acme/TaskBundle/Resources/views/Form 目錄自動導入個性化模闆。修改你的配置檔案:
YAML格式:
# app/config/config.yml
framework:
templating:
form:
resources:
- 'AcmeTaskBundle:Form'
# ...
XML格式:
<!-- app/config/config.xml -->
<framework:config ...>
<framework:templating>
<framework:form>
<resource>AcmeTaskBundle:Form</resource>
</framework:form>
</framework:templating>
<!-- ... -->
</framework:config>
PHP代碼格式:
// app/config/config.php
$container->loadFromExtension('framework', array(
'templating' => array('form' =>
array('resources' => array(
'AcmeTaskBundle:Form',
)))
// ...
));
此時在Acme/TaskBundle/Resources/views/Form目錄中的任何片段都可以全局範圍内定義表單輸出了。
CSRF 保護
CSRF--Cross-site request forgery,跨站僞造請求 是惡意攻擊者試圖讓你的合法使用者在不知不覺中送出他們本不想送出的資料的一種方法。
幸運的是,CSRF攻擊可以通過在你的表單中使用CSRF 記号來阻止。
預設情況下,Symfony自動為你嵌入一個合法的CSRF令牌。這就意味着你不需要做任何事情就可以得到CSRF保護。CSRF保護是通過在你的表單中添加一個隐藏字段,預設的名叫_token。它包含一個值,這個值隻有你和你的使用者知道。這確定了是使用者而不是其它實體在送出資料。Symfony自動校驗該token是否存在以及其準确性。
_token 字段是一個隐藏字段并且會自動的渲染,隻要你在你的模闆中包含了form_rest()函數。它確定了沒有被渲染過的字段全部渲染出來。CSRF令牌可以按照表單來個性化,比如:
class TaskType extends AbstractType
{
// ...
public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'Acme\TaskBundle\Entity\Task',
'csrf_protection' => true,
'csrf_field_name' => '_token',
// 一個唯一的鍵值來保證生成令牌
'intention' => 'task_item',
);
}
// ...
}
要關閉CSRF保護,設定csrf_protection 選項為false。intentsion選項是可選的,但為不同的表單生成不同的令牌極大的加強了安全性。
使用一個無底層類表單
大多數情況下,一個表單要綁定一個對象的,并且表單中所有的字段擷取或者儲存它們的資料到該對象屬性。但有時候,你可能隻想使用一個沒有類的表單,傳回一個送出資料的數組,這個非常容易實作:
// 确認你在類上方導入了Request對象
use Symfony\Component\HttpFoundation\Request
// ...
public function contactAction(Request $request)
{
$defaultData = array('message' => 'Type your message here');
$form = $this->createFormBuilder($defaultData)
->add('name', 'text')
->add('email', 'email')
->add('message', 'textarea')
->getForm();
if ($request->getMethod() == 'POST') {
$form->bindRequest($request);
// 資料是一個數組并包含 "name", "email", 和"message" 鍵
$data = $form->getData();
}
// ... 渲染表單
}
預設情況下,一個表單真的假設你想要一個資料數組而不是資料對象。
這裡有兩種方式你可以改變它的行為并綁定一個對象;
1.當建立表單時傳入一個對象(作為createFormBuilder的第一個參數或者createForm的第二個參數)。
2.在你的表單中聲明data_class 選項
如果以上兩種方式都沒有,那麼表單會傳回一個數組資料。在這個示例中因為$defaultData不是一個對象,又沒有設定data_class選項,則$form->getData()最終傳回一個數組。
你也可以通過Request對象直接通路POST的值,
$this->get('request')->request->get('name');
注意,大多數的情況下我們使用getData()方法是更好一點的選擇。因為它傳回的是經過表單架構轉換過的資料。
添加校驗規則
唯一遺漏的地方就是校驗規則了,通常當你調用$form->isvalid()時,對象會調用你在類東提供的校驗規則進行校驗。但如果沒有類,你怎麼來添加對你表單資料的限制規則呢?答案是自己建立限制,然後傳入到表單。
// 在controller類前導入命名空間
use Symfony\Component\Validator\Constraints\Email;
use Symfony\Component\Validator\Constraints\MinLength;
use Symfony\Component\Validator\Constraints\Collection;
$collectionConstraint = new Collection(array(
'name' => new MinLength(5),
'email' => new Email(array('message' => 'Invalid email address')),
));
// 建立一個表單沒有預設值,傳入限制選項。
$form = $this->createFormBuilder(null, array(
'validation_constraint' => $collectionConstraint,
))->add('email', 'email')
// ...
;
現在,當你調用$form->bindRequest($request)時,限制就會被建立并作用于你的表單資料。如果你使用表單類,重寫getDefaultOptions 方法來指定可選項:
namespace Acme\TaskBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
use Symfony\Component\Validator\Constraints\Email;
use Symfony\Component\Validator\Constraints\MinLength;
use Symfony\Component\Validator\Constraints\Collection;
class ContactType extends AbstractType
{
// ...
public function getDefaultOptions(array $options)
{
$collectionConstraint = new Collection(array(
'name' => new MinLength(5),
'email' => new Email(array('message' => 'Invalid email address')),
));
return array('validation_constraint' => $collectionConstraint);
}
}
這樣你有了足夠的靈活性來建立表單類和限制了,它傳回一個資料數組而不是一個對象。大多數情況下,這個是不錯的,而綁定一個表單到一個對象,從某種程度上說更加健壯。對于簡單表單來說是個不錯的選擇。
總結思考
你現在已經了解了所有建造複雜功能性的表單所需要的所有建造塊。當生成表單時,記住一個表單的首要目标是從一個對象把資料傳遞給一個HTML表單以友善使用者修改它們。第二個目标就是把使用者送出的資料重寫送出回對象。
參考URL:http://symfony.com/doc/current/book/forms.html
分類: Symfony2