🎻

[Symfony] input type="month"で年月を入力できるFormTypeの作り方

2020/07/05に公開約4,300字

はじめに

SymfonyのFormTypeで、HTML5の <input type="month"> をレンダリングしてくれるようなフォームフィールドを作る方法を説明します。

背景をちょっとだけ詳しく

今どきのブラウザは日付系のフォームをいい感じにカレンダーから入力できるようにレンダリングしてくれるので、Symfonyで DateType を使って日付系のフィールドを扱うときも、あえて をそれぞれセレクトさせるようなUIにするより、'widget' => 'single_text' を指定して <input type="date"> とかをレンダリングしてしまったほうが使いやすいことが多いですよね。

年月日の場合は 'widget' => 'single_text' を指定するだけで <input type="date"> になってくれるので特に何も考えなくていいのですが、 年月 を扱いたい場合にSymfonyのデフォルトの機能だけでは実現できないので、ちょっと工夫が必要になります。

Symfonyのコードを読んでみる

そもそも、 DateType<input type="date"> をレンダリングしてくれるカラクリはどうなっているのでしょうか?

ソースを見てみる と、 DateType クラスの finishView() メソッドの中で $view->vars['type'] = 'date'; という処理によって input type="date" が指定されているようです。

ということは、 DateType を継承した MonthType とかを作って、 finishView() メソッドで $view->vars['type'] = 'month'; としてあげればイケそうな気がしますね。

MonthTypeを作って finishView() メソッドをオーバーライドする

やってみましょう。

class MonthType extends DateType
{
    public function finishView(FormView $view, FormInterface $form, array $options)
    {
        parent::finishView($view, $form, $options);

        $view->vars['type'] = 'month';
    }
}
class FooType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('yearMonth', MonthType::class, [
                'label' => '年月',
                'widget' => 'single_text',
            ])
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => Foo::class,
        ]);
    }
}
class Foo
{
    /**
     * @ORM\Column(type="date")
     */
    private $yearMonth;
    
    // 略
}

実行してみると、以下のように <input type="month"> がレンダリングされました。よさそうですね。

しかし、送信してみると以下のようにバリデーションでエラーになってしまいました🙄

年月の値をDateTimeオブジェクトと変換できるようにする

<input type="month"> の値は yyyy-MM 形式の文字列になるので、そのままだとPHP側で DateTime オブジェクトとマッピングできず、エラーになります。

format オプションを明示してあげることでこの問題は解決できます。

  class FooType extends AbstractType
  {
      public function buildForm(FormBuilderInterface $builder, array $options)
      {
          $builder
              ->add('yearMonth', MonthType::class, [
                  'label' => '年月',
                  'widget' => 'single_text',
+                 'format' => 'yyyy-MM',
              ])
          ;
      }
  
      public function configureOptions(OptionsResolver $resolver)
      {
          $resolver->setDefaults([
              'data_class' => Foo::class,
          ]);
      }
  }

これで、例えばPHP側が 2020/07/01 00:00:00DateTime だった場合にはフォームの値は 2020-07 になり、フォームで 2020-07 をセットして送信した場合はPHP側では 2020/07/01 00:00:00DateTime として扱われるようになります👍

最終的なMonthTypeのコード

今回作った MonthType'widget' => 'single_text' 'format' => 'yyyy-MM' とセットで使うことを想定しているので、これらのオプションの指定は MonthType 自身に持たせてしまったほうが使い回すときに楽そうですね。

というわけで MonthType のコードは以下のようにしておくとよいかなと思います。

class MonthType extends DateType
{
    public function configureOptions(OptionsResolver $resolver)
    {
        parent::configureOptions($resolver);
    
        $resolver->setDefaults([
            'widget' => 'single_text',
            'format' => 'yyyy-MM',
        ]);
    }

    public function finishView(FormView $view, FormInterface $form, array $options)
    {
        parent::finishView($view, $form, $options);

        $view->vars['type'] = 'month';
    }
}

これで、使う側で widget format をいちいち指定しなくてよくなります。

  ->add('yearMonth', MonthType::class, [
      'label' => '年月',
-     'widget' => 'single_text',
-     'format' => 'yyyy-MM',
  ])
GitHubで編集を提案

Discussion

ログインするとコメントできます