Respect Validation
ライブラリ
Respect Validation
概要
Respect Validationは、「PHPで最も素晴らしいバリデーションエンジン」として知られる高機能なバリデーションライブラリです。Fluent Interfaceによる直感的なメソッドチェーン記法で複雑なバリデーションルールを簡潔に表現でき、150以上の豊富な組み込みバリデーションルール、詳細なエラーメッセージ制御、カスタムルールの簡単な作成などを提供します。2025年現在、バージョン2.4系が最新で、PHP 8.1以上をサポート。小規模なフォームバリデーションから大規模なAPIデータ検証まで、あらゆる規模のプロジェクトで活用されているPHPコミュニティの定番ライブラリです。
詳細
Respect Validation 2.4.xは、2025年現在のPHP 8.1以上に対応した最新版で、PHPエコシステムにおいて最も人気の高いバリデーションライブラリの一つです。その最大の特徴は、v::numericVal()->positive()->between(1, 255)->isValid($input)のような流れるような記法で複雑なバリデーションルールを構築できるFluent Interfaceです。ライブラリには150以上の多様な組み込みルールが含まれており、基本的なデータ型検証から高度なビジネスロジックまで幅広くカバー。カスタムバリデーターの作成も簡単で、独自のビジネス要件に合わせた柔軟な拡張が可能です。メソッドチェーンによる組み合わせで、複雑な条件も可読性高く表現できます。
主な特徴
- Fluent Interface: メソッドチェーンによる直感的なバリデーションルール構築
- 150以上のルール: 豊富な組み込みバリデーターで多様な検証ニーズに対応
- チェイナブルバリデーター: 複数の条件を連鎖させて複雑なロジックを表現可能
- カスタムルール作成: 独自のビジネスロジックに特化したバリデーターを簡単に実装
- 詳細なエラー制御: バリデーション結果の粒度調整と詳細なエラーレポート
- 例外ベースのハンドリング: 構造化された例外処理でエラー情報を管理
メリット・デメリット
メリット
- 非常に直感的で読みやすいFluent Interface記法
- 豊富な組み込みルールによる高い即戦力性
- カスタムバリデーターの作成が簡単
- 詳細なエラーメッセージとコンテキスト情報
- 軽量でパフォーマンスが良い
- アクティブなコミュニティと継続的な開発
- フレームワークに依存しないスタンドアロン設計
デメリット
- 大量のデータに対してはパフォーマンスが低下する可能性
- 複雑なネストしたオブジェクト検証には追加のコードが必要
- 国際化機能が限定的(カスタム実装が必要)
- バリデーションルールの静的解析が困難
- エラーメッセージのカスタマイズに学習コストがある
- デバッグ時にチェーンの途中でどこが失敗したかわかりにくい場合がある
参考ページ
書き方の例
インストールと基本セットアップ
# Composerでインストール
composer require respect/validation
# 特定のバージョンを指定する場合
composer require respect/validation:^2.4
# 追加の機能が必要な場合(メール検証の強化)
composer require egulias/email-validator
# 電話番号検証の強化
composer require giggsey/libphonenumber-for-php-lite
基本的なバリデーション実装
<?php
use Respect\Validation\Validator as v;
use Respect\Validation\Exceptions\ValidationException;
// シンプルな値のバリデーション
$ageValidator = v::intVal()->positive()->between(1, 120);
// 基本的な検証
if ($ageValidator->isValid(25)) {
echo "有効な年齢です";
}
// 詳細なエラーメッセージを取得
try {
$ageValidator->assert(-5);
} catch (ValidationException $e) {
echo $e->getMessage();
// "-5" must be greater than 1
}
// 複数の条件を組み合わせた複雑なバリデーション
$emailValidator = v::stringType()
->notEmpty()
->email()
->domain(v::in(['gmail.com', 'yahoo.com', 'example.com']));
// ユーザー登録データの検証例
$userData = [
'username' => 'johndoe123',
'email' => '[email protected]',
'age' => 28,
'website' => 'https://johndoe.com',
'tags' => ['developer', 'php', 'javascript']
];
$userValidator = v::key('username', v::stringType()->alnum()->length(3, 30))
->key('email', v::stringType()->email())
->key('age', v::intVal()->between(18, 100))
->key('website', v::optional(v::url()))
->key('tags', v::arrayType()->each(v::stringType()->notEmpty()));
try {
$userValidator->assert($userData);
echo "ユーザーデータは有効です";
} catch (ValidationException $e) {
echo "バリデーションエラー: " . $e->getMessage();
}
// より詳細なエラー情報の取得
try {
$userValidator->assert($userData);
} catch (ValidationException $e) {
// 全てのエラーメッセージを取得
foreach ($e->getMessages() as $message) {
echo $message . "\n";
}
// 構造化されたエラー情報
print_r($e->getMessages());
}
Fluent Interfaceの高度な使用例
<?php
use Respect\Validation\Validator as v;
// パスワード強度の検証
$passwordValidator = v::stringType()
->notEmpty()
->length(8, 50)
->regex('/[A-Z]/') // 大文字を含む
->regex('/[a-z]/') // 小文字を含む
->regex('/[0-9]/') // 数字を含む
->regex('/[^A-Za-z0-9]/') // 特殊文字を含む
->not(v::contains('password'))
->not(v::contains('123456'));
// 日本の郵便番号バリデーション
$zipCodeValidator = v::stringType()
->notEmpty()
->regex('/^\d{3}-?\d{4}$/')
->callback(function($value) {
// ハイフンを除去して長さをチェック
$digits = preg_replace('/[^0-9]/', '', $value);
return strlen($digits) === 7;
});
// クレジットカード番号の検証
$creditCardValidator = v::stringType()
->notEmpty()
->regex('/^[0-9\s-]+$/')
->callback(function($value) {
// Luhnアルゴリズムによる検証
$number = preg_replace('/[^0-9]/', '', $value);
return luhnCheck($number);
});
// 条件付きバリデーション
$productValidator = v::key('name', v::stringType()->notEmpty())
->key('price', v::numericVal()->positive())
->key('category', v::stringType()->in(['electronics', 'books', 'clothing']))
->key('discount_price', v::optional(
v::when(
v::key('category', v::equals('electronics')),
v::numericVal()->positive()->callback(function($value, $context) {
// 割引価格が元の価格より安いかチェック
return $value < $context['price'];
}),
v::nullType()
)
));
// ファイルアップロードのバリデーション
$fileValidator = v::arrayType()
->key('name', v::stringType()->notEmpty())
->key('type', v::stringType()->in(['image/jpeg', 'image/png', 'image/gif']))
->key('size', v::intVal()->between(1, 5242880)) // 最大5MB
->key('error', v::equals(UPLOAD_ERR_OK));
// ネストしたデータ構造の検証
$orderValidator = v::arrayType()
->key('customer', v::arrayType()
->key('name', v::stringType()->notEmpty())
->key('email', v::email())
->key('phone', v::stringType()->regex('/^[0-9\-\+\(\)\s]+$/'))
)
->key('items', v::arrayType()->each(
v::arrayType()
->key('product_id', v::intVal()->positive())
->key('quantity', v::intVal()->positive())
->key('price', v::numericVal()->positive())
))
->key('total', v::numericVal()->positive())
->callback(function($order) {
// 合計金額の整合性チェック
$calculatedTotal = array_sum(array_map(function($item) {
return $item['quantity'] * $item['price'];
}, $order['items']));
return abs($calculatedTotal - $order['total']) < 0.01;
});
function luhnCheck($number): bool
{
$sum = 0;
$alternate = false;
for ($i = strlen($number) - 1; $i >= 0; $i--) {
$digit = intval($number[$i]);
if ($alternate) {
$digit *= 2;
if ($digit > 9) {
$digit = ($digit % 10) + 1;
}
}
$sum += $digit;
$alternate = !$alternate;
}
return ($sum % 10) === 0;
}
カスタムルールの作成
<?php
namespace App\Validation\Rules;
use Respect\Validation\Rules\AbstractRule;
/**
* 日本の電話番号バリデーションルール
*/
class JapanesePhoneNumber extends AbstractRule
{
private bool $allowMobile;
private bool $allowLandline;
public function __construct(bool $allowMobile = true, bool $allowLandline = true)
{
$this->allowMobile = $allowMobile;
$this->allowLandline = $allowLandline;
}
public function validate($input): bool
{
if (!is_string($input)) {
return false;
}
// ハイフンとスペースを除去
$cleanNumber = preg_replace('/[\s\-\(\)]/', '', $input);
if ($this->allowMobile && $this->isMobileNumber($cleanNumber)) {
return true;
}
if ($this->allowLandline && $this->isLandlineNumber($cleanNumber)) {
return true;
}
return false;
}
private function isMobileNumber(string $number): bool
{
// 携帯電話: 090, 080, 070で始まる11桁
return preg_match('/^0[789]0\d{8}$/', $number) === 1;
}
private function isLandlineNumber(string $number): bool
{
// 固定電話: 地域番号で始まる10桁または11桁
return preg_match('/^0\d{9,10}$/', $number) === 1 &&
!preg_match('/^0[789]0/', $number); // 携帯番号を除外
}
}
// カスタムルールの例外クラス
namespace App\Validation\Exceptions;
use Respect\Validation\Exceptions\ValidationException;
class JapanesePhoneNumberException extends ValidationException
{
protected $defaultTemplates = [
self::MODE_DEFAULT => [
self::STANDARD => '{{name}} は有効な日本の電話番号である必要があります',
],
self::MODE_NEGATIVE => [
self::STANDARD => '{{name}} は日本の電話番号であってはいけません',
],
];
}
// カスタムルールの使用例
use Respect\Validation\Validator as v;
use App\Validation\Rules\JapanesePhoneNumber;
// カスタムルールをチェーンに組み込み
$contactValidator = v::key('name', v::stringType()->notEmpty())
->key('phone', v::stringType()->setName('電話番号')->addRule(new JapanesePhoneNumber()))
->key('mobile_only', v::optional(
v::stringType()->setName('携帯電話番号')
->addRule(new JapanesePhoneNumber(true, false))
));
// より複雑なカスタムルール: 営業時間の検証
class BusinessHours extends AbstractRule
{
private array $allowedDays;
private string $startTime;
private string $endTime;
public function __construct(
array $allowedDays = [1, 2, 3, 4, 5], // 月-金
string $startTime = '09:00',
string $endTime = '18:00'
) {
$this->allowedDays = $allowedDays;
$this->startTime = $startTime;
$this->endTime = $endTime;
}
public function validate($input): bool
{
if (!$input instanceof \DateTimeInterface) {
return false;
}
$dayOfWeek = (int)$input->format('N'); // 1=月曜日, 7=日曜日
$time = $input->format('H:i');
return in_array($dayOfWeek, $this->allowedDays) &&
$time >= $this->startTime &&
$time <= $this->endTime;
}
}
// 営業時間バリデーターの使用
$appointmentValidator = v::key('datetime',
v::dateTime()->addRule(new BusinessHours([1, 2, 3, 4, 5], '09:00', '17:00'))
);
例外処理とエラーハンドリング
<?php
use Respect\Validation\Validator as v;
use Respect\Validation\Exceptions\ValidationException;
use Respect\Validation\Exceptions\NestedValidationException;
class ValidationService
{
/**
* 基本的な例外ハンドリング
*/
public function validateUserRegistration(array $data): array
{
$validator = v::key('username', v::stringType()->alnum()->length(3, 20))
->key('email', v::email())
->key('password', v::stringType()->length(8, 50))
->key('age', v::intVal()->between(13, 120));
try {
$validator->assert($data);
return ['success' => true, 'errors' => []];
} catch (NestedValidationException $e) {
return [
'success' => false,
'errors' => $this->formatNestedErrors($e)
];
}
}
/**
* ネストした例外の詳細なエラーフォーマット
*/
private function formatNestedErrors(NestedValidationException $e): array
{
$errors = [];
// 各フィールドのエラーを個別に処理
foreach ($e->getMessages() as $field => $messages) {
if (is_array($messages)) {
$errors[$field] = array_values($messages);
} else {
$errors[$field] = [$messages];
}
}
return $errors;
}
/**
* カスタムエラーメッセージの適用
*/
public function validateWithCustomMessages(array $data): array
{
$validator = v::key('email', v::email()->setName('メールアドレス'))
->key('age', v::intVal()->between(18, 65)->setName('年齢'))
->key('website', v::optional(v::url()->setName('ウェブサイト')));
try {
$validator->assert($data);
return ['success' => true];
} catch (NestedValidationException $e) {
// カスタムメッセージテンプレートを適用
$customMessages = [
'email' => [
'email' => 'メールアドレスの形式が正しくありません'
],
'age' => [
'intVal' => '年齢は整数で入力してください',
'between' => '年齢は18歳から65歳の間で入力してください'
],
'website' => [
'url' => 'ウェブサイトは有効なURL形式で入力してください'
]
];
return [
'success' => false,
'errors' => $this->applyCustomMessages($e, $customMessages)
];
}
}
private function applyCustomMessages(NestedValidationException $e, array $customMessages): array
{
$errors = [];
foreach ($e->getChildren() as $child) {
$field = $child->getName();
if ($child instanceof NestedValidationException) {
foreach ($child->getChildren() as $fieldError) {
$ruleName = $this->getRuleName($fieldError);
if (isset($customMessages[$field][$ruleName])) {
$errors[$field][] = $customMessages[$field][$ruleName];
} else {
$errors[$field][] = $fieldError->getMessage();
}
}
} else {
$errors[$field][] = $child->getMessage();
}
}
return $errors;
}
private function getRuleName(ValidationException $e): string
{
// バリデーション例外からルール名を抽出
$className = get_class($e);
$ruleName = str_replace(['Respect\\Validation\\Exceptions\\', 'Exception'], '', $className);
return lcfirst($ruleName);
}
/**
* 段階的バリデーション(一部のフィールドのみ検証)
*/
public function validateStep(array $data, string $step): array
{
$validators = [
'basic' => v::key('name', v::stringType()->notEmpty())
->key('email', v::email()),
'personal' => v::key('age', v::intVal()->between(18, 120))
->key('phone', v::stringType()->regex('/^[0-9\-\+\(\)\s]+$/')),
'preferences' => v::key('newsletter', v::boolType())
->key('notifications', v::arrayType())
];
if (!isset($validators[$step])) {
return ['success' => false, 'errors' => ['step' => ['無効なステップです']]];
}
try {
$validators[$step]->assert($data);
return ['success' => true, 'errors' => []];
} catch (NestedValidationException $e) {
return [
'success' => false,
'errors' => $this->formatNestedErrors($e)
];
}
}
/**
* バリデーション結果のサマリー生成
*/
public function generateValidationSummary(array $data, array $rules): array
{
$summary = [
'total_fields' => count($data),
'validated_fields' => 0,
'failed_fields' => 0,
'errors' => [],
'warnings' => []
];
foreach ($rules as $field => $validator) {
try {
if (isset($data[$field])) {
$validator->assert($data[$field]);
$summary['validated_fields']++;
} else {
$summary['warnings'][] = "フィールド '$field' が見つかりません";
}
} catch (ValidationException $e) {
$summary['failed_fields']++;
$summary['errors'][$field] = $e->getMessage();
}
}
$summary['success_rate'] = round(
($summary['validated_fields'] / $summary['total_fields']) * 100, 2
);
return $summary;
}
}
// 使用例
$validationService = new ValidationService();
$userData = [
'username' => 'john123',
'email' => 'invalid-email',
'password' => '123', // 短すぎる
'age' => 25
];
$result = $validationService->validateUserRegistration($userData);
if (!$result['success']) {
foreach ($result['errors'] as $field => $errors) {
echo "フィールド '$field' のエラー:\n";
foreach ($errors as $error) {
echo " - $error\n";
}
}
}
実用的なベストプラクティス
<?php
use Respect\Validation\Validator as v;
use Respect\Validation\Exceptions\ValidationException;
/**
* 実用的なバリデーションパターン集
*/
class ValidationPatterns
{
/**
* 日本のフォーマットに特化したバリデーター
*/
public static function getJapaneseValidators(): array
{
return [
'zip_code' => v::regex('/^\d{3}-?\d{4}$/'),
'phone' => v::regex('/^0\d{1,4}-?\d{1,4}-?\d{4}$/'),
'mobile' => v::regex('/^0[789]0-?\d{4}-?\d{4}$/'),
'katakana' => v::regex('/^[ァ-ヶー]+$/u'),
'hiragana' => v::regex('/^[ひ-ゞー]+$/u'),
'kanji' => v::regex('/^[一-龯]+$/u'),
'full_width_alnum' => v::regex('/^[0-9A-Za-z]+$/u'),
];
}
/**
* API リクエストバリデーション
*/
public static function validateApiRequest(array $data, string $endpoint): array
{
$validators = [
'users' => [
'POST' => v::key('name', v::stringType()->length(1, 100))
->key('email', v::email())
->key('role', v::in(['admin', 'user', 'moderator'])),
'PUT' => v::key('id', v::intVal()->positive())
->key('name', v::optional(v::stringType()->length(1, 100)))
->key('email', v::optional(v::email()))
],
'products' => [
'POST' => v::key('name', v::stringType()->notEmpty())
->key('price', v::numericVal()->positive())
->key('category_id', v::intVal()->positive()),
'PUT' => v::key('id', v::intVal()->positive())
->keyOptional('name', v::stringType()->notEmpty())
->keyOptional('price', v::numericVal()->positive())
]
];
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
if (!isset($validators[$endpoint][$method])) {
return [
'valid' => false,
'errors' => ['endpoint' => "Unsupported endpoint: $endpoint $method"]
];
}
try {
$validators[$endpoint][$method]->assert($data);
return ['valid' => true, 'errors' => []];
} catch (ValidationException $e) {
return [
'valid' => false,
'errors' => $e->getMessages()
];
}
}
/**
* ファイルアップロードバリデーション
*/
public static function validateFileUpload(array $file, array $options = []): array
{
$defaults = [
'max_size' => 5242880, // 5MB
'allowed_types' => ['image/jpeg', 'image/png', 'image/gif'],
'required' => true
];
$config = array_merge($defaults, $options);
if ($config['required'] && (!isset($file['tmp_name']) || empty($file['tmp_name']))) {
return ['valid' => false, 'errors' => ['ファイルが選択されていません']];
}
if (!$config['required'] && empty($file['tmp_name'])) {
return ['valid' => true, 'errors' => []];
}
$errors = [];
// ファイルサイズチェック
if ($file['size'] > $config['max_size']) {
$maxMB = round($config['max_size'] / 1024 / 1024, 1);
$errors[] = "ファイルサイズは{$maxMB}MB以下にしてください";
}
// MIMEタイプチェック
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
if (!in_array($mimeType, $config['allowed_types'])) {
$allowedStr = implode(', ', $config['allowed_types']);
$errors[] = "許可されていないファイル形式です。許可形式: $allowedStr";
}
// ファイル名チェック
if (!v::stringType()->notEmpty()->length(1, 255)->isValid($file['name'])) {
$errors[] = "ファイル名が無効です";
}
// アップロードエラーチェック
if ($file['error'] !== UPLOAD_ERR_OK) {
$errors[] = "ファイルアップロードエラー: " . $this->getUploadErrorMessage($file['error']);
}
return [
'valid' => empty($errors),
'errors' => $errors,
'file_info' => [
'mime_type' => $mimeType,
'size' => $file['size'],
'name' => $file['name']
]
];
}
private static function getUploadErrorMessage(int $error): string
{
$messages = [
UPLOAD_ERR_INI_SIZE => 'ファイルサイズが上限を超えています',
UPLOAD_ERR_FORM_SIZE => 'フォームのファイルサイズ上限を超えています',
UPLOAD_ERR_PARTIAL => 'ファイルが部分的にしかアップロードされませんでした',
UPLOAD_ERR_NO_FILE => 'ファイルがアップロードされませんでした',
UPLOAD_ERR_NO_TMP_DIR => '一時ディレクトリがありません',
UPLOAD_ERR_CANT_WRITE => 'ディスクへの書き込みに失敗しました',
UPLOAD_ERR_EXTENSION => 'アップロードが拡張機能により停止されました'
];
return $messages[$error] ?? '不明なエラー';
}
/**
* パフォーマンスを考慮したバリデーション
*/
public static function validateLargeDataset(array $items, callable $validator): array
{
$results = [
'processed' => 0,
'valid' => 0,
'invalid' => 0,
'errors' => [],
'performance' => [
'start_time' => microtime(true),
'memory_start' => memory_get_usage()
]
];
foreach ($items as $index => $item) {
try {
if ($validator($item)) {
$results['valid']++;
} else {
$results['invalid']++;
$results['errors'][$index] = 'バリデーション失敗';
}
} catch (Exception $e) {
$results['invalid']++;
$results['errors'][$index] = $e->getMessage();
}
$results['processed']++;
// メモリ使用量監視
if ($results['processed'] % 1000 === 0) {
$currentMemory = memory_get_usage();
if ($currentMemory > 128 * 1024 * 1024) { // 128MB制限
$results['warnings'][] = "メモリ使用量が多すぎます: " . round($currentMemory / 1024 / 1024, 2) . "MB";
break;
}
}
}
$results['performance']['end_time'] = microtime(true);
$results['performance']['duration'] = $results['performance']['end_time'] - $results['performance']['start_time'];
$results['performance']['memory_peak'] = memory_get_peak_usage();
$results['performance']['items_per_second'] = round($results['processed'] / $results['performance']['duration'], 2);
return $results;
}
}
// 使用例
$patterns = new ValidationPatterns();
// 日本語バリデーションの例
$japaneseValidators = ValidationPatterns::getJapaneseValidators();
$zipCode = '123-4567';
if ($japaneseValidators['zip_code']->isValid($zipCode)) {
echo "有効な郵便番号です\n";
}
// APIリクエストバリデーション
$apiData = [
'name' => 'John Doe',
'email' => '[email protected]',
'role' => 'admin'
];
$apiResult = ValidationPatterns::validateApiRequest($apiData, 'users');
if ($apiResult['valid']) {
echo "APIリクエストは有効です\n";
}
// ファイルアップロードバリデーション
$uploadedFile = [
'name' => 'photo.jpg',
'type' => 'image/jpeg',
'tmp_name' => '/tmp/phpXXXXXX',
'error' => UPLOAD_ERR_OK,
'size' => 1048576 // 1MB
];
$fileResult = ValidationPatterns::validateFileUpload($uploadedFile, [
'max_size' => 2097152, // 2MB
'allowed_types' => ['image/jpeg', 'image/png']
]);
if ($fileResult['valid']) {
echo "ファイルは有効です\n";
} else {
foreach ($fileResult['errors'] as $error) {
echo "エラー: $error\n";
}
}