はじめに
こんにちは、次世代デジタル基盤開発事業部の安彦です。
これまでPHP未経験だったのですが、6月から業務で使い始めました。
Laravelの所感としては、しっかりしたMVCモデルで、書き慣れたらすごい速さで実装が進むなぁと感じました。
しかし、気になることが1点...
バリデーションが面倒すぎる!!
これを克服する簡単な実装ができたので共有します。
作りたい画面
氏名と各科目の点数を入力するフォームを用意して、
期待する値が入力された場合には、「リクエスト成功」と表示し、
想定外の値が入力された場合には、「各入力欄にエラー」を表示したいです。
よくある基本的な入力フォームですが、これをLaravelで作ると少し面倒なコーディングが必要でした。
Laravel での一般的な実装方法
view
まず、view用に resources/views/exam_score.blade.php
を作成します。
今回はDBの話をしないので、Eloquentは登場しません。
成功時も同じviewを返すので、inputタグのvalue属性に
コントローラの戻り値($inputs
)と バリデーションエラーの戻り値(old()
)の両方を使用します。
<form method="post"> @csrf <div> <label>氏名</label> <input name="full_name" type="text" value="{{ $inputs['full_name'] ?? old('full_name')}}" /> <div class="error"> @error('full_name') {{$message}} @enderror </div> </div> <table> <tr> <th>科目</th> <th>点数</th> </tr> @foreach(['国語', '数学', '英語'] as $i => $sub) <tr> <td>{{$sub}}</td> <td> <input name="scores[]" value="{{ $inputs['scores'][$i] ?? old("scores.{$i}") }}" /> </td> </tr> <tr> <td></td> <td class="error"> @error("scores.{$i}") {{$message}} @enderror </td> </tr> @endforeach </table> <button type="submit">送信</button> </form> @isset($requestClass) <div class="result"> <span class="success">リクエスト成功<br></span> </div> @endisset
ルーティング
続いて routes/web.php
にルーティングを記載
use Illuminate\Support\Facades\Route; use App\Http\Controllers\ExamController; Route::get('/', [ExamController::class, 'showRegister']); Route::post('/', [ExamController::class, 'insert']);
コントローラ
続いて、コントローラ
今回はフォームリクエストを作成してバリデーションを実装するので、
自作のリクエストを引数にとり、viewを返すのみです。
namespace App\Http\Controllers; use App\Http\Requests\PostRequest; use Illuminate\Routing\Controller as BaseController; class ExamController extends BaseController { public function showRegister() { return view('exam_score'); } public function insert(PostRequest $req) { return view('exam_score', ['inputs'=>$req->all()]); } }
リクエスト
そして、本題のバリデーションを行うリクエストです。
Laravelドキュメントのバリデーションを元に実装します。
rules
メソッドにバリデーションルールを記載します。
'パラメータ名' => ['ルール名', ... ]
の形なので、
Illuminate\Http\Request
の validate
メソッドを使う場合と一緒です。
今回は lang
ディレクトリvalidation.php
を使わずにエラーメッセージを調整します。
messages
メソッドで、'パラメータ名.ルール名' => '表示する文字列'
の配列を返します。
このrulesとmessagesに同じ単語を何度も入力する作業が大変面倒に感じました。
namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; class PostRequest extends FormRequest { public function rules(): array { return [ 'full_name' => ['required', 'min:3', 'max:10'], 'scores' => ['required', 'array'], 'scores.*' => ['required', 'integer', 'min:0', 'max:100'], ]; } public function messages(): array { return [ 'full_name.required' => '氏名を入力してください', 'full_name.min' => '氏名は3文字以上で入力してください', 'full_name.max' => '氏名は10文字以内で入力してください', 'scores.required' => 'スコア一覧を入力してください', 'scores.array' => 'スコア一覧を入力に誤りがあります', 'scores.*.required' => 'スコアを入力してください', 'scores.*.integer' => 'スコアは整数を入力してください', 'scores.*.min' => 'スコアは0以上の値を入力してください', 'scores.*.max' => 'スコアは100以下の値を入力してください', ]; } }
オレオレ実装したリクエストクラス
リクエストクラス
同じ単語を二度と打たなくて良いようにした考えた結果、
こんなリクエストクラスになりました。
実装するのはaddValidate
メソッドのみ!
InputParam
クラスの配列を引数にして、
親クラスのmergeInputParams
メソッドを呼び出します。
InputParam
をcreate('パラメータ名', '表示用パラメータ名')
でインスタンス化して、
required()
などLaravelのルール名に似た名前のメソッドをメソッドチェーンする形です。
ルール指定をメソッド化したことで、
文字列よりも補完が効きやすいのがかなりメリットかなと思います。
namespace App\Http\Requests; use App\Http\Requests\EasyValidator\InputParam; use App\Http\Requests\EasyValidator\Request; class PostEasyRequest extends Request { protected function addValidate(): void { $this->mergeInputParams([ InputParam::create('full_name', '氏名')->required()->stringMin(3)->stringMax(10), InputParam::create('full_name', '氏名')->required()->stringMin(3)->stringMax(10), InputParam::create('scores', 'スコア一覧')->required()->array(), InputParam::create('scores.*', 'スコア')->required()->integer()->intMin(0)->intMax(100), ]); } }
コントローラのメソッドの引数をPostEasyRequest
に置き換えるだけで、使用できます。
class ExamController extends BaseController { public function insert(PostEasyRequest $req) { return view('exam_score', ['inputs'=>$req->all()]); } }
リクエストの親クラス
続いて、細かい実装を見ていきます。
まず、先のリクエストが継承していた親クラスです。
「Laravel での一般的な実装方法」でも継承していた、FormRequest
を継承しています。
子クラスはaddValidate
を実装する必要があります。
addValidate
はvalidator
中で呼びれ、
validator
はLaravelのFormRequestで呼び出されます。
namespace App\Http\Requests\EasyValidator; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Contracts\Validation\Factory as ValidationFactory; class Request extends FormRequest { // これを実装する protected function addValidate(): void { $this->mergeInputParams([]); } public function validator(ValidationFactory $factory) { $this->addValidate(); return $this->createDefaultValidator($factory); } // 下に続く
次に、addValidate
で呼び出していたmergeInputParams
です。
引数の[]InputParam
のルールとメッセージを取得して、
自身のaddRules
プロパティに追加しています。
最後に、rules
・messages
メソッドが各プロパティを返しています。
「Laravel での一般的な実装方法」でそれぞれ実装していたメソッドを、
自身のプロパティを返す形に変更している仕組みです。
protected array $addRules = []; protected array $addMessages = []; /** * @param []InputParam $inputs */ protected function mergeInputParams(array $inputs): void { foreach ($inputs as $input) { $this->addRules = array_merge($this->addRules, $input->getRules()); $this->addMessages = array_merge($this->addMessages, $input->getMessages()); } } public function rules(): array { return $this->addRules; } public function messages(): array { return $this->addMessages; } // クラス終わり }
各パラメータの情報を管理するクラス
次に「氏名」などの各パラメータの情報をもつクラスです。
プロパティのrulesは、Laravelのバリデーションルール名の配列で、
messagesは、'title.required' => 'A title is required'
のように、「Laravel での一般的な実装方法」でのmessagesメソッドで返す形のマップを保持します。
コンストラクタ側に記載しているparamNameとdisplayNameはPOSTパラメータ名とメッセージでの表示名です。
メソッドチェーンでルールを追加していくので、createメソッドのみpublicにして、
全ルールで共通のpramNameとdisplayNameだけ初期化します。
namespace App\Http\Requests\EasyValidator; class InputParam { /** @var string[] */ private array $rules; /** @var array<string,string> */ private array $messages; private function __construct( private string $paramName = '', private string $displayName = '', ) { $this->rules = []; $this->messages = []; } public static function create( string $paramName, string $displayName, ): InputParam { return new InputParam($paramName, $displayName); } // 下に続く
次に Request
クラスのmergeInputParams
メソッドで呼び出されるメソッドです。
それぞれ、ルールとメッセージのマップを返します。
public function getRules() { // ex) ['full_name' => ['required', 'max:10']] return [$this->paramName => $this->rules]; } public function getMessages() { // ex) ['full_name.required' => '氏名必須hogehoge', 'full_name.max' => '氏名最大hogehoge'] return $this->messages; } // 下に続く
次に、Laravel準拠のルールを設定して、自身を返すsetRule
メソッドです。
public function setRule(string $rule, string $messageKey, string $message) { $this->rules[] = $rule; $this->messages[$messageKey] = $message; return $this; } // 下に続く
最後に、各ルールを設定するメソッドを用意します。
required
などLaravelで使えるバリデーション名と、表示したいメッセージを設定しています。
min/max
はLaravelだと数値と文字列の区別がないため、
stringMax
やintMax
などメソッドを分けることで、メッセージをカスタマイズしています。
nullable
やboolean
などLaravelのバリデーションも使いたいときに実装する作りになっています。
/** ルール required を設定 */ public function required() { $rulePrefix = 'required'; $messageKey = "{$this->paramName}.{$rulePrefix}"; $message = "{$this->displayName}を入力してください"; return $this->setRule($rulePrefix, $messageKey, $message); } /** ルール max を設定 */ public function stringMax(int $value=100) { $rulePrefix = 'max'; $rule = "{$rulePrefix}:{$value}"; $messageKey = "{$this->paramName}.{$rulePrefix}"; $message = "{$this->displayName}は{$value}文字以内で入力してください"; return $this->setRule($rule, $messageKey, $message); } /** ルール min を設定 */ public function stringMin(int $value=10) { $rulePrefix = 'min'; $rule = "{$rulePrefix}:{$value}"; $messageKey = "{$this->paramName}.{$rulePrefix}"; $message = "{$this->displayName}は{$value}文字以上で入力してください"; return $this->setRule($rule, $messageKey, $message); } /** ルール max を設定 */ public function intMax(int $value=100) { $rulePrefix = 'max'; $rule = "{$rulePrefix}:{$value}"; $messageKey = "{$this->paramName}.{$rulePrefix}"; $message = "{$this->displayName}は{$value}以下の値を入力してください"; return $this->setRule($rule, $messageKey, $message); } public function integer() { $rulePrefix = 'integer'; $messageKey = "{$this->paramName}.{$rulePrefix}"; $message = "{$this->displayName}は整数を入力してください"; return $this->setRule($rulePrefix, $messageKey, $message); } public function array() { $rulePrefix = 'array'; $messageKey = "{$this->paramName}.{$rulePrefix}"; $message = "{$this->displayName}の入力に誤りがあります"; return $this->setRule($rulePrefix, $messageKey, $message); } /** ルール min を設定 */ public function intMin(int $value=10) { $rulePrefix = 'min'; $rule = "{$rulePrefix}:{$value}"; $messageKey = "{$this->paramName}.{$rulePrefix}"; $message = "{$this->displayName}は{$value}以上の値を入力してください"; return $this->setRule($rule, $messageKey, $message); } // TODO: required, stringMax 同様に Laravelで使用可能なバリデーションを実装 } // クラス終わり
実装は以上です。
前記のとおり、メソッドチェーンでルールとメッセージを設定できます。
また、SetRule
メソッドを直接使うことで、各リクエスト固有のルールも設定可能です。
namespace App\Http\Requests; use App\Http\Requests\EasyValidator\InputParam; use App\Http\Requests\EasyValidator\Request; class PostEasyRequest extends Request { protected function addValidate(): void { $this->mergeInputParams([ InputParam::create('full_name', '氏名')->required()->stringMin(3)->stringMax(10), InputParam::create('full_name', '氏名')->required()->stringMin(3)->stringMax(10), InputParam::create('scores', 'スコア一覧')->required()->array(), InputParam::create('scores.*', 'スコア')->required()->integer()->intMin(0)->intMax(100), // setRuleで独自のルールも設定可能 InputParam::create('email', 'メールアドレス')->required()->stringMax(100) ->setRule('unique:App\Infrastructure\Eloquents\User,email', 'unique', 'そのメールアドレスは使用されています'), ]); } }
おわりに
最後まで読んで頂きありがとうございます。
今回は、Laravelの理解が浅い筆者が、
Laravelのバリデーションをオレオレ実装で使いやすく?してみました。
(Request
クラスでvalidator
メソッドを実装してLaravelのFormRequestに呼ばれるようにする箇所は、先輩エンジニアの力を借りました。)
メッセージのカスタマイズだけであれば多言語化ファイルを使った方が、
Laravelに素直なコードになった気がしています。
ただ、Laravelにはバリデーションのように決まった文字列の配列を渡す機会が多いので、
その場面ではメソッドチェーン化するのが有効かなと思いました。
テコテックの採用活動について
テコテックでは新卒採用、中途採用共に積極的に募集をしています。
採用サイトにて会社の雰囲気や福利厚生、募集内容をご確認いただけます。
ご興味を持っていただけましたら是非ご覧ください。
tecotec.co.jp