この記事ではPHPの正規表現を前提に話を進めます。PHP以外の多くの言語に正規表現は実装されており、別言語でも後述のことに似たことを気にする必要はあるでしょうが、また別の方法で対処した方が良いこともあります。
正規表現で文字列の最初と最後を表現するにはよく”^”, “$”が使われます。これは正確には行頭、行末を表す記号であり、真に文字列の頭と末を表現するのは”\A”, “\z”です。
データの整合性を守るためのバリデーションルールではしばしばある正規表現に一致するならばよし、しないならばダメというルールが作られます。この時、”^”,”$”で最初と最後を表現すると受け入れたくない値をバリデーションで弾けないことがあります。これは例えば次の様になります。
<?php
var_dump(preg_match_all('/^aaa$/', 'aaa')); // 期待通りにOK
var_dump(preg_match_all('/^aaa$/', 'bbb')); // 期待通りにNG
var_dump(preg_match_all('/^aaa$/', 'aaaa')); // 期待通りにNG
var_dump(preg_match_all('/^aaa$/', "aaa\n")); // 期待通りのNGにならない
var_dump(preg_match_all('/\Aaaa\z/', "aaa\n")); // 期待通りのNGになる
// @see http://sandbox.onlinephpfunctions.com/code/d04236ae45aef7c0c9f04380f71b4f66f39dd9ee
入って欲しくない改行コードを通してしまいます。加えてこれの面倒なところはinputタグを介したブラウザ上からの入力では検知困難(少なくとも自分は検知する方法を知らない)という点にあります。顧客にブラウザ上から試していただくテストではまず見つかりません。inputタグは基本的に一行の文字列で完結しているからです。
問題を発見するにはプログラム上、コマンドライン上からバリデーションルールの設置個所にリクエストを投げるか、テストコードを書く必要があります。私的にはテストコードがおすすめです。理由はテストにかかる労力の軽減です。大体の場合でバリデーションルールのテストケースは多くなります。というのもテストケースの境界が複雑になるためです。まともに手入力でテストするととても疲れます。反して入力回数が多いのみなのでテストの記述は楽です(手続きが複雑すぎる場合、テスト自体にバグが含まれやすくなったりでテストを書くのが割に合わないこともあります)。
次の例はひらがな、カタカナ、半角カタカナ、伸ばし棒、全角スペース、半角スペースのみを受け入れる振り仮名ルールのテストメソッドです。バリデーションルールはこんな感じに明確なテストを書きやすいのでテストコードによるテストをした方がよいでしょう。
public function testFurigana(): void
{
$furiganaRule = new Furigana();
$this->assertTrue($furiganaRule->passes('dummyLabel', 'あ'));
$this->assertTrue($furiganaRule->passes('dummyLabel', 'ア'));
$this->assertTrue($furiganaRule->passes('dummyLabel', 'ガ'));
$this->assertTrue($furiganaRule->passes('dummyLabel', ' '));
$this->assertTrue($furiganaRule->passes('dummyLabel', ' '));
$this->assertTrue($furiganaRule->passes('dummyLabel', 'ー'));
$this->assertTrue($furiganaRule->passes('dummyLabel', ''));
$this->assertTrue($furiganaRule->passes('dummyLabel', null));
$this->assertFalse($furiganaRule->passes('dummyLabel', []));
$this->assertFalse($furiganaRule->passes('dummyLabel', (object) []));
$this->assertFalse($furiganaRule->passes('dummyLabel', "\n"));
$this->assertFalse($furiganaRule->passes('dummyLabel', "あ\n"));
$this->assertFalse($furiganaRule->passes('dummyLabel', "\r"));
$this->assertFalse($furiganaRule->passes('dummyLabel', 'abc'));
$this->assertFalse($furiganaRule->passes('dummyLabel', '123'));
$this->assertFalse($furiganaRule->passes('dummyLabel', '振り仮名'));
}