Claude Codeをに実装をお任せした時にハマったことです。
結論(先に解決策)
PHP は**「整数として正規な10進数文字列」を配列キーに使うと、自動的に int に変換**します。
$map = ['52000' => '名古屋圏'];
var_dump(array_keys($map)); // int(52000) ← string ではない!
そのため、後で === '52000'(文字列)で厳密比較すると永遠に false になり、foreach でのマッチが全部外れます。
対処はキーを使わないリスト形式に変えるのが一番安全です。
// NG: キーが int 化される
$map = ['52000' => '名古屋圏', '53000' => '大阪圏'];
// OK: list-of-arrays。キーを持たないので型が壊れない
$map = [
['code' => '52000', 'label' => '名古屋圏'],
['code' => '53000', 'label' => '大阪圏'],
];
foreach ($map as $item) {
if ($item['code'] === '52000') { /* マッチする */ }
}
症状
参照テーブルが「空っぽ」になる、特定コードのラベルだけ出ない、foreach でヒットしないのに目視ではキーが存在している——といった**「あるはずなのにマッチしない」**現象。
$regions = ['52000' => '名古屋圏', '53000' => '大阪圏'];
$code = '52000'; // API などから来る5桁の都市圏コード(文字列)
$label = null;
foreach ($regions as $key => $name) {
if ($key === $code) { // ← ここが絶対 true にならない
$label = $name;
}
}
var_dump($label); // NULL(名古屋圏が取れない)
isset($regions[$code]) 自体は通ることがあるので余計に混乱します(後述)。
原因
PHP マニュアルの仕様です。配列キーは int か string のどちらかしか取れず、次のキャストが行われます。
整数として妥当な値を含む文字列は、(先頭にゼロが付くなどの例外を除き)int にキャストされる。
つまり:
| 書いたキー | 実際に格納される型 |
|---|---|
'52000' |
int(52000) |
'01000'(先頭ゼロ) |
string('01000')(int として非正規なので文字列のまま) |
'52000abc' |
string(数値として非正規) |
' 52000'(空白) |
string |
'52000' は「正規な整数文字列」なので int 化されます。
そこへ === '52000'(string)をぶつけると、int(52000) === string('52000') は型が違うので false。これが「永遠にマッチしない」の正体です。
var_dump(52000 === '52000'); // bool(false) … 厳密比較は型も見る
var_dump(52000 == '52000'); // bool(true) … 緩い比較なら通る(が型は揃わない)
なぜ isset だと気づきにくいか
isset($arr['52000']) の 添字アクセスでも同じ int 化が起きるため、isset は true を返します。「キーは存在するのに === で比較すると無い」というねじれた状態になり、原因にたどり着きにくいのです。
$regions = ['52000' => '名古屋圏'];
var_dump(isset($regions['52000'])); // bool(true) … 添字も int 化されるため一致
// でも array_keys で取り出した key は int なので、=== '52000' は false
解決
方法1: キーを持たないリスト形式にする(推奨)
数値文字列を値として持てば、型は一切壊れません。前述の list-of-arrays が確実です。
$regions = [
['code' => '52000', 'label' => '名古屋圏'],
['code' => '53000', 'label' => '大阪圏'],
];
function region_label(array $regions, string $code): ?string {
foreach ($regions as $r) {
if ($r['code'] === $code) { // string 同士で確実にマッチ
return $r['label'];
}
}
return null;
}
方法2: どうしてもキーで引くなら型を揃える
連想配列のまま使いたい場合は、比較側も int にそろえるか、緩い比較に統一します。ただし「コードは文字列で扱う」という前提を崩すので、プロジェクト全体で一貫させてください。
// 添字アクセスなら int 化が両側で起きるので一致する
$label = $regions[$code] ?? null; // OK($code が '52000' でも int 52000 として引かれる)
// foreach で key を使うなら (string) で戻すか == にする
foreach ($regions as $key => $name) {
if ((string)$key === $code) { /* ... */ }
}
ただし
'01000'のような先頭ゼロのコードは int 化されないため、コード体系に5桁ゼロ埋め(都道府県コード等)が混在する場合は型が揺れます。**「コードは常に文字列、配列キーには使わない」**と決めてしまうのが事故が少ないです。
補足
- 同種の罠に、
json_decode($s, true)で得た配列でも数値文字列キーは int 化される、array_mergeで数値キーが振り直される、などがあります。**「PHP の配列キーは int か string、数値文字列は int に倒れる」**という一点を覚えておけば全部説明できます。 - ゼロ埋めコード(
'01000','13100'など行政コード)を扱うときは、キーにせず値として持ち、比較は必ず===で文字列同士にそろえるのが安全です。