PHP でオブジェクトを扱っていると、Error: Call to a member function getName() on null という致命的エラーに一度は出会います。取得できるはずのオブジェクトが null だったせいで、そこから先のプロパティやメソッドに触れなくなるエラーです。防ぐには if ( $user !== null ) のようなチェックを重ねることになりますが、階層が深くなるほど本題のコードが埋もれていきます。PHP 8.0 で追加された nullsafe 演算子(?->) は、この入れ子の null チェックを演算子ひとつに置き換えるための機能です。この記事では基本の書き方から、チェーンが途中で打ち切られる仕組み、?? との使い分け、そして「使えない書き方」までを解説します。
目次
null かもしれないオブジェクトに触ると PHP は止まる
まず、nullsafe 演算子が無かった頃にどう書いていたかを見ておきます。ユーザーを取得して、そのプロフィール、さらにその住所から市区町村名を取り出す、という 3 階層のコードです。
<?php
// 見つからなければ null を返す想定
$user = $repository->findUser( 5 );
// これをそのまま書くと、$user が null のとき落ちる
$city = $user->getProfile()->getAddress()->city;
// Error: Call to a member function getProfile() on null
ここで出る Error は警告ではなく致命的エラーです。try で捕まえない限りスクリプトはその場で終了します。しかも getProfile() が null を返すかもしれませんし、getAddress() も同じです。ひとつずつ守ろうとすると、次のような形になります。
$city = null;
if ( $user !== null ) {
$profile = $user->getProfile();
if ( $profile !== null ) {
$address = $profile->getAddress();
if ( $address !== null ) {
$city = $address->city;
}
}
}
echo $city;
やりたいのは「市区町村名がほしい」という 1 行だけなのに、13 行に膨らみました。しかも $profile や $address という、あとで使いもしない一時変数が増えています。nullsafe 演算子を使うと、これがそのまま 1 行に戻ります。
アロー演算子を ?-> に変えるだけ
書き方のルールはひとつです。いつもの -> の前に ? を付けて ?-> にする。それだけで、左側が null のときは例外を投げずに式全体が null になります。プロパティへのアクセスでもメソッド呼び出しでも、書き方は同じです。
<?php
class User
{
public string $name = 'Taro';
public function greet(): string
{
return "Hello, {$this->name}";
}
}
$user = new User();
$none = null;
// オブジェクトが入っているときは、-> と同じ結果
echo $user?->name; // Taro
echo $user?->greet(); // Hello, Taro
// null のときはエラーにならず、式全体が null になる
var_dump( $none?->name ); // NULL
var_dump( $none?->greet() ); // NULL
注目したいのは $none?->greet() の行です。null に対してメソッドを呼んでいるにもかかわらず、エラーにも警告にもならず NULL が返っています。?-> は「左側が null だったら、その場で評価をやめて null を返す」演算子なので、右側のメソッドは呼ばれていません。
PHP マニュアルでは、この動きを is_null() によるチェックで各アクセスを包むのと同じ効果だが、より簡潔だと説明しています。実際に先ほどの 13 行はこうなります。
$city = $user?->getProfile()?->getAddress()?->city;
echo $city; // 途中のどれかが null なら null
13 行が 1 行になり、一時変数も消えました。ネストが減るぶん、本来の処理が読みやすくなります。
チェーンは null になった時点で打ち切られる
?-> を理解するうえでいちばん大事なのが、この 短絡評価(short-circuit) です。マニュアルには「オブジェクトの評価がチェインの一部だった場合は、残りのチェインはスキップされます」と書かれています。つまり $user?->getProfile()?->getAddress()?->city で $user が null だった場合、getProfile() も getAddress() も ->city も、一切実行されません。左から順に評価して、最初に null にぶつかった瞬間に式全体が終わります。
記事の冒頭で見た 13 行の if のネストが、まさにこの動きを手で書いたものです。マニュアルも $repository?->getUser( 5 )?->name という 1 行を、is_null() による二重の分岐と等価なコードとして示しています。
メソッドの引数も評価されない
「残りのチェインはスキップされる」の範囲は、メソッド名やプロパティ名だけではありません。そのメソッドに渡す引数の式も評価されないのです。引数の中で重い処理や副作用のある関数を呼んでいる場合、これは動作の違いとして目に見えます。
<?php
function makeKey(): string
{
// 呼ばれたかどうかを目で確かめるためのログ
echo "makeKey が呼ばれました\n";
return 'key';
}
$logger = null;
// $logger が null なので、makeKey() は呼ばれない
$logger?->write( makeKey() );
// 出力: (なにも出ない)
ふつうの関数呼び出しなら、引数は必ず先に評価されてから渡されます。?-> ではその手前で打ち切られるため、引数の式にも到達しないわけです。パフォーマンス面ではありがたい挙動ですが、引数の中で状態を変えるコードを書いていると、通る・通らないが左側の値で変わる点は意識しておいてください。
チェーンの途中にある配列アクセスもスキップされる
チェーンには [] による配列アクセスも含まれます。$obj?->items['key'] のように書いた場合、$obj が null なら配列アクセスの部分も、添字の中に書いた式も評価されません。
<?php
function currentKey(): string
{
echo "currentKey が呼ばれました\n";
return 'city';
}
$profile = new Profile(); // data = [ 'city' => '那覇市' ]
$empty = null;
// オブジェクトがあるときは、添字の式も評価される
echo $profile?->data[ currentKey() ];
// currentKey が呼ばれました
// 那覇市
// null のときは、添字の currentKey() すら呼ばれない
var_dump( $empty?->data[ currentKey() ] ); // NULL
ここで混同しやすいのが、「配列アクセスがチェーンに含まれる」ことと「配列そのものに nullsafe を使えること」は別だという点です。後者は次の章で説明します。
?? との違いと、組み合わせ方
?-> と見た目が似ているため混同されがちなのが、PHP 7.0 からある null 合体演算子 ?? です。この 2 つは役割がはっきり分かれています。?-> は「アクセスする行為そのものを安全にする」もの、?? は「結果が null だったときの代わりの値を決める」ものです。
| 項目 | ?->(nullsafe 演算子) | ??(null 合体演算子) |
|---|---|---|
| 追加バージョン | PHP 8.0 | PHP 7.0 |
| 役割 | null のオブジェクトに触れてもエラーにしない | 左辺が null のとき右辺の値を返す |
| 置き換えるもの | if ( $obj !== null ) のネスト | isset() による三項演算子 |
| 左に書くもの | オブジェクト(かもしれない値) | 任意の式 |
結果が null のとき | null がそのまま返る | 既定値に置き換わる |
| 書き込みに使えるか | 使えない | ??= で代入に使える(PHP 7.4 以降) |
言い換えると、?-> だけでは「エラーにはならないが null が返ってくる」ところまでしか面倒を見てくれません。画面に表示する値なら、そのあと ?? で既定値に差し替えるのが実用的です。この 2 つは競合せず、並べて書けます。
$user = null;
// ?-> だけ:エラーは出ないが、表示は空になる
echo $user?->name; // (なにも出ない)
// ?? を足す:null のときの表示を決められる
echo $user?->name ?? 'ゲスト'; // ゲスト
// チェーンでも同じ
$city = $user?->getProfile()?->getAddress()?->city ?? '未設定';
echo $city; // 未設定
ここでは「?-> が安全にたどる係、?? が最後の受け皿係」という役割分担だけ押さえておけば十分です。
使える書き方・使えない書き方
?-> は「読み取りのためのアクセス演算子」です。この一線を越える書き方はすべて禁止されており、実行する前の段階で Fatal error になります。まとめると次のとおりです。
| 書き方 | 可否と結果 |
|---|---|
$obj?->prop | 使える(プロパティの読み取り) |
$obj?->method() | 使える(メソッド呼び出し) |
$obj?->a()?->b()?->c | 使える(チェーン。途中で null なら以降スキップ) |
$obj?->items['key'] | 使える(チェーン内の配列アクセス) |
$obj?->prop = 'v'; | 不可:Can't use nullsafe operator in write context |
unset( $obj?->prop ); | 不可:同じく write context のエラー |
$ref = &$obj?->prop; | 不可:Cannot take reference of a nullsafe chain |
$arr?['key'] | 不可:そういう構文は存在せず Parse error |
Foo?::bar() | 不可:静的アクセス用の nullsafe 演算子は無い |
代入や unset では使えない
「値を読むときに安全なら、書くときも安全にしてほしい」と思うところですが、書き込みの文脈(write context)で ?-> を使うことは許されていません。次のコードは実行されず、ファイルを読み込んだ時点で落ちます。
$user = null;
$user?->name = 'Taro';
// Fatal error: Can't use nullsafe operator in write context
unset( $user?->name );
// Fatal error: Can't use nullsafe operator in write context
理由を考えると納得できます。$user が null のとき、'Taro' という値はどこへ行けばよいのでしょうか。代入先が存在しない以上、「静かに無視する」か「エラーにする」かのどちらかしかなく、PHP は後者を選んだということです。値を書き込みたいときは、素直に if で分岐します。
if ( $user !== null ) {
$user->name = 'Taro';
}
同じ理屈で、参照を取ることもできません。$ref = &$obj?->prop; は Cannot take reference of a nullsafe chain になります。参照はメモリ上の場所を指すものですが、?-> は場所ではなく null という値を返すことがあるため、両立しないのです。
配列の要素には nullsafe 演算子が無い
JavaScript のオプショナルチェーンには ?.[ ] という配列・オブジェクト添字用の書き方がありますが、PHP に対応する構文はありません。$arr?['key'] と書くと、そもそも文法として解釈できず Parse error(構文エラー)になります。
$arr = null;
echo $arr?['key'];
// Parse error: syntax error, unexpected token ";"
ここは ?-> の出番ではなく ?? の出番です。連想配列に対しては $arr['key'] ?? '既定値' と書けば、キーが無くても警告なく既定値を返せます。「オブジェクトなら ?->、配列なら ??」と覚えておくと迷いません。
静的プロパティ・静的メソッドは対象外
?-> はインスタンスに対するアクセス演算子 -> の nullsafe 版です。クラスに対する静的アクセス :: には、対応する nullsafe 版が用意されていません。そもそもクラス名は null になりようがないので、必要になる場面もほとんどないはずです。
ただし、?-> のうしろに :: が続くチェーンは書けます。この場合も短絡評価が働き、左側が null なら静的メソッドの呼び出しごとスキップされて null が返ります。
$factory = null;
// $factory が null なら ::create() は呼ばれず、結果は null
var_dump( $factory?->resolveClass()::create() ); // NULL
実践例:オブジェクトで返ってくる API レスポンスを読む
?-> がいちばん効くのは、階層があって、どこが欠けるか分からないデータを扱うときです。典型例が、外部 API の JSON レスポンスをオブジェクトとして受け取る場合です。json_decode() は第 2 引数を省略すると stdClass のオブジェクトを返すので、存在しないキーに触れると警告や致命的エラーになります。
<?php
$json = '{
"user": {
"name": "Taro",
"address": { "city": "那覇市" }
}
}';
// 連想配列ではなくオブジェクトとして受け取る
$res = json_decode( $json );
// 全部そろっているとき
echo $res?->user?->address?->city ?? '住所なし'; // 那覇市
// address キーが無いレスポンスが来たとき
$res2 = json_decode( '{ "user": { "name": "Jiro" } }' );
echo $res2?->user?->address?->city ?? '住所なし'; // 住所なし
// レスポンスが壊れていて null になったとき
$res3 = json_decode( 'こわれたJSON' );
echo $res3?->user?->address?->city ?? '住所なし'; // 住所なし
3 パターンすべてが同じ 1 行で書けています。json_decode() は解析に失敗すると null を返すので、いちばん外側にも ?-> を付けておくのがポイントです。
リレーションをたどるコードが短くなる
ORM を使っていて、関連レコードが必ず存在するとは限らないケースも同じです。記事に紐づく著者、著者に紐づく所属部署、のようにたどっていくと、どこかで null が返り得ます。テンプレート側で表示するときは、次のように書けます。
<?php
// 著者が未設定、または著者に部署が無いこともある
$department = $article?->author?->department?->name ?? '所属なし';
// 著者のアバターも同じ要領で
$avatar = $article?->author?->avatar?->url() ?? '/img/no-image.png';
echo htmlspecialchars( $department ); // 所属なし
従来なら isset( $article->author->department ) と包んでいたところです。isset() はメソッド呼び出しの結果には使えない(isset( $obj->method() ) は書けない)ので、メソッドが絡むチェーンでは ?-> のほうが素直です。
思ったとおりに動かないとき
?-> は仕様が単純なぶん、勘違いから来るつまずきが多い演算子でもあります。実際に踏みやすいものを順に見ていきます。
PHP 7 系では構文エラーになる
?-> は PHP 8.0.0 で追加された構文です。PHP 7.4 以前で実行すると Parse error(構文エラー)になります。構文エラーはファイルを読み込む段階で起きるため、そのファイル全体が 1 行も実行されません。if ( PHP_VERSION_ID >= 80000 ) で囲んでも回避できない点に注意してください。
PHP 7 系もサポート対象に含むライブラリやプラグインを配布するなら ?-> は使えず、従来どおりの if や isset() で書くことになります。動かす環境のバージョンは php -v か PHP_VERSION で確認しておきましょう。
チェーンに ?-> と -> が混ざっている
いちばん多い勘違いが「先頭に 1 つ付ければチェーン全体が守られる」というものです。守られるのは、その ?-> の左側が null だった場合だけです。途中のメソッドが null を返し、その先が普通の -> になっていると、そこで落ちます。
$user = new User(); // $user 自体は null ではない
// getProfile() が null を返すと、次の -> で落ちる
$city = $user?->getProfile()->getAddress()?->city;
// Error: Call to a member function getAddress() on null
// null になりうる箇所すべてに ?-> を付ける
$city = $user?->getProfile()?->getAddress()?->city; // OK
逆に言えば、「ここは絶対 null にならない」という箇所は -> のままにしておくのが正解です。全部を ?-> にすると、本来なら即座に気づけたバグが null に化けて流れていってしまいます。
null 以外の値には効かない
?-> が特別扱いするのは null だけです。文字列や配列、false といったオブジェクト以外の値が入っていても、それらは null ではないのでチェーンは止まらず、通常どおりアクセスが試みられて警告が出ます。
$value = 'text'; // null ではなく文字列
var_dump( $value?->name );
// Warning: Attempt to read property "name" on string
// NULL
結果は NULL になるものの、警告は出ています。「?-> を付けたのに Warning が消えない」ときは、変数の中身が null ではなく別の型ではないか var_dump() で確認してみてください。false を返しうる関数の結果を、そのままオブジェクト扱いしているケースがよくあります。
未定義の変数・未定義のプロパティの警告は消えない
?-> は「変数の中身が null だったとき」の演算子であって、「変数が存在しないとき」の演算子ではありません。そもそも定義されていない変数に対して使うと、Undefined variable の警告はそのまま出ます。
// $user を一度も定義していない
var_dump( $user?->name );
// Warning: Undefined variable $user
// NULL
// 変数の存在ごと面倒を見たいなら ?? を使う
var_dump( $user?->name ?? null ); // NULL(警告なし)
存在しないプロパティを読んだときの Warning: Undefined property も同様に残ります。これらを黙らせるのは ?? の役割で、?-> の守備範囲ではありません。ここでも 2 つの演算子の役割分担がそのまま効いてきます。
null が返ってきた理由が分からない
長いチェーンでは、返ってきた null が「どの段で打ち切られた結果なのか」が式からは分かりません。$a?->b()?->c()?->d が null でも、$a が無かったのか、b() が空だったのか、d の値がそもそも null だったのかは区別できないのです。
デバッグのときは、いったんチェーンをほどいて 1 段ずつ var_dump() するのが確実です。そして「どの段で切れたかを区別したい」と感じたなら、それはチェーンが長すぎるというサインでもあります。中間の値を変数に入れて名前を付けるなど、コード側を見直したほうが早く解決します。
?-> を使いすぎると設計の問題が見えなくなる
最後に、使いどころの話をしておきます。PHP マニュアルには「nullsafe 演算子は、プロパティやメソッドが値を返す際、null が正しく、期待されうる値とみなせる場合に一番よく使います。エラーを示すためなら、例外をスローするほうが好ましいです」という注記があります。これがそのまま指針になります。
つまり ?-> を付けてよいのは、null が正常な状態としてあり得る箇所だけです。「ミドルウェアの認証を通った後なので、ログインユーザーは必ず存在する」「必須項目なので著者は必ず紐づいている」といった前提があるなら、そこは -> のままにしておくべきです。もし null が来たなら、それは想定外の状態であり、エラーとして表に出したほうが早く直せます。
手当たり次第に ?-> を付けると、バグが致命的エラーではなく「なぜか画面が空欄」という症状に変わります。落ちないぶん発見が遅れ、原因の箇所からも遠ざかります。?-> は null チェックを消す道具ではなく、「ここは null でも構わない」という意思表示を 2 文字で書く道具だと捉えておくと、付ける・付けないの判断に迷わなくなります。
まとめ
nullsafe 演算子 ?-> は PHP 8.0 で追加された演算子で、-> の前に ? を付けるだけで使えます。左側が null のときは例外を投げず、式全体が null になります。チェーンの途中で null にぶつかると、そこから先のメソッド呼び出し・プロパティアクセス・配列アクセスはすべてスキップされ、メソッドに渡す引数や添字の式さえ評価されません。これによって、入れ子になった if ( $obj !== null ) をまとめて 1 行に圧縮できます。
役割が近い ?? とは補い合う関係です。?-> が「安全にたどる」係、?? が「結果が null のときの既定値を決める」係なので、$user?->name ?? 'ゲスト' のように並べて書くのが実用的な形になります。配列のキーに対しては ?-> が使えないので、そちらは ?? の担当です。
一方で ?-> は読み取り専用の演算子です。代入・unset()・参照の取得といった書き込みの文脈では Fatal error になり、$arr?['key'] のような配列版の構文も、静的アクセス :: 用の nullsafe 版も存在しません。PHP 7 系では構文エラーになる点も、配布するコードでは確認が必要です。
ただし、すべてのアロー演算子を ?-> に置き換えないでください。null が正常にあり得る箇所にだけ付ける、という線引きを守れば、コードは短くなり、本当のバグはエラーとして表に出てくれます。まずは外部 API のレスポンスを読む箇所から試してみてください。