[Symfony/Validation] バリデーションから他の制約クラスを呼び出して使う方法
やりたいこと
あるバリデーションロジックの中から、別の既存のバリデーション制約クラスを呼び出して使いたいという話です。
実際に例を見てみましょう。
Before
[Symfony/Validation] Callback制約の使い方
こちらの過去記事 のシチュエーションを例にとります。
use Symfony\Component\Validator\Constraints as Assert;
class User
{
/**
* @Assert\NotBlank()
*/
private $name;
/**
* @Assert\NotBlank()
* @Assert\Email()
*/
private $email;
/**
* @Assert\NotBlank()
*/
private $password;
/**
* @Assert\Callback()
*/
public function validatePassword(ExecutionContextInterface $context)
{
if (strpos($this->name, $this->password) !== false || strpos($this->email, $this->password) !== false) {
$context->buildViolation('名前やメールアドレスに含まれる文字列はパスワードに設定できません')->atPath('password')->addViolation();
}
}
}
こんな感じでバリデーションが設定されたエンティティがあるとします。
今回は例として、 validatePassword()
メソッドを validate()
メソッドに改名し、プロパティのアノテーションで実施しているバリデーションも含めてすべての検証を validate()
メソッド内で行うようにしてみたいと思います。
普通わざわざそんな実装にはしないと思います😅ここではあくまで例だと思ってお付き合いください🙏
After(微妙パターン)
use Symfony\Component\Validator\Constraints as Assert;
class User
{
private $name;
private $email;
private $password;
/**
* @Assert\Callback()
*/
public function validate(ExecutionContextInterface $context)
{
if (empty($this->name)) {
$context->buildViolation('この項目は必須です')->atPath('name')->addViolation();
}
if (empty($this->email)) {
$context->buildViolation('この項目は必須です')->atPath('email')->addViolation();
}
if (empty($this->password)) {
$context->buildViolation('この項目は必須です')->atPath('password')->addViolation();
}
if (!preg_match(Assert\EmailValidator::PATTERN_HTML5, $this->email)) {
$context->buildViolation('メールアドレスが正しくありません')->atPath('email')->addViolation();
}
if (strpos($this->name, $this->password) !== false || strpos($this->email, $this->password) !== false) {
$context->buildViolation('名前やメールアドレスに含まれる文字列はパスワードに設定できません')->atPath('password')->addViolation();
}
}
}
すべてのバリデーションを自力でやるとこんな感じになってしまうでしょう。
しかしこれだと完全に既存の NotBlank
や Email
の再実装ですし、エラーメッセージもハードコードしないといけないのも微妙です。
After(いい感じパターン)
というわけで、 validate()
メソッドの中から NotBlank
や Email
といった既存のバリデーション制約クラスを呼び出して使いたくなります。
結論としては、以下のような方法で実現できます👍
use Symfony\Component\Validator\Constraints as Assert;
class User
{
private $name;
private $email;
private $password;
/**
* @Assert\Callback()
*/
public function validate(ExecutionContextInterface $context)
{
$notBlankConstraint = new Assert\NotBlank();
$emailConstraint = new Assert\Email();
$validator = $context->getValidator()->inContext($context);
$validator
->atPath('name')
->validate($this->name, $notBlankConstraint)
;
$validator
->atPath('email')
->validate($this->email, $notBlankConstraint)
->validate($this->email, $emailConstraint)
;
$validator
->atPath('password')
->validate($this->password, $notBlankConstraint)
;
if (strpos($this->name, $this->password) !== false || strpos($this->email, $this->password) !== false) {
$context->buildViolation('名前やメールアドレスに含まれる文字列はパスワードに設定できません')->atPath('password')->addViolation();
}
}
}
カスタムバリデーションなどの中から既存のバリデーション制約クラスを使いたいなーと思ったときに、思い出してください😇
参考:How to use Dynamic Constraints with Symfony/Validator | Děláš v PHP? Jsi jedním z nás
引数ありの制約クラスの場合
ちなみに、制約クラスの中にはコンストラクタが引数をとるものもあるので、その例も一応書いておきます✋
例えば Choice
制約に callback
引数をつけて使う場合のBefore/Afterはこんな感じです。(これも、Afterのほうが良い実装だとかそういう話ではありません。あくまで例です🙏)
Before
use Symfony\Component\Validator\Constraints as Assert;
class User
{
const DIVISION_SALES = '営業部';
const DIVISION_DEVELOPMENT = '開発部':
const DIVISION_GENERAL = '総務部';
public static function getValidDivisions(): array
{
return [
self::DIVISION_SALES,
self::DIVISION_DEVELOPMENT,
self::DIVISION_GENERAL,
];
}
/**
* Assert\Choice(callback="getValidDivisions")
*/
private $division;
}
After
use Symfony\Component\Validator\Constraints as Assert;
class User
{
const DIVISION_SALES = '営業部';
const DIVISION_DEVELOPMENT = '開発部':
const DIVISION_GENERAL = '総務部';
public static function getValidDivisions(): array
{
return [
self::DIVISION_SALES,
self::DIVISION_DEVELOPMENT,
self::DIVISION_GENERAL,
];
}
private $division;
/**
* @Assert\Callback()
*/
public function validate(ExecutionContextInterface $context)
{
$choiceConstraint = new Assert\Choice([
'callback' => [$this, 'getValidDivisions'],
]);
$context
->getValidator()
->inContext($context)
->atPath('division')
->validate($this->division, $choiceConstraint)
;
}
}
応用すると「他のプロパティの値に応じて選択肢を変える」といった実装も簡単にできそうですね👍
switch ($this->type) {
case self::TYPE_A:
$callback = [$this, 'getValidChoicesForTypeA'];
break;
case self::TYPE_B:
$callback = [$this, 'getValidChoicesForTypeB'];
break;
case self::TYPE_C:
default:
$callback = [$this, 'getValidChoicesForTypeC'];
break;
}
$choiceConstraint = new Assert\Choice([
'callback' => $callback,
]);
Discussion