はじめに
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
を作成します。
<?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([]);
}
}
Controller
とtemplate
は以下の通りです。
<?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(),
]);
}
}
{% 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 %}
画面表示は以下のようになります。
今はバリデーションを何もかけていないので、次のような入力をしても、リクエストが通ってしまいます。
それでは、各フィールドにバリデーションを追加していきます。
バリデーションの追加
長さ・必須
姓名は、一般的にはそれぞれ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' => '名を入力してください。'
]),
]
])
...
フォームで不正な値を送信すると、このような表示になります。
比較
次に、年齢が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歳未満は利用できません。'
])
]
])
...
画面表示はこのようになります。
メールアドレス等
続いて、メールアドレスの形式にバリデーションをかけます。こちらはズバリEmail
というクラスがあるので、こちらを使います。
Email
には、バリデーションに失敗した時に表示するmessage
オプションと、メールの検証に用いるルールを指定するためのmode
オプションがあります。
mode
オプションに設定できる値は次のようになっています。
-
loose
: 正規表現で緩く検証を行う(非推奨) -
html5
: HTML5の電子メール入力要素と同じ正規表現を使って検証を行う - strict :
RFC 5322
にしたがって検証を行う。(別途egulias/email-validator
ライブラリのインストールが必要)
ドキュメントはこちら。
その他、特殊な文字列を検証するためのクラスとしてはUrl、Regex、Json、Hostname、Ipなどがあります。
が、電話番号はないので、日本で一番メジャーな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桁で入力してください。',
])
]
])
...
画面表示はこのようになります。
チェックボックス
続いて、チェックボックスにチェックが入っていることを検証します。一応HTMLの要素でバリデーションはかかっていますが、開発者ツールなどで容易く解除できてしまうので、サーバーでのバリデーションも行います。
チェックボックスがチェックされているか( = true
であるか)はIsTrue
クラスで検証できます。オプションには、エラーメッセージを設定するmessage
を渡します。
ドキュメントはこちら。
...
->add('isConfirmed', CheckboxType::class, [
'label' => '利用規約に同意する',
'constraints' => [
new IsTrue([
'message' => 'チェックボックスにチェックを入れてください。'
])
]
])
...
画面表示はこのようになります。
複数フィールドにまたがるバリデーション
最後に「メールアドレスか電話番号のどちらか一つは必須」という部分のバリデーションを行います。
今までは、フォームの各フィールドにバリデーションをかけてきましたが、このように「複数フィールドにまたがるバリデーション」を行うときは、フォーム全体のバリデーションとして設定します。
フォーム全体のバリデーションをかける方法は、FormType
のconfigureOptions()
メソッドでFormType
全体のデフォルトオプションとしてバリデーションを追加するというものになります。このような複雑なバリデーションを行う時はCallback
クラスを使うのが良いです。
Callback
クラスはcallback
というオプション名でcallable
なものをとります。callable
に渡ってくる引数は、第一引数が「フォームから送られてきた値の配列 or それらをEntityにマッピングしたもの」、第二引数が「現在のフォームのコンテキストクラス」になります。
以下のように、第一引数のデータを見て、第二引数のコンテキストにエラーを追加していく、という流れになります。
FormType
を使っていると、このようなバリデーションを書きたくなることがよくあるのですが、わかりやすいサンプルが何故かドキュメントにないんですよね。。。。
一応ドキュメントはこちらになります。
※FormType
とEntity
のマッピングを行なっている時は、第一引数に渡ってくる値は配列ではなく、マッピングした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();
}
},
]),
],
]);
}
...
画面表示はこのようになります。
最後に
今回は、具体的な例を出しながら、FormType
にバリデーションをかける方法を書きました。
ここで紹介した以外にも、さまざまなバリデーションクラスが用意されています。
https://symfony.com/doc/current/validation.html#supported-constraints
また、バリデーションクラスを自分で作成することももちろんできます。
https://symfony.com/doc/current/validation/custom_constraint.html
便利な機能なのですが、あまり紹介してある記事やドキュメントを見かけなかったので書いてみました。何かのお役に立てれば幸いです。