match

(PHP 8)

match 式は、値の一致をチェックした結果に基づいて評価結果を分岐します。 switch 文と似ていますが、 match 式は複数の候補と比較される制約式を持ちます。 switch 文とは異なり、 三項演算子のように値を評価します。 switch 文とは異なり、 弱い比較(==)ではなく、 型と値の一致チェック(===) に基づいて行われます。 match 式は PHP 8.0.0 以降で利用可能です。

例1 match 式の構造

<?php
$return_value = match (制約式) {
    単一の条件式 => 返却式,
    条件式1, 条件式2 => 返却式,
};

例2 基本的な match 式の使い方

<?php
$food = 'cake';

$return_value = match ($food) {
    'apple' => 'This food is an apple',
    'bar' => 'This food is a bar',
    'cake' => 'This food is a cake',
};

var_dump($return_value);

上の例の出力は以下となります。

string(19) "This food is a cake"

例3 比較演算子と match 式を一緒に使う例

<?php
$age = 18;

$output = match (true) {
    $age < 2 => "Baby",
    $age < 13 => "Child",
    $age <= 19 => "Teenager",
    $age >= 40 => "Adult",
    $age > 19 => "Young adult",
};

var_dump($output);

上の例の出力は以下となります。

string(8) "Teenager"

注意:

match 式の結果は、必ずしも使う必要はありません。

注意:

match 式を単一の式として使う場合は、必ずセミコロン ; で終わらなければなりません。

match 式は、 switch 文と似ていますが、いくつかの違いがあります:

  • match 式の比較は、 switch 文が行う弱い比較ではなく、 厳密に値を比較(===) します。

  • match 式は値を返します。

  • match 式の分岐は、 switch 文のように後の分岐に抜けたりはしません。

  • match 式は、全ての場合を網羅していなければいけません。

switch 文のように、 match 式はマッチさせる分岐をひとつひとつ実行します。 はじめは、コードは何も実行されません。 以前のすべての条件式が、制約式とマッチしなかった場合にのみ、条件式が評価されます。 マッチした条件式に対応する返却式のみが評価されます。 たとえば、以下のようになります:

<?php
$result = match ($x) {
    foo() => 'value',
    $this->bar() => 'value', // foo() === $x であれば $this->bar() は呼び出されません
    $this->baz => beep(), // $x === $this->baz でなければ beep() は呼び出されません。
    // などなど
};

match 式の分岐は、複数の式をカンマ区切りで含めても構いません。 これは論理ORであり、複数の分岐の右辺を同じにする場合の短縮記法です。

<?php
$result = match ($x) {
    // この分岐は:
    $a, $b, $c => 5,
    // 以下の3つの分岐と等しい:
    $a => 5,
    $b => 5,
    $c => 5,
};

default パターンという特別な場合があります。 このパターンは前の分岐にマッチしなかったあらゆる場合にマッチします。 たとえば、以下のようになります:

<?php
$expressionResult = match ($condition) {
    1, 2 => foo(),
    3, 4 => bar(),
    default => baz(),
};

注意:

複数の default パターンがあった場合、 E_FATAL_ERROR が発生します。

match 式は、全ての場合を網羅していなければいけません。 制約式がどの分岐でも処理できなかった場合、 UnhandledMatchError がスローされます。

例4 処理されない match 式の例

<?php
$condition = 5;

try {
    match ($condition) {
        1, 2 => foo(),
        3, 4 => bar(),
    };
} catch (\UnhandledMatchError $e) {
    var_dump($e);
}

上の例の出力は以下となります。

object(UnhandledMatchError)#1 (7) {
  ["message":protected]=>
  string(22) "Unhandled match case 5"
  ["string":"Error":private]=>
  string(0) ""
  ["code":protected]=>
  int(0)
  ["file":protected]=>
  string(6) "script"
  ["line":protected]=>
  int(5)
  ["trace":"Error":private]=>
  array(0) {
  }
  ["previous":"Error":private]=>
  NULL
}

厳密な一致チェックを行わずに match 式を使う

制約式に true を指定することで、 厳密な一致チェックを行わずに match 式を使うことができます。

例5 整数の範囲に応じてmatch式を分岐させる一般的な使い方

<?php
$age = 23;

$result = match (true) {
    $age >= 65 => 'senior',
    $age >= 25 => 'adult',
    $age >= 18 => 'young adult',
    default => 'kid',
};

var_dump($result);

上の例の出力は以下となります。

string(11) "young adult"

例6 文字列の内容に応じてmatch式を分岐させる一般的な使い方

<?php

$text = 'Bienvenue chez nous';

$result = match (true) {
    str_contains($text, 'Welcome'), str_contains($text, 'Hello') => 'en',
    str_contains($text, 'Bienvenue'), str_contains($text, 'Bonjour') => 'fr',
    // ...
};

var_dump($result);

上の例の出力は以下となります。

string(2) "fr"
add a note

User Contributed Notes 12 notes

up
101
darius dot restivan at gmail dot com
5 years ago
This will allow for a nicer FizzBuzz solution:

<?php

function fizzbuzz($num) {
    print match (0) {
        $num % 15 => "FizzBuzz" . PHP_EOL,
        $num % 3  => "Fizz" . PHP_EOL,
        $num % 5  => "Buzz" . PHP_EOL,
        default   => $num . PHP_EOL,
    };
}

for ($i = 0; $i <=100; $i++)
{
    fizzbuzz($i);
}
up
84
Anonymous
5 years ago
<?php
function days_in_month(string $month, $year): int
{
    return match(strtolower(substr($month, 0, 3))) {
        'jan' => 31,
        'feb' => is_leap($year) ? 29 : 28,
        'mar' => 31,
        'apr' => 30,
        'may' => 31,
        'jun' => 30,
        'jul' => 31,
        'aug' => 31,
        'sep' => 30,
        'oct' => 31,
        'nov' => 30,
        'dec' => 31,
        default => throw new InvalidArgumentException("Bogus month"),
    };
}
?>

can be more concisely written as

<?php
function days_in_month(string $month, $year): int
{
    return match(strtolower(substr($month, 0, 3))) {
        'apr', 'jun', 'sep',  'nov'  => 30,        
        'jan', 'mar', 'may', 'jul', 'aug',  'oct', 'dec'  => 31,
        'feb' => is_leap($year) ? 29 : 28,
        default => throw new InvalidArgumentException("Bogus month"),
    };
}
?>
up
58
Hayley Watson
5 years ago
As well as being similar to a switch, match expressions can be thought of as enhanced lookup tables — for when a simple array lookup isn't enough without extra handling of edge cases, but a full switch statement would be overweight.

For a familiar example, the following
<?php

function days_in_month(string $month): int
{
    static $lookup = [
    'jan' => 31,
    'feb' => 0,
    'mar' => 31,
    'apr' => 30,
    'may' => 31,
    'jun' => 30,
    'jul' => 31,
    'aug' => 31,
    'sep' => 30,
    'oct' => 31,
    'nov' => 30,
    'dec' => 31
    ];

    $name = strtolower(substr($name, 0, 3));

    if(isset($lookup[$name])) {
        if($name == 'feb') {
            return is_leap($year) ? 29 : 28;
        } else {
            return $lookup[$name];
        }
    }
    throw new InvalidArgumentException("Bogus month");
}

?>

with the fiddly stuff at the end, can be replaced by

<?php
function days_in_month(string $month): int
{
    return match(strtolower(substr($month, 0, 3))) {
        'jan' => 31,
        'feb' => is_leap($year) ? 29 : 28,
        'mar' => 31,
        'apr' => 30,
        'may' => 31,
        'jun' => 30,
        'jul' => 31,
        'aug' => 31,
        'sep' => 30,
        'oct' => 31,
        'nov' => 30,
        'dec' => 31,
        default => throw new InvalidArgumentException("Bogus month"),
    };
}
?>

Which also takes advantage of "throw" being handled as of PHP 8.0 as an expression instead of a statement.
up
16
thomas at zuschneid dot de
3 years ago
While match allows chaining multiple conditions with ",", like:
<?php
$result = match ($source) {
    cond1, cond2 => val1,
    default => val2
};
?>
it seems not valid to chain conditions with default, like:
<?php
$result = match ($source) {
    cond1 => val1,
    cond2, default => val2
};
?>
up
19
php at joren dot dev
4 years ago
If you want to execute multiple return expressions when matching a conditional expression, you can do so by stating all return expressions inside an array.

<?php
    $countries = ['Belgium', 'Netherlands'];
    $spoken_languages = [
        'Dutch' => false,
        'French' => false,
        'German' => false,
        'English' => false,
    ];

    foreach ($countries as $country) {
        match($country) {
            'Belgium' => [
                $spoken_languages['Dutch'] = true,
                $spoken_languages['French'] = true,
                $spoken_languages['German'] = true,
            ],
            'Netherlands' => $spoken_languages['Dutch'] = true,
            'Germany' => $spoken_languages['German'] = true,
            'United Kingdom' => $spoken_languages['English'] = true,
        };
    }

    var_export($spoken_languages); 
    // array ( 'Dutch' => true, 'French' => true, 'German' => true, 'English' => false, )

?>
up
7
tolga dot ulas at tolgaulas dot com
2 years ago
Yes it currently does not support code blocks but this hack works:

match ($foo){
    'bar'=>(function(){
        echo "bar";
    })(),
    default => (function(){
        echo "baz";
    })()
};
up
6
Sbastien
3 years ago
I use match instead of storing PDOStatement::rowCount() result and chaining if/elseif conditions or use the ugly switch/break :

<?php

$sql = <<<SQL
    INSERT INTO ...
    ON DUPLICATE KEY UPDATE ...
    SQL;

$upkeep = $pdo->prepare($sql);

$count_untouched = 0;
$count_inserted = 0;
$count_updated = 0;

foreach ($data as $record) {
    $upkeep->execute($record);
    match ($upkeep->rowCount()) {
        0 => $count_untouched++,
        1 => $count_inserted++,
        2 => $count_updated++,
    };
}

echo "Untouched rows : {$count_untouched}\r\n";
echo "Inserted rows : {$count_inserted}\r\n";
echo "Updated rows : {$count_updated}\r\n";
up
7
mark at manngo dot net
4 years ago
While you can’t polyfill a language construct, you can mimic the basic behaviour with a simple array.

Using example 2 above:

<?php
    $food = 'apple';
    $return_value = match ($food) {
        'apple' => 'This food is an apple',
        'bar' => 'This food is a bar',
        'cake' => 'This food is a cake',
    };    
    print $return_value;
?>

… you can get something similar with:

<?php
    $food = 'apple';    
    $return_value = [
        'apple' => 'This food is an apple',
        'bar' => 'This food is a bar',
        'cake' => 'This food is a cake',
    ][$food];
    print $return_value;
?>
up
1
rodrigoliveirasantos11 at gmail dot com
3 months ago
You can enforce a type return on match by wrapping in a self-calling function, like this:

<?php

$operationId = 3;

$operationLabel = (fn (): string => match($operationId) {
    1 => 'Add',
    2 => 'Subtract',
    3 => ['Not string'], // Invalid case,
    default => 'Unknown'
})()

?>

In the above example, the following error will be throwed: 

Fatal error: Uncaught TypeError: (...): Return value must be of type string, array returned in /home/user/scripts/code.php:9
Stack trace: ...
up
0
bdkr at protonmail dot com
16 days ago
In response to yawarmushtaq52 at gmail dot com.... 

Switches were only ugly due to how people used them. Lot's of logic INSIDE THE SWITCH is never a good idea. K&R, as opposed to GNU or Whitesmiths doesn't help. The inability of people to think freely / pragmatically about what they've been taught. 

First things first, matches can be ugly too. 

<?php
match ($action) {
    'save'   => saveProduct(),

    'update' => (static function() {
        Log::info('Updating product...');
        return updateProduct();
    })(), 

    default  => throw new InvalidArgumentException("Unknown action: $action"),
};
?>

Here is how most people see switches.
<?php
switch (true) {
    case $age < 18:
        $discount = 0.20; // 20% minor discount
        break;
        
    case $age >= 65:
        $discount = 0.15; // 15% senior discount
        break;
        
    case $hasMembership && $age < 30:
        $discount = 0.10; // 10% young adult member discount
        break;
        
    default:
        $discount = 0.00;
        break;
}
?>

All 17 lines of it. But it always could've looked like...

<?php
switch (true) 
  {
  case $age < 18:                   { $discount = 0.20; break; }  // 20% minor discount
  case $hasMembership && $age < 30: { $discount = 0.10; break; }  // 10% young adult member discount 
  case $age < 30:
  case $age < 40:
  case $age < 50:                   { $discount = check_for_specials($age); break; }
  case $age < 65:                   { $discount = mid_years_specials($age); break; }
  case $age >= 65:                  { $discount = 0.15; break; }  // 15% senior discount
  default:                          { $discount = 0.00; }
  }
?>

Capable of more than the first example, easier to read, and 6 less lines. 
Additionally, "falling through" means it's easy for me to see from 19 to 50 and know what's going on without having to read from side to side.  It's an easy visual indicator to the logic. The equivalent match is more verbose. 

<?php
$discount = match (true) 
  {
  $age < 18                   => 0.20,
  $hasMembership && $age < 30 => 0.10,
  $age < 30                   => check_for_specials($age),
  $age < 40                   => check_for_specials($age),
  $age < 50                   => check_for_specials($age),
  $age < 65                   => mid_years_specials($age),
  $age >= 65                  => 0.15,
  default                     => 0.00,
  };
?>

Sure! We could collapse 19 > 50 for both examples. 

<?php
switch (true) 
  {
  case $age < 18:                   { $discount = 0.20; break; }
  case $hasMembership && $age < 30: { $discount = 0.10; break; }
  case $age < 50:                   { $discount = check_for_specials($age); break; }
  case $age < 65:                   { $discount = mid_years_specials($age); break; }
  case $age >= 65:                  { $discount = 0.15; break; }
  default:                          { $discount = 0.00; break; }
  }

$discount = match (true) 
  {
  $age < 18                   => 0.20,
  $hasMembership && $age < 30 => 0.10,
  $age < 50                   => check_for_specials($age),
  $age < 65                   => mid_years_specials($age),
  $age >= 65                  => 0.15,
  default                     => 0.00,
  };
?>

Do those really look that different? Is Switch really that ugly?
up
0
yawarmushtaq52 at gmail dot com
7 months ago
Since `match` is a better alternative in MOST cases for modern software, adding this comment so that if someone googles "`switch` in PHP" then this page also shows in the search results.
up
1
tm
4 years ago
If you are using a match expression for non-identity checks as described above make sure whatever you are using is actually returning `true` on success.

Quite often you rely on truthy vs. falsy when using if conditions and that will not work for match (for example `preg_match`). Casting to bool will solve this issue.
To Top