PHP
Symfony
symfony3

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

More than 1 year has passed since last update.

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

フレームワークの勉強のため、Symfony3でタスク管理アプリ作ってみたのパート3です。

Symfonyでのフォーム取扱

Symfonyでフォームの取扱はFormsを利用します。

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

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

フォーム用DTO

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

<?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()でフォーム作成を始められます。また、同じフォームを2度利用するので、別メソッドに切り分けておきます。

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テンプレートに渡して、フォームを描画します。

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

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

    {{ 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セクションで次の設定を加えます。

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

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

フォームデータの受取

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

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()とアノテートするだけ。これでエラーチェックを行うことができます。こんなコードになります。

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を分けてよかった気がします。