LaravelでValidationを実装した話


# 概要

- バリデーションエラーログのフォーマットをカスタマイズ

# バージョンとか

- Laravel 5.6
- PHP7.2

# シナリオ

Webアプリケーションを作成する案件で、フレームワークや言語等は問わない。
今回は、 PHPの `Laravel` で開発することにしました。

社内では `Golang` か `NodeJs` で行うのがデフォルトっぽく、
特に `Golang` をメインで開発を行っているみたいです。

## 管理方法
```
管理管轄
 |
 +- DockerContainer(App) Golangだったり
 |
 +- DockerContainer(App) Nodejsだったり
 |
 +- DockerContainer(本App)
    |
    +- src
       |
       [Laravel Project]
```

Dockerでまとめて管理する方法で、エラーログをAPPごとに標準出力で出すことにより、
管理側で一括でログを確認できるような構造になっています。

## フォーム内容

`form-data` 形式でフォームデータをPOSTでリクエストします。

```
name        = (必須,英数字+[.-_],255文字以内)
id          = (必須,英数字20~24桁) 
recorded_at = (必須,タイムスタンプ数値10桁)
```

## バリデーションフォーマット

バリデーションのフォーマットは仕様書により固定されており、
以下がバリデーションエラー時のフォーマットである。

**`Key: {keyname}, Value: {values}, Violation: {violation}`**

## API

バリデーション時のAPIレスポンスは基本`400エラー`とします
エラーレスポンスはJSONで返します。
バリデーションエラーではないものは、メッセージをそのまま返します。
(以下の表はすべてサンプルで作成した仕様です)

|エラーコード|フォーマット|内容|
|:---:|:---|:----|:--:|
|A0001|Key:, Value:, Violation:|バリデーションエラー時|
|A0005|File Extension Error|ファイル拡張子エラー時|

```json
{
	"errors":[
		{
			"code":"A0001",
			"message":"Key: {keyname}, Value: {values}, Violation: {violation}"
		},
		{
			"code":"A0005",
			"message":"File Extension Error"
		},
	]
}
```

# 本題

Laravelでバリデーションエラーが出た場合、自動的にメッセージが作成されますが、
今回は指定されたメッセージに変更しないといけないので、変更していきます。

## 基本

RouteやControllerなどの説明は省きます。

基本的なバリデーションはこちらのコードで行なえます

```php
$validator = Validator::make($request->all(), [
            "name"        => "required|max:255|not_regex:/[^a-zA-Z0-9._-]/",
            "id"          => "required|not_regex:/[^a-zA-Z0-9]/",
            "recorded_at" => "required|digits:10"
        ]);
```

エラーハンドリングをする際は、以下のコードでハンドリングが行えます。

```php
if ($validator->fails()) {
    foreach ($validator->errors()->all() as $error_text) {
     	   // $error_textにエラー内容が入る
    }
}
```

## カスタムエラーメッセージ

カスタムエラーメッセージを使用したい場合は、`Validator::make()`の第三引数に配列で指定できます。
指定の方法はたくさんありますので公式を御覧ください。
https://laravel.com/docs/5.6/validation#custom-error-messages

```php

$err_msg = "Key: :attribute, Value: :values, Violation: ";
$custom_error_msg = [
    "required"            => $err_msg . "required",
    "max"                 => $err_msg . "maximum length",
    "alpha_num"           => $err_msg . "alphabet&number",
    "digits"              => $err_msg . "number length",
    "device_id.not_regex" => $err_msg . "alphabet&number",
    "name.not_regex"      => $err_msg . "alphabet&number+[._-]"
];

$validator = Validator::make($request->all(), [
    "name"        => "required|max:255|not_regex:/[^a-zA-Z0-9._-]/",
    "device_id"   => "required|not_regex:/[^a-zA-Z0-9]/",
    "recorded_at" => "required|digits:10"
    ], $custom_error_msg);

```

ですが、これだとエラー出力時にこう出力されます。

```json
{
	"errors":[
		{
			"code":"A0001",
			"message":"Key: name, Value: :values, Violation: alphabet&number+[._-]"
		},
``` 

**`:values`**が置換されていません。
LaravelのValidatorは標準で `:attribute` をKey名に置換されて出力されるのですが、
それ以外のプレースホルダーは自分で実装しなければいけません。

## カスタムプレースホルダー

まず `Illuminate\Validation\Validator` を継承したサービスモジュールを作成します。

```php:app/Service/TestValidator.php

<?php
namespace App\Service;
use Illuminate\Validation\Validator;

class TestValidator extends Validator
{
    public function __construct($translator, $data, $rules, $messages = [])
    {
        parent::__construct($translator, $data, $rules, $messages);

        // ここに記述
    }
}
```

次に、Replacerを実装します。

```php:app/Service/TestValidator.php

<?php
namespace App\Service;

use Illuminate\Validation\Validator;

class TestValidator extends Validator
{
    public function __construct($translator, $data, $rules, $messages = [])
    {
        parent::__construct($translator, $data, $rules, $messages);
		
		 // $message    = カスタムエラーメッセージの内容
		 // $attribute  = エラーが発生したキー名
		 // $rule       = バリデーションのルール 例:required
		 // $parameters = 場合によって変わるが、digitsであれば文字数などが配列で入る
		 //
		 // クロージャなので、$dataを値渡ししてあげる。
		 // $data = リクエストデータが入る
        $replacer = function($message, $attribute, $rule, $parameters) use ($data) {
            
            // $message 内の :values を $data[$attribute] に置換する
            return str_replace(':values', $data[$attribute], $message);
        };

		 // 上記のクロージャを適応するルール
        $this->addReplacers([
            "required"=>$replacer,
            "max" => $replacer,
            "alpha_num" => $replacer,
            "digits" => $replacer,
            "mimes" => $replacer,
            "file" => $replacer,
            "not_regex" => $replacer
        ]);

    }
}
```

`$this->addReplacers()` により、置換が行われます。

## ValidatorServiceProviderを作成

このままでは`Validator::make`を実行しても上記の `TestValidator` を読み取ってくれないので、
`ValidatorServiceProvider`を作成する。

```php:app/Providers/ValidatorServiceProvider.php

<?php

namespace App\Providers;

use App\Service\TestValidation;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Validator;

class ValidatorServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap services.
     *
     * @return void
     */
    public function boot()
    {
        Validator::resolver(function ($translator, $data, $rules, $messages) {
            return new TestValidation($translator, $data, $rules, $messages);
        });
    }

    /**
     * Register services.
     *
     * @return void
     */
    public function register()
    {
        //
    }
}
```

これでしっかりエラーメッセージが表示されます。

```json
{
	"errors":[
		{
			"code":"A0001",
			"message":"Key: name, Value: 全角文字ダメ！, Violation: alphabet&number+[._-]"
		},
``` 

# おわりに

やり方がわかるまで苦労しましたが、そこまで変更点もなく自由にエラーメッセージを変更できたのでとても楽でした。
ずっとLaravel使ってるので、エラーハンドリングとかをもっとマスターしていきたいなと。

以上