Laravel 9.x 自作バリデーションルールで多言語対応をする

始めに

Laravelのマニュアルには、Ruleクラスを作成してカスタムバリデーションルールが作成できることは記載されている。

しかし、言語ファイルを利用した多言語化対応については記載されていないかった。

Ruleファイルでも多言語化し、標準のバリデーションと同様の構造を持たせるように対応をする。

マニュアル通りにルールクラスを作成する

まずはおさらいがてら、通常の流れで作成を行う。

artsanコマンドで作成。

php artisan make:rule MySpecialRule

このコマンドにより、app/Rules/MySpecialRule.phpが作成される。

app/Rules/MySpecialRule.php

namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;

class MySpecialRule implements Rule
{
    /**
     * Create a new rule instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Determine if the validation rule passes.
     *
     * @param  string  $attribute
     * @param  mixed  $value
     * @return bool
     */
    public function passes($attribute, $value)
    {
        //
    }

    /**
     * Get the validation error message.
     *
     * @return string
     */
    public function message()
    {
        return 'The validation error message.';
    }
}

処理を追加する

今回はエラーメッセージに注目するため、常にバリデーションが失敗するだけのものを作成。

passesメソッドに以下のように記述する

public function passes($attribute, $value)
{
    // 常に偽を返す
    return false;
}

機能テストの作成

わざわざControllerを用意したりリクエストするのも手間なので、artisanコマンドでテストを作成していく。

php artisan make:test MySpecialRuleTest

このコマンドによりtests/Feature/SpecialRuleTest.phpが作成される。

tests/Feature/SpecialRuleTest.php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;

class MySpecialRuleTest extends TestCase
{
    /**
     * A basic feature test example.
     *
     * @return void
     */
    public function test_example()
    {
        $response = $this->get('/');

        $response->assertStatus(200);
    }
}

test_exampleメソッドを書き換えて該当のルールを確認していく。

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;

// コントローラやモデルで使ういつものやつ
use Illuminate\Support\Facades\Validator;
use App\Rules\MySpecialRule;

class MySpecialRuleTest extends TestCase
{
    /**
     * A basic feature test example.
     *
     * @return void
     */
    public function test_example()
    {
        $validator = Validator::make(['input_name' => 'input_value'], [
            // 新規作成のルールはインスタンスをルール配列に渡す
            'input_name' => [new MySpecialRule()],
        ]);
        
        // わざとテストが失敗するようにassertする。
        // 失敗すると、第二引数に指定した文字列が画面に表示される
        $this->assertFalse($validator->fails(), print_r($validator->errors()->get('input_name'),true));
    }
}

テストを実行する

php artisan test tests/Feature/MySpecialRuleTest.php

結果:

 FAIL  Tests\Feature\MySpecialRuleTest
  ⨯ example

  ---

  • Tests\Feature\MySpecialRuleTest > example
  Array
  (
      [0] => The validation error message.
  )
  
  Failed asserting that true is false.

  at tests/Feature/MySpecialRuleTest.php:26
     22▕             // 新規作成のルールはインスタンスをルール配列に渡す
     23▕             'input_name' => [new MySpecialRule()],
     24▕         ]);
     25▕ 
  ➜  26▕         $this->assertFalse($validator->fails(), print_r($validator->errors()->get('input_name'),true));
     27▕     }
     28▕ }
     29▕ 


  Tests:  1 failed
  Time:   0.08s

作成したルール通り、エラーメッセージが表示されている。

しかし、このままでは、翻訳がされない。

翻訳に対応する

1. 翻訳ファイルを準備する

Laravelの標準バリデータの翻訳は、lang/en/validation.phpに集約している。

ファイルの内容は、連想配列となっており、以下のようにキーを追加する。

lang/en/validation.php

return [
    // (省略...)

    'my_special' => 'The ":value" entered for :attribute is invalid.',

    // (省略...)
];

また、日本語に対応するため、上記ファイルをlang/ja/validation.phpとしてコピーし、 以下のように書き換える

lang/ja/validation.php

return [
    // (省略...)

    'my_special' => ':attribute に入力された ":value" は無効です。',

    // (省略...)
];

2. Ruleクラスを修正する

現状のRuleクラスでは、翻訳が出来ない。

Laravelのバリデータインスタンスを読み込ませることで翻訳を行えるようにする。

app/Rules/MySpecialRule.php

namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;
use Illuminate\Contracts\Validation\ValidatorAwareRule;

class MySpecialRule implements Rule, ValidatorAwareRule // インターフェースを追加
{
    /**
     * バリデータインスタンス
     *
     * @var \Illuminate\Validation\Validator
     */
    protected $validator; // 現用バリデータを格納するメンバ変数追加

    /**
     * Laravelに返すメッセージ
     *
     * @var string
     */
    protected $message;

    /**
     * Create a new rule instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * 現用バリデータのセット
     *
     * @param  \Illuminate\Validation\Validator  $validator
     * @return $this
     */
    public function setValidator($validator) // インターフェースの仕様を実装
    {
        $this->validator = $validator;

        return $this;
    }

    /**
     * Determine if the validation rule passes.
     *
     * @param  string  $attribute
     * @param  mixed  $value
     * @return bool
     */
    public function passes($attribute, $value)
    {
        // 現用のバリデータの翻訳機から、メッセージを取得する
        // {翻訳ファイル名}.{配列キー} <= validation.my_special
        $message = $this->validator->getTranslator()->get('validation.my_special');

        // 自分で追加したプレースホルダーについては以下のように単純置換する
        // $validator->replacer()というものもあるが、やることはあまり変わらないはず。。。
        $this->message = strtr($message, [':value' => $value]);

        return false;
    }

    /**
     * Get the validation error message.
     *
     * @return string
     */
    public function message()
    {
        return $this->message; // passesメソッドで作成したメッセージを返すようにする
    }
}

再度テストを行う

上述したテストを以下のように修正する

tests/Feature/MySpecialRuleTest.php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;

use Illuminate\Support\Facades\Validator;
use App\Rules\MySpecialRule;

class MySpecialRuleTest extends TestCase
{
    /**
     * A basic feature test example.
     *
     * @return void
     */
    public function test_example()
    {
        // ロケールを英語に変更する
        $this->app->setLocale('en');

        $validator = Validator::make(['input_name' => 'input_value'], [
            // 新規作成のルールはインスタンスをルール配列に渡す
            'input_name' => [new MySpecialRule()],
        ]);

        $this->assertFalse($validator->fails(), print_r($validator->errors()->get('input_name'),true));
    }

    public function test_example2()
    {
        // ロケールを日本語に変更する
        $this->app->setLocale('ja');

        $validator = Validator::make(['input_name' => 'input_value'], [
            // 新規作成のルールはインスタンスをルール配列に渡す
            'input_name' => [new MySpecialRule()],
        ]);

        $this->assertFalse($validator->fails(), print_r($validator->errors()->get('input_name'),true));
    }
}

以下のように実行

php artisan test tests/Feature/MySpecialRuleTest.php

結果:

 FAIL  Tests\Feature\MySpecialRuleTest
  ⨯ example
  ⨯ example2

  ---

  • Tests\Feature\MySpecialRuleTest > example
  Array
  (
      [0] => The "input_value" entered for input name is invalid.
  )
  
  Failed asserting that true is false.

  at tests/Feature/MySpecialRuleTest.php:27
     23▕             // 新規作成のルールはインスタンスをルール配列に渡す
     24▕             'input_name' => [new MySpecialRule()],
     25▕         ]);
     26▕ 
  ➜  27▕         $this->assertFalse($validator->fails(), print_r($validator->errors()->get('input_name'),true));
     28▕     }
     29▕ 
     30▕     public function test_example2()
     31▕     {

  • Tests\Feature\MySpecialRuleTest > example2
  Array
  (
      [0] => input name に入力された "input_value" は無効です。
  )
  
  Failed asserting that true is false.

  at tests/Feature/MySpecialRuleTest.php:38
     34▕             // 新規作成のルールはインスタンスをルール配列に渡す
     35▕             'input_name' => [new MySpecialRule()],
     36▕         ]);
     37▕ 
  ➜  38▕         $this->assertFalse($validator->fails(), print_r($validator->errors()->get('input_name'),true));
     39▕     }
     40▕ }
     41▕ 


  Tests:  2 failed
  Time:   0.09s

翻訳に対応できた。

感想

Laravelは、柔軟にカスタマイズ可能であるが、細かいことを調整しようとすると、Laravelのマニュアルだけだと足りない。

そのうえ、マニュアルに記載の拡張方法は、既存実装には適用しておらず、全然違う構造で成り立っているので、結果を出すのに時間を要した。

尚、今回作成の参考にしたのは、以下に配置されているPasswordクラス。

vendor/laravel/framework/src/Illuminate/Validation/Rules/Password.php

該当クラスでは、もう少し複雑なバリデーションルールにも対応しているため、かなり参考になった。

以上。