owned this note
owned this note
Published
Linked with GitHub
# Form
創建和處理 HTML 表單既困難重複性又高。
需要處理呈現 HTML 表單字段、驗證提交的資料、將表單資料映射到object等等。
Symfony 包含一個強大的表單功能,可提供所有這些功能以及更多用於真正複雜場景的功能。
## 用法
使用 Symfony 表單時推薦的工作流程如下:
1. 在Symfony控制器中或使用專用的表單class構建表單;
2. 在模板中呈現表單,以便用戶可以編輯和提交;
3. 處理表單以驗證提交的資料,將其轉換為 PHP data 並對其進行處理(例如將其保存在資料庫中)。
在接下來的部分中詳細解釋了這些步驟。
為了使範例更易於理解,所有反例都假設正在構建一個顯示“任務(task)”的簡單 Todo list應用程式。
用戶使用 Symfony 表單創建和編輯任務。
每個任務都是以下Task class的一個實例:
```php=
// 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;
}
}
```
這個class是一個“plain-old-PHP-object”,因為到目前為止,它與 Symfony 或任何其他library無關。
這是一個普通的PHP object,直接解決了你應用程式內部的問題(即在你的應用程式中需要被表示出來的任務)。但是也可以相同的方式編輯[Doctrine entities](https://hackmd.io/mG7joo9fTiqcBg2JVFKGew)。
### 表單類型
在創建你的第一個 Symfony 表單之前,理解“表單類型”的概念很重要。
在其他項目中,區分“表單”和“表單fields”是很常見的。
在 Symfony 中,它們都是“表單類型”:
- 單一個```<input type="text">```表單字段是“表單類型”(例如```TextType```);
- 一組用於輸入郵政地址的幾個 HTML 字段是“表單類型”(例如```PostalAddressType```);
- 整個```<form>```具有多個字段編輯的用戶簡歷是一個“表單類型”(例如```UserProfileType```)。
起初這可能會使你困惑,但很快你就會覺得很自然。
此外,它簡化了代碼並使“組合(composing)”和“嵌入(embedding)”表單filds更容易實現。
Symfony 提供了數十種[表單類型](https://symfony.com/doc/4.3/reference/forms/types.html) ,您也可以創建[自己的表單類型](https://symfony.com/doc/4.3/form/create_custom_field_type.html)。
## 建立表單
Symfony 提供了一個“表單構建器”object,使用流暢的界面來描述表單字段。稍後,此構建器創建用於呈現和處理內容的實際表單object。
### 在控制器中創建表單
如果控制器從[AbstractController](https://symfony.com/doc/4.3/controller.html#the-base-controller-class-services)擴展,請使用```createFormBuilder()```幫助程式(helper):
```php=
// 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 object and initializes some 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();
// ...
}
}
```
如果您的控制器沒有從AbstractController擴展,將需要在你的控制器中獲取[服務](https://symfony.com/doc/4.3/controller.html#controller-accessing-services)並使用```form.factory```服務的```createBuiler()```方法。
在此範例中,已向表單中添加了兩個字段 -```task```和```dueDate``` - 對應於```Task```class的```task```和```dueDate```屬性。
還為每個表單分配了一個表單類型(form type)(例如```TextType``` 和```DateType```),由其完全限定的class名稱表示。
最後,添加了一個帶有自定義標籤的提交(submit)按鈕,用於將表單提交到server。
### 創建表單類
Symfony 建議在控制器中盡可能少放邏輯。
這就是為什麼最好將復雜的表單移動到專用class而不是在控制器action中定義它們的原因。
此外,在class中定義的表單可以在多個action和服務重複使用。
表單class是實現 ```FormTypeInterface``` 的表單類型。
但是,最好從```AbstractType```擴展,它已經實現了表單介面並提供了一些功能:
```php=
// src/Form/Type/TaskType.php
namespace App\Form\Type;
use Symfony\Component\Form\AbstractType;
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\Form\FormBuilderInterface;
class TaskType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('task', TextType::class)
->add('dueDate', DateType::class)
->add('save', SubmitType::class)
;
}
}
```
:::success
在你的project中安裝```MakerBundle```以使用```make:form```和```make:registration-form```指令生成表單class。
:::
表單class包含創建任務表單所需的所有說明。
在從AbstractController擴展的控制器中,使用```createForm()```幫助程式(否則,使用服務```form.factory```的```create()```方法):
```php=
// src/Controller/TaskController.php
namespace App\Controller;
use App\Form\Type\TaskType;
// ...
class TaskController extends AbstractController
{
public function new()
{
// creates a task object and initializes some data for this example
$task = new Task();
$task->setTask('Write a blog post');
$task->setDueDate(new \DateTime('tomorrow'));
$form = $this->createForm(TaskType::class, $task);
// ...
}
}
```
每個表單都需要知道保存基礎資料的class的名稱(例如```App\Entity\Task```)。
通常來說,這只是根據傳遞給```createForm()```(例如``` $task```)的第二個參數的對象來猜測的。稍後,當您開始嵌入表單時,這是不夠的。
此建議,並非是必要的,透過將以下內容添加到表單類型class,來明確的指定```data_class```選項,通常是個好主意:
```php=
// src/Form/Type/TaskType.php
namespace App\Form\Type;
use App\Entity\Task;
use Symfony\Component\OptionsResolver\OptionsResolver;
// ...
class TaskType extends AbstractType
{
// ...
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Task::class,
]);
}
}
```
## 渲染(rendering)表格
既然已經創建了表單,下一步就是渲染它。
不是將整個表單object傳遞給模板,而是使用```createView()```方法構建另一個object能讓表單視覺化:
```php=
// src/Controller/TaskController.php
namespace App\Controller;
use App\Entity\Task;
use App\Form\Type\TaskType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
class TaskController extends AbstractController
{
public function new(Request $request)
{
$task = new Task();
// ...
$form = $this->createForm(TaskType::class, $task);
return $this->render('task/new.html.twig', [
'form' => $form->createView(),
]);
}
}
```
然後,使用一些表單輔助函數來呈現表單內容:
```php=
{# templates/task/new.html.twig #}
{{ form(form) }}
```
```form()```函式渲染所有的fields,並在```<form>```出現開始和結束tag。(也就是會看到```<form></form>```)
預設情況下,表單方法是``` POST```,目標 URL 會顯示相同的表單,但兩者都是可更改的。
請注意```$task```object是如何渲染並呈現```task```此輸入字段屬性的值(即“Write a blog post”)。
這是表單的第一項工作:從object中獲取資料並將其轉換為適合在 HTML 表單中呈現的格式。
:::success
表單系統非常聰明,可以透過```Task```class上的```getTask()```和```setTask()```方法,來訪問受保護屬性的值 ```task```。
除非屬性是公開的,否則它必須有“getter”和“setter”方法,以便 Symfony 可以獲取和放置資料到屬性上。
對於boolean屬性,您可以使用“isser”或“hasser”方法(例如 ```isPublished()```或```hasReminder()```)而不是getter(例如 ```getPublished()```或```getReminder()```)。
:::
儘管這個渲染很短,也不是很靈活。
一般而言,需要更多方法控制整個表單或其某些字段的外觀。
例如,由於Bootstrap 4 與 Symfony 表單的集成,可以設置此選項以生成與 Bootstrap 4 CSS 框架兼容的表單:
```php=
# config/packages/twig.yaml
twig:
form_themes: ['bootstrap_4_layout.html.twig']
```
內建的symfony表單主題包含bootstrap 3 and 4 and foundation 5
你也可以創建你[自己的](https://symfony.com/doc/4.3/form/form_themes.html#create-your-own-form-theme)表單主題
除了表單主題之外,Symfony 還允許使用多種功能[自定義](https://symfony.com/doc/4.3/form/form_customization.html)字段的呈現方式,以分別呈現每個字段部分(widgets、標籤、錯誤、幫助消息等)。
## 處理表單
處理表單的推薦方法是使用單個action來呈現表單和處理表單提交。
可以使用單獨的action,使用一個action可以簡化一切,同時保持代碼簡潔和可維護。
處理表單意味著將用戶提交的資料轉換回object的屬性。
為此,必須將用戶提交資料寫入表單object:
```php=
// ...
use Symfony\Component\HttpFoundation\Request;
public function new(Request $request)
{
// just setup a fresh $task object (remove the example data)
$task = new Task();
$form = $this->createForm(TaskType::class, $task);
$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(),
]);
}
```
此控制器遵循處理表單的通用模式,並具有三種可能的路徑:
1. 最初在瀏覽器中load頁面時,表單尚未提交且```$form->isSubmitted()```回傳```false```.
因此,表單被創建和呈現;
2. 當用戶提交表單時,```handleRequest()``` 會識別出這一點並立即將提交的資料寫回```$task```object的```task```和```dueDate```屬性中。
然後驗證這個object(驗證將在下一節中解釋)。
如果無效,則 ```isValid()```回傳```false```並再次呈現表單,但現在出現驗證錯誤;
3. 當用戶提交帶有有效資料的表單時,提交的資料再次寫入表單,但這次```isValid()```回傳```true```。
現在您有機會在將用戶重新導向到其他頁面(例如“thank you”或“success”頁面)之前使用該object```$task```執行一些action(例如將資料放進資料庫);
>note
在成功提交表單後重新導向用戶是一種最佳做法,可防止用戶點擊瀏覽器的“重新整理”按鈕並重複傳遞資料。
:::danger
Caution
該```createView()```方法應在call```handleRequest()```方法後使用。
否則,在使用表單事件時,事件中所做的更改```*_SUBMIT```將不會被應用在view上(如驗證錯誤)。
:::
>note
如果需要更加能控制提交表單的確切時間或將哪些資料傳遞給它,可以使用 ```submit()```方法來處理表單提交。
## 驗證表單
在上一節中,了解瞭如何使用有效或無效數據提交表單。
在 Symfony 中,問題不在於“表單”是否有效,而是```$task```在表單將提交的資料應用於它之後底層對象(在本範例中)是否有效。
呼叫```$form->isValid()```是詢問```$task```object是否有有效資料的快速方式。
在使用驗證之前,在您的應用程式中添加對它的支援:
```php=
composer require symfony/validator
```
驗證是通過向class添加一組規則(稱為constraints)來完成的。
要查看此action,請添加驗證約束,以便```task```字段不能為空且```dueDate```字段不能為空且必須是有效的 DateTime object。
```
// src/Entity/Task.php
namespace App\Entity;
use Symfony\Component\Validator\Constraints as Assert;
class Task
{
/**
* @Assert\NotBlank
*/
public $task;
/**
* @Assert\NotBlank
* @Assert\Type("\DateTime")
*/
protected $dueDate;
}
```
如果重新提交包含無效資料的表單,會看到相應的錯誤隨表單一起顯示出來。
閱讀 Symfony 驗證[文件](https://symfony.com/doc/4.3/validation.html)以了解更多信息。
## 其他常見表單功能
### 將選項傳遞給表單
如果在class中創建表單,則在控制器中構建表單時,您可以將自定義選項作為第三個可選參數傳遞給表單```createForm()```:
```php=
// src/Controller/TaskController.php
namespace App\Controller;
use App\Form\Type\TaskType;
// ...
class TaskController extends AbstractController
{
public function new()
{
$task = new Task();
// use some PHP logic to decide if this form field is required or not
$dueDateIsRequired = ...
$form = $this->createForm(TaskType::class, $task, [
'require_due_date' => $dueDateIsRequired,
]);
// ...
}
}
```
如果您現在嘗試使用該表單,您將看到一條錯誤消息:“require_due_date”選項不存在。
那是因為表單必須使用以下```configureOptions()```方法聲明它們接受的所有選項:
```php=
// src/Form/Type/TaskType.php
namespace App\Form\Type;
use Symfony\Component\OptionsResolver\OptionsResolver;
// ...
class TaskType extends AbstractType
{
// ...
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
// ...,
'require_due_date' => false,
]);
// you can also define the allowed types, allowed values and
// any other feature supported by the OptionsResolver component
$resolver->setAllowedTypes('require_due_date', 'bool');
}
}
```
現在可以在```buildForm()```方法中使用這個新的表單選項:
```php=
// src/Form/Type/TaskType.php
namespace App\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\FormBuilderInterface;
class TaskType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
// ...
->add('dueDate', DateType::class, [
'required' => $options['require_due_date'],
])
;
}
// ...
}
```
### 表單類型選項
每種表單類型都有許多選項來配置它,如Symfony[表單類型參考](https://symfony.com/doc/4.3/reference/forms/types.html)中所述。
兩個常用的選項是```required```和```label```。
#### required選項
最常見的選項是```required```選項,它可以應用於任何領域。
預設情況下,此選項設置為true,這意味著支持 HTML5 的瀏覽器需要在提交表單之前填寫所有表單內容。
如果不希望出現這種行為,在一個或多個fields上請禁用整個表單的客戶端驗證或將```required```選項設置為```false```:
```php=
->add('dueDate', DateType::class, [
'required' => false,
])
```
該```required```選項不執行任何server-side驗證。
如果用戶為該field提交一個空白值(例如,使用舊瀏覽器或 Web 服務),它將被接受為有效值,除非還使用了``` SymfonyNotBlank```或```NotNull```驗證約束。
#### label選項
預設情況下,表單field的標籤是屬性名稱的人性化版本(```user```->``` User```;``` postalAddress```-> ```Postal Address```)。
在field上設置 ```label``` 選項以明確定義它們的標籤::
```php=
->add('dueDate', DateType::class, [
// set it to FALSE to not display the label for this field
'label' => 'To Be Completed Before',
])
```
:::success
預設情況下,```<label>```必填字段的標籤透過required CSS class來渲染 ,因此您可以為應用這些 CSS 樣式的必填字段顯示星號:
```php=
label.required:before {
content: "*";
}
```
:::
### 更改操作和 HTTP 方法
預設情況下,表單將通過 HTTP POST 請求提交到渲染表單的同一 URL。
在控制器中構建表單時,使用```setAction()```和```setMethod()```方法來改變這一點:
```php=
// src/Controller/TaskController.php
namespace App\Controller;
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;
class TaskController extends AbstractController
{
public function new()
{
// ...
$form = $this->createFormBuilder($task)
->setAction($this->generateUrl('target_route'))
->setMethod('GET')
// ...
->getForm();
// ...
}
}
```
在class中構建表單時,將action和方法作為表單選項傳遞:
```php=
// src/Controller/TaskController.php
namespace App\Controller;
use App\Form\TaskType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class TaskController extends AbstractController
{
public function new()
{
// ...
$form = $this->createForm(TaskType::class, $task, [
'action' => $this->generateUrl('target_route'),
'method' => 'GET',
]);
// ...
}
}
```
最後,可以通過將它們傳遞給```form()```或```form_start()```輔助函數來覆蓋模板中的action和方法:
```php=
{# templates/task/new.html.twig #}
{{ form_start(form, {'action': path('target_route'), 'method': 'GET'}) }}
```
>note
>如果表單的方法不是GET or POST, but PUT, PATCH or DELETE, >Symfony 將插入一個隱藏字段, 其名稱```_method``` 存儲此方法。
>
>表單將在普通POST 請求中提交,但Symfony 的路由能夠檢測 ```_method```參數並將其解釋為PUT,PATCH或 DELETE請求。
>請參閱框架配置參考 ([FrameworkBundle](https://symfony.com/doc/4.3/reference/configuration/framework.html#configuration-framework-http_method_override))選項。
### 更改表單名稱
如果您檢查呈現的表單的 HTML 內容,您將看到 ```<form>```名稱和field名稱是從type class name產生的(例如```<form name="task" ...>```和```<select name="task[dueDate][date][month]" ...>```)。
如果要修改它,請使用```createNamed()```方法:
```php=
// src/Controller/TaskController.php
namespace App\Controller;
use App\Form\TaskType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class TaskController extends AbstractController
{
public function new()
{
$task = ...;
$form = $this->get('form.factory')->createNamed('my_name', TaskType::class, $task);
// ...
}
}
```
甚至可以通過將名稱設置為空字串來完全取消該名稱。
### 客戶端 HTML 驗證
多虧了 HTML5,許多瀏覽器可以在客戶端強制執行某些驗證約束。
最常見的驗證是通過```required```在必需的field上添加屬性來啟動的。
對於支持 HTML5 的瀏覽器,如果用戶嘗試提交該field為空的表單,這將導致顯示原本瀏覽器的訊息。
生成的表單通過添加觸發驗證的合理 HTML 屬性來充分利用這一新功能。
但是,可以通過將```novalidate```屬性添加到```<form>```tag或 ```formnovalidate```提交tag來禁用客戶端驗證。
當你想要測試服務器端驗證約束但被瀏覽器阻止(例如,提交空白field)時,這相當有用。
```php=
{# templates/task/new.html.twig #}
{{ form_start(form, {'attr': {'novalidate': 'novalidate'}}) }}
{{ form_widget(form) }}
{{ form_end(form) }}
```
### 表單類型猜測
如果表單處理的對象包含驗證約束,Symfony 可以審查metadata來猜測您的field類型並設置它。
在上面的例子中,Symfony 可以從驗證規則中猜測該```task```field是一個普通```TextType``` field,該```dueDate```field是一個 ```DateType ```field。
在構建表單時,省略該```add()```方法的第二個參數,或者傳遞```null```給它,以啟用 Symfony 的“猜測機制”:
```php=
// src/Form/Type/TaskType.php
namespace App\Form\Type;
use Symfony\Component\Form\AbstractType;
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\Form\FormBuilderInterface;
class TaskType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
// if you don't define field options, you can omit the second argument
->add('task')
// if you define field options, pass NULL as second argument
->add('dueDate', null, ['required' => false])
->add('save', SubmitType::class)
;
}
}
```
:::danger
當使用特定的表單驗證組時,字段類型猜測器在猜測您的字段類型時仍將考慮所有驗證約束(包括不屬於正在使用的驗證組的約束)。
:::
#### 表單類型選項猜測
當對某些field啟用猜測機制時(即您省略或```null```作為第二個參數傳遞 給```add()```),除了其表單類型外,還可以猜測以下選項:
- ```required```
```required```可以根據驗證規則(即field ```NotBlank```或```NotNull```)或 Doctrine metadata(即field ```nullable```)猜測該選項。
這非常好用,因為您的客戶端驗證將自動match你的驗證規則。
- ```maxlength```
如果該field是某種文本field,則該```maxlength```選項屬性可以從驗證約束猜測(如果Length或Range使用),或從Doctrine metadata(通過field的長度)。
如果您想更改其中一個猜測值,請通過在 options field array中傳遞選項來覆蓋它:
```php=
->add('task', null, ['attr' => ['maxlength' => 4]])
```
## 未映射的字段
通過表單編輯object時,所有表單field都被視為object的屬性。
object上不存在的表單上的任何field都將導致拋出異常。
如果需要表單中不會存儲在object中的額外field(例如添加“我同意這些條款”複選框),請在這些field中設置```mapped```選項```false```:
```php=
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)
;
}
```
這些“未映射的field”可以在控制器中設置和訪問:
```php=
$form->get('agreeTerms')->getData();
$form->get('agreeTerms')->setData(true);
```
此外,如果表單上有任何未包含在提交資料中的field,這些field將顯式設置為```null```。
在構建表單時,請記住表單的第一個目標是將資料從object (```Task```) 轉換為 HTML 表單,以便用戶可以修改該資料。
表單的第二個目標是獲取用戶提交的資料並將其重新應用於object。
在 [Symfony form](https://symfony.com/doc/4.3/forms.html#form-type-guessing)中還有很多東西需要學習和很多強大的技巧: