【PHP8.0】StartsWith/EndsWithがPHP本体に実装される

先日PHP8で[str_containsが導入される](https://qiita.com/rana_kualu/items/cb758cdc21d60e632491)ことが決まったばかりですが、さらにもっと直接的な『〇〇で始まる』『〇〇で終わる』関数までも導入されることになりました。

[Add str_starts_with() and str_ends_with() functions](https://wiki.php.net/rfc/add_str_starts_with_and_ends_with_functions)というRFCが投票中です。
2020/05/04時点では賛成50反対4で、ほぼ導入確定です。


# PHP RFC: Add str_starts_with() and str_ends_with() functions

## Introduction

・`str_starts_with`は、文字列が指定の文字列で始まるか否かをチェックし、結果をbool値で返します。
・`str_ends_with`は、文字列が指定の文字列で終わるか否かをチェックし、結果をbool値で返します。

これらの機能は既存の文字列関数、たとえば[substr](http://www.php.net/substr)や[strpos](http://www.php.net/strpos)/[strrpos](http://www.php.net/strrpos)、そして[strncmp](http://www.php.net/strncmp)に[substr_compare](http://www.php.net/substr_compare)、あまつさえ[strlen](http://www.php.net/strlen)などを駆使して実装されてきました。
これらユーザランドの実装には、様々な問題点があります。

str_starts_withとstr_ends_withの需要は高く、[Symfony](https://symfony.com/doc/5.0/components/string.html#methods-to-search-and-replace)、[Laravel](https://laravel.com/docs/7.x/helpers#method-starts-with)、[Yii](https://www.yiiframework.com/doc/api/2.0/yii-helpers-basestringhelper#startsWith()-detail)、[FuelPHP](https://fuelphp.com/docs/classes/str.html#/method_starts_with)、そして[Phalcon](https://docs.phalcon.io/3.4/en/api/phalcon_text)と、あらゆるフレームワークによってサポートされています。

文字列の始めと終わりをチェックすることは非常に一般的なタスクであり、簡単に行えるべきです。
多くのフレームワークがこのタスクを実装しているということは、このタスクを実行することが簡単ではないことを意味しています。
JavaScript/Java/Haskell/Matlabといった多くの高水準言語が標準でこの機能を実装している理由でもあります。
文字列の開始と終了をチェックすることは、これだけのためにフレームワークを導入したり、ユーザランドで最適ではない(どころかバグが入るかもしれない)実装を行ったりする必要のあるべき作業ではありません。


### Downsides of Common Userland Approaches

この機能のアドホックな実装は、専用関数に比べると直感的ではありません。
PHPのニュービーや、他言語と同時開発する開発者にとっては特にそうです。
また、特に`===`を含む場合、実装を簡単に間違えます。
さらに多くのユーザランド実装はパフォーマンス上の問題があります。

注意：以下の実装には、E_WARNINGを防ぐために`$needle === "" || `や`strlen($needle) <= strlen($haystack) && `のようなガードを入れましょう。

#### str_starts_with

```php
substr($haystack, 0, strlen($needle)) === $needle
```

`$haystack`の無駄なコピーが発生するため、メモリ効率が良くありません。

```php
strpos($haystack, $needle) === 0
```

`$needle`が見つからなかった場合に`$haystack`を最後まで調べてしまうため、CPU効率が悪くなります。

```php
strncmp($haystack, $needle, strlen($needle)) === 0 // generic
strncmp($subject, "prefix", 6) === 0 // ad hoc
```

これは効率的ですが、`$needle`の文字数を別に渡す必要があり冗長です。

#### str_ends_with

```php
substr($haystack, -strlen($needle)) === $needle
```

str_starts_with同様、メモリ効率がよくありません。

```php
strpos(strrev($haystack), strrev($needle)) === 0
```

str_starts_with同様CPU効率が悪いだけでなく、文字列反転処理まで入るので、さらに非効率です。

```php
strrpos($haystack, $needle) === strlen($haystack) - strlen($needle)
```

冗長であり、CPURL効率が悪くなることがあります。

```php
substr_compare($haystack, $needle, -strlen($needle)) === 0 // generic
substr_compare($subject, "suffix", -6) === 0 // ad hoc
```

効率的ですが、冗長です。

## Proposal

2つの関数、`str_starts_with()`と`str_ends_with()`を導入します。

```php
str_starts_with ( string $haystack , string $needle ) : bool
str_ends_with ( string $haystack , string $needle ) : bool
```

`str_starts_with()`は、`$haystack`が`$needle`で始まるかどうかを調べます。
`strlen($needle) > strlen($haystack)`であれば即座にfalseを返し、そうでなければ両文字列を比較し、先頭一致すればtrueを、一致しなければfalseを返します。

`str_ends_with()`も同じですが、後方一致です。

以下に例を示します。

```php
$str = "beginningMiddleEnd";
if (str_starts_with($str, "beg")) echo "printed\n"; // true
if (str_starts_with($str, "Beg")) echo "not printed\n"; // false
if (str_ends_with($str, "End")) echo "printed\n"; // true
if (str_ends_with($str, "end")) echo "not printed\n"; // false
 
// 空文字
if (str_starts_with("a", "")) echo "printed\n"; // true
if (str_starts_with("", "")) echo "printed\n"; // true
if (str_starts_with("", "a")) echo "not printed\n"; // false
if (str_ends_with("a", "")) echo "printed\n"; // true
if (str_ends_with("", "")) echo "printed\n"; // true
if (str_ends_with("", "a")) echo "not printed\n"; // false
```

空文字に関しては、受理済の[str_containsのRFC](https://qiita.com/rana_kualu/items/cb758cdc21d60e632491)の挙動に従います。
これはJavaやPythonなどと共通の動作です。

## Backward Incompatible Changes

ユーザランドに同名の関数がある場合は競合します。

## Proposed PHP Version(s)

PHP8

## RFC Impact

・SAPI：全てのPHP環境に関数が追加されます
・エクステンション：無し
：Opcache：無し
・New Constants：無し
・php.ini Defaults：無し


## Votes

投票は2020/05/04まで。
投票者の2/3+1の賛成で受理されます。

## Patches and Tests

[https://github.com/php/php-src/pull/5300](https://github.com/php/php-src/pull/5300)


## References

他言語の類似機能
・JavaScript: [String#startsWith()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith) / [String#endsWith()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith)
・Python: [str#startswith()](https://docs.python.org/3/library/stdtypes.html#str.startswith) / [str#endswith()](https://docs.python.org/3/library/stdtypes.html#str.endswith)
・Java: [String#startsWith()](https://docs.oracle.com/javase/7/docs/api/java/lang/String.html#startsWith(java.lang.String)) / [String#endsWith()](https://docs.oracle.com/javase/7/docs/api/java/lang/String.html#endsWith(java.lang.String))
・Ruby: [String#start_with?()](https://ruby-doc.org/core-2.1.1/String.html#method-i-start_with-3F) / [String#end_with?()](https://ruby-doc.org/core-2.1.1/String.html#method-i-end_with-3F)
・Go: [strings.HasPrefix()](https://golang.org/pkg/strings/#HasPrefix) / [strings.HasSuffix()](https://golang.org/pkg/strings/#HasSuffix)
・Haskell: [Data.String.Utils.startswith](https://hackage.haskell.org/package/MissingH-1.4.0.1/docs/Data-String-Utils.html#v:startswith) / [Data.String.Utils.endswith](https://hackage.haskell.org/package/MissingH-1.4.0.1/docs/Data-String-Utils.html#v:endswith)
・MATLAB: [startsWith())](https://www.mathworks.com/help/matlab/ref/startswith.html) / [endsWith()](https://www.mathworks.com/help/matlab/ref/endswith.html)

bugs.php.net
・[bug #50434](http://bugs.php.net/bug.php?id=50434) / [bug #60630](http://bugs.php.net/bug.php?id=60630) / [bug #67035](http://bugs.php.net/bug.php?id=67035) / [bug #74449](http://bugs.php.net/bug.php?id=74449)

過去のRFC
・[PHP RFC: rfc:add_str_begin_and_end_functions](https://wiki.php.net/rfc/add_str_begin_and_end_functions)


## Rejected Features

大文字小文字を区別しない版とマルチバイト版は、以前のRFCには含まれていましたが、このRFCでは廃止されました。
理由は[str_contains](https://wiki.php.net/rfc/str_contains#case-insensitivity_and_multibyte_strings)を参照してください。


# 感想

PHPの文字列関数って[やたら大量に用意されてる](https://www.php.net/manual/ja/ref.strings.php)わりに意外と基本的なところが抜けていたのですが、PHP8でstr_continsとこの関数が追加されたことによって、テキスト処理に必要なものは出揃うことになったのではないでしょうか。

他に必要なのって何かありますかね。デフォルト関数の命名規則とか？