PHP入門:型・==と===・文字列・配列を実行結果で学ぶ
PHP 8.5 で実際に動かして確認。PHP 8 で変わった 0 == "a"、strict_types の TypeError、日本語の strlen と mb_strlen、配列キーの変換と foreach & の罠。
シリーズ:PHP講座 1/6(初級)。 配属先で PHP のコードを読み書きすることになった新人エンジニア向けです。読み終えると、スクリプトを実行し、PHP が値をどう比較・変換するかを予測でき、日本語の文字列を壊さずに扱い、PHP 初心者が必ず踏む配列の罠を2つ避けられるようになります。
社内の古い PHP システムを引き継いだ人、あるいは数年前の入門書で PHP を覚えた人は、「ネットの回答と動きが違う」と戸惑うことがあります。その原因の多くは、PHP 8 で == の動き、型宣言の扱い、そして「黙って通っていたミスが警告になる範囲」が変わったことにあります。以下の出力はすべて Apple Silicon Mac(M1 Pro)上の PHP 8.5.6 CLI で実際に実行した結果で、エラーメッセージ中のファイルパスだけ ~/demo/ に置き換えています。
5分で最初の PHP スクリプト
php -v でバージョンが表示されれば準備完了です(未導入なら macOS は Homebrew、Windows は windows.php.net の zip、Ubuntu は apt install php-cli php-mbstring が一般的です。この記事のために何かを入れ直してはいません)。hello.php を作ります。
<?php
echo "Hello, PHP " . PHP_VERSION . "\n";
$ php hello.php
Hello, PHP 8.5.6
ポイントは3つです。
- PHP のファイルは 開始タグ
<?phpで始まります。PHP だけのファイルでは閉じタグ?>を書きません。閉じタグの後ろの改行が余計に出力される事故を防げます。 echoは出力、ドット.は文字列の 連結 です。+は数値専用です。PHP_VERSIONは組み込みの 定数(値が変わらない名前)です。
1行だけ試したいときは php -r 'echo 1 + 1;' が便利です。以下でも何度か使います。
変数と型
変数 は $ で始めます。型は宣言せず、値が型を持っています。get_debug_type()(PHP 8.0 以降)はエラーメッセージと同じ言葉で型名を返します。
<?php
$n = 42; $f = 3.14; $s = "42"; $b = true; $x = null; $a = [1, 2];
foreach ([$n, $f, $s, $b, $x, $a] as $v) {
echo get_debug_type($v), " ";
}
echo "\n";
var_dump("10" + 5, "1.5" + 1, 7 / 2, intdiv(7, 2), 0.1 + 0.2 == 0.3);
int float string bool null array
int(15)
float(2.5)
float(3.5)
int(3)
bool(false)
var_dump() は型と値を同時に表示する、学習中いちばん使う道具です。他の言語から来た人が驚くのは2点です。数値形式の文字列 "10" に + 5 すると数値に変換されて 15 になること。そして / は割り切れなければ必ず float を返すことです。整数の割り算には intdiv() を使います。0.1 + 0.2 == 0.3 が false なのは、どの言語でも同じ2進浮動小数点の誤差です。
== と ===、PHP 8 で何が変わったか
== は 緩やかな比較 で、比べる前に片方を変換することがあります。=== は 厳密な比較 で、型と値が両方同じときだけ true です。定番のケースを PHP 8.5.6 で実行しました。
<?php
$cases = [
['0 == "a"', 0 == "a"], ['"1" == "01"', "1" == "01"],
['"10" == "1e1"', "10" == "1e1"], ['100 == "1e2"', 100 == "1e2"],
['null == false', null == false], ['[] == false', [] == false],
['"0" == false', "0" == false], ['"" == null', "" == null],
['1 === "1"', 1 === "1"], ['42 == "42abc"', 42 == "42abc"],
['42 == " 42"', 42 == " 42"], ['42 == "42 "', 42 == "42 "],
];
foreach ($cases as [$label, $r]) printf("%-16s %s\n", $label, var_export($r, true));
0 == "a" false
"1" == "01" true
"10" == "1e1" true
100 == "1e2" true
null == false true
[] == false true
"0" == false true
"" == null true
1 === "1" false
42 == "42abc" false
42 == " 42" true
42 == "42 " true
1行目が有名な PHP 8 の変更点です。PHP 7 までは 0 == "a" で "a" が数値 0 に変換されて true になり、in_array("abc", [0]) まで true になって、ログイン判定などを壊していました。PHP 8.0 からは、文字列が数値形式でなければ 数値の方を文字列に変換 して比べるので false です(今回実行したのは 8.5 だけです。PHP 7 の挙動は RFC「Saner string to number comparisons」の記述によります)。
ただし残りの行も見てください。数値に見える文字列同士は今も数値として比べるので、"1" == "01" も "10" == "1e1" も true です。商品コードや会員番号、パスワードなら立派なバグです。null・false・""・"0"・[] はどれも false と緩やかに等しくなります。
| 比べるもの | 使うもの |
|---|---|
| ID・コード・トークン・ユーザー入力 | ===(秘密値は hash_equals()) |
| 配列の中を探す | in_array($x, $arr, true)、array_search($x, $arr, true) |
| 値がないかどうか | $x === null、または isset() / ??(第2回) |
| フォームから文字列で届く数値 | 先に (int) や filter_var で変換し、=== |
既定は ===。== は変換の中身を説明できるときだけ使う、と覚えておけば十分です。
declare(strict_types=1) と実際の TypeError
関数の引数と戻り値に型を書けます。既定は 型変換モード(coercive mode) で、変換できる値なら黙って変換します。
<?php
function addTax(int $price): int {
return (int) round($price * 1.1);
}
var_dump(addTax("1000"));
var_dump(addTax(1000.0));
var_dump(addTax("1000 yen"));
int(1100)
int(1100)
Fatal error: Uncaught TypeError: addTax(): Argument #1 ($price) must be of type int, string given, called in ~/demo/loose.php on line 7 and defined in ~/demo/loose.php:2
Stack trace:
#0 ~/demo/loose.php(7): addTax('1000 yen')
#1 {main}
thrown in ~/demo/loose.php on line 2
"1000" は何も言われずに 1000 になりました。ファイルの 最初の文 に declare(strict_types=1); を書くと、同じ呼び出しが拒否されます。
<?php
declare(strict_types=1);
function addTax(int $price): int {
return (int) round($price * 1.1);
}
var_dump(addTax(1000));
var_dump(addTax("1000"));
int(1100)
Fatal error: Uncaught TypeError: addTax(): Argument #1 ($price) must be of type int, string given, called in ~/demo/strict.php on line 8 and defined in ~/demo/strict.php:4
読み方:エラーの種類は TypeError、「Argument #1 ($price)」がどの引数か、「called in … line 8」が悪い値を渡した場所です。直すのは関数ではなく呼び出し側です。
初心者が見落としやすい点が2つあります。
- strict_types は 宣言したファイルから行う呼び出し に効きます。関数が定義されたファイルではありません。strict なライブラリでも、strict でないファイルから呼べば変換されます。
- strict モードでも
int→floatは許されます(float $xに5を渡すとfloat(5))。逆に型変換モードでint引数に1.5を渡すと、Deprecated: Implicit conversion from float 1.5 to int loses precisionが出て値はint(1)になります。この Deprecated は PHP 8.1 で追加されました。
ターミナルでは同じエラーが PHP 付きでもう一度出ることがあります。CLI がログ(標準エラー)と画面の両方に書いているためで、記事では重複を省きました。
文字列:引用符・変数展開・ヒアドキュメント
<?php
$name = "Mina";
$n = 3;
echo 'Hi $name\n', "\n";
echo "Hi $name, you have {$n} new messages\n";
$user = ['name' => 'Mina'];
echo "Hi {$user['name']}\n";
echo <<<TXT
Dear $name,
You have {$n} items.
TXT;
echo "\n";
echo <<<'RAW'
No $interpolation here\n
RAW;
Hi $name\n
Hi Mina, you have 3 new messages
Hi Mina
Dear Mina,
You have 3 items.
No $interpolation here\n
- シングルクォート:ほぼ何も解釈しません。
$nameも\nも書いたまま。 - ダブルクォート:変数が 展開(値に置き換え)され、
\nなどのエスケープが効きます。単純な変数以外は{…}で囲みます。 - ヒアドキュメント
<<<TXTは複数行のダブルクォート文字列、Nowdoc<<<'RAW'はそのシングルクォート版です。PHP 7.3 から終了マーカーをインデントでき、その分のインデントが各行から取り除かれます。
日本語の文字列:strlen と mb_strlen
PHP の文字列は バイト列 です。strlen() はバイト数を数えます。人が読む文字列には mbstring 拡張の mb_(マルチバイト)関数が必要です。
<?php
foreach (["hello", "こんにちは", "안녕하세요", "👍🏽"] as $s) {
printf("%-12s strlen=%2d mb_strlen=%d\n", $s, strlen($s), mb_strlen($s));
}
$s = "こんにちは";
var_dump(substr($s, 0, 4), mb_substr($s, 0, 2));
var_dump(strrev("안녕"));
var_dump(mb_strwidth("こんにちは"), grapheme_strlen("👍🏽"));
hello strlen= 5 mb_strlen=5
こんにちは strlen=15 mb_strlen=5
안녕하세요 strlen=15 mb_strlen=5
👍🏽 strlen= 8 mb_strlen=2
string(4) "こ�"
string(6) "こん"
string(6) "��눕�"
int(10)
int(1)
出力そのものが証拠です。printf("%-12s") は バイト数 で桁をそろえるので、日本語と韓国語の行だけずれています。かな・漢字は UTF-8 で1文字3バイト。substr($s, 0, 4) は2文字目を途中で切って不正なバイト(�)を作り、strrev は文字ではなくバイトを逆順にしました。肌の色付きの絵文字はコードポイント2つ(mb_strlen = 2)ですが見た目は1文字で、1と答えるのは intl 拡張の grapheme_strlen() だけです。mb_strwidth は全角を幅2として数えるので、固定幅の帳票レイアウトに使えます。
実務のルール:氏名の文字数制限、「先頭20文字」のプレビュー、英字以外の大文字化には、mb_strlen・mb_substr・mb_strtoupper を使います。新しい関数もあります。mb_str_pad(8.3)、mb_trim と mb_ucfirst(8.4)です。8.5 で試すと、trim(" 全角スペース ") は全角スペースを残し、mb_trim は取り除きました。フォームでよく混ざる全角スペース対策に効きます。
配列は「順序付きマップ」
PHP の 配列 は C や Java のリストとは違い、順序付きマップ です。キー(int か string)と値の組を、入れた順に保持します。罠は、PHP が一部のキーを勝手に変換することです。
<?php
$a = [];
$a["1"] = "string one";
$a[1] = "int one";
$a[true] = "bool true";
$a["01"] = "string 01";
$a[1.7] = "float 1.7";
$a[null] = "null";
$a["1.5"] = "string 1.5";
var_dump($a);
Deprecated: Implicit conversion from float 1.7 to int loses precision in ~/demo/arr.php on line 7
Deprecated: Using null as an array offset is deprecated, use an empty string instead in ~/demo/arr.php on line 8
array(4) {
[1]=>
string(9) "float 1.7"
["01"]=>
string(9) "string 01"
[""]=>
string(4) "null"
["1.5"]=>
string(10) "string 1.5"
}
7回代入して、キーは4つ。"1"・1・true・1.7 はすべて整数キー 1 になり、前の値を上書きしました。"01" と "1.5" は正規の整数表記ではないので文字列のまま。null は "" になり、PHP 8.5 ではこれに Deprecated が出るようになりました。
実務で効いてくるのはここです。DB から取った会員 ID("100"、"200")をキーにすると整数キーになり、array_merge() は整数キーを振り直します。
<?php
$byId = ["100" => "Alice", "200" => "Bob"];
print_r(array_merge($byId, ["300" => "Chen"]));
print_r($byId + ["300" => "Chen"]);
Array
(
[0] => Alice
[1] => Bob
[2] => Chen
)
Array
(
[100] => Alice
[200] => Bob
[300] => Chen
)
キーを残したいなら +(または array_replace)を使います。また ["a", "b", "c"] に unset($list[1]) するとキーは 0 と 2 だけになり、array_is_list()(8.1 以降)は false、次の $list[] = "d" はキー 3 に入りました。番号を詰めるには array_values() です。
よくある間違い:foreach の参照で最後の要素が重複する
全商品の価格を2倍にしてから表示したい、というよくあるコードです。
<?php
$prices = [100, 200, 300];
foreach ($prices as &$p) {
$p = $p * 2;
}
foreach ($prices as $p) {
echo $p, " | ";
}
echo "\n";
print_r($prices);
200 | 400 | 400 |
Array
(
[0] => 200
[1] => 400
[2] => 400
)
実際に起きていること: &$p によって $p は各要素への 参照(同じ記憶領域の別名)になります。1つ目のループが終わっても $p は $prices[2] への参照のままです。2つ目のループは各要素を「$p に」代入するので、実際には $prices[2] に 200、400 と書き込み、最後は $prices[2] を自分自身にコピーします。600 は警告もなく消えます。
修正: 参照ループの直後に unset($p);。そもそも参照を使わない方がより安全です。
<?php
$prices = [100, 200, 300];
$doubled = array_map(fn($x) => $x * 2, $prices); // new array
foreach ($prices as $k => $v) { $prices[$k] = $v * 2; } // in place, by key
echo implode(" ", $doubled), " / ", implode(" ", $prices), "\n";
200 400 600 / 200 400 600
未定義の変数・キーの Warning
PHP 8 では、「存在しないものを使った」ミスの多くが、目立たない Notice から Warning に格上げされました。スクリプトは止まらないので、警告をきちんと読む必要があります。
<?php
echo $count + 1, "\n";
$user = ["name" => "Mina"];
echo $user["email"], "|\n";
$list = [10, 20];
echo $list[5], "|\n";
$s = "abc";
echo $s[10], "|\n";
$total = null;
echo $total["sum"], "|\n";
echo $user["email"] ?? "(no email)", "\n";
Warning: Undefined variable $count in ~/demo/undef.php on line 2
1
Warning: Undefined array key "email" in ~/demo/undef.php on line 4
|
Warning: Undefined array key 5 in ~/demo/undef.php on line 6
|
Warning: Uninitialized string offset 10 in ~/demo/undef.php on line 8
|
Warning: Trying to access array offset on null in ~/demo/undef.php on line 10
|
(no email)
未定義の $count は null として扱われ、null + 1 で 1 が出ました。変数名の打ち間違いがクラッシュではなく「合計値が違う」というバグになる仕組みです。最後の行が、正当に存在しないこともあるキーへの対処法、null 合体演算子 ?? です。左辺が存在しないか null なら右辺を返し、警告は出ません。第2回でフォーム入力に使います。
実測:文字列の組み立てと配列の検索
どの習慣が効くのかを知るための小さな計測です。条件:数値文字列10万件(mt_srand(42))、各ケース7回実行の中央値、PHP 8.5.6 CLI、Apple Silicon Mac(M1 Pro)。CLI では OPcache と JIT が既定で無効で、その状態で測っています。JIT を有効にした実行でも大差はありませんでした(in_array が 53〜54 ms から 47.9 ms、他は 0.1 ms 以内の差)。
<?php
mt_srand(42);
$words = [];
for ($i = 0; $i < 100_000; $i++) $words[] = (string) mt_rand(1, 999_999);
function bench(callable $fn, int $runs = 7): float {
$t = [];
for ($r = 0; $r < $runs; $r++) {
$s = hrtime(true); $fn(); $t[] = (hrtime(true) - $s) / 1e6;
}
sort($t);
return $t[intdiv($runs, 2)];
}
printf("concat: %.3f ms\n", bench(function () use ($words) {
$out = ''; foreach ($words as $w) { $out .= $w . ','; } return $out; }));
printf("implode: %.3f ms\n", bench(fn() => implode(',', $words)));
// 実際のファイルでは foreach 合計と array_sum、in_array と isset も計測
| 処理(10万件) | 中央値・1回目 | 中央値・2回目 |
|---|---|---|
ループ内で .= |
2.872 ms | 2.856 ms |
implode() |
0.784 ms | 0.775 ms |
foreach で合計 |
2.354 ms | 2.363 ms |
array_sum() |
1.811 ms | 1.811 ms |
1万件の配列に in_array を1,000回 |
53.29 ms | 54.10 ms |
array_flip した配列に isset を1,000回 |
0.021 ms | 0.022 ms |
implode() は .= の約3.7倍、array_sum() はループの約1.3倍速いという結果でした。差は本物ですが、体感できるのは数十万件規模からです。本当に効くのは最後の組です。in_array は毎回リスト全体をなめますが、array_flip() で作った配列への isset($set[$key]) はハッシュ検索なので、今回は約 2,500倍 速く、リストが長いほど差が広がります(前述のキー変換により、数値文字列は flip 後に整数キーになります。isset では問題ありませんが、キーを取り出して使うときは注意)。
正直な限界・バージョンの注記
- すべて Apple M1 Pro の macOS 上、PHP 8.5.6 CLI(Homebrew ビルド、mbstring と intl あり)で実行しました。PHP 7 や古い 8.x は 実行していません。
0 == "a"の PHP 7 での結果と以下の「追加バージョン」は、PHP マニュアルと RFC に基づく記述です。 - バージョン:文字列と数値の比較変更・
get_debug_type()・未定義変数/キーの Warning 化(8.0)、float→int の Deprecated・array_is_list()(8.1)、mb_str_pad(8.3)、mb_trim・mb_ucfirst(8.4)、「Using null as an array offset is deprecated」(8.5)。strict_typesは 7.0 からあります。 - 計測は1台のマシンでの7回の中央値で、共有サーバーでは値が変わります。CLI 既定どおり OPcache なしで測りました。OPcache と JIT は第6回で改めて測ります。
grapheme_strlenには intl 拡張が必要です。格安レンタルサーバーでは無効なこともあるのでphp -mで確認してください。
練習問題
1. 実行せずに結果を予想し、php -r で確かめてください:"abc" == 0、"1" == "01"、"10" == "1e1"、null == false、"abc" == "ABC"。
解答
false、true、true、true、false(PHP 8.5.6)。"abc" == 0 は PHP 8 から false。数値形式の文字列2つは数値として比べられます。null == false は bool としての緩やかな比較。文字列比較は大文字小文字を区別します。=== なら5つとも false です。
2. 会員登録フォームで「氏名は10文字まで」を strlen($name) <= 10 で判定しています。8文字の「たなかはなこさん」が弾かれるのはなぜで、どう直しますか。
解答
strlen("たなかはなこさん") は 24 バイト(8 × 3)なので判定に落ちます。mb_strlen($name) <= 10 に直します。mb_strlen を使った fits() 関数で試すと、この名前に true を返しました。
3. ["1" => "a", 1 => "b", "1.0" => "c", true => "d"] の要素数と、キーは何になりますか。
解答
2つです。"1"・1・true はどれも整数キー 1 になり、最終的な値は "d"。"1.0" は正規の整数表記ではないので文字列キーのままです。print_r は [1] => d と [1.0] => c を表示しました。
次回へ
- 次回:PHP講座 2/6:関数・フォーム・ビルトインサーバー
- シリーズの続き:クラス・インターフェース・enum、PDO・SQLite と SQL インジェクション、XSS・CSRF・セッション、OPcache・JIT・プロファイリング
- 他の言語の第1回:Python:型とミュータブル、Java:型・文字列・等価性
- 実務で出会う PHP の型の驚き:プリペアドステートメントで JSON の数値型が変わる
よくある質問
PHP 8 で 0 == "a" は true ですか、false ですか?
false です。PHP 8.0 から、数値と「数値形式でない文字列」を比べるときは数値の方を文字列に変換して文字列同士で比べます。PHP 7 では "a" が 0 に変換されて true でした。ただし数値形式の文字列同士は今も数値として比べるので、PHP 8.5 でも "1" == "01" と "10" == "1e1" は true です。だから既定は === にします。
5文字の日本語なのに strlen() が 15 を返すのはなぜですか?
strlen() は文字数ではなくバイト数を数えるからです。UTF-8 ではひらがな・漢字は1文字3バイトなので、こんにちは は strlen=15、mb_strlen=5 でした。画面に出す文字列には mb_strlen や mb_substr などの mb_ 関数を使います。
foreach の後で配列の最後の要素が重複するのはなぜですか?
foreach ($a as &$v) の後も $v は最後の要素への参照のまま残ります。続く foreach ($a as $v) が毎回その最後の要素に値を書き込むため、[200, 400, 600] が [200, 400, 400] になりました。参照ループの直後に unset($v) を書くか、array_map やキー指定の代入で参照を使わないようにします。