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 > 19 => "Young adult",
$age >= 40 => "Old adult"
};
var_dump($output);
?>
上の例の出力は以下となります。
string(8) "Teenager"
注意:
match
式の結果は、必ずしも使う必要はありません。
注意:
match
式は、必ずセミコロン;
で終わらなければなりません。
match
式は、
switch
文と似ていますが、いくつかの違いがあります:
-
match
式の比較は、 switch 文が行う弱い比較ではなく、 厳密に値を比較(===
) します。 -
match
式は値を返します。 -
match
式の分岐は、switch
文のように後の分岐に抜けたりはしません。 -
match
式は、全ての場合を網羅していなければいけません。
switch
文のように、
match
式はマッチさせる分岐をひとつひとつ実行します。
はじめは、コードは何も実行されません。
以前のすべての条件式が、制約式とマッチしなかった場合に条件式が実行されます。
条件式に一致する式が評価された場合に、返却式が評価されます。
たとえば、以下のようになります:
<?php
$result = match ($x) {
foo() => ...,
$this->bar() => ..., // 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(33) "Unhandled match value of type int" ["string":"Error":private]=> string(0) "" ["code":protected]=> int(0) ["file":protected]=> string(9) "/in/ICgGK" ["line":protected]=> int(6) ["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"
User Contributed Notes 9 notes
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);
}
<?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"),
};
}
?>
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.
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
};
?>
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";
Yes it currently does not support code blocks but this hack works:
match ($foo){
'bar'=>(function(){
echo "bar";
})(),
default => (function(){
echo "baz";
})()
};
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, )
?>
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;
?>
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.