5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

SymfonyのFormTypeにバリデーションをかける

Posted at

はじめに

SymfonyではEntityにバリデーションを書くことができます。公式ドキュメントには以下のような例があります。

// src/Entity/Author.php
namespace App\Entity;

// ...
use Symfony\Component\Validator\Constraints as Assert;

class Author
{
    /**
     * @Assert\NotBlank
     */
    private $name;
}

ですが、バリデーションは必ずしもEntityと一対一で紐づくものでもなく、Entityにマッピングされないリクエストでもバリデーションをかけたい時があります。
そんな時のために、実はバリデーションクラスはFormTypeにも適用することができます。

何故か公式ドキュメントには、FormTypeにバリデーションをかける方法が記載されていない(自分が見つけられていないだけ?)のですが、割と便利なので、よく使うバリデーションを紹介します。

FormTypeの作成

まず、以下のような何もバリデーションのかかっていないFormTypeを作成します。

src/Form/SomeFormType.php
<?php

namespace App\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\TelType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class SomeFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('lastName', TextType::class, [
                'label' => '姓',
            ])
            ->add('firstName', TextType::class, [
                'label' => '名',
            ])
            ->add('birthday', DateType::class, [
                'label' => '生年月日',
                'widget' => 'single_text',
            ])
            ->add('gender', ChoiceType::class, [
                'label' => '性別',
                'choices' => [
                    '男性' => 'men',
                    '女性' => 'female',
                ],
            ])
            ->add('email', EmailType::class, [
                'label' => 'メールアドレス',
                'help' => 'メールアドレスか電話番号のどちらか一つは入力してください。',
                'required' => false,
            ])
            ->add('tel', TelType::class, [
                'label' => '電話番号',
                'help' => 'メールアドレスか電話番号のどちらか一つは入力してください。',
                'required' => false,
            ])
            ->add('isConfirmed', CheckboxType::class, [
                'label' => '利用規約に同意する',
            ])
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([]);
    }
}

Controllertemplateは以下の通りです。

src/Controller/FormTestController.php
<?php

namespace App\Controller;

use App\Form\SomeFormType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;

class FormTestController extends AbstractController
{
    /**
     * @Route("/form/test", name="form_test")
     */
    public function index(Request $request)
    {
        $form = $this->createForm(SomeFormType::class)->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            $postData = $form->getData();
            // do something
        }
        return $this->render('form_test/index.html.twig', [
            'form' => $form->createView(),
        ]);
    }
}

templates/form_test/index.html.twig
{% extends 'base.html.twig' %}
{% block body %}
<div class="container">
    <div class="card my-5">
        <div class="card-body">
            <div class="p-5">
                {{ form_start(form) }}
                {{ form_rest(form) }}
                <button class="btn btn-primary btn-block mt-5">Submit</button>
                {{ form_end(form) }}
            </div>
        </div>
    </div>
</div>
{% endblock %}

画面表示は以下のようになります。

スクリーンショット 2021-09-27 23.37.26.png

今はバリデーションを何もかけていないので、次のような入力をしても、リクエストが通ってしまいます。
それでは、各フィールドにバリデーションを追加していきます。

スクリーンショット 2021-09-27 23.40.16.png

バリデーションの追加

長さ・必須

姓名は、一般的にはそれぞれ10文字程度あれば十分です。そのような「長さに関するバリデーション」はLengthクラスが利用できます。(※ 以降出てくるバリデーションクラスのnamespaceはSymfony\Component\Validator\Constraintsです。同じ名前のクラスがある場合もあるので、namespaceを間違えないようにしてください。)

バリデーションクラスには、引数としてoptionを設定した配列を渡します。optionに設定できる値は各バリデーションごとに異なります。
Lengthには「文字列の最大長」を設定するmaxオプションと、「最大長を超えた時に表示するメッセージ」を設定するmaxMessageなどがあります。(もちろんminバージョンもあります)
Lengthに関する完全なオプションはこちらを参照してください。

これらのバリデーションクラスは、以下のようにフォームのフィールドにconstraintsという名前で配列の形式で渡すことで適用することができます。

また、空文字列も許容したくないので、NotNullも同時に適用します。

...
            ->add('lastName', TextType::class, [
                'label' => '姓',
                'constraints' => [
                    new Length([
                        'max' => 10,
                        'maxMessage' => '姓は最大{{ limit }}文字で入力してください。'
                    ]),
                    new NotNull([
                        'message' => '姓を入力してください。'
                    ]),
                ]
            ])
            ->add('firstName', TextType::class, [
                'label' => '名',
                'constraints' => [
                    new Length([
                        'max' => 10,
                        'maxMessage' => '名は最大{{ limit }}文字で入力してください。'
                    ]),
                    new NotNull([
                        'message' => '名を入力してください。'
                    ]),
                ]
            ])
...

フォームで不正な値を送信すると、このような表示になります。

スクリーンショット 2021-09-28 0.53.04.png

スクリーンショット 2021-09-28 0.52.49.png

比較

次に、年齢が20歳以上になるようにバリデーションをかけます。年齢が20歳以上になるためには、生年月日が本日から20年以上前になっていれば良いわけです。
このような「〇〇より小さい」などの比較を行う時はLessThanが利用できます。LessThanは、フォームから送られてきた値と比較するvalueと、バリデーションに失敗した時に表示するmessageオプションをとります。

ドキュメントはこちら

今回は日付の比較を行なっていますが、PHPで比較可能なものであればOKです。

...
            ->add('birthday', DateType::class, [
                'label' => '生年月日',
                'widget' => 'single_text',
                'constraints' => [
                    new LessThan([
                        'value' => new DateTime('20 years ago'),
                        'message' => '20歳未満は利用できません。'
                    ])
                ]
            ])
...

画面表示はこのようになります。

スクリーンショット 2021-09-27 23.47.40.png

メールアドレス等

続いて、メールアドレスの形式にバリデーションをかけます。こちらはズバリEmailというクラスがあるので、こちらを使います。
Emailには、バリデーションに失敗した時に表示するmessageオプションと、メールの検証に用いるルールを指定するためのmodeオプションがあります。
modeオプションに設定できる値は次のようになっています。

  • loose: 正規表現で緩く検証を行う(非推奨)
  • html5: HTML5の電子メール入力要素と同じ正規表現を使って検証を行う
  • strict : RFC 5322にしたがって検証を行う。(別途egulias/email-validatorライブラリのインストールが必要)

ドキュメントはこちら

その他、特殊な文字列を検証するためのクラスとしてはUrlRegexJsonHostnameIpなどがあります。

が、電話番号はないので、日本で一番メジャーな10文字 or 11文字のバリデーションをかけます。

...
            ->add('email', EmailType::class, [
                'label' => 'メールアドレス',
                'help' => 'メールアドレスか電話番号のどちらか一つは入力してください。',
                'required' => false,
                'constraints' => [
                    new Email([
                        'message' => 'メールアドレスの形式が不正です。',
                        'mode' => 'html5',
                    ])
                ]
            ])
            ->add('tel', TelType::class, [
                'label' => '電話番号',
                'help' => 'メールアドレスか電話番号のどちらか一つは入力してください。',
                'required' => false,
                'constraints' => [
                    new Length([
                        'max' => 11,
                        'min' => 10,
                        'minMessage' => '電話番号は10~11桁で入力してください。',
                        'maxMessage' => '電話番号は10~11桁で入力してください。',
                    ])
                ]
            ])
...

画面表示はこのようになります。

スクリーンショット 2021-09-27 23.55.16.png

チェックボックス

続いて、チェックボックスにチェックが入っていることを検証します。一応HTMLの要素でバリデーションはかかっていますが、開発者ツールなどで容易く解除できてしまうので、サーバーでのバリデーションも行います。

チェックボックスがチェックされているか( = trueであるか)はIsTrueクラスで検証できます。オプションには、エラーメッセージを設定するmessageを渡します。

ドキュメントはこちら

...
            ->add('isConfirmed', CheckboxType::class, [
                'label' => '利用規約に同意する',
                'constraints' => [
                    new IsTrue([
                        'message' => 'チェックボックスにチェックを入れてください。'
                    ])
                ]
            ])
...

画面表示はこのようになります。

スクリーンショット 2021-09-27 23.57.52.png

複数フィールドにまたがるバリデーション

最後に「メールアドレスか電話番号のどちらか一つは必須」という部分のバリデーションを行います。
今までは、フォームの各フィールドにバリデーションをかけてきましたが、このように「複数フィールドにまたがるバリデーション」を行うときは、フォーム全体のバリデーションとして設定します。

フォーム全体のバリデーションをかける方法は、FormTypeconfigureOptions()メソッドでFormType全体のデフォルトオプションとしてバリデーションを追加するというものになります。このような複雑なバリデーションを行う時はCallbackクラスを使うのが良いです。

Callbackクラスはcallbackというオプション名でcallableなものをとります。callableに渡ってくる引数は、第一引数が「フォームから送られてきた値の配列 or それらをEntityにマッピングしたもの」、第二引数が「現在のフォームのコンテキストクラス」になります。

以下のように、第一引数のデータを見て、第二引数のコンテキストにエラーを追加していく、という流れになります。

FormTypeを使っていると、このようなバリデーションを書きたくなることがよくあるのですが、わかりやすいサンプルが何故かドキュメントにないんですよね。。。。

一応ドキュメントはこちらになります。

FormTypeEntityのマッピングを行なっている時は、第一引数に渡ってくる値は配列ではなく、マッピングしたEntityになるので注意です。

...
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'constraints' => [
                new Callback([
                    'callback' => function (array $data, ExecutionContextInterface $context) {
                        $email = $data['email'];
                        $tel = $data['tel'];
                        if ($email === null && $tel === null) {
                            $context->buildViolation('メールアドレスか電話番号のどちらか一つは入力してください。')
                                ->atPath('[email]')
                                ->addViolation();
                            $context->buildViolation('メールアドレスか電話番号のどちらか一つは入力してください。')
                                ->atPath('[tel]')
                                ->addViolation();
                        }
                    },
                ]),
            ],
        ]);
    }
...

画面表示はこのようになります。

スクリーンショット 2021-09-28 0.20.15.png

最後に

今回は、具体的な例を出しながら、FormTypeにバリデーションをかける方法を書きました。
ここで紹介した以外にも、さまざまなバリデーションクラスが用意されています。
https://symfony.com/doc/current/validation.html#supported-constraints

また、バリデーションクラスを自分で作成することももちろんできます。
https://symfony.com/doc/current/validation/custom_constraint.html

便利な機能なのですが、あまり紹介してある記事やドキュメントを見かけなかったので書いてみました。何かのお役に立てれば幸いです。

5
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?