フォームを操る、"Form"

[Symfony Component Advent Calendar 2023](https://qiita.com/advent-calendar/2023/symfony-component)の11日目の記事です。

## フォームを操る、"Form"

[Form](https://symfony.com/doc/current/components/form.html)は名前の通り、フォームを生成したり処理したりするコンポーネントです。

### インストール

```shell
composer require symfony/form
```

### 使い方

1. `Forms::createFormFactory()`で`FormFactory`オブジェクトを作成
2. `createBuilder()` でFormBuilderオブジェクトを作成
3. `add()`メソッドを使って項目を追加
4. `getForm()`メソッドを使って`Form`オブジェクトを作成

によって、指定したフォームの内容を受け取れる`Form`オブジェクトを扱えるようになります。
`handleRequest()`を実行すると、実行したフォームメソッドに合わせて`$_GET`, `$_POST`から値を取得して反映させます。反映した値は`getData()`で取得できます。

```php
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Forms;

$formFactory = Forms::createFormFacgtory();
$form = $formFactory->createNamedBuilder('form')
    ->add('name', TextType::class)
    ->add('email', EmailType::class)
    ->add('submit', SubmitType::class)
    ->getForm()
;

$form->handleRequest();
$form->getData(); // フォームの入力値 例：['name' => 'すみだ', 'email' => 'test@some.domain']
```

### Symfonyの場合

Symfonyの場合は、`AbstractController`に`FormFactory`が事前にDIされており、`createFormBuilder`メソッドを実行すると`FormBuilder`オブジェクトが取得できます。あとは↑と同じですが、`handleRequest()`に`Request`オブジェクトを渡すようになります。

```php

class FormController extends AbstractController
{
    public function index(Request $request)
    {
        $form = $this->createFormBuilder()
            ->add('name', TextType::class)
            ->add('email', EmailType::class)
            ->add('submit', SubmitType::class)
            ->getForm();

        $form->handleRequest($request);
        
        return $this->render('form/index.html.twig', [
            'form' => $form,
        ]);
    }
}
```

そして、Twig側ですが`{{ form_start(form) }}`, `{{ form_end(form) }}`でフォームタグを出力、`{{ form_widget(form) }}`で入力タグを出力します。入力された値は自動でフォームに反映されています。

```twig
    {{ form_start(form) }}
    {{ form_widget(form) }}
    {{ form_end(form) }}
```

出力例

![スクリーンショット 2023-12-07 21.17.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/9882/804b6930-e742-82cf-90f7-835ea05135a2.png)

入力タグですが`{{ form_widget(form.name) }}`のように項目を一つずつ出力することもできます。この場合は入力タグしか出力されず、`{{ form_row(form.name) }}`を使うと項目ラベルと入力タグを出力します。


### FormType

Controllerに直接フォームの内容を書くのは見た目が悪いです。`FormType`クラスを作成すると、そこにフォームの内容を抜き出せます。また、`configureOptions()`メソッド内で、`['data_class' => '{DTOやEntityのクラス名}']`を指定すると、`getData()`で取得する値が指定したクラスのオブジェクトになります。

```php:src/Form/MealFormType.php
<?php

namespace App\Form;

use App\Dto\MealDto;
use App\Enum\MealType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\EnumType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class MealFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('type', EnumType::class, ['class' => MealType::class])
            ->add('content', TextType::class)
            ->add('submit', SubmitType::class)
        ;
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => MealDto::class
        ]);
    }
}
```

Controller側では`createForm({FormTypeのクラス名})`を呼ぶと指定の`Form`オブジェクトを取得できます。
また、`createForm('{FormTypeのクラス名}', {オブジェクト})`と、第二引数にフォームと同じクラスのオブジェクトを渡すと、そのオブジェクトに値を代入してくれます。`handleRequest($request)`後は、入力した値が代入されています。
代入するためには、Setterメソッドを用意するかプロパティがpublicである必要があります。

```php
    public function meal(Request $request): Response
    {
        $dto = new MealDto(); // この状態では空のオブジェクト
        $form = $this->createForm(MealFormType::class, $dto);
        
        $form->handleRequest($request); // ここで、$dtoに入力された値が代入される
        
        return $this->render('form/meal.html.twig', [
            'form' => $form,
        ]);
    }
```


### バリデーション
[Validator](https://qiita.com/ippey_s/items/eb6c4c102cbb7080b881)コンポーネントをインストールすればバリデーションも可能です。バリデーションの記述方法は←のリンク先を見ていただくとして、`isValid()`を実行すると、バリデーションが問題ないか判断できます。

```php
    public function meal(Request $request): Response
    {
        $dto = new MealDto();
        $form = $this->createForm(MealFormType::class, $dto);
        
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            // 入力が正常だった場合の処理
        }

        $errors = $form->getErrors(); // バリデーションエラー取得
        return $this->render('form/meal.html.twig', [
            'form' => $form,
        ]);
    }
```

Twig側では`{{ form_errors(form) }}`でエラー内容が出力されます。`form_widget()`同様、項目一つずつ出力することもできます。

```twig
{{ form_errors(form) }} フォームのエラー内容すべて
{{ form_errors(form.content }} content項目だけのエラー内容
```

### CSRF対策

`Security CSRF`コンポーネントを使うとCSRF対策もできます。

```shell
composer require symfony/security-csrf
```

Symfonyの場合、このモジュールをインストールすれば、自動でCSRF対策されたフォームが出力されます。

```html:出力結果
<form name="form" method="post">
    <div><label for="form_name" class="required">Name</label><input type="text" id="form_name" name="form[name]" required="required"></div>
    <div><label for="form_email" class="required">Email</label><input type="email" id="form_email" name="form[email]" required="required"></div>
    <div><button type="submit" id="form_submit" name="form[submit]">Submit</button></div>
    <!-- ここが自動的に挿入される -->
    <input type="hidden" id="form__token" name="form[_token]" value="ad7e7d217c4fa272093.clWlAiEDkjGhsTCi1gZw2M2R6gZizcw_OSANwM4MIc0.CzLwYFdTw0DH-HvyrHQzkp3bkn4tnKNwWGVBtP9ucK4LMvRLZWXBAcTyZQ">
    <!-- /ここまで -->
</form>
```

## まとめ

今回は`Form`でした。`Twig`, `Security CSRF`, `Validator` と絡めることで強力なフォーム体験を得ることができるので、おすすめのコンポーネントのひとつです。

※ ちょっとクセがあります。