【PHP】8.xで追加された新関数が使えそう|具体的な使い方とよくあるミスを解説

PHP

実は私、PHP8を自己学習以外で触れたことがなく、7以前と何が違うのかあんまり考えてことなかったんです。
せっかく最新バージョンを使ってても、やってることがPHP5と何も変わっていないことが多々…といいますか…(業務で一番触っていたってのもある)。

調べてみると、現場で複数行に渡って書いていた処理がシンプルに書けるようになっていたので、これを機にまとめてみることにしました。

今回は個人的に「使える!」と感じた新関数7個に絞って、開発ストーリーと合わせて紹介します。

str_contains()

概要

項目内容
意味文字列に指定の文字が含まれているか調べる
追加バージョンPHP 8.0(2020年11月26日)
戻り値含まれていれば true、含まれていなければ false

具体的な用途

シナリオ:注文コメントに「ギフト」が含まれているかチェックしたい

ECサイトで、ユーザーが注文時に入力したコメントに「ギフト」という文字が含まれているか確認して、ギフト包装フラグを立てたいとします。

$comment = "誕生日ギフトとして送りたいです。のし包装をお願いします。";

// PHP 8.0以前
if (strpos($comment, 'ギフト') !== false) {
    $is_gift = true;
}

// PHP 8.0以降(str_contains)
if (str_contains($comment, 'ギフト')) {
    $is_gift = true;
    echo "ギフト包装フラグを設定しました";
}

strpos() !== false みたいな、否定の条件で制御するのよく見ますよね。
わからなくもないけど、できればもっとストレートにしたいところ。

str_contains() を使うことで、すんなり「何がしたいか」読めるコードになります。

よくあるミス

// ❌ 空文字列を渡すと常にtrueになる
if (str_contains($comment, '')) {
    // 常にtrueになるため、意図しない動作になる
}

// ✅ 空文字列チェックを先に行う
$keyword = $_GET['keyword'] ?? '';
if ($keyword !== '' && str_contains($comment, $keyword)) {
    // 安全
}

注意: $needle に空文字列 '' を渡すと、$haystack が何であっても常に true を返します。フォームからの入力値をそのまま渡す場合は、事前にバリデーションを行いましょう。

str_starts_with() / str_ends_with()

概要

項目内容
意味文字列が指定の文字列で始まる/終わるか調べる
追加バージョンPHP 8.0(2020年11月26日)
戻り値先頭/末尾が一致すれば true

具体的な用途

シナリオ:アップロードされた画像ファイル名の拡張子と接頭辞を検証したい

ユーザーがプロフィール画像をアップロードする機能で、ファイル名が img_ で始まり .jpg または .png で終わることを確認してから保存したいとします。

$filename = "img_profile_20240101.jpg";

// PHP 8.0以前(substr を使った書き方)
if (substr($filename, 0, 4) === 'img_') {
    echo "正しい接頭辞です";
}

// PHP 8.0以降(str_starts_with / str_ends_with)
if (str_starts_with($filename, 'img_')) {
    echo "正しい接頭辞です";
}
if (str_ends_with($filename, '.jpg') || str_ends_with($filename, '.png')) {
    echo "対応した拡張子です";
    // ファイル保存処理へ
}

ファイル名を substr() で判定するやり方は業務でもよく見かけました。
拡張子の判定ってアップロード処理では必須ですもんね…。
個人的な感覚では、str_ends_with() のほうが頻出しそう。

よくあるミス

// ❌ 大文字・小文字を区別してしまう
$filename = "IMG_profile.JPG";
if (str_ends_with($filename, '.jpg')) {
    // falseになる!大文字の .JPG は一致しない
}

// ✅ 小文字に統一してから比較する
if (str_ends_with(strtolower($filename), '.jpg')) {
    // 正しく動作する
}

注意: どちらの関数も大文字・小文字を区別(case-sensitive) します。ユーザー入力を扱う場合は strtolower() などで正規化してから使いましょう。

array_is_list()

概要

項目内容
意味配列が0始まりの連番キー(リスト)かどうか調べる
追加バージョンPHP 8.1(2021年11月25日)
戻り値0始まりの連番キーであれば true

具体的な用途

シナリオ:カートに入っている商品の合計金額を計算したい

ECサイトで、APIやDBから取得した商品データが「フラットな配列」なのか「商品IDをキーにした連想配列」なのかで処理を分岐させたいとします。

// パターンA:フラットなリスト(0始まり連番)
$cart_list = [
    ['name' => 'Tシャツ', 'price' => 3000],
    ['name' => 'パーカー', 'price' => 6000],
];

// パターンB:商品IDをキーにした連想配列
$cart_assoc = [
    101 => ['name' => 'Tシャツ', 'price' => 3000],
    205 => ['name' => 'パーカー', 'price' => 6000],
];

function calcTotal(array $cart): int {
    if (array_is_list($cart)) {
        $total = 0;
        foreach ($cart as $item) {
            $total += $item['price'];
        }
        return $total;
    } else {
        return array_sum(array_column($cart, 'price'));
    }
}

echo calcTotal($cart_list);  // 9000
echo calcTotal($cart_assoc); // 9000

これ一瞬「そんな使うことあるか?」ってなったんですが、要するに「構造がどんなもんかわかる=受け取るデータ形式が変わっても安全に処理を分岐できる」ってことです。

特にAPI連携では、呼び出し元によってフラット配列だったり連想配列だったりしますもんね。

よくあるミス

// ❌ unset した配列はリストではなくなる
$items = ['apple', 'banana', 'cherry']; // キー:[0, 1, 2]
unset($items[1]);
var_dump(array_is_list($items)); // false!キーが [0, 2] になっているため

// ✅ array_values() でキーを振り直す
$items = array_values($items);
var_dump(array_is_list($items)); // true

注意: 途中の要素を unset() した配列はキーが飛んでリストとみなされなくなります。unset 後は array_values() でキーを振り直す習慣をつけましょう。

json_validate()

概要

項目内容
意味JSON文字列がパースせずに妥当な形式かだけを検証する
追加バージョンPHP 8.3(2023年11月23日)
戻り値正しいJSON形式であれば true

具体的な用途

シナリオ:外部APIからのWebhookをDBに保存する前に検証したい

在庫管理システムで倉庫システムからJSONが送られてきます。変換は不要で「正しいJSON形式かどうか」だけを素早くチェックしたい場面です。

$raw_json = file_get_contents('php://input');

// PHP 8.3以前(json_decodeでチェック=不要なメモリ消費が発生)
$data = json_decode($raw_json);
if (json_last_error() !== JSON_ERROR_NONE) {
    http_response_code(400);
    exit;
}

// PHP 8.3以降(json_validate)
if (!json_validate($raw_json)) {
    http_response_code(400);
    echo "無効なJSONです";
    exit;
}

save_to_database($raw_json); // バリデーションOKなのでそのまま保存

json_decode() でチェックする方法、なんとなく使ってたんですがメモリの無駄だとは知らず…。

エラー内容を出力せず、検証だけでいいなら json_validate() のほうがシンプルだし、何より効率的。

よくあるミス

// ❌ エラー内容は取得できない
if (!json_validate($json)) {
    echo "JSONが無効です"; // なぜ無効なのかはわからない
}

// ✅ 詳細が必要な場合は json_decode() と組み合わせる
json_decode($json);
if (json_last_error() !== JSON_ERROR_NONE) {
    echo "JSONエラー:" . json_last_error_msg();
    // 例:JSONエラー:Syntax error
}

注意: json_validate() は「有効か無効か」のみ返します。エラー詳細が必要な場面では従来通り json_decode() + json_last_error_msg() を使いましょう。

array_find()

概要

項目内容
意味条件に一致する最初の要素の値を返す
追加バージョンPHP 8.4(2024年11月21日)
戻り値条件に一致した最初の、一致なしは null

具体的な用途

シナリオ:注文一覧から特定の注文IDのデータを1件だけ取り出したい

管理画面で、注文一覧の配列から「注文ID = 1023」のデータだけを取り出してメール送信処理に渡したいとします。

$orders = [
    ['id' => 1021, 'customer' => '山田太郎',   'total' => 5000],
    ['id' => 1022, 'customer' => '佐藤花子',   'total' => 12000],
    ['id' => 1023, 'customer' => '田中一郎',   'total' => 3500],
    ['id' => 1024, 'customer' => '鈴木次郎',   'total' => 8800],
];

// PHP 8.4以前(foreachで探す)
$target = null;
foreach ($orders as $order) {
    if ($order['id'] === 1023) {
        $target = $order;
        break;
    }
}

// PHP 8.4以降(array_find)
$target = array_find($orders, fn($order) => $order['id'] === 1023);

if ($target) {
    echo "{$target['customer']} さんへ注文確認メールを送信しました";
}

これ!!!!!! これですわ!!!!!!!!
めっちゃ待ってた!!!!! もう全部PHP8にしたい!!!!!!!

1件だけ取り出したいのに foreach やら break やら書いて、しかも条件が複雑になればなるほどネストが深くなって…(白目)。

array_find() を使えば1行で実現できますし、コードレビューでの指摘も減りそうです。

よくあるミス

// ❌ 複数件取得しようとしても1件しか返らない
$result = array_find($orders, fn($o) => $o['total'] > 5000);
// → 最初に一致した ['id' => 1022, ...] のみ返る

// ✅ 複数件が必要な場合は array_filter() を使う
$results = array_filter($orders, fn($o) => $o['total'] > 5000);
// → 条件に一致するすべての要素が返る

注意: array_find()最初に一致した1件のみを返します。複数件取得したい場合は array_filter() を使いましょう。

fdiv()

概要

項目内容
意味ゼロ除算でも例外を投げない安全な浮動小数点除算を行う
追加バージョンPHP 8.0(2020年11月26日)
戻り値除算結果(ゼロ除算時は INF / -INF / NAN

具体的な用途

シナリオ:売上レポートの達成率を計算したい

月次レポートで「目標売上に対する達成率」を計算する処理を作っています。目標が0円の月(新規プロジェクト開始月など)にエラーが発生しないようにしたいとします。

$monthly_data = [
    ['month' => '2024-01', 'target' => 500000, 'actual' => 420000],
    ['month' => '2024-02', 'target' => 0,       'actual' => 150000], // 目標未設定
    ['month' => '2024-03', 'target' => 600000, 'actual' => 630000],
];

foreach ($monthly_data as $data) {
    // PHP 8.0以前(ゼロチェックが必要)
    $rate_old = $data['target'] > 0
        ? ($data['actual'] / $data['target']) * 100
        : 0;

    // PHP 8.0以降(fdiv でシンプルに)
    $rate = fdiv($data['actual'], $data['target']) * 100;

    // INF(無限大)の場合は「目標未設定」と表示
    $display = is_finite($rate) ? round($rate, 1) . '%' : '目標未設定';
    echo "{$data['month']} : {$display}\n";
}
// 2024-01 : 84.0%
// 2024-02 : 目標未設定
// 2024-03 : 105.0%

割り算の前に「0じゃないか確認する」工程、親の顔より見てきました。
fdiv() に任せてしまえば、除算部分はすっきり書けるんですね~。

例だと無限大の処理として INF が使われてますが、ここまで丁寧に書いたことはないな…。

よくあるミス

// ❌ fdiv の戻り値をそのまま表示・DBに保存しない
$rate = fdiv(100, 0); // INF が返る
echo $rate;           // INF と表示されてしまう

// ✅ is_finite() / is_nan() で確認してから使う
$rate = fdiv(100, 0);
if (!is_finite($rate)) {
    $rate = 0; // または null、エラー処理へ
}

注意: fdiv() はゼロ除算でも例外を投げず INFNAN を返します。DBへの保存や画面表示の前に is_finite() で確認する処理を忘れないようにしましょう。

array_any() / array_all()

概要

項目内容
意味条件を満たす要素が1件でもあるか/全件満たすかを調べる
追加バージョンPHP 8.4(2024年11月21日)
戻り値array_any:1件でも一致すれば true / array_all:全件一致で true

具体的な用途

シナリオ:カート内に送料無料対象の商品があるか、または全商品が在庫ありかチェックしたい

ECサイトで「カート内に1点でも送料無料対象品があれば送料無料」「全商品が在庫ありのときだけ注文ボタンを有効にする」という処理を作りたいとします。

$cart = [
    ['name' => 'Tシャツ',   'free_shipping' => true,  'stock' => 5],
    ['name' => 'パーカー',   'free_shipping' => false, 'stock' => 0],
    ['name' => '帽子',      'free_shipping' => false, 'stock' => 3],
];

// PHP 8.4以前(array_filter + count で代用)
$is_free_shipping = count(array_filter($cart, fn($i) => $i['free_shipping'])) > 0;
$all_in_stock     = count(array_filter($cart, fn($i) => $i['stock'] > 0)) === count($cart);

// PHP 8.4以降(array_any / array_all)
$is_free_shipping = array_any($cart, fn($item) => $item['free_shipping']);
$all_in_stock     = array_all($cart, fn($item) => $item['stock'] > 0);

echo $is_free_shipping ? "送料無料" : "送料あり"; // 送料無料
echo $all_in_stock     ? "注文可能" : "在庫切れあり"; // 在庫切れあり

なんだこの便利な関数…。
今まで count(array_filter(...)) > 0 って、せかせか書いてた方は私だけじゃないはず。

array_any() / array_all() を使えば「1件でもあるか」「全件満たすか」が関数名だけでもなんとなくわかりますよね。

よくあるミス

// ❌ array_any() と in_array() の用途を混同しない
$names = ['Tシャツ', 'パーカー', '帽子'];

in_array('帽子', $names);                        // 単純な値の存在確認
array_any($names, fn($n) => str_contains($n, 'シャツ')); // 条件式での存在確認

// ✅ 条件が複雑な場合に array_any() / array_all() が威力を発揮する
$has_expensive = array_any($cart, fn($item) => $item['price'] > 10000);

注意: array_all() はコールバックが一度でも false を返した時点で処理を止めます。つまり全件ループするわけではないので、大量データでも無駄な処理が走りません。ただし、副作用のある処理(DBへの書き込みなど)をコールバック内に含めると、途中で止まって処理が中断されるため、コールバックは純粋な条件判定のみに留めましょう。

まとめ

PHP8系の新関数は「今まで冗長だった書き方を、その関数を見るだけで意図が伝わる・シンプルなコードに変える」ものが多いと感じました。

リファクタリング大好き人間としては、ぜひ活用したいところです。その前にPHPのバージョン上げようね…。

タイトルとURLをコピーしました