SymfonyのForm：自分のやり方（My Practice）

[Symfony Advent Calendar 2018](https://qiita.com/advent-calendar/2018/symfony)の8日目が空いていたので、参加することにしました。SymfonyのFormコンポーネントでの自分の使い方です。

## DTOを定義する。

データのエンティティは直接使わないで、DTOクラスを定義して使ってます。まず、ユーザー用DTOクラス、`UserDTO`と、そこに「埋め込まれた（Embed）」された住所用DTOクラス、`AddressDTO`を定義。

```php
use Symfony\Component\Validator\Constraints as Assert;

class UserDTO 
{
    /**
     * @var string
     * @Assert\NotBlank()
     */
    public $email;
    /**
     * @var AddressDTO
     * @Assert\Valid()    // ←(1)Validをつけること！
     */
    public $address;
}

class AddressDTO
{
    /**
     * @var string
     * @Assert\NotBlank()
     */
    public $post_code;
    /**
     * @var string
     * @Assert\NotBlank()
     */
    pubic $address;
}
```

直接エンティティにバリデーションのアノテーションを使うこともできますが、個人的に[DTOを別途用意するのが好き](https://blog.martinhujer.cz/symfony-forms-with-request-objects/)です。

DTOなので、プロパティはpublicにしてます。

ちなみにDTOを使う理由ですが…

- エラーがあった時でも不正な状態のエンティティを作らないで済みます。
- ValidationはDTOを見ればいいので、すぐわかります。
- エンティティからセッターを外せます。
- データのバリデーションは、エンティティ内かDBで行ってます。

### バリデーションのConstraints

`@Assert\NotBlank()`はSymfony/Validationの機能で[Constraints](https://symfony.com/doc/current/validation.html#constraints)と呼ばれてます。


### Assert\Valid()タグ

別フォームを埋め込むときは、埋め込むクラス（ここでは`UserDTO`)に`Assert\Valid()`アノテーションを書いておくこと。

これがないと、[埋め込まれたクラスのバリデーションが効きません](https://symfony.com/doc/current/reference/constraints/Valid.html)。

## FormTypeを定義する

`AbstractType`を継承して、フォームを定義します。

```php
use Symfony\Component\Form\AbstractType;

class UserType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('name', TextType::class, [
                'label' => '確認先名',
            ])
            ->add('email', MailType::class, [
                'label' => 'メールアドレス',
            ])
            ->add('address', AddressType::class, [
                'error_bubbling' => false,
            ]);
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => UserDTO::class,
        ));
    }
}

class AddressType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('post_code', TextType::class, [
                'label' => '郵便番号',
            ])
            ->add('address', TextType::class, [
                'label' => '住所',
            ]);
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => AddressDTO::class,
        ));
    }
}
```

`TextType::class`や`EmailType::class`は[ビルドインフィールドタイプ](https://symfony.com/doc/current/forms.html#built-in-field-types)と呼んでいて、いろいろなオプションを選べます。

### configureOptionsメソッド

`configureOptions`で利用するDTOあるいはエンティティを指定します。指定がなくても動いてしまいますが、これがないと[エラーがバブル](https://symfony.com/doc/current/reference/forms/types/form.html#error-bubbling)してしまいます。

また、指定がないと埋め込んだフォームのDTOに対して配列でアクセスしようとするようです。

要は「指定しとけ」っていうことです。

## Controller/Twigで利用する

ここまで書いておけば、Controllerで簡単にフォームを利用できます。

```php
class MyController extends Controller
{
    public function form()
    {
        $form = $this->createForm(UserType::class);
        return $this->render('form.html.twig', [
            'form' => $form->createView(),
        ]);
    }
}
```

### Twigテンプレート

```twig
{{ form_start(form) }}
    {{ form_widget(form) }}
    <input type="submit" class="btn" value="保存">
{{ form_end(form) }}
```

[Bootstrapテーマなどを設定](https://symfony.com/doc/current/form/bootstrap4.html)しておけば、上の4行だけで美しいフォームが出ます。

## 最後に

いつものように書きながら調べて、初めて気が付いたことが多いです。
アウトプット大事。

もっと言えば、ちゃんとマニュアル通りにコーディングすればいいだけ、という話だったりします。ただドキュメント量が多いのと、あちこちに散らかっているので分かりずらいです。