【PHP8.0】もう`strpos($haystack, $needle)!==false`って書かなくていいんだ

ある文字列中に特定の文字列が存在するかを調べる方法として[strpos](https://www.php.net/manual/ja/function.strpos.php)が存在します。

しかし、そもそもstrposは『ある文字列中で特定の文字列が何文字目に出てくるか』を調べる関数であり、第一に使用目的が異なる関数です。
そしてこちらも有名な話ですが、先頭が一致すると0が返ってくるので、緩やかな比較ではfalseと区別されません。
PHPのよくある落とし穴のひとつです。

```php
if(strpos('放課後アトリエといろ', 'アトリエ')){
	echo '"放課後アトリエといろ"には"アトリエ"が含まれる'; // 表示される
}

if(strpos('放課後アトリエといろ', '放課後')){
	echo '"放課後アトリエといろ"には"放課後"が含まれる'; // 表示されない！！
}

if(strpos('放課後アトリエといろ', '放課後') !== false ){
	echo '"放課後アトリエといろ"には"放課後"が含まれる'; // 表示される
}
```

マニュアルにも大きく警告が出されています。
![01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26088/8ecfe364-dfea-513d-2dcc-72010ee680f1.png)

この状況がついに[PHP8で変わります](https://wiki.php.net/rfc/str_contains)。


# PHP RFC: str_contains

## Introduction

`str_contains`は、ある文字列に特定の文字列が含まれているかをチェックし、見つかったか否かによってtrue/falseいずれかのbooleanを返します。

ある文字列に特定の文字列が含まれているかをチェックするために、一般的には[strpos](http://php.net/manual/en/function.strpos.php)や[strstr](http://php.net/manual/en/function.strstr.php)が使用されます。
`str_contains`は凡そあらゆるプロジェクトで非常によく使われるユースケースなので、独自の関数を追加するに値するはずです。
`strpos`や`strstr`には、いくつかの欠点が存在します。

・あまり直感的ではない
・間違いをしやすい(特に!==の場合)
・新規PHP開発者が覚えにくい

このため、多くのPHPフレームワークは独自のヘルパ関数を提供しています。
このことこそが`str_contains`の重要性や必要性をよく示しています。


## Proposal

このRFCは、新しい関数`str_contains`を提唱します。

```php
	str_contains ( string $haystack , string $needle ) : bool
```

引数`$haystack`と`$needle`を取り、`$haystack`の中に`$needle`が存在するか否かをbooleanで返します。

```php
	str_contains("abc", "a"); // true
	str_contains("abc", "d"); // false
	
	// 空文字列はtrueになる
	str_contains("abc", "");  // true
	str_contains("", "");     // true
```

[空文字列の扱いについて](https://externals.io/message/108562#108646)。

> As of PHP 8, behavior of '' in string search functions is well defined, and we consider '' to occur at every position in the string, including one past the end. As such, both of these will (or at least should) return true. The empty string is contained in every string.

PHP8において、文字列検索関数において''の動作は明確に定義されており、終端を含む文字列全ての位置にマッチします。そのため、これらは両方ともtrueを返すべきです。空文字はあらゆる文字列に含まれています。 - Nikita Popov


## Case-insensitivity and multibyte strings

internalメーリングリストなどの議論において、この関数のマルチバイト対応版(mb_str_contains)は必要ないという結論に達しました。
理由として、この関数のマルチバイト版は非マルチバイト版と全く同じ動作になります。
文字列の見付かった位置によって動作が異なる場合は、マルチバイト版は異なる動作になります。
この関数は見付かった位置によって動作は変わらないので、マルチバイト版の必要はありません。

大文字と小文字を区別しない版については、需要が大文字と小文字を区別する版よりはるかに低いため、このRFCには含まれません。
区別しない版を取り込むと、有効なバリアントは`str_contains`/`mb_str_icontains`だけになります。
このように中途半端なバリアントをいきなり提供するとPHP開発者が混乱する可能性があるので、最初は小さく始めた方がよいでしょう。


## Backward Incompatible Changes

PHP自体に後方互換性のない変更はありません。

ユーザランドに同じ関数が実装されている場合、非互換の可能性があります。
しかし、そのようなグローバル関数はアプリケーションの起動プロセスのかなり早い段階で追加されるので、開発者はこの問題にすぐ気付くでしょう。


## Proposed PHP Version(s)

PHP 8


## Implementation

`str_contains`関数は、このプルリクで実装されています。
[https://github.com/php/php-src/pull/5179](https://github.com/php/php-src/pull/5179)


# 投票

投票は2020/03/16まで、受理には2/3の賛成が必要です。
2020/03/09時点では賛成43反対5で、まず間違いなく受理されます。


# 感想

よく考えたらずっと前からあってもおかしくない関数なのに、これまで存在してなかったのは不思議ですね。

実際のところ、文字数としてはわずか2字の差にすぎません。

```php
	if(strpos($heystack, $needle)!==false){
		echo '$heystackに$needleが含まれている';
	}

	// 同じ
	if(str_contains($heystack, $needle)){
		echo '$heystackに$needleが含まれている';
	}
```

しかし、読みやすさ、理解のしやすさという点では圧倒的に`str_contains`に分がありますね。


以下はマルチバイト対応関数が用意されない点の補足です。
マルチバイト対応`strpos`として[mb_strpos](https://www.php.net/manual/ja/function.mb-strpos)がありますが、文字が存在するか否かをチェックするだけであれば実は`strpos`でも問題ありません。

```php
	echo strpos('あい', 'い');    // 3
	echo mb_strpos('あい', 'い'); // 1

	echo strpos('あい', 'あ');    // 0
	echo mb_strpos('あい', 'あ'); // 0

	echo strpos('あい', 'う');    // false
	echo mb_strpos('あい', 'う'); // false
```

値が変わるのは『存在したときの文字数の数え方』だけです。
文字列の有無を確認するだけであれば、存在しない場合は常にfalse、存在する場合は常にint型となって差が出ないわけですね。

同様に`str_contains`も、存在しない場合は常にfalse、存在する場合は常にtrueなるため、あえて`mb_str_contains`を用意する必要はないというわけです。