カスタムバリデーション
業務においてカスタムバリデーションを行うことになりました。カスタムバリデータの作成方法は Symfony2 カスタムバリデーションの作成 | QUARTETCOM TECH BLOG や How to Create a custom Validation Constraint (The Symfony CookBook) が参考になります。
このあたりを見ながら「/
を含む場合に不正」とするファイル名バリデータ
を作成してみます。
カスタムバリデータを作成
<?php
namespace Acme\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
class Filename extends Constraint
{
public $message = 'ファイル名が正しくありません。';
}
<?php
namespace Acme\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
class FilenameValidator extends ConstraintValidator
{
public function validate($value, Constraint $constraint)
{
if (!$constraint instanceof Filename) {
throw new UnexpectedTypeException($constraint, Filename::class);
}
if (preg_match('/\//', $value, $matches)) {
$this->context
->buildViolation($constraint->message)
->addViolation()
;
}
}
}
以上で完成です。
カスタムバリデータを使用
さっそくファイル名バリデータ
を使ってみましょう。
エントリポイントとなるmain.php
を作成して実行してみます。
<?php
use Symfony\Component\Validator\Validation;
use Acme\Validator\Constraints\Filename;
$validator = Validation::createValidator();
$violations = $validator->validate('/', new Filename());
foreach ($violations as $violation) {
echo $violation->getMessage().PHP_EOL;
}
$ php main.php
ファイル名が正しくありません。
参考URLのとおりに行えば特に難しくありませんね。
コンテキスト
さてAcme\Validator\Constraints\FilenameValidator
において$this->context
という記述があります。ここにあるcontext
とは何でしょうか?疑問に思ったので今回はその役割を調べてみました。
context
はSymfony\Component\Validator\Context\ExecutionContextInterfaceを実装したインスタンスです。このインターフェイスの冒頭のコメントにコンテキストの役割が記されていましたので以下に引用します。
/**
* The context of a validation run.
*
* The context collects all violations generated during the validation. By
* default, validators execute all validations in a new context:
*
* $violations = $validator->validate($object);
*
* When you make another call to the validator, while the validation is in
* progress, the violations will be isolated from each other:
*
* public function validate($value, Constraint $constraint)
* {
* $validator = $this->context->getValidator();
*
* // The violations are not added to $this->context
* $violations = $validator->validate($value);
* }
*
* However, if you want to add the violations to the current context, use the
* {@link ValidatorInterface::inContext()} method:
*
* public function validate($value, Constraint $constraint)
* {
* $validator = $this->context->getValidator();
*
* // The violations are added to $this->context
* $validator
* ->inContext($this->context)
* ->validate($value)
* ;
* }
*
* Additionally, the context provides information about the current state of
* the validator, such as the currently validated class, the name of the
* currently validated property and more. These values change over time, so you
* cannot store a context and expect that the methods still return the same
* results later on.
*
* @since 2.5
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface ExecutionContextInterface extends LegacyExecutionContextInterface
バリデータがバリデーションを行うごとに独立した「コンテキスト」という状態が定義されるようです。このコンテキストがバリデータの現在の状態を保持し、バリデーション対象クラスや現在バリデーションしているプロパティの情報などを持つようです。
個人的にはわかったようなわからないような感じでした。
使い所は?
先ほど引用したコメントにある「独立した場合」と「コンテキストを共有した場合」の使いわけをする場面というのはどういう時なのでしょうか?
ひとつの案としてビルトインされているバリデータや既存のカスタムバリデータを複数使用する時にありえるのではないかというアドバイスを同僚にもらいましたので試してみます。
試してみる
仮に「一意なEメールアドレスを登録するアプリ」における「Eメールバリデータ = MyEmailValidator
」を作成することとします。
以下をバリデーションの不正条件としました。
- メールアドレスとしてのフォーマットが正しくない
- 登録済である
ここではEmail および[Symfony2 カスタムバリデーションの作成 |
QUARTETCOM TECH BLOG](http://tech.quartetcom.co.jp/2014/12/24/symfony2-custom-validation/) で作成したUnusedEmail と同等のものを使用することとします。 |
なお、エントリポイントとなるスクリプトは以下です。
<?php
// main.php
use Symfony\Component\Validator\Validation;
use Acme\Validator\Constraints\MyEmail;
$validator = Validation::createValidator();
$violations = $validator->validate('test@example', new MyEmail());
foreach ($violations as $violation) {
echo $violation->getMessage().PHP_EOL;
}
実装方法として以下の3パターンを考えてみました。
Email
あるいはUnusedEmail
のどちらに違反しても違反の内容に変化がないEmail
あるいはUnusedEmail
のどれに違反したのかを明確に- 1,2 のハイブリッド
1. Email
あるいはUnusedEmail
のどちらに違反しても違反の内容に変化がない
Email
制約に違反した時点でUnusedEmail
の制約チェックは行われません。
MyEmailValidator
は「Email
制約で違反しているのか」「UnusedEmail
制約で違反しているのか」の情報を保持せず、あくまでMyEmailValidator
のレイヤとしての情報を保持しているのみです。
Email
制約に違反している時点でUnusedEmail
の制約チェックを行うまでもなくMyEmail
制約としては違反です。余計なコストをかける必要もないですしMyEmail
制約としては妥当な振舞に思えます。
<?php
namespace Acme\Validator\Constraints;
use Acme\Validator\Constraints\UnusedEmail;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints\Email;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
class MyEmailValidator extends ConstraintValidator
{
public function validate($value, Constraint $constraint)
{
if (!$constraint instanceof MyEmail) {
throw new UnexpectedTypeException($constraint, MyEmail::class);
}
$validator = $this->context->getValidator();
$emailConstraint = new Email();
$unusedEmailConstraint = new UnusedEmail();
if (count($validator->validate($value, $emailConstraint)) > 0
|| count($validator->validate($value, $unusedEmailConstraint)) > 0
) {
$this->context
->buildViolation($constraint->message)
->addViolation()
;
}
}
}
結果は以下です。カスタムバリデータMyEmail
のコンテキストに独自メッセージを保持するviolationが登録されたのがわかります。
$ php main.php
Eメールアドレスが正しくありません。 # MyEmailによるメッセージ。
2. Email
あるいはUnusedEmail
のどれに違反したのかを明確に
Email
制約に違反してもしなくてもUnusedEmail
の制約チェックが行われます。
MyEmailValidator
は「Email
制約で違反しているのか」「UnusedEmail
制約で違反しているのか」の情報を保持しています。MyEmailValidator
はこれらの情報の器としてしか機能していないように見受けられます。
バリデーションを実行した側としてはどういった制約で違反しうるのかを一度で情報を得たい場合もあるでしょう。そういった際にはこのようなコンテキスト運用が有用に思われます。
<?php
namespace Acme\Validator\Constraints;
use Acme\Validator\Constraints\UnusedEmail;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints\Email;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
class MyEmailValidator extends ConstraintValidator
{
public function validate($value, Constraint $constraint)
{
if (!$constraint instanceof MyEmail) {
throw new UnexpectedTypeException($constraint, MyEmail::class);
}
$validator = $this->context->getValidator();
$emailConstraint = new Email();
$unusedEmailConstraint = new UnusedEmail();
$validator
->inContext($this->context)
->validate($value, $emailConstraint)
->validate($value, $unusedEmailConstraint)
;
}
}
結果は以下です。カスタムバリデータMyEmail
のコンテキストにEmail
およびUnusedEmail
で発生したviolationが登録されたのがわかります。
$ php main.php
This value is not a valid email address. # Emailによるメッセージ
他のサービスで使用されています。 # UnusedEmailによるメッセージ
3. 1,2 のハイブリッド
参考までに。
<?php
namespace Acme\Validator\Constraints;
use Acme\Validator\Constraints\UnusedEmail;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints\Email;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
class MyEmailValidator extends ConstraintValidator
{
public function validate($value, Constraint $constraint)
{
if (!$constraint instanceof MyEmail) {
throw new UnexpectedTypeException($constraint, MyEmail::class);
}
$validator = $this->context->getValidator();
$emailConstraint = new Email();
$unusedEmailConstraint = new UnusedEmail();
// 2. のロジック
$violations = $validator
->inContext($this->context)
->validate($value, $emailConstraint)
->validate($value, $unusedEmailConstraint)
->getViolations()
;
// 1. のロジック
if (count($violations) > 0) {
$this->context
->buildViolation($constraint->message)
->addViolation()
;
}
}
}
結果は以下です。カスタムバリデータMyEmail
のコンテキストに独自メッセージを保持するviolation、Email
およびUnusedEmail
で発生したviolationが並行して登録されたのがわかります。
$ php main.php
This value is not a valid email address. # Emailによるメッセージ
他のサービスで使用されています。 # UnusedEmailによるメッセージ
Eメールアドレスが正しくありません。 # MyEmailによるメッセージ。
まとめ
以上より、バリデーションの制約結果情報をひとまとまりに保持するのがコンテキストの役割のようです。的確なコンテキスト運用をすることでバリデーションの結果情報を有用に使用できることができるようになるかと思います。
今回は以下のコードを重点的に探っていきました。
- Symfony\Component\Validator\Context
- Symfony\Component\Validator\Validator
- Symfony\Component\Validator\Violation
まだまだ奥は深そうですが、疑問ドリブンでコードの海に飛び込んで一度さらりとでも読んでおいたという事実がいつか役に立つんじゃないかなと期待しています。いつまでも覚えていられる自信はまったくありませんが…
あとコードリーディングにもPhpStorm便利。
補足
なお、カスタムバリデータ内でないときは以下のように$validator->startContext()
することで新たなコンテキストを作成し、複数のバリデーションで共有することができます。ご参考まで。
<?php
// main.php
use Acme\Validator\Constraints\UnusedEmail;
use Symfony\Component\Validator\Constraints\Email;
use Symfony\Component\Validator\Validation;
$value = 'test@example';
$validator = Validation::createValidator();
$violations = $validator
->startContext()
->validate($value, new Email())
->validate($value, new UnusedEmail())
->getViolations()
;
foreach ($violations as $violation) {
echo $violation->getMessage().PHP_EOL;
}
$ php main.php
This value is not a valid email address.
他のサービスで使用されています。