PHPで業務システムや管理画面を作っていると、**「すでに確定(公開・ロック)しているデータは勝手に変更させたくない」**という仕様によく遭遇します。
しかし、画面からの入力(文字列や空文字の混在)とDBの値(型が曖昧だったり0埋めされていたり)をただ比較しようとすると、型変換の罠("00" のバグ)にハマったり、エディタ上(VS CodeのIntelephenseなど)で配列の中身が mixed になってしまってコード補完が効かなくなるストレスに直面しがちです。
今回は、これらの課題をスマートに解決する確実なフィルタリング手法とエディタの型推論(PHPDoc)の書き方をわかりやすくまとめました。
1. 実現したいこと(今回の仕様)
-
DB側の状態: 特定のカテゴリ(
id)ごとに、すでに確定(ロック)されている「月(target_month)」のデータが存在する。 - 画面からの入力: ユーザーが画面上のチェックボックスなどで変更を加えて送信してきた「月」の配列(空文字なども混ざる可能性がある)。
- 目的: 画面から送られてきたデータの中から、「DB側ですでにロックされている月」だけを安全に抽出し、不正な変更を削ぎ落とした配列に作り直す。
2. 実装コード:安全に月をフィルタリングする
さっそく、仕様を満たすクリーンなコードを見てみましょう。
(※変数名は分かりやすいように、DBデータを $dbItems、画面入力を $postMonths に整理しています)
// 1. 事前準備: DBから取得した「変更不可(ロック)」の月をIDごとにリスト化
$lockedMonthsById = [];
foreach ($dbItems as $row) {
// ※ エディタに型を教えるためのインライン型アノテーション(後述)
/** @var array{id: int, target_month: int, name: string} $row */
$id = $row['id'];
// DBから取得した月を「数値キャスト ➔ 2桁の0埋め文字列」に統一(例: 4 ➔ "04")
$lockedMonth = sprintf('%02d', (int)$row['target_month']);
// IDをキーにした二次元配列に格納
$lockedMonthsById[$id][] = $lockedMonth;
}
// 2. 画面入力のループ処理とフィルタリング
$filteredResults = [];
foreach ($targetIds as $index => $id) {
// 画面から送られてきた該当IDの月配列を取得(なければ空配列)
$months = $postMonths[$index] ?? [];
$name = $postNames[$index] ?? '';
// 【メイン処理】空文字を除外 + ロックされている月だけを生き残らせる
$cleanMonths = array_values(array_filter($months, function ($month) use ($id, $lockedMonthsById) {
// 条件①: 画面からの入力が空文字(未選択)なら即除外
if ($month === '') {
return false;
}
// 比較のために画面からの入力も「2桁の0埋め文字列」に揃える
$formattedMonth = sprintf('%02d', (int)$month);
// 現在処理しているIDに紐づく、ロックされた月のリストを取得
$lockedMonths = $lockedMonthsById[$id] ?? [];
// 条件②: ロックされている月リストの中に、今回の月が存在するかチェック
// 存在していれば true(配列に残る)、なければ false(配列から消える)
return in_array($formattedMonth, $lockedMonths);
}));
// 安全にフィルタリングされた配列を結果に格納
$filteredResults[$id] = [
'name' => $name,
'months' => $cleanMonths,
];
}
3. コードのポイントを初心者向けに解説
① array_filter と array_values の名コンビ
array_filter は、配列の要素を1つずつチェックし、中の関数(クロージャ)が true を返した要素だけを生き残らせる便利な関数です。
ただし、これを使うと生き残ったデータのインデックスが [0 => "04", 2 => "08"] のように歯抜け(連番が崩れる)になってしまいます。
最後に array_values() で包んであげることで、[0 => "04", 1 => "08"] のように綺麗に連番を振り直すのがPHP配列操作のお約束です。
② 表記ブレを破壊する sprintf('%02d', (int)$value)
画面からくる値が "4" や 4、DBからくる値が "04" のように表記がブレていると、in_array などの比較が正しく行われません。
一度 (int) で強制的に数値にしたあと、sprintf('%02d', ...) を通すことで、双方が必ず "04" や "12" のような「2桁の文字列」に揃うため、安全に比較ができます。
⚠️「00」の罠に注意!
もし画面から "abc" などの不正なゴミデータが紛れ込んだ場合、(int)"abc" は 0 になり、sprintf によって "00" に変換されてしまいます。
もしDB側にバグ等で "00" という値が存在すると、「ゴミデータとDBの値が一致した」と判定されてチェックをすり抜ける原因になります。画面からの入力が数値である保証がない場合は、事前に ctype_digit() などで数値チェックを挟むとより堅牢になります。
4. エディタで配列のキーが mixed になる問題を解決する
PHPで配列(array)を使っているとき、関数の戻り値(@return)に細かく構造を書いても、呼び出し側の foreach の中で個別のキー($row['id'] など)を取り出した瞬間に、エディタ上で mixed(型不明) に戻ってしまうことがあります。
// 関数の戻り値で細かく書いても、ループを跨ぐとエディタが型を忘れてしまう…
/** @return array{id: int, target_month: int, name: string}[] */
解決策:インライン型アノテーションの活用
エディタ(VS CodeのIntelephenseなど)は、複雑な配列が複数のループを跨ぐと推論を諦めてしまいます。
そのため、データを取り出す・使う直前(foreachの直前や内部)で、「この変数の中身はこれだよ!」ともう一度教えてあげる(インライン型アノテーション)のが最も確実です。
foreach ($dbItems as $row) {
// 💡 ループの先頭で、この変数の構造をエディタに明示する
/** @var array{id: int, target_month: int, name: string} $row */
// これで $row['id'] にカーソルを当てても mixed にならず int と認識される!
$id = $row['id'];
}
※ キー: 型 のコロンの後に半角スペースを入れる(id: int)のが、現在の多くのエディタで正しく認識させるコツです。
まとめ
- 画面入力の絞り込みには、
array_filter➔array_valuesのセットが最強。 - 文字列や数値の表記ブレは、
sprintf('%02d', (int)$var)で2桁の文字列に統一して安全に比較する。 - エディタが配列の型を忘れて
mixed迷子になったら、foreach内で/ @var ... */を書いてあげる。
これらを意識するだけで、バグが少なく、開発中のコード補完もサクサク効く快適なPHPコードが書けるようになります。ぜひ試してみてください!