PHP8では数値文字列の考え方が変わり、判定で後ろ空白がtrueを返すようになりました。
前の空白はOKなのに、なんで後ろ空白はダメなのか? 不思議に思っていた人にとってはひとつモヤモヤが解消されます。
具体的にはis_numeric()の判定結果が変わります。
2020年11月26日、5年ぶりにPHPのメジャーバージョンが上がりました。PHP8です。
メジャーアップデートだけに変更点も大きいですが、数値文字列の判定のように言われてみないとわからない変更もあります。
サンプルコードを実行して説明します。かんたんです。
後ろ空白も数値文字列になる
PHP7まで、数値文字列の後ろに半角スペースがあると数値文字列としてNGでした。
それなのに前に半角スペースがあるとOK。
なんじゃそら? と思っている人は多かったと思います。
それがPHP8では改善されて、前後の半角スペースは数値文字列としてOKになりました。
変更点はこれだけ。
数値文字列 | PHP7 | PHP8 |
---|---|---|
'123' | ○ | ○ |
' 123' | ○ | ○ |
'123 ' | ✕ | ○ |
' 123' '123 ' (前後の全角スペース) | ✕ | ✕ |
'1 23' | ✕ | ✕ |
たったこれだけなんですけど、影響の出るところは意外と多いです。
is_numeric() 関数
文字列同士の比較
型宣言
インクリメントとデクリメント演算
サンプルコードでどう変わるのか見ていきましょう。
is_numeric
is_numeric()は、数値文字列が数値としてOKかNGかを判定する関数です。
<?php
function dump( $val ) {
if ( is_numeric( $val ) ) {
var_dump( '[' . $val . '] is numeric.' );
} else {
var_dump( '[' . $val . '] is string.' );
}
}
dump( '123' ) ; // スペースなし
dump( '123 ' ) ; // 半角スペース(後)
dump( ' 123' ) ; // 半角スペース(前)
dump( '1 23' ) ; // 半角スペース(間)
dump( '123 ' ) ; // 全角スペース(後)
dump( ' 123' ) ; // 全角スペース(前)
string(17) "[123] is numeric."
string(17) "[123 ] is string."
string(18) "[ 123] is numeric."
string(17) "[1 23] is string."
string(19) "[123 ] is string."
string(19) "[ 123] is string."
string(17) "[123] is numeric."
string(18) "[123 ] is numeric."
string(18) "[ 123] is numeric."
string(17) "[1 23] is string."
string(19) "[123 ] is string."
string(19) "[ 123] is string."
PHP8では、後ろ半角スペースは数値文字列として判定されるようになってます。
数値文字列の比較
数値文字列の判定が変わったので、数値文字列の比較結果も変わります。
<?php
function comp( $val1, $val2 ) {
if ( $val1 < $val2 ) {
var_dump( '[' . $val1 . '] < [' . $val2 .']' );
} else {
var_dump( 'not [' . $val1 . '] < [' . $val2 .']' );
}
}
function equal( $val1, $val2 ) {
if ( $val1 == $val2 ) {
var_dump( '[' . $val1 . '] == [' . $val2 .']' );
} else {
var_dump( 'not [' . $val1 . '] == [' . $val2 .']' );
}
}
$a = '10';
$b = '20 ';
$c = ' 30';
comp( $a, $b );
comp( $a, $c );
comp( $b, $c );
$a = '10';
$b = '10 ';
$c = ' 10';
equal( $a, $b );
equal( $a, $c );
equal( $b, $c );
string(13) "[10] < [20 ]"
string(13) "[10] < [ 30]"
string(18) "not [20 ] < [ 30]"
string(18) "not [10] == [10 ]"
string(14) "[10] == [ 10]"
string(19) "not [10 ] == [ 10]"
string(13) "[10] < [20 ]"
string(13) "[10] < [ 30]"
string(14) "[20 ] < [ 30]"
string(14) "[10] == [10 ]"
string(14) "[10] == [ 10]"
string(15) "[10 ] == [ 10]"
『string(13) "[10] < [20 ]"の結果が同じじゃないか?』と思うかもしれませんが、これはたまたま。
これを信じてしまうとバグになります。
それを払拭するため、サンプルコードにイコール(==)のコードも追記しました。
ちなみに、イコール(===)を使うことはできません。これを使うと完全な文字列比較になってまったく別の結果になってしまいます。
インクリメント・デクリメント(算術計算)
数値文字列は、そのままでも数値として算術計算ができます。これも、数値文字列の判定が変わったので結果も変わります。
<?php
$a = '10';
$b = ' 10';
$c = '10 ';
$d = '1 0';
function increment( $val ) {
$temp = $val;
var_dump( '++[' . $temp . '] = ' );
var_dump( ++$temp );
}
function plus( $val ) {
var_dump( '[' . $val . '] + 100 = ' );
var_dump( $val + 100 );
}
function multiplied( $val ) {
var_dump( '[' . $val . '] * 20 = ' );
var_dump( $val * 20 );
}
increment( $a );
increment( $b );
increment( $c );
increment( $d );
plus( $a );
plus( $b );
plus( $c );
plus( $d );
multiplied( $a );
multiplied( $b );
multiplied( $c );
multiplied( $d );
string(9) "++[10] = "
int(11)
string(10) "++[ 10] = "
int(11)
string(10) "++[10 ] = "
string(3) "10 "
string(10) "++[1 0] = "
string(3) "1 1"
string(13) "[10] + 100 = "
int(110)
string(14) "[ 10] + 100 = "
int(110)
string(14) "[10 ] + 100 = "
PHP Notice: A non well formed numeric value encountered in /home/vagrant/sample.php on line 16
int(110)
string(14) "[1 0] + 100 = "
PHP Notice: A non well formed numeric value encountered in /home/vagrant/sample.php on line 16
int(101)
string(12) "[10] * 20 = "
int(200)
string(13) "[ 10] * 20 = "
int(200)
string(13) "[10 ] * 20 = "
PHP Notice: A non well formed numeric value encountered in /home/vagrant/sample.php on line 21
int(200)
string(13) "[1 0] * 20 = "
PHP Notice: A non well formed numeric value encountered in /home/vagrant/sample.php on line 21
int(20)
気になるところは、"10 " を使っても、警告は出るけど計算しているところ。
もうひとつ、"1 0" も計算しています。ただし空白より後ろは無視されて。
string(9) "++[10] = "
int(11)
string(10) "++[ 10] = "
int(11)
string(10) "++[10 ] = "
int(11)
string(10) "++[1 0] = "
string(3) "1 1"
string(13) "[10] + 100 = "
int(110)
string(14) "[ 10] + 100 = "
int(110)
string(14) "[10 ] + 100 = "
int(110)
string(14) "[1 0] + 100 = "
PHP Warning: A non-numeric value encountered in /home/vagrant/sample.php on line 16
int(101)
string(12) "[10] * 20 = "
int(200)
string(13) "[ 10] * 20 = "
int(200)
string(13) "[10 ] * 20 = "
int(200)
string(13) "[1 0] * 20 = "
PHP Warning: A non-numeric value encountered in /home/vagrant/sample.php on line 21
int(20)
PHP8では、後ろ空白も数値文字列になるので警告が消えます。
ただ、真ん中の空白の場合は同じ結果。個人的にはエラーを返してもいいんじゃないかと思います。
サンプルでは数値文字列で計算しましたが、ふつうはしません。『is_numeric()でチェックしてからキャストしろよ!』と怒られるレベルです。
理由は、中途半端に計算してくれるのがバグの温床になるから。
数値文字列のキャスト
さっき、『キャストしろよ!』と言ったのでキャストしてみます。
<?php
$a = '10';
$b = ' 10';
$c = '10 ';
$d = '1 0';
function cast( $val ) {
var_dump( '[' . $val . '] cast: ' );
var_dump( (int)$val );
}
cast( $a );
cast( $b );
cast( $c );
cast( $d );
string(11) "[10] cast: "
int(10)
string(12) "[ 10] cast: "
int(10)
string(12) "[10 ] cast: "
int(10)
string(12) "[1 0] cast: "
int(1)
キャストでは、PHP7と8で結果は同じです。
PHP7では、キャストすると後ろ空白は正しく数値化されてました。数値文字列と矛盾しています。
なんでキャストしろって言うのか意味が分かると思います。キャストのほうが一歩進んでたから。
PH8ではこの矛盾が解消されてます。
ただ、キャストでも真ん中空白は前の数字だけが数値化されます。これ、どうにかして欲しい。
自動キャストする型宣言
最初の方に言った、PHP8の変更は型宣言にも影響するという点について。
最初は???と思ってしばらく考え込んでしまいましたが、これは関数の型宣言の自動キャストのことです。
『キャスト使えよ!』と言いましたが、むしろこっちのほうが使う場面は多く、関数のパラメータや戻り値で型宣言すると、内部で自動的にキャストします。
<?php
$a = '-10';
$b = ' -10';
$c = '-10 ';
$d = '-1 0';
function sample( int $val ) {
var_dump( $val );
}
var_dump( '[' . $a . '] param auto cast: ' );
sample( $a );
var_dump( '[' . $b . '] param auto cast: ' );
sample( $b );
var_dump( '[' . $c . '] param auto cast: ' );
sample( $c );
var_dump( '[' . $d . '] param auto cast: ' );
sample( $d );
string(23) "[-10] param auto cast: "
int(-10)
string(24) "[ -10] param auto cast: "
int(-10)
string(24) "[-10 ] param auto cast: "
PHP Notice: A non well formed numeric value encountered in /home/vagrant/sample.php on line 8
int(-10)
string(24) "[-1 0] param auto cast: "
PHP Notice: A non well formed numeric value encountered in /home/vagrant/sample.php on line 8
int(-1)
string(23) "[-10] param auto cast: "
int(-10)
string(24) "[ -10] param auto cast: "
int(-10)
string(24) "[-10 ] param auto cast: "
int(-10)
string(24) "[-1 0] param auto cast: "
PHP Fatal error: Uncaught TypeError: sample(): Argument #1 ($val) must be of type int, string given, called in /home/vagrant/sample.php on line 22 and defined in /home/vagrant/sample.php:8
Stack trace:
#0 /home/vagrant/sample.php(22): sample()
#1 {main}
thrown in /home/vagrant/sample.php on line 8
PHP8では関数のパラメータの型の厳格化も行われ、数値文字列がNGのデータだけエラーになります。
PHP8では簡単にできる。数値文字列の型変換
PHP8では、数値文字列を数値に変換する処理は3行でできます。
function convert_string_to_numeric( int $val ) {
return $val;
}
convert_string_to_numeric( '123' );
convert_string_to_numeric( '12 3' ); // エラーになる
convert_string_to_numeric( 'abc' ); // エラーになる
is_numeric()を使ってチェックする必要もないし、関数のパラメータの型宣言を使えばキャストもする必要もなし。intval()を使う必要もない。
値が数値文字列じゃなかったら、TypeError例外をスローするのでエラー処理もいらない。
かなりシンプルにできるようになりました。
declare(strict_types=1); を追加して型宣言の厳密化をすると、自動キャストできません。
(パラメータのキャストの前に厳密な型チェックでエラーになる。)
ついでにintval()も見てみる
さっき作った関数を使えば使う必要がないですが、数値文字列を数値に変換するintval()があります。
ついでにこれも実行してみましょう。結果から言うとキャストと同じ結果になります。
<?php
$a = '10';
$b = ' 10';
$c = '10 ';
$d = '1 0';
$e = 'aaa';
function sample( $val ) {
var_dump( '[' . $val . '] intval: ' );
var_dump( intval( $val ) );
}
sample( $a );
sample( $b );
sample( $c );
sample( $d );
sample( $e );
int(10)
string(14) "[ 10] intval: "
int(10)
string(14) "[10 ] intval: "
int(10)
string(14) "[1 0] intval: "
int(1)
string(14) "[aaa] intval: "
int(0)
PHP7と8で結果は同じです。ただキャストとちがって、"aaa" みたいに数値じゃないものはエラーにならずに0を返します。
"1 0"のような間に半角スペースがあるものは前半部分で計算して警告すらなし。
個人的には使わないかな? キャストのほうがいいと思います。