🎻

[Symfony] 一覧画面をデフォルトでフィルタリングする実装例

2020/06/27に公開

やりたかったこと

  • Foo エンティティの一覧画面がある
  • Foo エンティティは state(状態) というプロパティを持っている
  • state プロパティにはEnumっぽい感じで決められた値のいずれかが入っている
  • 一覧画面を、デフォルトで特定の状態を除外した内容にしたかった
  • かつ、何も除外せずに一覧を表示することもできるようにしたかった

基本方針

一覧画面をフィルタリングするための検索フォームを作って、そのフォームで除外対象の状態を指定できるようにする、という方針で行きます。(ここは前提だと思ってください🙏)

コントローラ

/**
 * @Route("/foo", name="foo_index", methods={"GET"})
 */
public function index(Request $request, FormFactoryInterface $formFactory, FooRepository $repository)
{
    $form = $formFactory->createNamed('', FooSearchType::class, $criteria = new FooCriteria(), [
        'method' => 'GET',
    ]);
    $form->handleRequest($request);

    $foos = $repository->findByCriteria($criteria); // リポジトリにメソッドを生やしておく
    
    return [
        'form' => $form->createView(),
        'foos' => $foos,
    ];
}

検索条件クラス

class FooCriteria
{
    public $excludingStates;
}

検索フォーム

class FooSearchType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('excludingStates', ChoiceType::class, [
                'required' => false,
                'multiple' => true,
                'choices' => [
                    '状態1' => '状態1',
                    '状態2' => '状態2',
                    '状態3' => '状態3',
                    '状態4' => '状態4',
                ],
            ])
        ;
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => FooCriteria::class,
            'csrf_protection' => false,
        ]);
    }
}

実装方法

さて、このベースに対して、要件である

  • 一覧画面を、デフォルトで特定の状態を除外した内容にしたい
  • かつ、何も除外せずに一覧を表示することもできるようにしたい

をどうすれば実現できるか考えてみます。

いくつかパッと思いついた方法がよくよく考えたら要件を満たせなかったので、まずはそのダメな方法を列挙してみます。

ダメな方法:フォームにデフォルト値をセットする

  $builder
      ->add('excludingStates', ChoiceType::class, [
          'required' => false,
          'multiple' => true,
          'choices' => [
              '状態1' => '状態1',
              '状態2' => '状態2',
              '状態3' => '状態3',
              '状態4' => '状態4',
          ],
+         'data' => ['状態1', '状態2'],
      ])
  ;

何も考えずにとりあえずこうしてみたんですが、よく考えなくても、これだけだと単に表示されるフォームが最初から選択状態になるだけで、実際に一覧が除外したものになるわけではありません😅

ダメな方法:検索条件にデフォルト値を持たせる

  class FooCriteria
  {
-     public $excludingStates;
+     public $excludingStates = ['状態1', '状態2'];
  }

ならばと、検索条件にデフォルト値を持たせて、特に指定されなければデフォルトでこれらの状態を除外するようにしてみました。

一見上手く行きそうな気がするんですが、これはよく考えると

  • かつ、何も除外せずに一覧を表示することもできるようにしたい

を実現できません。

除外対象を選択せずにフォームを送信した場合、 excludingStates クエリパラメータなしでリクエストされるので、コントローラで handleRequest したあとも $criteria には $excludingStates = ['状態1', '状態2']; がセットされたままになり、 何も除外したくないのにデフォルトのものが除外されてしまう という動作になります😓

最終的にやったこと:クエリパラメータがない場合のみリクエストオブジェクトに手動で値をセットする

excludingStates クエリパラメータなしのリクエスト」が、「何も除外したくない」という意味なのか、「特に何も指定していない」という意味なのかの見分けがつかないことが問題なので、結局強引に見分けるしかなく、 クエリパラメータが1つもなければリクエストオブジェクトに手動で初期値をセットする という方法で実装しました。

  /**
   * @Route("/foo", name="foo_index", methods={"GET"})
   */
  public function index(Request $request, FooRepository $repository)
  {
+     if (preg_replace('#^/foo/?#', '', $request->getRequestUri()) === '') {
+         $request->query->set('excludingStates', ['状態1', '状態2']);
+     }
+ 
      $form = $formFactory->createNamed('', FooSearchType::class, $criteria = new FooCriteria(), [
          'method' => 'GET',
      ]);
      $form->handleRequest($request);
  
      $foos = $repository->findByCriteria($criteria); // リポジトリにメソッドを生やしておく
      
      return [
          'form' => $form->createView(),
          'foos' => $foos,
      ];
  }

まとめ

  • Symfonyで一覧画面をデフォルトでフィルタリングする実装例を紹介しました
  • 「何も除外したくない」と「特に指定しないからデフォルトのものを除外してほしい」の区別がつかない問題が起こったので、URLのクエリパラメータが1つもない場合はコントローラからリクエストオブジェクトに直接初期値をセットするという(強引な)方法で実装しました
GitHubで編集を提案

Discussion