- PHP
PHPの??(null合体演算子)の使い方 ??=・?->・?:との違い
PHPの ??(null合体演算子)とは、左側が「未定義」または「null」のときだけ右側の値を返す演算子です。 $name = $_GET['name'] ?? 'guest'; のように書くと、キーが無くてもWarningを出さずに既定値へ切り替えられます。isset() と三項演算子を組み合わせた書き方を、1つの記号にまとめたものと考えると理解しやすくなります。
この記事では、?? と見た目が似ている ?:(エルビス演算子)、??=(null合体代入演算子)、?->(nullsafe演算子)の違いを、PHP 8.4.21 で実際に動かした出力と一緒に整理します。null・未定義・0・空文字・false・空配列の6種類の値に対して、??、?:、isset、empty がそれぞれ何を返すかを1枚の表にし、"文字列" . $a ?? "x" が思ったとおりに動かない理由(演算子の優先順位)と括弧での直し方まで扱います。
??・?:・??=・?->の早見表
まずは4つの演算子の役割を一覧で確認します。「何を判定して切り替えるか」が演算子ごとに違う点が最大のポイントです。
| 演算子 | 呼び方 | 右側に切り替わる条件 | 未定義でのWarning | 使えるバージョン | 代表的な書き方 |
|---|---|---|---|---|---|
?? |
null合体演算子 | 左が未定義、またはnull | 出ない | PHP 7.0以降 | $a = $_GET['q'] ?? ''; |
?: |
エルビス演算子(三項演算子の省略形) | 左がfalsy(null、0、"0"、""、false、[]) | 出る | PHP 5.3以降 | $a = $name ?: '名無し'; |
??= |
null合体代入演算子 | 左が未定義、またはnullのときだけ代入 | 出ない | PHP 7.4以降 | $opt['page'] ??= 1; |
?-> |
nullsafe演算子 | 左がnullなら以降の呼び出しを打ち切ってnullを返す | 未定義変数では出る | PHP 8.0以降 | $user?->address?->city |
値ごとの結果は次のとおりです(PHP 8.4.21 での実行結果。スクリプトは次の章に載せています)。
| 左側の値 | 値 ?? "x" |
値 ?: "x" |
isset(値) |
empty(値) |
|---|---|---|---|---|
| null | 'x' |
'x' |
false | true |
| 未定義の変数 | 'x'(Warningなし) |
'x'(Warningあり) |
false | true |
| 0 | 0 |
'x' |
true | true |
| "0" | '0' |
'x' |
true | true |
| ""(空文字) | '' |
'x' |
true | true |
| false | false |
'x' |
true | true |
| [](空配列) | [] |
'x' |
true | true |
| "abc" | 'abc' |
'abc' |
true | false |
?? は「nullか未定義か」だけを見て、?: は「falsyかどうか」を見ます。 0や空文字をそのまま残したいなら ??、0や空文字も既定値に置き換えたいなら ?: を選びます。
値ごとの結果を1枚の表で確かめる(実行結果つき)
早見表の内容は、次のスクリプトで実際に出力したものです。そのまま保存して php table.php で実行できます。
検証に使った環境
| 項目 | 内容 |
|---|---|
| PHP | 8.4.21(CLI) |
| 実行方法 | php ファイル名.php |
| エラー設定 | error_reporting(E_ALL) |
| 未実行の範囲 | PHP 7.x、PHP 8.0〜8.3、PHP 8.5(この記事の出力はすべて8.4.21のもの) |
全パターンを出力するスクリプト
<?php
// table.php : 値ごとの ?? / ?: / isset / empty の結果を一覧にする
error_reporting(E_ALL);
ini_set('display_errors', '0'); // 表を崩さないよう Warning は件数だけ数える
$warnings = 0;
set_error_handler(function (int $no, string $msg) use (&$warnings) {
$warnings++;
return true;
});
function show(mixed $v): string
{
return var_export($v, true) === "array (\n)" ? '[]' : var_export($v, true);
}
$cases = [
'null' => null,
'0' => 0,
'"0"' => '0',
'""' => '',
'false' => false,
'[]' => [],
'"abc"' => 'abc',
];
printf("%-8s | %-8s | %-8s | %-6s | %-6s\n", '値', '?? "x"', '?: "x"', 'isset', 'empty');
foreach ($cases as $label => $v) {
printf("%-8s | %-8s | %-8s | %-6s | %-6s\n",
$label,
show($v ?? 'x'),
show($v ?: 'x'),
show(isset($v)),
show(empty($v))
);
}
// 未定義の変数は配列に入れられないので個別に書く
$before = $warnings;
$r1 = show($undef ?? 'x');
$w1 = $warnings - $before;
$before = $warnings;
$r2 = show($undef ?: 'x');
$w2 = $warnings - $before;
printf("%-8s | %-8s | %-8s | %-6s | %-6s\n", '未定義', $r1, $r2, show(isset($undef)), show(empty($undef)));
echo "Warning の件数 ?? : {$w1} 件 / ?: : {$w2} 件\n";
実行結果(PHP 8.4.21):
値 | ?? "x" | ?: "x" | isset | empty
null | 'x' | 'x' | false | true
0 | 0 | 'x' | true | true
"0" | '0' | 'x' | true | true
"" | '' | 'x' | true | true
false | false | 'x' | true | true
[] | [] | 'x' | true | true
"abc" | 'abc' | 'abc' | true | false
未定義 | 'x' | 'x' | false | true
Warning の件数 ?? : 0 件 / ?: : 1 件
(全角文字の幅の都合で、1行目と「未定義」の行の縦線が少しずれて表示されます。出力はそのまま載せています。)
表から読み取れる3つのこと
| 気づき | 内容 |
|---|---|
?? と isset は同じ基準 |
isset がfalseになる行(nullと未定義)だけ、?? は右側の 'x' に切り替わっている |
?: と empty は同じ基準 |
empty がtrueになる行すべてで、?: は右側の 'x' に切り替わっている |
| 未定義で差が出る | 結果の値はどちらも 'x' でも、?: はWarningを1件出し、?? は出さない |
つまり、$a ?? 'x' は isset($a) ? $a : 'x' と同じ意味、$a ?: 'x' は $a ? $a : 'x' と同じ意味です。empty($a) ? 'x' : $a は結果の値こそ ?: と同じになりますが、empty は未定義でもWarningを出さないという違いがあります。
??の基本の書き方と、issetを使った書き方との比較
書き換えの前と後
?? が入る前は、isset() と三項演算子を組み合わせて書いていました。どちらも同じ結果になります。
<?php
// 従来の書き方
$username = isset($_GET['user']) ? $_GET['user'] : 'nobody';
// ?? を使った書き方(PHP 7.0以降)
$username = $_GET['user'] ?? 'nobody';
$_GET['user'] を1回しか書かなくてよいので、キー名の書き間違いが起きにくく、行も短くなります。
連結して「最初に見つかった値」を使う
?? は続けて書けます。左から順に調べて、最初にnullでも未定義でもなかった値が採用されます。
<?php
$x = null; $y = null; $z = 'z';
echo $x ?? $y ?? $z, "\n";
実行結果:
z
フォームやクエリ文字列で「GETにあればそれ、無ければPOST、それも無ければ既定値」と書きたいときは $_GET['user'] ?? $_POST['user'] ?? 'nobody' のように並べられます。
多次元配列・プロパティでも途中の存在確認が要らない
?? の左側は、途中の階層が無くてもWarningになりません。
<?php
error_reporting(E_ALL);
ini_set('display_errors', '1');
$user = ['name' => 'sato'];
$obj = new stdClass();
echo "5: "; echo $undef ?? 'x', "\n"; // 未定義の変数
echo "6: "; echo $user['age'] ?? 'x', "\n"; // 無いキー
echo "7: "; echo $obj->age ?? 'x', "\n"; // 無いプロパティ
echo "8: "; echo $user['addr']['city'] ?? 'x', "\n"; // 途中の階層も無い
実行結果:
5: x
6: x
7: x
8: x
4行ともWarningは出ていません。isset($user['addr']['city']) が途中のキーの有無を気にせず書けるのと同じ動きです。
??が守ってくれる範囲と、守ってくれない範囲
?? がWarningやErrorを抑えてくれるのは「変数・配列の要素・プロパティの読み取り」です。関数やメソッドの呼び出し、定数の参照は対象外です。次のスクリプトで確かめました。
<?php
error_reporting(E_ALL);
ini_set('display_errors', '1');
function f(): ?string { return null; }
echo f() ?? 'default', "\n"; // 戻り値が null → 右側
$s = 'abc';
echo $s[10] ?? 'none', "\n"; // 範囲外の文字位置 → 右側
try { echo APP_ENV ?? 'local', "\n"; } // 未定義の定数
catch (Error $e) { echo get_class($e), ': ', $e->getMessage(), "\n"; }
try { echo nofunc() ?? 'x'; } // 未定義の関数
catch (Error $e) { echo get_class($e), ': ', $e->getMessage(), "\n"; }
class P { public string $name; } // 型付きプロパティ(未初期化)
$p = new P();
echo $p->name ?? '未初期化', "\n";
try { echo $p->name ?: 'x'; }
catch (Error $e) { echo get_class($e), ': ', $e->getMessage(), "\n"; }
実行結果:
default
none
Error: Undefined constant "APP_ENV"
Error: Call to undefined function nofunc()
未初期化
Error: Typed property P::$name must not be accessed before initialization
| 左側に書いたもの | ?? の結果 |
|---|---|
| 未定義の変数・配列キー・プロパティ | Warningなしで右側 |
| nullを返す関数・メソッドの戻り値 | 右側 |
文字列の範囲外の位置($s[10]) |
Warningなしで右側 |
| 初期化前の型付きプロパティ | Errorにならず右側(?: だとError) |
| 未定義の定数 | Error(defined('APP_ENV') ? APP_ENV : 'local' と書く) |
| 未定義の関数・メソッド | Error |
定数は ?? では守れないので、defined() で確認する書き方を使います。上のスクリプトと同じ環境で echo defined('APP_ENV') ? APP_ENV : 'local'; を実行すると local と表示されました。
??と?:(エルビス演算子)の違いと使い分け
0や空文字を「値」として扱うかどうかで選ぶ
?: は三項演算子 条件 ? A : B の真ん中を省いた形で、「左がtruthyなら左、そうでなければ右」を返します。クエリ文字列に q=(空文字)と page=0 が届いた想定で比べます。
<?php
error_reporting(E_ALL);
ini_set('display_errors', '1');
$_GET = ['q' => '', 'page' => '0'];
$q1 = isset($_GET['q']) ? $_GET['q'] : 'all'; // 従来の書き方
$q2 = $_GET['q'] ?? 'all'; // null合体
$q3 = $_GET['q'] ?: 'all'; // エルビス
$q4 = !empty($_GET['q']) ? $_GET['q'] : 'all'; // empty で判定
var_dump($q1, $q2, $q3, $q4);
$p1 = $_GET['page'] ?? 1;
$p2 = $_GET['page'] ?: 1;
var_dump($p1, $p2);
実行結果:
string(0) ""
string(0) ""
string(3) "all"
string(3) "all"
string(1) "0"
int(1)
??(とissetの三項)は、空文字や"0"を「届いた値」としてそのまま残します。?:(とemptyの三項)は、空文字や"0"を「値なし」とみなして既定値に置き換えます。
ページ番号や数量のように0が意味を持つ値は ??、表示名のように空文字なら既定の文言にしたい値は ?: と覚えると選びやすくなります。
?:は未定義だとWarningが出る
?: は左側をふつうに評価するので、変数やキーが無いとWarningが出ます。PHP 8.4.21 での文面は次のとおりです(ファイルのパスは環境によって変わるため、/home/taro/app/ に置き換えて表示しています)。
<?php
error_reporting(E_ALL);
ini_set('display_errors', '1');
$user = ['name' => 'sato'];
$obj = new stdClass();
echo "1: "; echo $undef ?: 'x', "\n";
echo "2: "; echo $user['age'] ?: 'x', "\n";
echo "3: "; echo $obj->age ?: 'x', "\n";
$n = null;
echo "4: "; echo $n['age'] ?: 'x', "\n";
実行結果:
1:
Warning: Undefined variable $undef in /home/taro/app/warn.php on line 7
x
2:
Warning: Undefined array key "age" in /home/taro/app/warn.php on line 8
x
3:
Warning: Undefined property: stdClass::$age in /home/taro/app/warn.php on line 9
x
4:
Warning: Trying to access array offset on null in /home/taro/app/warn.php on line 11
x
| 左側 | PHP 8.4 のメッセージ |
|---|---|
| 未定義の変数 | Warning: Undefined variable $undef |
| 無い配列キー | Warning: Undefined array key "age" |
| 無いプロパティ | Warning: Undefined property: stdClass::$age |
| nullに対する配列アクセス | Warning: Trying to access array offset on null |
同じ4つを ?? に置き換えると、前の章で見たとおりWarningは1件も出ません。
両方を組み合わせる:未定義でも安全で、空文字も既定値にする
「キーが無くてもWarningを出したくない」かつ「空文字なら既定値にしたい」という場面では、?? で受けてから ?: を当てます。
<?php
$_GET = ['q' => '', 'page' => '0'];
$q5 = ($_GET['q'] ?? '') ?: 'all'; // キーはあるが空文字
$q6 = ($_GET['nokey'] ?? '') ?: 'all'; // キー自体が無い
var_dump($q5, $q6);
実行結果:
string(3) "all"
string(3) "all"
内側の ?? でWarningを防ぎ、外側の ?: で空の値を置き換えるという2段構えです。括弧を付けておくと、読む人にも順番が伝わります。
文字列連結や足し算と組み合わせるときの優先順位
?? を使い始めて最初につまずきやすいのが、演算子の優先順位です。?? は、文字列連結の .、足し算の +、比較の === よりも優先順位が低いため、括弧なしで混ぜると思った順番で評価されません。PHPマニュアルの「演算子の優先順位」のページにも、文字列連結と ?? を並べたときの注意が例つきで載っています(今回ページを開いて確認しました)。
"文字列" . $a ?? "x" が既定値にならない例
<?php
error_reporting(E_ALL);
ini_set('display_errors', '1');
$a = null;
echo "A: ", "名前: " . $a ?? "未設定", "\n"; // 直す前
echo "B: ", "名前: " . ($a ?? "未設定"), "\n"; // 直した後
$user = [];
echo "C: ", "名前: " . $user['name'] ?? "未設定", "\n"; // 直す前
echo "D: ", "名前: " . ($user['name'] ?? "未設定"), "\n"; // 直した後
実行結果:
A: 名前:
B: 名前: 未設定
C:
Warning: Undefined array key "name" in /home/taro/app/prec.php on line 10
名前:
D: 名前: 未設定
Aの行は、PHPが次のように解釈しています。
<?php
// 書いたもの
"名前: " . $a ?? "未設定";
// PHPの解釈(. が先に結び付く)
("名前: " . $a) ?? "未設定";
"名前: " . $a は "名前: " という文字列になり、nullではないので右側の "未設定" は使われません。Cの行ではさらに、$user['name'] が ?? の直接の左側ではなく連結の中に入ってしまうため、?? の「Warningを出さない」効果も届かなくなり、Warningが表示されています。
足し算・比較・キャストでも同じことが起きる
<?php
error_reporting(E_ALL);
ini_set('display_errors', '1');
$price = 500;
echo "G: "; var_dump($price ?? 0 + 100); // $price ?? (0 + 100)
echo "H: "; var_dump(($price ?? 0) + 100);
$flag = 'on';
echo "K: "; var_dump($flag ?? 'none' === 'none'); // $flag ?? ('none' === 'none')
echo "N: "; var_dump((int) $_GET['page'] ?? 1); // ((int) $_GET['page']) ?? 1
echo "O: "; var_dump((int) ($_GET['page'] ?? 1));
実行結果:
G: int(500)
H: int(600)
K: string(2) "on"
N:
Warning: Undefined array key "page" in /home/taro/app/prec2.php on line 12
int(0)
O: int(1)
| 書いた式 | PHPの解釈 | 結果 | 括弧を付けた形 |
|---|---|---|---|
"名前: " . $a ?? "未設定" |
("名前: " . $a) ?? "未設定" |
既定値にならない | "名前: " . ($a ?? "未設定") |
$price ?? 0 + 100 |
$price ?? (0 + 100) |
500のまま(100が足されない) | ($price ?? 0) + 100 |
$flag ?? 'none' === 'none' |
$flag ?? ('none' === 'none') |
真偽値ではなく "on" |
($flag ?? 'none') === 'none' |
(int) $_GET['page'] ?? 1 |
((int) $_GET['page']) ?? 1 |
Warningが出て0 | (int) ($_GET['page'] ?? 1) |
?? をほかの演算子と同じ式に書くときは、?? の部分を括弧で囲む。 これだけ覚えておけば、優先順位の表を暗記しなくても安全に書けます。
優先順位の位置関係(抜粋)
| 優先順位 | 演算子 |
|---|---|
| 高い | !、キャスト (int) など |
| ↑ | + -(PHP 8.0以降は . より高い) |
| ↑ | .(文字列連結) |
| ↑ | < > == === などの比較 |
| ↑ | &&、その次に論理和の記号 |
| ↑ | ??(右結合) |
| ↑ | ? :(三項演算子。PHP 8.0以降は入れ子に括弧が必要) |
| 低い | = ??= などの代入 |
?? は右結合なので、$x ?? $y ?? $z は $x ?? ($y ?? $z) と解釈されます。左から順に調べるという結果は変わりません。
??=(null合体代入演算子)で初期化する
??= は、左側が未定義またはnullのときだけ右側を代入する演算子です(PHP 7.4以降)。$a ??= 1; は $a = $a ?? 1; とほぼ同じ意味で、if (!isset($a)) { $a = 1; } を1行にしたものです。
設定配列に既定値を入れる
<?php
$opt = ['limit' => 0, 'sort' => null];
$opt['limit'] ??= 20; // 0 は null ではないので上書きされない
$opt['sort'] ??= 'id'; // null なので代入される
$opt['page'] ??= 1; // キーが無いので代入される
var_export($opt);
実行結果:
array (
'limit' => 0,
'sort' => 'id',
'page' => 1,
)
limit の0が残っている点に注目してください。0や空文字を「設定済み」として尊重したいとき、??= はちょうどよい基準になります。
カウンターの初期化
配列で出現回数を数えるとき、最初の1回目だけ0を入れる処理がすっきり書けます。
<?php
$count = [];
foreach (['php', 'go', 'php', 'php'] as $w) {
$count[$w] ??= 0;
$count[$w]++;
}
var_export($count);
実行結果:
array (
'php' => 3,
'go' => 1,
)
右側は必要なときだけ実行される
??= の右側は、代入が必要なときにだけ評価されます。時間のかかる処理を右側に置いても、値がすでにあれば呼ばれません。
<?php
function heavy(): string { echo "(heavy を実行)\n"; return 'computed'; }
$cache = ['k' => 'cached'];
$cache['k'] ??= heavy(); // すでに値がある → heavy() は呼ばれない
echo $cache['k'], "\n";
$cache['m'] ??= heavy(); // キーが無い → heavy() が呼ばれる
echo $cache['m'], "\n";
実行結果:
cached
(heavy を実行)
computed
この性質を使うと、「最初に呼ばれたときだけ読み込み、2回目以降は保持した値を返す」処理を1行で書けます。
<?php
class Repo {
private ?array $rows = null;
public function rows(): array {
return $this->rows ??= $this->load();
}
private function load(): array {
echo "(load を実行)\n";
return [1, 2, 3];
}
}
$r = new Repo();
echo count($r->rows()), "\n";
echo count($r->rows()), "\n";
実行結果:
(load を実行)
3
3
load() は1回しか実行されていません。なお、読み込んだ結果がnullになり得る処理では、毎回 load() が呼ばれることになります(nullは「未設定」と同じ扱いのため)。その場合は「読み込み済みかどうか」を別の真偽値で持つと確実です。
深い階層にもまとめて代入できる
<?php
$tree = [];
$tree['a']['b']['c'] ??= 'deep';
echo json_encode($tree), "\n";
実行結果:
{"a":{"b":{"c":"deep"}}}
途中の配列が無くても、Warningなしで必要な階層が作られます。
?->(nullsafe演算子)と??を連結する
?-> は、左側のオブジェクトがnullなら、その先のプロパティ参照やメソッド呼び出しをやめてnullを返す演算子です(PHP 8.0以降)。?? と組み合わせると、「途中のどこかがnullなら既定値」という処理が1行で書けます。
4パターンで動きを確かめる
<?php
error_reporting(E_ALL);
ini_set('display_errors', '1');
class Address {
public function __construct(public ?string $city = null) {}
public function label(): string {
echo "(label を実行)\n";
return "市: {$this->city}";
}
}
class User {
public function __construct(public string $name, public ?Address $address = null) {}
public function getAddress(): ?Address { return $this->address; }
}
$u1 = new User('sato', new Address('Osaka')); // すべて揃っている
$u2 = new User('kato'); // address が null
$u3 = null; // ユーザー自体が null
$u4 = new User('abe', new Address(null)); // city が null
foreach (['u1' => $u1, 'u2' => $u2, 'u3' => $u3, 'u4' => $u4] as $k => $u) {
echo $k, ': ', $u?->address?->city ?? '未登録', "\n";
}
実行結果:
u1: Osaka
u2: 未登録
u3: 未登録
u4: 未登録
$u?->address?->city は、途中でnullに出会った時点で式全体がnullになり、最後の ?? '未登録' が既定値に置き換えます。
プロパティだけなら??でも書ける。メソッド呼び出しには?->が必要
プロパティをたどるだけなら、?? だけでも同じ結果になります。違いが出るのはメソッド呼び出しです。
<?php
// (上のクラス定義と $u2 に続けて実行)
echo "--- ?? だけで書く\n";
echo $u2->address->city ?? '未登録', "\n";
echo "--- メソッド呼び出しは ?? だけでは守れない\n";
try {
echo $u2->getAddress()->label() ?? '未登録', "\n";
} catch (Error $e) {
echo get_class($e), ': ', $e->getMessage(), "\n";
}
echo $u2->getAddress()?->label() ?? '未登録', "\n";
実行結果:
--- ?? だけで書く
未登録
--- メソッド呼び出しは ?? だけでは守れない
Error: Call to a member function label() on null
未登録
?? が見逃してくれるのは「読み取り」だけで、nullに対するメソッド呼び出しはErrorで止まります。 メソッドを挟むチェーンでは ?-> を使い、最後に ?? で既定値を付ける形が定番です。
打ち切られた先の引数は評価されない
?-> で打ち切られると、その先のメソッドも引数も実行されません。
<?php
// (上のクラス定義と $u2, $u3 に続けて実行)
var_dump($u3?->name);
var_dump($u2?->getAddress()?->label()); // label() は呼ばれない
function arg(): string { echo "(arg を実行)\n"; return 'x'; }
var_dump($u3?->foo(arg())); // arg() も呼ばれない
実行結果:
NULL
NULL
NULL
「(label を実行)」「(arg を実行)」が1度も表示されていないことから、打ち切りが確認できます。
?->が対象にするのはnullだけ
<?php
error_reporting(E_ALL);
ini_set('display_errors', '1');
var_dump($nothing?->name); // 未定義の変数
実行結果:
Warning: Undefined variable $nothing in /home/taro/app/nullsafe.php on line 4
NULL
| 比べる点 | ?? |
?-> |
|---|---|---|
| 未定義の変数 | Warningなし | Warningあり |
| nullのプロパティ参照 | 右側に切り替え | nullを返す |
| nullに対するメソッド呼び出し | Errorで止まる | nullを返す(呼び出さない) |
| 既定値の指定 | できる | できない(?? と組み合わせる) |
代入の左側($a?->b = 1) |
対象外 | 書けない |
配列($arr['k']) |
使える | 配列には使わない(オブジェクト用) |
変数自体が未定義かもしれない場面は多くありませんが、その場合は ($nothing ?? null)?->name のように ?? で受けてから ?-> をつなげます。
演算子ごとの対応PHPバージョンとWarningの出方
対応バージョン表
| 機能 | 書き方 | 導入バージョン | 確認方法 |
|---|---|---|---|
| エルビス演算子 | $a ?: 'x' |
PHP 5.3 | 一般に知られた仕様(今回は未確認・未実行) |
| null合体演算子 | $a ?? 'x' |
PHP 7.0 | PHPマニュアルの7.0移行ガイドを開いて確認、8.4.21で実行 |
| null合体代入演算子 | $a ??= 'x' |
PHP 7.4 | PHPマニュアルの7.4移行ガイドを開いて確認、8.4.21で実行 |
| nullsafe演算子 | $a?->b |
PHP 8.0 | PHPマニュアルの8.0移行ガイドを開いて確認、8.4.21で実行 |
throw式(?? の右側にthrow) |
$a ?? throw new E() |
PHP 8.0 | 同上、8.4.21で実行 |
PHP 7.0〜7.3の環境では ??= が、PHP 7.x の環境では ?-> が構文エラーになります。動かす環境のバージョンは php -v で確認できます。
PHP 8.0でNoticeからWarningに変わった
PHPマニュアルの「PHP 8.0 下位互換性のない変更点」には、未定義の変数・未定義の配列キー・未定義のプロパティの読み取りなどが、NoticeからWarningへ引き上げられたことが記載されています(今回ページを開いて確認しました)。
| 状況 | PHP 7.x | PHP 8.0以降(8.4.21で実行して確認) |
|---|---|---|
| 未定義の変数 | Notice | Warning: Undefined variable $undef |
| 無い配列キー | Notice | Warning: Undefined array key "age" |
| 無いプロパティ | Notice | Warning: Undefined property: stdClass::$age |
| nullへの配列アクセス | Notice(7.4) | Warning: Trying to access array offset on null |
| nullへのメソッド呼び出し | Error | Error: Call to a member function label() on null |
PHP 7.x 側の列は移行ガイドの記載にもとづくもので、7.x での実行は今回行っていません(未実行)。PHP 7から8へ上げたあとに画面やログへWarningが増えた場合、該当の行を ?? に書き換えるのがいちばん手早い直し方です。
??の右側で例外を投げる(PHP 8.0以降)
PHP 8.0で throw が式として書けるようになり、?? の右側に置けます。「必須の値が無ければその場で止める」処理が1行になります。
<?php
$_GET = [];
try {
$id = $_GET['id'] ?? throw new InvalidArgumentException('id は必須です');
} catch (InvalidArgumentException $e) {
echo $e->getMessage(), "\n";
}
実行結果:
id は必須です
どれを使うかの判断表
| やりたいこと | 選ぶ書き方 | 例 |
|---|---|---|
| キーが無いかnullなら既定値。0や空文字は残す | ?? |
$page = $_GET['page'] ?? 1; |
| 空文字・0・falseも既定値に置き換える(変数は必ず定義済み) | ?: |
$label = $name ?: '名無し'; |
| キーが無いかもしれず、空文字も既定値にしたい | ?? と ?: |
$q = ($_GET['q'] ?? '') ?: 'all'; |
| 値が無いときだけ代入したい | ??= |
$opt['limit'] ??= 20; |
| 一度だけ計算して保持したい | ??= |
return $this->rows ??= $this->load(); |
| オブジェクトがnullかもしれないままメソッドをつなぐ | ?-> と ?? |
$u?->getAddress()?->label() ?? '未登録' |
| 必須の値が無ければ止めたい | ?? throw |
$id = $_GET['id'] ?? throw new E(); |
| 「存在するか」だけを真偽値で知りたい | isset() |
if (isset($_POST['agree'])) { ... } |
| nullとキーなしを区別したい | array_key_exists() |
array_key_exists('memo', $row) |
判断の流れを図にすると次のようになります。
flowchart TD
A[既定値に切り替えたい] --> B{0や空文字も<br>既定値にしたいか}
B -- いいえ --> C{代入先と判定対象が同じか}
C -- はい --> D["??= を使う"]
C -- いいえ --> E["?? を使う"]
B -- はい --> F{変数やキーが<br>未定義の可能性があるか}
F -- いいえ --> G["?: を使う"]
F -- はい --> H["(?? '') ?: 既定値 と書く"]
A2[nullかもしれないオブジェクトの<br>メソッドを呼びたい] --> I["?-> でつなぎ、最後に ?? を付ける"]
つまずきやすい点のチェックリスト
| チェック項目 | 起きること | こう書くとよい |
|---|---|---|
?? を . や + と同じ式に括弧なしで書いている |
既定値にならない、Warningが出る | ?? の部分を括弧で囲む |
0や空文字を既定値にしたいのに ?? を使っている |
0や空文字がそのまま残る | ?: か (?? '') ?: 既定値 |
0を正しい値として残したいのに ?: を使っている |
0が既定値に置き換わる | ?? に替える |
?? でメソッド呼び出しのnullを守ろうとしている |
Call to a member function ... on null |
?-> を使う |
?? で未定義の定数を守ろうとしている |
Undefined constant のError |
defined() で確認する |
??= で保持する値がnullになり得る |
毎回右側が実行される | 読み込み済みフラグを別に持つ |
PHP 7.3以前で ??= を書いている |
構文エラー | $a = $a ?? 値; と書く |
isset($a) ?? 'x' と書いている |
isset の戻り値は常に真偽値なので右側は使われない |
$a ?? 'x' と書く |
よくある質問
Q. ?? と isset() はどちらを使えばよいですか?
A. 「値を取り出して、無ければ既定値にする」なら ??、「あるかどうかで処理を分ける」なら isset() が向いています。判定の基準はどちらも同じ(未定義またはnullかどうか)で、この記事の表でも isset がfalseの行だけ ?? が右側に切り替わっていました。
Q. $a ?? 'x' と $a ?: 'x' はどう違いますか?
A. ?? はnullと未定義だけを右側に切り替え、?: は0、"0"、空文字、false、空配列も右側に切り替えます。また、未定義の変数やキーに対して ?: はWarningを出しますが、?? は出しません。0や空文字に意味がある値は ?? を選ぶと安心です。
Q. "合計: " . $total ?? 0 が思ったとおりに動きません。
A. 文字列連結の . のほうが ?? より優先順位が高く、("合計: " . $total) ?? 0 と解釈されるためです。"合計: " . ($total ?? 0) のように ?? の部分を括弧で囲むと、意図どおり既定値が使われます。
Q. ?-> があれば ?? は要らなくなりますか?
A. 役割が違うので両方使います。?-> はnullのときにチェーンを打ち切ってnullを返すだけで、既定値は指定できません。$user?->address?->city ?? '未登録' のように、最後に ?? を付けて既定値を決めるのが基本の形です。
まとめ
??は「未定義またはnull」のときだけ右側を返す。 判定基準はisset()と同じで、Warningも出ない。?:は「falsy」のとき右側を返す。 0・"0"・空文字・false・空配列も置き換わり、未定義ではWarningが出る。- 0や空文字を残したいなら
??、置き換えたいなら?:。 両方の性質が欲しいときは($x ?? '') ?: 既定値。 ??は.+===より優先順位が低い。 ほかの演算子と混ぜるときは??の部分を括弧で囲む。??=は値が無いときだけ代入する。 右側は必要なときだけ実行されるので、初期化や一度きりの読み込みに向く。?->はnullならチェーンを打ち切る。 メソッド呼び出しを含むときは?->でつなぎ、最後に??で既定値を付ける。- 対応バージョンは
??が7.0、??=が7.4、?->が8.0。 PHP 8.0以降、未定義の変数やキーの読み取りはWarningになる。
参考・出典
- PHPマニュアル「演算子の優先順位」 https://www.php.net/manual/ja/language.operators.precedence.php
- PHPマニュアル「PHP 7.0 新機能」 https://www.php.net/manual/ja/migration70.new-features.php
- PHPマニュアル「PHP 7.4 新機能」 https://www.php.net/manual/ja/migration74.new-features.php
- PHPマニュアル「PHP 8.0 新機能」 https://www.php.net/manual/ja/migration80.new-features.php
- PHPマニュアル「PHP 8.0 下位互換性のない変更点」 https://www.php.net/manual/ja/migration80.incompatible.php
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。