Symfony Component Advent Calendar 2022の15日目の記事です。
最初に
SymfonyはPHPのフレームワークのひとつです。しかし、公式サイトの説明文には
Symfony is a set of PHP Components, a Web Application framework, a Philosophy, and a Community — all working together in harmony.
(SymfonyはPHPコンポーネントのセットで、Webアプリケーションフレームワークで、哲学、そしてコミュニティです。それらがハーモニーを奏でながら動作しています。)
と書かれている通り、PHPコンポーネントのセットで、たくさんのコンポーネントを提供しており、それらを組み合わせてひとつのフレームワークとして動作しています。Symfonyのコンポーネントは、Symfony上だけで動作するのではなく、他のPHPフレームワークやアプリケーションでも動作している強力なものが揃っています。
今回はそれらの中から、役立ちそうなもの・お薦めしたいものを紹介していきたいと思います。
※記事内ではautoloadのインポートは省略します。
データをチェック、"Validator"
Validatorは、データに誤りがないか・このまま使って大丈夫か、検証するコンポーネントです。
インストール
composer require symfony/validator
検査項目の設定
Validator
にはConstraints
という検査項目があらかじめ用意されています。どのプロパティがどのような検証を行う必要があるかアトリビュートやオブジェクトで設定していきます。
このValidator
のサンプルコードで、よくEntity
に対しての検査の設定を目にするので、誤解されがちですが、Entity
だけではなく、他のクラスや変数・配列に対しても設定できます。
実際、自分も最近までEntity
だけだと誤解してました。
アトリビュート指定の場合
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Validator\ValidatorInterface;
class Item
{
#[Assert\NotBlank] // 空ではないかチェック
#[Assert\Length(max: 100)] // 文字列長をチェック
private string $name;
#[Assert\NotBlank]
#[Assert\Positive] // 正の値かチェック
private int $price;
}
オブジェクト指定の場合
use Symfony\Component\Validator\Constraints as Assert;
class SomeClass
{
public function someMethod(ValidatorInterface $validator)
{
// 変数に対して
$email = 'メールアドレス';
// 検査オブジェクトの作成
$emailConstraint = new Assert\Email(); // メールアドレスかチェック
$validator->validate($email, $emailConstraint); // 後述
// 配列に対して
$row = [
'name' => 'ほげ',
'email' => 'メールアドレス',
'date_of_birth' => '1979-03-28',
];
$constraint = new Assert\Collection([ // 検査オブジェクトのセット作成
'name' => [new Assert\NotBlank(), new Assert\Length(max:200)],
'email' => new Assert\Email(),
'date_of_birth' => new Assert\Date(), // 日付型かチェック
]);
$validator->validate($row, $constraint);
}
}
データの検証
すでに上記で記述していますが、検証を行うにはValidator
オブジェクトのvalidate()
メソッドを利用します。このメソッドはConstraintViolationList
オブジェクトを返しますが、これはエラー内容ConstraintViolationInterface`のリストになっています。
use Symfony\Component\Validator\Validator\ValidatorInterface;
public function someMethod(ValidatorInterface $validator)
{
$item = new Item();
...
$errors = $validator->validate($item);
if (count($errors) > 0) { // データになにかしらの誤りがあれば、件数は1件以上になる
array_map(function (ConstraintViolationInterface $violation) {
echo($violation->getMessage()); // エラーメッセージ出力
}, $errors);
}
}
グループ化
『登録時にはチェックしたいけど、更新時は無視したい』などといった、検査する場所で検査内容を変えたい場合があります。その場合はgroups
を指定することで、対応できます。
class Item
{
#[Assert\NotBlank] // グループ指定なし(常にチェック)
#[Assert\Length(max: 200, groups: ['register', 'update']) // 'register', 'update'時のみチェック
#[Assert\Length(min: 50, groups: ['update']) // 文字列長チェック(50文字以上)。'update'時のみチェック
private string $name;
}
$item = new Item();
...
# 'register'グループとして実施するので、50文字以上チェックは実施されない
$validator->validate($item, groups: 'register');
独自チェック
アプリケーションによっては、デフォルトで用意された検査内容ではチェックし切れないものもあります。その場合は独自のConstraint
, Validator
クラスを作ることで対応できます。
Constraint
には、エラー時のメッセージやオプションの設定、Validator
には検査ロジックを作成します。基本的にConstraint
クラスの名前に Validatorをつけたクラスの名前が対応するValidator
クラスになります。
Constraint
namespace App\Validator;
use Symfony\Component\Validator\Attribute\HasNamedArguments;
use Symfony\Component\Validator\Constraint;
#[\Attribute]
class ValidEmailDomain extends Constraint
{
public $message = '"{{ string }}"は許可されていないドメインを利用しています';
#[HasNamedArguments]
public function __construct(public readonly array $validaDomains, array $groups = null, mixed $payload = null)
{
parent::__construct([], $groups, $payload);
}
}
Validator
namespace App\Validator;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Exception\UnexpectedValueException;
class ValidEmailDomainValidator extends ConstraintValidator
{
public function validate($value, Constraint $constraint): void
{
assert($constraint instanceof ValidEmailDomain);
if ($value === null || $value === '') {
return; // 空の場合はチェックをかけない
}
foreach ($constraint->validDomains as $domain)
{
$pattern = sptintf("/\@%s\$/", $domain);
if (preg_match($pattern, $value) {
return; // 許可ドメインと一致したら終了
}
}
// 一致しなかった場合の処理
$this->context
->buildViolation($constraint->message)
->setParameter('{{ string }}', $value)
->addViolation()
;
}
}
独自チェックの追加
use App\Validator as AppAssert;
use Symfony\Component\Validator\Constraints as Assert;
class User
{
#[Assert\NotBlank]
#[AppAssert\ValidEmailDomain(['somedomain.co.jp', 'somedomain.com'])] // 独自チェック
private $email;
}
まとめ
今回はValidator
を紹介しました。データの検証はアプリケーションの中でも重要な機能の一つですが、このコンポーネントではアトリビュートでどのプロパティがどのような検査項目を持っているのか一目瞭然なので、便利です。また、あくまでアトリビュートだけなので、検査項目の設定をしてもプログラム的な依存がないところもおすすめです。