PHP で「文字列に特定のキーワードが含まれているか」や「文字列中に特定の文字列を含むか」を確認したい場合、どのようなコードを書くだろうか?
もし、あなたが strpos()
やstrstr()
を使う方法を思いついたのなら、これだけは覚えて帰ってほしい。
文字列検索には str_contains() を使え。
- 結論:文字列検索には str_contains() を使う
- 昔はstrpos()やstrstr()などを使っていた
- strpos() や strstr() ではダメなのか?
- strpos() を使うべきでない理由
- strstr() を使うべきでない理由
- preg_match()を使うべきでない理由
- 「いや、キーワードが先頭にあるかを知りたいんだ」という人は
- 「日本語でも使えるの?」との疑問について
- もっと詳しく?
- これでわかっただろう
結論:文字列検索には str_contains() を使う
str_contains()
は、「指定した部分文字列が、文字列中に含まれるかを調べる」ための関数である。この関数はPHP8.0 で追加された。
php.net:str_contains
str_contains(string $haystack, string $needle): bool
needle が haystack に含まれるかを調べます。 大文字小文字は区別されます。
〜中略〜
haystack に needle が含まれていた場合 true そうでない場合、false を返します。
サンプル
<?php if( str_contains("なんだお前たち!", "なんだ") ) { echo "なんだかんだと聞かれたら、"; // 表示される!! echo "答えてあげるが世の情け"; } if( str_contains("誰だお前たち!!", "なんだ") ) { echo "なんだかんだと聞かれたら、"; // 表示されない echo "答えてあげるが世の情け"; }
<?php $result = str_contains("なんだお前たち!", "なんだ"); var_dump($result); // bool(true) $result = str_contains("誰だお前たち!", "なんだ"); var_dump($result); // bool(false)
上記の通り、検索結果が bool で返ってくる。とても明確な仕様だ。
名前もすばらしい。contains(含む、包含する)とあるので、読めば何をする関数かわかりやすい。
少なくともstrpos()
、strstr()
よりは直感的だ。
よって、文字列検索には str_contains() を使え。
昔はstrpos()
やstrstr()
などを使っていた
しかし、Google 検索で以下のような検索を行うとstrpos()
やstrstr()
(もしくは mb_strpos
や mb_strstr
)を提案するサイトが出てくる。
なぜか?
それはstr_contains()
が 2020/11/26 にリリースされた PHP8.0 で導入された関数だからだ。*1*2
それ以前のPHPにはstr_contains()
が存在せず、strpos()
や strstr()
を代替品として使う手法が一般的だったのだ。
しかし、2023年7月現在サポートされている PHP8.0、8.1、8.2 にはstr_contains()
が存在する。
つまり、str_contains()
が使えないバージョンのPHPはもうEOLを迎えているのだ。*3
そのため文字列検索には str_contains() を使え。
strpos()
や strstr()
ではダメなのか?
今まで使っていた
strpos()
やstrstr()
がなくなるわけではないから、今まで通り使っても問題ないでしょ??
ひょっとしたら、あなたはそう思うかもしれない。確かにもっともだ。
しかし、よくよく考えて欲しい。もともとstrpos()
は「文字列内の部分文字列が最初に現れる位置を見つける」ための関数だ。*4
また、strstr()
も「文字列が最初に現れる位置を見つけ、そこから文字列の終わりまでを返す関数」である。*5
つまり、これらの関数は、文字列が含まれているかを確認する関数ではない。本来の目的が純粋な検索ではないのだ。
紙を切るために包丁を使っている人を見たら「いやハサミを使いなよ!」と言いたくなるだろう?それと似た感覚だ。 違和感が伝わったのであれば、文字列検索には str_contains() を使え。
strpos()
を使うべきでない理由
もっと現実的な理由がある。
それは、strpos()
などを文字列検索に使うと、バグを生み出す可能性が高くなってしまうことだ。
strpos()
などの戻り値は、FALSE だけでなく 0 を返すこともあるのがその原因だ。
まず、strpos()
の定義を確認しよう。
strpos(string $haystack, string $needle, int $offset = 0): int|false
文字列 haystack の中で、 needle が最初に現れる位置を探します。
〜中略〜
needle が見つかった位置を、 haystack 文字列の先頭 (offset の値とは無関係) からの相対位置で返します。 文字列の開始位置は 0 であり、1 ではないことに注意しましょう。 needle が見つからない場合は false を返します。
ドキュメントには上記の説明に合わせて、以下のように警告も記載されている。
つまり、strpos()
関数は、「文字列の先頭にキーワードが存在する場合は 0」を返し、「文字列にキーワードが存在しない場合は FALSE」を返すのだ。
これがどれだけ危険なことか、あなたが PHPer であればわかるだろう。
❌ strpos() が意図しない動きをするコード
<?php if( strpos("お前たちは誰だ!", "なんだ") ) { echo "なんだかんだと聞かれたらああああ、"; // 表示されない echo "答えてあげるが世の情け"; // (意図通り) } if( strpos("なんだお前たち!", "なんだ") ) { echo "なんだかんだと聞かれたら\n"; // 表示されない!! echo "答えてあげるが世の情け"; // ("なんだ"を含むのに...) }
<?php $result = strpos("お前たちは誰だ!", "なんだ"); var_dump($result); // bool(false) ← "なんだ"がないので FALSE が返る $result = strpos("なんだお前たち!", "なんだ"); var_dump($result); // int(0) ← "なんだ"が文字列の先頭に存在するので 0 が返る!
上記の通り、文字列の先頭にキーワードが存在する場合は 0 が返ってくるので、 そのまま条件式として利用すると、"暗黙の型変換"が行われ意図していない比較結果になってしまう。
この問題はさまざまな記事で取り上げられているように、!==
による「厳密な比較」を行うことで回避は可能だ。
これにより、厳密に FALSE が返ってきた時のみ検知することができる。
✅ strpos() を意図通りに動かすコード
<?php if( strpos("なんだお前たち!", "なんだ") !== FALSE ) { echo "なんだかんだと聞かれたら\n"; // 表示される echo "答えてあげるが世の情け"; // (意図通りの挙動) }
もちろん、このとき !=
は使ってはいけない。0 が返ってきたときに true と判定してしまうからだ。
いかがだろうか?strpos()
を文字列検索目的に利用する場合、これだけのことを理解した上で利用する必要がある。
また、これらの考慮をうっかり忘れると、「キーワードが文字列の先頭にあるときだけ意図しない挙動をするバグ」を生み出すことになるだろう。
余計なことを考えながら実装するぐらいだったら、文字列検索には str_contains() を使え。
strstr()
を使うべきでない理由
strstr()
も同じだ。比較結果が意図しない値になる可能性がある。
strstr()
の定義は以下のとおり。
strstr(string $haystack, string $needle, bool $before_needle = false): string|false
haystack の中で needle が最初に現れる場所を含めてそこから文字列の終わりまでを返します。
〜中略〜
部分文字列を返します。 needle が見つからない場合は false を返します。
strpos()
の場合は以下のようなコードを書くことで意図しない動きをしてしまう。以下の例では、
検索対象である "0" が文字列中に存在するのに関わらず、文字列が表示されてしまう。
❌ strstr() が意図しない動きをするコード
<?php if( strstr("今すぐ買うべき技術書トップ10", "0") ) { echo "文字列中に 0 があるよ!"; // 意図せず表示されない!! }
<?php $returnVal = strstr("今すぐ買うべき技術書トップ10", "0"); var_dump($returnVal); // string(1) "0"
何より strstr という名前が検索っぽくない。それより文字列検索には str_contains() を使おう。
preg_match()
を使うべきでない理由
単純な文字列検索であれば、preg_match()
も使うべきでない。
preg_match( string $pattern, string $subject, array &$matches = null, int $flags = 0, int $offset = 0 ): int|false
pattern で指定した正規表現により subject を検索します。
〜中略〜
preg_match() は、pattern が指定した subject にマッチした場合に 1 を返します。 マッチしなかった場合は 0 を返します。 失敗した場合に false を返します
ここでの「失敗した場合」とは、指定した正規表現のパターンが不正な場合も含まれる。
つまり、preg_match()
では指定した正規表現が間違っていた場合は FALSE を返し、文字列にマッチしなかった場合は 0 を返すのだ。
よって前述の関数と同じように結果の比較で誤った実装を行なってしまう可能性が高くなってしまう。黙って文字列検索には str_contains() を使うべきだ。
「いや、キーワードが先頭にあるかを知りたいんだ」という人は
少なくともstrpos()
は危ない。文字列の先頭にキーワードがあるときは 0 、キーワードが存在しない場合は FALSE を返すため比較時にバグが発生しやすくなる。
この場合は、同じく PHP8.0 で追加されたstr_starts_with ()
を使おう。戻り値が bool のみであり安全になるだろう。*6
「日本語でも使えるの?」との疑問について
もちろん使える。str_contains()
の RFC にマルチバイト文字列についての記載がある。*7
安心して文字列検索には str_contains() を使え。
もっと詳しく?
この関数が PHP8.0 で実装されることになった詳しい経緯は、当時の RFC や マージリクエストを確認するといいだろう。 スモールスタートにするため大文字小文字を区別する仕様のみ実装したことや、マルチバイト文字への対応についての意見などが確認できる。
- PHP RFC: str_contains
- Internals Mailing Lists
- マージリクエスト
しっかりと議論が行われたことを確認したら、文字列検索には str_contains() を使え。
これでわかっただろう
文字列検索にはstr_contains()
を使うべき理由がわかっただろう。
str_contains()
の方が他の関数を使うより直感的であり、なにより無用な心配をしなくて済む。
なぜ今までこの関数がなかったのか不思議だが、実装されたからには積極的にstr_contains()
を使え。
str_contains()
がundefined function
でエラーになる??
... PHP のバージョンが 7.4?
なるほど。それじゃあ、しかたがない。諦めは心の養生というものだ。
Written by: Y-Kanoh
*1:https://www.php.net/archive/2020.php#2020-11-26-3
*2:https://www.php.net/manual/ja/function.str-contains.php
*3:https://www.php.net/supported-versions.php
*4:https://www.php.net/manual/ja/function.strpos.php
*5:https://www.php.net/manual/ja/function.strstr.php
*6:https://www.php.net/manual/ja/function.str-starts-with.php