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

  • 0
    Like
  • 0
    Comment

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

    フレームワークの勉強のため、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を分けてよかった気がします。