今回は ポエム です。技術的には全然高度なお話ではなく、ただ、気持ち悪かったのをどうにかしたかったというお話です。ポエムが苦手な方は「戻る」ボタンを押すことをオススメします。
前提
- PHP 7.4.9
- Laravel 8.5.0
3行でわかるまとめ
Laravel のバリデーションルールの指定方法が気持ち悪い(とくにクラス定数を使ったとき)。
いろいろ試してみたが、気持ち悪さを解消できなかった。
自作パッケージを作って、ようやく気持ち悪さを解消できた。
クラス定数問題
Laravel では、バリデーションルールは 文字列 で指定します。例えば、 title
属性に「必須入力であること」と「1000文字以下であること」というルールを指定する場合は、次のように書きます。
$request->validate([
'title' => 'required|max:1000',
]);
|
で区切らずに 配列 で指定することもできます。
$request->validate([
'title' => ['required', 'max:1000'],
]);
しかし、マジックナンバーは極力回避したいので 1000
を変数にします。PHP の仕様により、次のように文字列の中に変数を埋め込むことができます。
$titleMaxLength = 1000;
$request->validate([
'title' => ['required', "max:{$titleMaxLength}"],
]);
$titleMaxLength
は処理の中で変化するものではありません。こういうときは、できれば 定数 にしたいところです。
しかし、定数は文字列の中で展開されないため、次のように書いてもうまく動きません。
class TodoController extends Controller
{
const TITLE_MAX_LENGTH = 1000;
public function store(Request $request)
{
// この書き方では期待通りの動きをしない。
$request->validate([
'title' => ['required', "max:{self::TITLE_MAX_LENGTH}"],
]);
// ...
}
}
もちろん、文字列演算子 を使って、文字列に定数を結合すれば、うまく動きます。
class TodoController extends Controller
{
const TITLE_MAX_LENGTH = 1000;
public function store(Request $request)
{
// この書き方ならば期待通りの動きをする。
$request->validate([
'title' => ['required', 'max:' . self::TITLE_MAX_LENGTH],
]);
// ...
}
}
このように書くのが定石でしょう。私も実際に仕事でそう書いたことがありますし、それが悪いというつもりは全くありません。
ただ、個人的にはどうしても居心地の悪さを感じるのです。例えば、「10文字以上であること」というルールを追加すると、このようになります。
class TodoController extends Controller
{
const TITLE_MIN_LENGTH = 10;
const TITLE_MAX_LENGTH = 1000;
public function store(Request $request)
{
$request->validate([
'title' => ['required', 'min:' . self::TITLE_MIN_LENGTH, 'max:' . self::TITLE_MAX_LENGTH],
]);
// ...
}
}
随分と横に長くなってしまいました。でも、あらかじめ変数に入れておけば改善することができます。
class TodoController extends Controller
{
const TITLE_MIN_LENGTH = 10;
const TITLE_MAX_LENGTH = 1000;
public function store(Request $request)
{
$titleRules = [
'required',
'min:' . self::TITLE_MIN_LENGTH,
'max:' . self::TITLE_MAX_LENGTH,
];
$request->validate([
'title' => $titleRules,
]);
// ...
}
}
それでも、個人的にはどうもしっくり来ません。 'max:' . self::TITLE_MAX_LENGTH
という風に文字列結合しないといけないのでしょうか?
メソッド化
汚いものには蓋をする。 メソッド化の目的をそのように説明したら、きっと四方八方から石を投げつけられると思います。でも、試して見る価値はあるのではないかと思ったのです。
getConstant()
というメソッドを作り、引数で渡された定数名をもとに、定数の値を取得します。その中で使っている constant()
(PHP マニュアル)は定数の値を返す標準関数です。定数が見つからなかった場合に E_WARNING
レベルのエラーを発生させ、Laravel では例外が送出されます。1
class TodoController extends Controller
{
const TITLE_MIN_LENGTH = 10;
const TITLE_MAX_LENGTH = 1000;
public function store(Request $request)
{
$titleRules = [
'required',
"min:{$this->getConstant('TITLE_MIN_LENGTH')}",
"max:{$this->getConstant('TITLE_MAX_LENGTH')}",
];
$request->validate([
'title' => $titleRules,
]);
// ...
}
public function getConstant($constantName) {
return constant('self::' . $constantName);
}
}
たしかに、これは動きます。でも、「いや、そこまでしなくても...」という声が聞こえてきそうです。頑張ったわりに、そこまで見やすくもないですし...
余談1
ちなみに、クラス定数を取得するならば、 ReflectionClass::getConstant()
(PHP マニュアル)を使うこともできます。
public function getConstant($constantName) {
return (new \ReflectionClass(__CLASS__))->getConstant($constantName);
}
しかし、定数が見つからなかった場合の動作が constant()
と異なります。 constant()
は E_WARNING
レベルのエラーを発生させます。一方、ReflectionClass::getConstant()
はエラーを発生させないため( FALSE
を返すだけです)、Laravel でも例外になりません。あえて ReflectionClass::getConstant()
を使う理由は無いように思いました。23
パッケージ化
話が脱線しましたが、そもそも、どこが嫌だったかと言うと、 文字列結合 していることでした。この点をもう少し考えてみます。
'max:1000'
というバリデーションルールは、以下の2つの部分から構成されています。
-
ルール名
max
-
ルールの引数
1000
この2つを :
で繋いで1つの文字列にしなければならないところに、そもそも無理があるように思います。素直に、それぞれを分けて指定できるようにすればいいのではないでしょうか?
例えば、 addRule(適用対象, ルール名, ルールの引数)
という風に書くことができれば、クラス定数も文字列結合することなく渡すことができます。以下の例では、 addRule()
でルールを追加し、 format()
で Laravel のバリデータに渡せる配列形式に変換してから、バリデーションを実行しています。
class TodoController extends Controller
{
const TITLE_MAX_LENGTH = 1000;
public function store(Request $request)
{
// title 属性は必須であること。
$rules = ValidationRuleFormatter::addRule('title', 'required')
// title 属性は1000文字以下であること。
->addRule('title', 'max', self::TITLE_MAX_LENGTH)
// Laravel のバリデータに合うようにフォーマットする。
->format();
// バリデーションを実行する。
$request->validate($rules);
// ...
}
}
そして、これを Laravel の パッケージ として作ってみました。以下のように composer でインストールすれば、すぐにお使いいただけます(笑)
composer require aminevsky/laravel-validation-rule-formatter
おそらく需要は皆無だと思っていますが、本人は長年のモヤモヤが解決して満足しています(自己満足)
おしまい。
余談2
なんだかんだ初めて Laravel のパッケージを作ったのですが、約200行ほどのちゃっちいプログラムだったこともあってか、意外と簡単に作ることができました。4
パッケージ開発については Laravel ドキュメント もありますが、 Laravel Package Development が一番よくまとまっているように感じました。
PSR-4 や、composer で ローカルディレクトリをリポジトリに指定する方法 に触れられるのも、パッケージ開発ならではだと思います。
-
通常、PHP では
E_WARNING
が発生しても、処理は継続します。しかし、Laravel ではIlluminate\Foundation\Bootstrap\HandleException
のbootstrap()
でerror_reporting(-1)
と設定されているため、全てのエラーと警告が表示されます(PHP マニュアル)。さらにHandleException
クラスのhandleError()
でErrorException
へ変換されるため、例外が引き起こされたように見えるわけです。 ↩ -
コンパイルが無く、実行時にはじめて間違いに気づくことが多い PHP としては、こういう挙動はあまり嬉しくありません。そもそも
ReflectionClass
インスタンスを作るほどのことでも無いように思えますし。 ↩ -
error_reporting(-1)
に設定している Laravel としては関係ありませんが、そもそもconstant()
もE_WARNING
レベルのエラーを出力するだけで処理が継続するのはいいのか?という疑問はあります。ただし、PHP 8 では RFC: Reclassifying engine warnings が採択されたため、Error
例外が発生するようになるようです。3v4l.org での 動作例 も参照してください。 ↩ -
本来ならば、このくらいのプログラムは1日で完成させないといけないところですが、3日もかかってしまったのは、全く恥ずべきことだと思います。いやテストコードも無く、クラス分けもしていない状態であれば1日目で出来上がっていたのですが、クラス分けで悩んだ時間のなんと長かったことか... しかもあれだけ悩んだわりに、出来上がったものを見ると、自分でも「この程度かよ」と思うレベルでしかない。一応5年以上プログラミングをやっていて、未だにこの程度ですからね。本当に向いていないんだろうな。無職なので、いろいろ求人を見るけど、やっぱりどこもいい会社は求めるレベルが高くて、私みたいな雑魚はお呼びでない。「モダンな環境でやりたいな」とか「魅力的な事業をやっている会社で働きたいな」とか理想は多々あるけれども、ようやくわかったよ。そういうのは能力が無いと得られないものなんだということを。私みたいな能力の無い者に残された道は2つしかない。自分の理想を捨てて働きたくないところで我慢して働くか、潔く退場するか。そんなことばかり考えていると、ときどき、頭がパニックになったり過呼吸を引き起こしたりして(とくに午前中になりやすい)、Qiita すら昨日は1文字も書けず、今日、ようやく勢いで、どうにか1つ書けたわけですね。はぁ... ↩