バリデーションルール最弱決定会議【Laravel10.x】

2024/03/29に公開

今回はShimadaさんをお呼びしました。(以下「島」)

  • 今回のメンバーと主な実績
    • snmatsui
      • Laravel歴8年
      • Laravel5.1→8.xへのアップグレード主導
      • laravel/frameworkへPR採用
    • Shimada
      • Laravel歴3年
      • 社内表彰獲得(2024/03)

2人合わせて合計10年以上のLaravel開発経験を持つ私たちがLaravelバリスタとしてLaravelの考察を行っていきます。
日々Laravelを使って開発をしている皆さんのためになればと思います。

レギュレーション

「Laravelバリスタとしては最初から用意されているバリデーションルールは全てを使いこなしていきたいわけですが、
ルールの中には残念ながら用途が少なかったり、使いにくい機能が存在します。」
「今回はそんなルールの使い方などを考察しつつ、その中でも最も使いどころが少ないバリデーションルール最弱を決めていきます。」
島「フレームワークに対して強弱言ってるのもおかしいですけどね」

  • 環境はLaravel10.x
    • ただし特筆すべき事情がある場合は過去のバージョンも考慮

島「ちょうど先日11.xがリリースされましたけど、情報がまだ少ないと思うので10.xで考えていきましょう」

  • unlessは対象外

島「ifの反対の効果を持つやつですね」
「英語ネイティブな人は使いやすいのかもしれないですが、それ以外の人にはあまり馴染みが無くて却って読みづらい気がするんですよね、これを許すと今後毎回上位に入ってきて面白くなさそうなので予め除外ということで」
島「そもそも二重否定だから、結構嫌ってる人もいると思うんですよね」

島「最近デザインが変わって見慣れないという噂の」
「いつもお世話になっております」

最弱候補

「Laravel10.xの組み込みValidationルールは以下の102つあります」

accepted / accepted_if:他のフィールド,値,... / active_url / after:日付 / after_or_equal:日付 / alpha / alpha_dash / alpha_num / array / ascii / bail / before:日付 / before_or_equal:日付 / between:min,max / boolean / confirmed / current_password / date / date_equals:日付 / date_format:フォーマット,… / decimal:min,max / declined / declined_if:他のフィールド,値,... / different:フィールド / digits:値 / digits_between:最小値,最大値 / dimensions / distinct / doesnt_start_with:foo,bar,... / doesnt_end_with:foo,bar,... / email / ends_with:foo,bar,... / enum / exclude / exclude_if:他のフィールド,値 / exclude_unless:他のフィールド,値 / exclude_with:他のフィールド / exclude_without:他のフィールド / exists:テーブル,カラム / extensions:foo,bar,... / file / filled / gt:field / gte:field / hex_color / image / in:foo,bar... / in_array:他のフィールド.* / integer / ip / ipv4 / ipv6 / json / lt:field / lte:field / lowercase / mac_address / max:値 / max_digits:値 / mimetypes:text/plain,... / mimes:foo,bar,... / min:値 / min_digits:値 / multiple_of:値 / missing / missing_if:他のフィールド,値,... / missing_unless:他のフィールド,値 / missing_with:foo,bar,... / missing_with_all:foo,bar,... / not_in:foo,bar,... / not_regex:正規表現 / nullable / numeric / present / present_if:他フィールド,値,… / present_unless:他フィールド,値 / present_with:foo,bar,... / present_with_all:foo,bar,... / prohibited / prohibited_if:他のフィールド,値,… / prohibited_unless:他のフィールド,値,… / prohibits:他のフィールド,… / regex:正規表現 / required / required_if:他のフィールド,値,... / required_if_accepted:他のフィールド,... / required_unless:他のフィールド,値,... / required_with:foo,bar,... / required_with_all:foo,bar,... / required_without:foo,bar,... / required_without_all:foo,bar,... / required_array_keys:foo,bar,... / same:フィールド / size:値 / starts_with:foo,bar,... / string / timezone / unique:テーブル,カラム / uppercase / url / ulid / uuid

島「思ったより多いですね」
「普段よく使うやつとかありますか?」
島「requiredとかnullableですかね」

required
フィールドが入力データ中に存在し、かつ空でないことをバリデートします。フィールドが以下の条件のいずれかの場合、そのフィールドを「空」であると判定します。

nullable
フィールドがnull値であることを許容します。

5.3からnullableをつけないと空欄とかが無効として扱われるようになっちゃったから、間違いなく最強に近い環境トップメタなルールでしょう」

after:日付
フィールドは、指定された日付以降の値であることをバリデートします。日付を有効なDateTimeインスタンスに変換するため、strtotimePHP関数に渡します。

島「日付系はさくっとかけて便利ですね」
「自分で作ろうとすると地味に面倒なやつ」

「ルール全部について触れていると、紙面がいくらあっても足りないので、よく使うものだったり、用途がすぐに思いつくものは一気に消します。」

最弱回避 102->15

accepted / accepted_if:他のフィールド,値,... / active_url / after:日付 / after_or_equal:日付 / alpha / alpha_dash / alpha_num / array / ascii / bail / before:日付 / before_or_equal:日付 / between:min,max / boolean / confirmed / current_password / date / date_equals:日付 / date_format:フォーマット,… / decimal:min,max / declined / declined_if:他のフィールド,値,... / different:フィールド / digits:値 / digits_between:最小値,最大値 / dimensions / distinct / doesnt_start_with:foo,bar,... / doesnt_end_with:foo,bar,... / email / ends_with:foo,bar,... / enum / exclude / exclude_if:他のフィールド,値 / exclude_unless:他のフィールド,値 / exclude_with:他のフィールド / exclude_without:他のフィールド / exists:テーブル,カラム / extensions:foo,bar,... / file / filled / gt:field / gte:field / hex_color / image / in:foo,bar... / in_array:他のフィールド.* / integer / ip / ipv4 / ipv6 / json / lt:field / lte:field / lowercase / mac_address / max:値 / max_digits:値 / mimetypes:text/plain,... / mimes:foo,bar,... / min:値 / min_digits:値 / multiple_of:値 / missing / missing_if:他のフィールド,値,... / missing_unless:他のフィールド,値 / missing_with:foo,bar,... / missing_with_all:foo,bar,... / not_in:foo,bar,... / not_regex:正規表現 / nullable / numeric / present / present_if:他フィールド,値,… / present_unless:他フィールド,値 / present_with:foo,bar,... / present_with_all:foo,bar,... / prohibited / prohibited_if:他のフィールド,値,… / prohibited_unless:他のフィールド,値,… / prohibits:他のフィールド,… / regex:正規表現 / required / required_if:他のフィールド,値,... / required_if_accepted:他のフィールド,... / required_unless:他のフィールド,値,... / required_with:foo,bar,... / required_with_all:foo,bar,... / required_without:foo,bar,... / required_without_all:foo,bar,... / required_array_keys:foo,bar,... / same:フィールド / size:値 / starts_with:foo,bar,... / string / timezone / unique:テーブル,カラム / uppercase / url / ulid / uuid /

島「もう使わなそうなのしか無さそうwww」

残り15つ

accepted / accepted_if:他のフィールド,値,... / active_url / declined / declined_if:他のフィールド,値,... / doesnt_start_with:foo,bar,... / doesnt_end_with:foo,bar,... / lowercase / mac_address / multiple_of:値 / not_in:foo,bar,... / same:フィールド / size:値 / timezone / uppercase

「ではここからカテゴリにわけつつ、アルファベット順に見ていきましょう」

accepted / accepted_if:他のフィールド,値,...

accepted
フィールドが、"yes"、"on"、1、"1"、true、"true"であることをバリデートします。これは、「利用規約」の承認または同様のフィールドをバリデーションするのに役立ちます。

accepted_if:他のフィールド,値,...
他のフィールドが指定した値と等しい場合、このフィールドは "yes"、"on"、1、"1"、"true"、trueであることをバリデートします。これは、「利用規則」の了承や似たようなフィールドをバリデートするのに便利です。

島「これやばいよなぁ、バリデーションルールを上から見ていくときに一番最初に目に入って、いつもこれは何に使うんだってなる。用途としては利用規約とかで使えるのか」
「フロントとバックエンド作るチームが完全に別々で、要件を詰めにくいようなときとかだとこのラフな感じが活きるのかもしれないんですけど」
島「そもそもそういったコミュニケーションが取れないと今後が不安ですね」

「AWSコンソールでリソースを削除するときの『完全に削除』を入力させるみたいな用途では使えるのかもしれないけれど、それでも入力する値が決まっているならinの方が厳密かと」

in:foo,bar...
フィールドが指定したリストの中の値に含まれていることをバリデートします。

accepted_ifの方は、現状in_ifっていう他のフィールドが〇〇なとき××であることを確認するみたいなルールが無いんで、『この項目が入力されているなら、このチェックは入っているはずだ』といった用途で使えるかもしれない、
それでもyesやtrueも許可しちゃうのがちょっと嫌な感じがしてしまいますが」

active_url

active_url
フィールドが、dns_get_record PHP関数により、有効なAかAAAAレコードであることをバリデートします。dns_get_recordへ渡す前に、parse_url PHP関数により指定したURLのホスト名を切り出します。

島「入力されたURLへ通信しようとしたら無効なドメインでタイムアウトまで待たないといけないといったことを防ぐのには役に立ちそうかと」
「攻撃されてDNSへのたくさんクエリ投げてしまうみたいな怖さはあるかもしれない」

「これがもっと問題だったのは、5.0のときはURLの形式をろくにチェックせずにDNS調べる関数に投げていたという」

(5.0)
フィルドがPHPの機能であるcheckdnsrrを通して、有効なURLであるかをバリデートします。

(validateActiveUrlのソース)

protected function validateActiveUrl($attribute, $value)
{
    $url = str_replace(array('http://', 'https://', 'ftp://'), '', strtolower($value));
    return checkdnsrr($url, 'A');
}

島「当時はざっくりしてますね」
「5.0がヤバすぎるのでノミネートしたんですが、今は解消されているのでまあ最弱回避でよいでしょう」

→💮最弱回避!!

declined / declined_if:他のフィールド,値,...

declined
The field under validation must be "no", "off", 0, "0", false, or "false".

declined_if:他のフィールド,値,...
The field under validation must be "no", "off", 0, "0", false, or "false" if another field under validation is equal to a specified value.

「これはまだReadoubleが和訳されてません!」
島「あっ本当ですね」

島「わざわざ否定させる用途って何なんでしょう?」
declined_ifは、さっきのaccepeted_ifと同じような使い道があるかもしれないけれど、declinedは本当に用途が思いつかない」

doesnt_start_with:foo,bar,... / doesnt_end_with:foo,bar,...

doesnt_start_with:foo,bar,...
フィールドの値が指定した値で開始しないことをバリデートします。

doesnt_end_with:foo,bar,...
フィールドの値が指定した値で終了しないことをバリデートします。

「これの対でstarts_withends_withももちろんあります。httpsで始めましょうとか」

starts_with:foo,bar,...
フィールドが、指定した値のどれかで始まることをバリデートします。
ends_with:foo,bar,...
フィールドの値が、指定された値で終わることをバリデートします。

「英文法上仕方ないとはいえ、startsがしれっと単数のstartになっているのも少し罠っぽくて気になりますね。」

「結局これを使うシチュエーションって何なんでしょう」
島「わかんないっすね」
「禁止ワードを設けたいと思っていても、『最初にだけあると嫌』みたいな状況も思いつかないですね。
これができたPR見に行きましょう」

[9.x] Adds not_starts_with validation #42683
makes the validation easier and we will not resort to using something else such as Regex.

島「正規表現を使わなくてもこのバリデーションができるようにしたかった?」

'name' => ['required', 'not_starts_with:mahmoud'],

島「mahmoudって何ですか?」
競走馬らしい」
「だからこの例を日本調にするならば、『ディープインパクトの子供を入力されたくないからディープ〇〇を禁止にする』的な用途?」

島「doesnt_end_withが導入されたPRを見てみたらメールアドレスのドメインを禁止したいって書いてました」

Added validation doesnt_end_with rule #43518
check if an email address doesn't finished by some domain names.

「この用途は確かに使えるかもしれない、フリーメールだけ禁止にするみたいな用途はありそう」
島「ドメインの打ち間違えを防止してあげるとか」
「そうすると、doesnt_start_withの方もnoreply@を禁止にするとかできそう」

→💮最弱回避!!

lowercase / uppercase

lowercase
フィールドが小文字であることをバリデートします。

uppercase
フィールドが大文字であることをバリデートします。

「大文字小文字くらいシステム側で変換しろよっていうことでノミネートです」
島「そういうサーバー側での変換って嫌がられたりするんですかね?」
「フロント側でリアルタイムで大文字小文字を変換して揃えていて、PHPではバリデーションだけ行うって場合があるか」

→💮最弱回避!!

mac_address

mac_address
フィールドがMACアドレスとして正しいことをバリデートします。

「業務端末の管理とか」
島「それ以外の用途ってあります?」
「それだけでしょうね...」

multiple_of:値

multiple_of:値
フィールドが、値の倍数であることをバリデートします。

「HTMLのinputにstepがあるからそれに合わせたんですかね?」

HTML 属性: step
step は、スピナーボタンを上下にクリックしたり、範囲上でスライダーを左右に動かしたり、異なる日付タイプを検証したりする際の刻み間隔を設定します。

島「PRでもそうだって書いてました」

[8.x] Add 'multiple_of' validation rule #34788
This validation rule can be useful if you use a number input with a step attribute.

「このstepってよく時間で15分感覚みたいに使いたいけど、このバリデーションは数字じゃないと使えないらしい」

not_in:foo,bar,...

not_in:foo,bar,...
フィールドが指定された値のリスト中に含まれていないことをバリデートします。Rule::notInメソッドのほうが、ルールの構成が読み書きしやすいでしょう。

「部分一致ではなくて完全一致だけ確かめるから、禁止ワードには使えないよね」
島「予約語禁止専用ルールかな」
「まあ予約語禁止って役割があるだけでも最弱ではないでしょう」

→💮最弱回避!!

same:フィールド

same:フィールド
フィールドが、指定したフィールドと同じ値であることをバリデートします。

「これはメールアドレスとかパスワードを確認のため2回入力してもらうときに使うconfirmedのキー名を指定できるようになったバージョンですね」

confirmed
フィールドが、{field}_confirmationフィールドと一致する必要があります。たとえば、バリデーション中のフィールドが「password」の場合、「password_confirmation」フィールドが入力に存在し一致している必要があります。

「とはいえ、confirmedじゃなくてあえてsameじゃなきゃいけない場合も思いつかないということでノミネートです。」
島「confirmedの実装を見てみると、中でsameを呼んでましたね。
confirmedは対応するキーがconfirmationになるから、英語ネイティブじゃないとちょっと躓かないですか?」
「確かに。sameで明示してあげた方がとっつきやすいかもしれない」

→💮最弱回避!!

size:値

size:値
フィールドは指定した値と同じサイズであることをバリデートします。文字列の場合、値は文字長です。数値項目の場合、値は整数値(属性にnumericかintegerルールを持っている必要があります)です。配列の場合、値は配列の個数(count)です。ファイルの場合、値はキロバイトのサイズです。

「これはそのままサンプルを見てほしくて」

// 文字列長が12文字ちょうどであることをバリデートする
'title' => 'size:12';

// 指定された整数が10であることをバリデートする
'seats' => 'integer|size:10';

// 配列にちょうど5要素あることをバリデートする
'tags' => 'array|size:5';

// アップロードしたファイルが512キロバイトぴったりであることをバリデートする
'image' => 'file|size:512';

島「『指定された整数が10であることをバリデートする』って桁数的な話ですか?」
「桁数はdigitsってルールがあるから、integer|size:10in:10と同じになるはず」

digits:値
フィールドが整数で、値の桁数であることをバリデートします。

アップロードしたファイルが512キロバイトぴったりであることをバリデートする

島「一番ヤバいのこれですけどね」
「語彙が貧弱で申し訳ないですけど、ヤバすぎる」

timezone

timezone
The field under validation must be a valid timezone identifier according to the DateTimeZone::listIdentifiers method.

「10.xで和訳されていないのはさっきのdeclinedとこれだけです」

'timezone' => 'required|timezone:Africa';
'timezone' => 'required|timezone:per_country,US';

島「日本だと使わないけど、国によっては複数のタイムゾーンを持っている場合もあるんで、そういった場合は役に立ちそうな感じがしますね。」
「元々は国内だけのサービスだったけど、海外にも出すことになって、後付けで別タイムゾーンにも対応することになったという場合は使いそう」

→💮最弱回避!!

最弱回避 15->7

accepted / accepted_if:他のフィールド,値,... / active_url / declined / declined_if:他のフィールド,値,... / doesnt_start_with:foo,bar,... / doesnt_end_with:foo,bar,... / lowercase / mac_address / multiple_of:値 / not_in:foo,bar,... / same:フィールド / size:値 / timezone / uppercase

最弱決定

accepted / accepted_if:他のフィールド,値,... / declined / declined_if:他のフィールド,値,... / mac_address / multiple_of:値 / size:値

「この7つの中から最弱を決定しましょう」
島「とりあえず明確に『これしか無い』タイミングがありうるmac_addressmultiple_ofは外していいんじゃないですか?」

mac_addressmultiple_of→💮最弱回避!!

accepteddeclinedだったらまずacceptedの方が使いやすそうなので、acceptedも抜けるんじゃないでしょうか」

acceptedaccepted_if→💮最弱回避!!

島「残ったのがdeclineddeclined_ifsizeの3つ」
declinedのパートで考えた通り、『場合によって0』はin_ifが無いからこれを使いたいって場合がありそうだけど、絶対に0ならin:0のがよいと思うのでdeclined_ifは外しましょう」

declined_if→💮最弱回避!!

島「sizeは文字列と配列の場合は普通に用途があるけど、いかんせん数値とファイルの場合がヤバすぎる」
「最弱会議におけるインパクトはあるよね」
島「決まった画像のサイズだけ受け付けるとかってできるんですかね?」
「pngやjpegとかの画像は圧縮されてるからサイズは変わるだろうし、そもそもdimensionsってルールが今はあるらしい」

dimensions
バリデーション対象のファイルが、パラメータにより指定されたサイズに合致することをバリデートします。
'avatar' => 'dimensions:min_width=100,min_height=200'
使用可能なパラメータは、min_width、max_width、min_height、max_height、width、height、ratioです。

「まあ『〇〇KBのファイルしかアップロードできないアップローダー』はトマソンみが強くておもしろいので、ちょっと組み合わせ技になっちゃいますけど、これが最弱じゃないでしょうか」

バリデーションルールの最弱はfile|sizeのときのsize、ルール単体だけで評価するならdeclinedに決定しました。

「今回これを作るにあたってバリデーションのページを見直したりしたんですが、
特殊なルールとして受け入れるパスワードのフォーマットを便利に作れるようになってるのを見つけて、
やはりReadobuleは見るたびに発見がありますね。

ソーシャルデータバンク テックブログ

Discussion