커스텀 검증 규칙이란
Laravel에는 풍부한 내장 검증 규칙이 준비되어 있지만, 애플리케이션 고유의 검증 로직이 필요한 장면도 있습니다. 커스텀 검증 규칙을 사용하면, 재이용 가능한 검증 로직을 클래스나 클로저로서 정의하고, 표준 규칙과 같이 사용할 수 있습니다.
커스텀 규칙을 정의하는 방법은 주로 2가지입니다.
룰 오브젝트 — 재이용성이 높고, 테스트하기 쉬움
클로저 — 한 번만 사용하는 심플한 규칙에 적합
룰 오브젝트
룰 클래스를 생성한다
make:rule Artisan 커맨드로 새로운 룰 클래스를 생성합니다. 생성된 클래스는 app/Rules 디렉토리에 배치됩니다.
php artisan make:rule Uppercase
ValidationRule 인터페이스를 구현한다
생성된 클래스에 validate 메서드를 구현합니다. 이 메서드는 검증 실패 시에 $fail 클로저를 호출합니다.
<? php
namespace App\Rules ;
use Closure ;
use Illuminate\Contracts\Validation\ ValidationRule ;
class Uppercase implements ValidationRule
{
/**
* バリデーションルールを実行する
*/
public function validate ( string $attribute , mixed $value , Closure $fail ) : void
{
if ( strtoupper ( $value ) !== $value ) {
$fail ( 'The :attribute must be uppercase.' );
}
}
}
$fail 클로저에 전달하는 문자열에는 :attribute 플레이스홀더를 사용할 수 있습니다. Laravel이 필드명으로 치환합니다.
룰 오브젝트를 적용한다
룰 오브젝트의 인스턴스를 검증 배열에 전달합니다.
use App\Rules\ Uppercase ;
$request -> validate ([
'name' => [ 'required' , 'string' , new Uppercase ],
]);
폼 리퀘스트의 rules() 메서드에서도 마찬가지로 사용할 수 있습니다.
public function rules () : array
{
return [
'name' => [ 'required' , 'string' , new Uppercase ],
];
}
번역 키를 사용한 에러 메시지
에러 메시지를 하드코드하는 대신, 번역 키를 사용할 수도 있습니다.
public function validate ( string $attribute , mixed $value , Closure $fail ) : void
{
if ( strtoupper ( $value ) !== $value ) {
$fail ( 'validation.uppercase' ) -> translate ();
}
}
번역 파일 lang/ja/validation.php에 메시지를 추가합니다.
return [
'uppercase' => ':attributeは大文字で入力してください。' ,
// ...
];
여러 에러 메시지를 추가한다
하나의 필드에 여러 에러를 보고하려면, $fail을 여러 번 호출합니다.
public function validate ( string $attribute , mixed $value , Closure $fail ) : void
{
if ( ! is_string ( $value )) {
$fail ( 'The :attribute must be a string.' );
return ;
}
if ( strlen ( $value ) < 8 ) {
$fail ( 'The :attribute must be at least 8 characters.' );
}
if ( ! preg_match ( '/[A-Z]/' , $value )) {
$fail ( 'The :attribute must contain at least one uppercase letter.' );
}
}
클로저 기반의 규칙
애플리케이션 내에서 한 번만 사용하는 심플한 규칙은, 클래스를 만들지 않고 클로저로 정의할 수 있습니다.
use Illuminate\Support\Facades\ Validator ;
use Closure ;
$validator = Validator :: make ( $request -> all (), [
'title' => [
'required' ,
'max:255' ,
function ( string $attribute , mixed $value , Closure $fail ) {
if ( $value === 'foo' ) {
$fail ( "The { $attribute } is invalid." );
}
},
],
]);
클로저 규칙은 인라인으로 정의하므로 간단하지만, 재이용이나 독립된 테스트가 어려우므로, 여러 곳에서 사용하는 로직은 룰 오브젝트로 분리하는 것을 추천합니다.
Implicit 규칙(값이 비어 있어도 실행한다)
기본으로는, 필드가 비거나 존재하지 않는 경우, 커스텀 규칙은 실행되지 않습니다. 빈 값에 대해서도 규칙을 실행하고 싶은 경우에는, --implicit 옵션을 붙여서 클래스를 생성합니다.
php artisan make:rule Uppercase --implicit
생성된 클래스는 ImplicitRule 인터페이스를 구현하고 있습니다. 이 인터페이스 자체는 추가의 메서드를 가지지 않고, Laravel에의 시그널로서 기능합니다.
<? php
namespace App\Rules ;
use Closure ;
use Illuminate\Contracts\Validation\ ImplicitRule ;
use Illuminate\Contracts\Validation\ ValidationRule ;
class RequiredIfJapanese implements ValidationRule , ImplicitRule
{
public function validate ( string $attribute , mixed $value , Closure $fail ) : void
{
if ( empty ( $value )) {
$fail ( 'The :attribute is required.' );
return ;
}
if ( ! preg_match ( '/ ^ [\p{Hiragana}\p{Katakana}\p{Han}ー] +$ /u' , $value )) {
$fail ( 'The :attribute must contain only Japanese characters.' );
}
}
}
ImplicitRule은 “속성이 필수이다”라는 것을 Laravel에 나타낼 뿐입니다. 값이 비어 있을 때에 실제로 검증을 실패시킬지 여부는, validate 메서드의 구현 나름입니다.
데이터 접근
DataAwareRule — 폼 전체의 데이터에 접근한다
다른 필드의 값에 기반하여 검증하고 싶은 경우에는, DataAwareRule 인터페이스를 구현합니다. setData 메서드가 검증 개시 전에 자동으로 호출됩니다.
<? php
namespace App\Rules ;
use Closure ;
use Illuminate\Contracts\Validation\ DataAwareRule ;
use Illuminate\Contracts\Validation\ ValidationRule ;
use Illuminate\Support\Facades\ DB ;
class UniqueForTenant implements DataAwareRule , ValidationRule
{
/**
* バリデーション対象の全データ
*
* @var array < string , mixed>
*/
protected array $data = [];
public function __construct (
protected string $table ,
protected string $column = 'value' ,
) {}
/**
* バリデーションデータをセットする
*
* @param array < string , mixed> $data
*/
public function setData ( array $data ) : static
{
$this -> data = $data ;
return $this ;
}
public function validate ( string $attribute , mixed $value , Closure $fail ) : void
{
$tenantId = $this -> data [ 'tenant_id' ] ?? null ;
$exists = DB :: table ( $this -> table )
-> where ( 'tenant_id' , $tenantId )
-> where ( $this -> column , $value )
-> exists ();
if ( $exists ) {
$fail ( 'The :attribute has already been taken for this tenant.' );
}
}
}
사용 예:
$request -> validate ([
'tenant_id' => 'required|integer' ,
'email' => [ 'required' , 'email' , new UniqueForTenant ( 'users' , 'email' )],
]);
ValidatorAwareRule — 검증기 인스턴스에 접근한다
검증기가 가진 모든 정보(실패한 규칙, 커스텀 메시지 등)에 접근하려면, ValidatorAwareRule 인터페이스를 구현합니다.
<? php
namespace App\Rules ;
use Closure ;
use Illuminate\Contracts\Validation\ ValidationRule ;
use Illuminate\Contracts\Validation\ ValidatorAwareRule ;
use Illuminate\Validation\ Validator ;
class ConditionalFormat implements ValidationRule , ValidatorAwareRule
{
protected Validator $validator ;
public function setValidator ( Validator $validator ) : static
{
$this -> validator = $validator ;
return $this ;
}
public function validate ( string $attribute , mixed $value , Closure $fail ) : void
{
// 他のフィールドがすでにバリデーション失敗している場合はスキップ
if ( $this -> validator -> errors () -> has ( 'type' )) {
return ;
}
$type = $this -> validator -> getData ()[ 'type' ] ?? null ;
if ( $type === 'phone' && ! preg_match ( '/ ^ \+ ?[0-9\-\s] +$ /' , $value )) {
$fail ( 'The :attribute must be a valid phone number.' );
}
if ( $type === 'email' && ! filter_var ( $value , FILTER_VALIDATE_EMAIL )) {
$fail ( 'The :attribute must be a valid email address.' );
}
}
}
실전적인 유스케이스
일본어 문자 체크
전각·반각의 문자 종류를 검증하는 규칙입니다.
php artisan make:rule JapaneseOnly
<? php
namespace App\Rules ;
use Closure ;
use Illuminate\Contracts\Validation\ ValidationRule ;
class JapaneseOnly implements ValidationRule
{
public function validate ( string $attribute , mixed $value , Closure $fail ) : void
{
// ひらがな・カタカナ・漢字・長音符のみ許可
if ( ! preg_match ( '/ ^ [\p{Hiragana}\p{Katakana}\p{Han}ー\s] +$ /u' , $value )) {
$fail ( ':attributeは日本語(ひらがな・カタカナ・漢字)で入力してください。' );
}
}
}
$request -> validate ([
'name_kana' => [ 'required' , 'string' , new JapaneseOnly ],
]);
전화번호 포맷 검증
일본의 전화번호 형식을 검증하는 규칙입니다.
php artisan make:rule JapanesePhone
<? php
namespace App\Rules ;
use Closure ;
use Illuminate\Contracts\Validation\ ValidationRule ;
class JapanesePhone implements ValidationRule
{
public function validate ( string $attribute , mixed $value , Closure $fail ) : void
{
// ハイフンあり・なし両対応、国際形式も許可
$normalized = preg_replace ( '/[\s\-\(\)]/' , '' , $value );
$patterns = [
'/ ^ 0 \d {9,10} $ /' , // 一般的な固定・携帯電話番号
'/ ^ \+ 81 \d {9,10} $ /' , // 国際形式
];
foreach ( $patterns as $pattern ) {
if ( preg_match ( $pattern , $normalized )) {
return ;
}
}
$fail ( ':attributeは有効な電話番号の形式で入力してください。' );
}
}
테넌트 ID 딸린 유니크 제약
멀티 테넌트 애플리케이션에서 자주 있는, 테넌트 스코프 내에서의 유니크 제약입니다.
룰 클래스를 작성한다
php artisan make:rule TenantUnique
<? php
namespace App\Rules ;
use Closure ;
use Illuminate\Contracts\Validation\ DataAwareRule ;
use Illuminate\Contracts\Validation\ ValidationRule ;
use Illuminate\Support\Facades\ DB ;
class TenantUnique implements DataAwareRule , ValidationRule
{
protected array $data = [];
public function __construct (
protected string $table ,
protected string $column ,
protected ? int $ignoreId = null ,
) {}
public function setData ( array $data ) : static
{
$this -> data = $data ;
return $this ;
}
public function validate ( string $attribute , mixed $value , Closure $fail ) : void
{
$tenantId = $this -> data [ 'tenant_id' ] ?? null ;
$query = DB :: table ( $this -> table )
-> where ( 'tenant_id' , $tenantId )
-> where ( $this -> column , $value );
if ( $this -> ignoreId !== null ) {
$query -> where ( 'id' , '!=' , $this -> ignoreId );
}
if ( $query -> exists ()) {
$fail ( ':attributeはすでに使用されています。' );
}
}
}
폼 리퀘스트에서 사용한다
<? php
namespace App\Http\Requests ;
use App\Rules\ TenantUnique ;
use Illuminate\Foundation\Http\ FormRequest ;
class CreateProjectRequest extends FormRequest
{
public function rules () : array
{
return [
'tenant_id' => 'required|integer|exists:tenants,id' ,
'name' => [
'required' ,
'string' ,
'max:100' ,
new TenantUnique ( 'projects' , 'name' ),
],
];
}
public function authorize () : bool
{
return true ;
}
}
갱신 시에는 ID를 제외한다
기존 레코드를 갱신하는 경우에는, 자기 자신의 ID를 제외하여 중복 체크합니다. class UpdateProjectRequest extends FormRequest
{
public function rules () : array
{
$projectId = $this -> route ( 'project' );
return [
'tenant_id' => 'required|integer|exists:tenants,id' ,
'name' => [
'required' ,
'string' ,
'max:100' ,
new TenantUnique ( 'projects' , 'name' , ignoreId : $projectId ),
],
];
}
}
서비스 프로바이더에서의 룰 등록
Validator::extend()로 룰을 추가한다
Validator::extend()를 사용하면, 문자열 형식('rule_name')으로 커스텀 룰을 사용할 수 있게 됩니다. AppServiceProvider의 boot() 메서드에서 등록합니다.
<? php
namespace App\Providers ;
use Illuminate\Support\Facades\ Validator ;
use Illuminate\Support\ ServiceProvider ;
use Illuminate\Validation\ Validator as ValidatorInstance ;
class AppServiceProvider extends ServiceProvider
{
public function boot () : void
{
Validator :: extend ( 'japanese_only' , function ( string $attribute , mixed $value , array $parameters , ValidatorInstance $validator ) : bool {
return preg_match ( '/ ^ [\p{Hiragana}\p{Katakana}\p{Han}ー\s] +$ /u' , $value ) === 1 ;
});
Validator :: replacer ( 'japanese_only' , function ( string $message , string $attribute ) : string {
return str_replace ( ':attribute' , $attribute , ':attributeは日本語で入力してください。' );
});
}
}
Validator::extend()로 등록한 룰은 문자열로서 지정할 수 있습니다.
$request -> validate ([
'name_kana' => 'required|japanese_only' ,
]);
Validator::extend()는 룰 오브젝트보다 오래된 등록 방식입니다. 신규 개발에서는 ValidationRule 인터페이스를 구현한 룰 오브젝트를 사용하는 것을 권장합니다.
Rule 클래스에 스태틱 메서드로서 추가한다
Rule 파사드에 매크로를 추가함으로써, Rule::myRule()과 같은 유창한 API(Fluent API)를 제공할 수 있습니다.
use Illuminate\Validation\ Rule ;
Rule :: macro ( 'tenantUnique' , function ( string $table , string $column , ? int $ignoreId = null ) {
return new \App\Rules\ TenantUnique ( $table , $column , $ignoreId );
});
// 使用例
$request -> validate ([
'name' => [ 'required' , Rule :: tenantUnique ( 'projects' , 'name' )],
]);
내부 구현의 상세
Illuminate\Validation\Validator가 커스텀 룰을 호출하는 구조를 봅시다.
검증기 내에서는 validateAttribute()가 각 필드를 처리합니다. 룰이 ValidationRule 인터페이스를 구현하고 있는 경우, validateUsingCustomRule() 메서드가 호출됩니다.
// Illuminate\Validation\Validator::validateUsingCustomRule() の簡略版
protected function validateUsingCustomRule ( $attribute , $value , $rule )
{
// DataAwareRule の場合、全データを注入する
if ( $rule instanceof DataAwareRule ) {
$rule -> setData ( $this -> getData ());
}
// ValidatorAwareRule の場合、バリデーター自身を注入する
if ( $rule instanceof ValidatorAwareRule ) {
$rule -> setValidator ( $this );
}
// validate() を呼び出す
$rule -> validate ( $attribute , $value , function ( $message , $translate = false ) use ( $attribute , $rule ) {
// $fail クロージャ — エラーメッセージを追加する
$this -> errors () -> add ( $attribute , $this -> makeReplacements (
$message ,
$attribute ,
get_class ( $rule ),
[], // 追加のプレースホルダー置換(例: ['min' => '8'] など)
));
});
}
ImplicitRule의 처리는 isImplicit() 메서드로 판단되며, 값이 비어 있는 경우에도 룰의 체크를 실행합니다.
// 空の値に対してルールを実行するかどうかを判断する
protected function isImplicit ( $rule ) : bool
{
return $rule instanceof ImplicitRule
|| in_array ( $rule , $this -> implicitRules );
}
DataAwareRule과 ValidatorAwareRule을 동시에 구현할 수도 있습니다. 양 인터페이스를 가진 클래스에서는, 양쪽의 주입이 이루어집니다.
관련 페이지
검증(입문) 컨트롤러나 폼 리퀘스트에서의 표준적인 검증 방법을 확인합니다.