【PHP】もうswitch文は書かなくていいんだ【match式って知ってる?】

PHP

PHPを書いて早4年。何度も何度もお世話になった switch 文。
そのswitch文の進化系のような式が、PHPバージョン8から新たに追加されたんです!

それが match 式!

個人的にはこれが結構感動したもんで、簡単なカート計算ロジックを例に、使用感(?)をお伝えしようと思います。

概要

記事の執筆時点では、8.5が最新でした。

これ何がすごいって、match は「文」じゃなくて「式」だってことなんですよね。

式。
そう、つまり、値を返せるのです。

実際にswitch文と比較してみましょう。

switch ($rank) {
    case 'gold':
        $rate = 0.20;
        break;
    case 'silver':
        $rate = 0.10;
        break;
    default:
        $rate = 0.0;
}

親の顔より見たなぁ…。
現状のままでも十分わかりやすいですが、やや縦長なのが気になりますよね。

match式で書いてみるとどうでしょうか。

$rate = match ($rank) {
    'gold'   => 0.20,
    'silver' => 0.10,
    default  => 0.0,
};

10行が5行になってる!!!!
半分!!!!!
半分ですよ!!!!???

break はいらないし、$rate = も毎回書かなくていいし、なにより「 $rank が〇〇ならこの値ね!」ってのがわかりやすい。
初学者であればこっちの方が馴染むんじゃないだろうか。

それだけじゃないんです。
match式の本領はこんなもんじゃないんですよ。

実装例

せっかくなので、ECサイトあるあるの「割引ルール」をカート計算で実装してみます。
今回はアパレルの通販をイメージし、ルールを3つ用意しました。

  • Tシャツ:3個買うと10%オフ → 3個以上のまとめ買いで、その商品の合計から10%オフ
  • 靴下:5個買うと1個無料 → 5個ぶん買うと、もう1個おまけ(6個ごとに1個タダ)
  • その他商品:割引なし

商品ごとに「どのルールを適用するか?」を分岐させる必要があります。

これ、switchで書こうとすると、各 case の中で計算して変数に代入して break して…とやることになります。
分岐が「値を返す」構造じゃないので、どうしても縦に長くなってしまうわけです。

ここでmatchの出番です。

カートの定義

まずはカートに何が入っているか定義しましょう。

$cart = [
    ['name' => 'Tシャツ', 'price' => 2000, 'qty' => 3, 'rule' => 'buy3_10off'],
    ['name' => '靴下',    'price' => 500,  'qty' => 6, 'rule' => 'buy5_get1'],
    ['name' => '帽子',    'price' => 3000, 'qty' => 1, 'rule' => 'none'],
]

ここでは、商品名・価格・個数・その商品の割引ルールを持たせています。

ルールの関数

Tシャツと靴下のルールは関数に切り出しておきました。

Tシャツ

function buyThreeOffPrice($price, $qty) {

    // 割引率
    $discount_rate = 0.1;

    // しきい値
    $threshold = 3;

    $subtotal = $qty * $price;
    if($qty >= $threshold) {
        $subtotal *= 1 - $discount_rate;
    }

    return (int) round($subtotal);
}

靴下

function buyFiveGetOnePrice($price, $qty) {

    // しきい値
    $threshold = 6;

    // 何個分サービスできるか
    $free = intdiv($qty, $threshold);
    $subtotal = $price * ($qty - $free);

    return (int) $subtotal;
}

集計の関数

あとはカートの集計ロジックを作りましょう!
match式が活躍してくれます。

match式の場合

function applyDiscountRule($item) {

    $price = $item['price'];
    $qty = $item['qty'];

     return match ($item['rule']) {
        'buy3_10off' => buyThreeOffPrice($price, $qty),
        'buy5_get1' => buyFiveGetOnePrice($price, $qty),
        'none' => $qty * $price,
        // どのルールにも当てはまらなかったら割引なし
        default => $price * $qty,
    };
}

switch文の場合

function applyDiscountRule($item) {

    $price = $item['price'];
    $qty = $item['qty'];

    switch ($item['rule']) {
        case 'buy3_10off':
            $result = buyThreeOffPrice($price, $qty);
            break;
        case 'buy5_get1':
            $result = buyFiveGetOnePrice($price, $qty);
            break;
        case 'none':
            $result = $price * $qty;
            break;
        default:
            // どのルールにも当てはまらなかったら割引なし
            $result = $price * $qty;
    }

    return $result;
}

ほらもう全然違う!!!!!

関数をそのまま渡せるの、超便利じゃないですか??
ルールが増えても、行を1つ足すだけ。
読む側も「どのルールがどの計算か」わかりやすい。

やっぱりswitch文だと break がいちいち面倒なんですよね…。
これもこれで好きなんだけどさ…。

計上

最後に、各関数を使って計上していきたいと思います。

$total = 0;

foreach ($cart as $c) {
    $item_subtotal = applyDiscountRule($c);
    $total += $item_subtotal;

    echo $c['name'] . " " . $c['qty'] . "点 小計: " . $item_subtotal . "円" . "\n";
}

echo "合計 " . $total . "円" . "\n";

実行結果

Tシャツ 3点 小計: 5400円
靴下 6点 小計: 2500円
帽子 1点 小計: 3000円
合計 10900円

match式のすごいとこ

こうした分岐のロジックって業務でも結構登場しますよね。
PHPのバージョンさえよけりゃ、かなりコードの改善に役立つのではないでしょうか?

  1. 式だからそのまま return できちゃう
    • 計算結果をいったん変数に入れて、最後に返して…みたいな回り道は不要。
    • 「そのまま返せる」感じが楽でいいです。
  2. break を書き忘れる事故がない
    • 私のようなおとぼけエンジニアはまぁやります。
    • match はフォールスルー(次の分岐に流れる挙動)がそもそも無いので、break という概念自体が存在しません。書き忘れようがない。平和。
  3. 実は比較が厳密(===)
    • switch は処理の中で緩い比較(==)をするので、0 == “gold” みたいな意図しないマッチが起きることがあります(マジ?)。
    • その点 match は厳密比較(===)なので、型まで含めてきっちり比べてくれるらしい。
  4. 網羅されてないと、ちゃんと怒ってくれる
    • これが地味~に助かるんです。
    • match は「どの分岐にも当てはまらず、default も無い」場合、UnhandledMatchError という例外を投げてくれます。
    • switch だと黙ってスルーされて「なんかどっかで何かが処理から漏れてる…?? んだが…??」となりがちなところを、match は即座にアピールしてくれるわけです。
    • タイポや考慮漏れに早く気づけるので、むしろ default を敢えて書かないままテストしてみるのも手らしい。なるほど。
// default を書かないと…
$rate = match ($rank) {
    'gold'   => 0.20,
    'silver' => 0.10,
    // default  => 0.0,
};
// → $rank が 'bronze' のとき UnhandledMatchError

おまけ:カンマで複数値をまとめる

条件に合致する値がたくさんあるなら、カンマ区切りでいけます。

$shipping = match ($prefecture) {
    '北海道', '沖縄' => 1500, // 離島料金
    '東京', '神奈川', '千葉' => 0,    // 送料無料エリア
    default => 800,
};

実際のところ

こういうのってだいたいDBでしきい値を持ってることが常なので、記事のように数字がべた書きされることはなくって、きっと変数になります。
キャンペーン時の割引率とか、ころころ変わりますもんね。

とはいえあれだけ長かったswitch文がここまでコンパクトに、可読性も高く書けちゃうなら、match式を採用する手は大いにありだと思います。
レガシーな環境で開発してると、PHP8に出会うことが中々ないので…使えるタイミングが来たら絶対に使おう!と、今から意気込んでおきます。

ではまた次回!

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