ポイント
- フォーム値の事前加工 は使えない。なぜなら加工前の値もバリデーションしたいため。
- クロージャの使用 – カスタムバリデーションルールのメソッドを定義 し、その中でフォーム値を加工してバリデーションを行う。
- ただし、
rules
メソッド内に 2 つ以上のクロージャのルールが存在するとテスト時にどのクロージャがテストに該当するのかわからなくなる。そのため試してはいないが ルールオブジェクトの使用 – カスタムバリデーションルールのメソッドを定義 をした方が良いと思われる。 - 今回はクロージャの使用を押し通したため、テストではエラーメッセージを確認することでどのルールで不正となったかを確かめている。
- ただし、
- カスタムバリデーションルールのメソッド内では
'required'
,'email:rfc,spoof'
といったバリデーションルールを宣言できない。とはいえ該当するバリデーションロジックを書くのは車輪の再開発となり、おかしい。探すと、Illuminate\Validation\Concerns\ValidatesAttributes
トレイトに各バリデーションルールの実装となっているのでこれをuse
して利用する。
今回取り上げる入力フォームの仕様
cc_emails という入力フォームを考えます。 名前の通り、 email の CC 欄に当てはまる値です。 複数該当する場合があり、 最大 10 個のemail を半角カンマ "," 区切りでつなげたものとします。
行いたいバリデーション
- null 可能であること
- 最大桁数は 2550 (email 1つあたり 255 文字あればよかろうと考えた) であること
- 半角カンマで分割した要素数は最大 10 個であること
- 半角カンマで分割した各要素は email のフォーマットであること
作成したフォームリクエストクラス
rule
メソッドの cc_emails
の部分が本ページで取り上げている箇所です。
max:2550
でフォームに入力された文字列全体を評価する一方で、続くクロージャのルールでフォームに入力された文字列をカンマで区切り、それぞれを評価する、という 2 重のバリデーションを行っているのがポイントです。
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\Concerns\ValidatesAttributes;
use Illuminate\Validation\Rule;
class UserSaveRequest extends FormRequest
{
use ValidatesAttributes;
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return false;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'name' => 'required|max:255',
'email' => [
'required',
'max:255',
'email:rfc,spoof',
Rule::unique('users')->ignore(Auth::id()),
],
'cc_emails' => [
'nullable',
'max:2550',
function ($attribute, $value, $fail) {
$values = explode(',', $value);
if (count($values) > 10) {
$fail(trans($attribute . ' に指定できる email は最大 10 個です。'));
}
},
function ($attribute, $value, $fail) {
$values = explode(',', $value);
$parameters = ['rfc', 'spoof'];
$invalidEmails = collect($values)
->filter(function ($value) use ($attribute, $parameters) {
return !$this->validateEmail($attribute, $value, $parameters);
})
->implode(', ');
if ($invalidEmails) {
$fail($attribute . ' に指定した次のメールアドレスは不正です。: ' . $invalidEmails);
}
},
],
'zip' => 'nullable|regex:/^\d{7}$/',
];
}
/**
* バリデーションエラーのカスタム属性の取得
*
* @return array
*/
public function attributes()
{
return [
'cc_emails' => 'CCメールアドレス',
];
}
}
cc_emailsのバリデーションを確認するためのテスト抜粋
いまいちだなあ、と思っている点があります。
- cc_emailsの最大email数エラーとなること
- cc_emailsの中にメールアドレスとして不正なemailが存在するエラーとなること
のテストで、エラーメッセージを確認したくなかったのですけれども、せざるをえなかった点を改善したいです。
上記2つのテストはいずれもルールをクロージャで定義しているために、 $validator->failed()
で確かめたいルールの内容がどちらも 'cc_emails' => ['Illuminate\Validation\ClosureValidationRule' => [],],
となってしまいどちらのルールで失敗となったのかわからないのです。そのため、エラーメッセージでどのルールで失敗したのかをさらに確認するようにしました。
$validator->failed()
で失敗したルールを確かめられるように、 ルールオブジェクトの使用 – カスタムバリデーションルールのメソッドを定義 でリファクタリングできるのではないかと考えています。
<?php
namespace Tests\Feature;
use App\Http\Requests\UserSaveRequest;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Validator;
use Tests\TestCase;
class UserSaveRequestTest extends TestCase
{
use RefreshDatabase;
/**
* @test
* @return void
*/
public function cc_emailsの桁数エラーとなること(): void
{
$data = [
'cc_emails' => str_repeat('a', 2539) . '@example.com',
'name' => '名前',
'email' => 'email@example.com',
];
$request = new UserSaveRequest();
$rules = $request->rules();
$validator = Validator::make($data, $rules);
$result = $validator->passes();
$this->assertFalse($result);
$expectedFailed = [
'cc_emails' => ['Max' => [2550],],
];
$this->assertEquals($expectedFailed, $validator->failed());
}
/**
* @test
* @return void
*/
public function cc_emailsの最大email数エラーとなること(): void
{
$data = [
'cc_emails' => '1@example.com,2@example.com,3@example.com,4@example.com,5@example.com,6@example.com,7@example.com,8@example.com,9@example.com,10@example.com,11@example.com',
'name' => '名前',
'email' => 'email@example.com',
];
$request = new UserSaveRequest();
$rules = $request->rules();
$validator = Validator::make($data, $rules);
$result = $validator->passes();
$this->assertFalse($result);
$expectedFailed = [
'cc_emails' => ['Illuminate\Validation\ClosureValidationRule' => [],],
];
$this->assertEquals($expectedFailed, $validator->failed());
$actualMessage = $validator->errors()->all()[0];
$this->assertEquals('cc_emails に指定できる email は最大 10 個です。', $actualMessage);
}
/**
* @test
* @return void
*/
public function cc_emailsの中にメールアドレスとして不正なemailが存在するエラーとなること(): void
{
$data = [
'cc_emails' => '1example.com,2@example.com,3atexample.com',
'name' => '名前',
'email' => 'email@example.com',
];
$request = new UserSaveRequest();
$rules = $request->rules();
$validator = Validator::make($data, $rules);
$result = $validator->passes();
$this->assertFalse($result);
$expectedFailed = [
'cc_emails' => ['Illuminate\Validation\ClosureValidationRule' => [],],
];
$this->assertEquals($expectedFailed, $validator->failed());
$actualMessage = $validator->errors()->all()[0];
$this->assertEquals('cc_emails に指定した次のメールアドレスは不正です。: 1example.com, 3atexample.com', $actualMessage);
}
}
おわりに
本投稿は、 Laravel のバリデーションクラスであるフォームリクエストをユニットテストするためのノウハウ – oki2a24 の発展形となります。
Laravel 7 において入力値そのものと入力値を分割した配列の両方を一度にバリデーションするための自分なりにたどり着いたやり方を記しました。
今回はルールをクロージャで記述しましたけれども、それによる課題も見えてきました。今後これを改善できればと思います。
以上です。