Symfonyでタスク管理アプリ作ってみた（フォーム編）

エンティティを作ったら、フォームの作成です。なんだかんだで、ウェブサイトの開発ではフォームの取扱が大きいです。

> フレームワークの勉強のため、[Symfony3でタスク管理アプリ作ってみた](http://qiita.com/asaokamei/items/8468aa366914664c518c)のパート３です。

Symfonyでのフォーム取扱
----

Symfonyでフォームの取扱は[Forms](http://symfony.com/doc/current/forms.html)を利用します。

エンティティからセッターを削除したので、エンティティをSymfonyのフォームに使えなくなってしまいました。そこで、フォーム用のDTOクラスを作成しました。

フォームだけのために、エンティティと同じようなクラスをわざわざ作るのは面倒、というか妙な気がします。セッター削除したのは失敗だったかと思ったのですが、おもったより簡単だったのと、フォームだけで利用するアノテーションがあるので、結果的にはエンティティがスッキリするのではと思ってます。

### フォーム用DTO

フォームDTOの`TaskDTO`クラスです。

```php
<?php
namespace AppBundle\Controller\CrudService;

use AppBundle\Entity\EntityTrait;
use Symfony\Component\Validator\Constraints as Assert;

class TaskDTO
{
    use EntityTrait;

    /**
     * @var string
     * @Assert\NotBlank()
     */
    public $title;

    /**
     * @var string
     */
    public $details;

    /**
     * @var \DateTime
     * @Assert\Type("\DateTime")
     */
    public $doneBy;
}
```

フォームから登録する値はパ**ブリックのプロパティとして定義**しました。フォーム内でしか使わないDTOならこれで十分じゃないか、という話をどこかで読んだので採用しました。今のところ問題なし。

ここでもアノテーションが活躍します。タスクのタイトルは必須ということで`@Assert\NotBlank()`アノテーションを使って指定しています。

### フォームオブジェクトの作成

Symfonyでのフォームオブジェクトの作成例です。Controller内であれば、`$this->createFormBuilder()`でフォーム作成を始められます。また、同じフォームを２度利用するので、別メソッドに切り分けておきます。

```php
private function getCreateForm()
{
    $task = new TaskDTO();
    return $this->createFormBuilder($task)
        ->add('title', TextType::class, ['label' => 'Task name', 'required' => true])
        ->add('doneBy', DateType::class, ['widget' => 'single_text', 'required' => false, 'label' => 'done by'])
        ->add('details', TextareaType::class, ['required' => false, 'label' => 'details'])
        ->getForm();
}
```

ここでの`required`パラメターですが、あくまでHTMLのJavaScript上でチェックするだけなので注意が必要です。サーバー側でチェックするには（例えば）先のフォーム用DTOでアノテーションを使う必要があります。

### フォームの描画

このメソッドで構築した`$form`をtwigテンプレートに渡して、フォームを描画します。

```php
return $this->render('task/project/create.html.twig', [
    'form' => $this->getCreateForm(),
]);
```

そしてtwigテンプレート内で、`$form`オブジェクトを描画します。

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

    {{ form_errors(form) }}

    <div class="form-group">
        {{ form_label(form.title) }}
        {{ form_widget(form.title, {"attr": "placeholder":"task name"}}) }}
        {{ form_errors(form.title) }}
    </div>
    ...
```

### bootstrap3レイアウトを利用

今回はBootstrap3を使っているので、フォームの表現をデフォルトで変更してしまいます。`app/config/config.yml`内のtwigセクションで次の設定を加えます。

```yml
# Twig Configuration
twig:
    form_themes:
        - 'bootstrap_3_layout.html.twig'
```

すると、Bootstrap3用にレンダリングされました。とても便利。

### フォームデータの受取

フォームからサブミットされたデータで、タスクを登録します。まずSymfonyが作成するRequestオブジェクトを利用します。コードとしては、こんな感じになりました。

```php
public function insertAction(Request $request): Response
{
    $form = $this->getCreateForm()
    $form = $form->handleRequest($request);
    if (!$form->isValid()) {
        return $this->render('task/project/create.html.twig', [
            'form' => $this->getCreateForm(),
        ]);
    }
    $dto  = $form->getData();
    $task = new Task($dto->toArray());
    $em   = $this->getDoctrine()->getManager();
    $em->persist($task);
    $em->flush();
}
```

Callbackの導入
----

ちょっと複雑なバリデーションを行う方法として、Callbackという機能がSymfonyにはあります。例えば、TaskのdoneByに過去の日付けを入れたくない、と言ったケースを考えて見ます。

先のTaskDTOにバリデーション用のメソッドを追加して、`@Assert\Callback()`とアノテートするだけ。これでエラーチェックを行うことができます。こんなコードになります。

```php
class TaskDTO 
{

    /**
     * @param \DateTime $now
     */
    public function __construct(\DateTime $now = null)
    {
        $this->now = $now ? clone($now): null;
        if ($this->now) {
            $this->now->setTime(0,0,0);
        }
    }

    /**
     * @param ExecutionContextInterface $context
     * @Assert\Callback()
     */
    public function validateDoneByHasFutureDate(ExecutionContextInterface $context)
    {
        if ($this->doneBy && $this->now && $this->doneBy < $this->now) {
            $context->buildViolation('please select future date as done-by.')
                    ->atPath('doneBy')
                    ->addViolation();
        }
    }
}
```

アノテーションやバリデーションが入り込んでくると考えると、エンティティとフォーム用DTOを分けてよかった気がします。