1. ホーム
  2. PHP

【PHP】nullsafe 演算子(?->)の使い方|null のときもエラーにせずプロパティ・メソッドにアクセスする方法

Share

PHP でオブジェクトを扱っていると、Error: Call to a member function getName() on null という致命的エラーに一度は出会います。取得できるはずのオブジェクトが null だったせいで、そこから先のプロパティやメソッドに触れなくなるエラーです。防ぐには if ( $user !== null ) のようなチェックを重ねることになりますが、階層が深くなるほど本題のコードが埋もれていきます。PHP 8.0 で追加された nullsafe 演算子(?->) は、この入れ子の null チェックを演算子ひとつに置き換えるための機能です。この記事では基本の書き方から、チェーンが途中で打ち切られる仕組み、?? との使い分け、そして「使えない書き方」までを解説します。

null かもしれないオブジェクトに触ると PHP は止まる

まず、nullsafe 演算子が無かった頃にどう書いていたかを見ておきます。ユーザーを取得して、そのプロフィール、さらにその住所から市区町村名を取り出す、という 3 階層のコードです。

index.php
<?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() も同じです。ひとつずつ守ろうとすると、次のような形になります。

index.php(従来の書き方)
$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 になります。プロパティへのアクセスでもメソッド呼び出しでも、書き方は同じです。

basic.php
<?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 行はこうなります。

index.php(nullsafe 演算子)
$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() による二重の分岐と等価なコードとして示しています。

メソッドの引数も評価されない

「残りのチェインはスキップされる」の範囲は、メソッド名やプロパティ名だけではありません。そのメソッドに渡す引数の式も評価されないのです。引数の中で重い処理や副作用のある関数を呼んでいる場合、これは動作の違いとして目に見えます。

short-circuit.php
<?php

function makeKey(): string
{
    // 呼ばれたかどうかを目で確かめるためのログ
    echo "makeKey が呼ばれました\n";

    return 'key';
}

$logger = null;

// $logger が null なので、makeKey() は呼ばれない
$logger?->write( makeKey() );

// 出力: (なにも出ない)

ふつうの関数呼び出しなら、引数は必ず先に評価されてから渡されます。?-> ではその手前で打ち切られるため、引数の式にも到達しないわけです。パフォーマンス面ではありがたい挙動ですが、引数の中で状態を変えるコードを書いていると、通る・通らないが左側の値で変わる点は意識しておいてください。

チェーンの途中にある配列アクセスもスキップされる

チェーンには [] による配列アクセスも含まれます。$obj?->items['key'] のように書いた場合、$obj が null なら配列アクセスの部分も、添字の中に書いた式も評価されません。

chain-array.php
<?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.0PHP 7.0
役割null のオブジェクトに触れてもエラーにしない左辺が null のとき右辺の値を返す
置き換えるものif ( $obj !== null ) のネストisset() による三項演算子
左に書くものオブジェクト(かもしれない値)任意の式
結果が null のときnull がそのまま返る既定値に置き換わる
書き込みに使えるか使えない??= で代入に使える(PHP 7.4 以降)

言い換えると、?-> だけでは「エラーにはならないが null が返ってくる」ところまでしか面倒を見てくれません。画面に表示する値なら、そのあと ?? で既定値に差し替えるのが実用的です。この 2 つは競合せず、並べて書けます。

combine.php
$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)で ?-> を使うことは許されていません。次のコードは実行されず、ファイルを読み込んだ時点で落ちます。

NG 例
$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 で分岐します。

OK 例
if ( $user !== null ) {
    $user->name = 'Taro';
}

同じ理屈で、参照を取ることもできません。$ref = &$obj?->prop; は Cannot take reference of a nullsafe chain になります。参照はメモリ上の場所を指すものですが、?-> は場所ではなく null という値を返すことがあるため、両立しないのです。

配列の要素には nullsafe 演算子が無い

JavaScript のオプショナルチェーンには ?.[ ] という配列・オブジェクト添字用の書き方がありますが、PHP に対応する構文はありません。$arr?['key'] と書くと、そもそも文法として解釈できず Parse error(構文エラー)になります。

NG 例
$arr = null;

echo $arr?['key'];
// Parse error: syntax error, unexpected token ";"

ここは ?-> の出番ではなく ?? の出番です。連想配列に対しては $arr['key'] ?? '既定値' と書けば、キーが無くても警告なく既定値を返せます。「オブジェクトなら ?->、配列なら ??」と覚えておくと迷いません。

静的プロパティ・静的メソッドは対象外

?-> はインスタンスに対するアクセス演算子 -> の nullsafe 版です。クラスに対する静的アクセス :: には、対応する nullsafe 版が用意されていません。そもそもクラス名は null になりようがないので、必要になる場面もほとんどないはずです。

ただし、?-> のうしろに :: が続くチェーンは書けます。この場合も短絡評価が働き、左側が null なら静的メソッドの呼び出しごとスキップされて null が返ります。

static.php
$factory = null;

// $factory が null なら ::create() は呼ばれず、結果は null
var_dump( $factory?->resolveClass()::create() ); // NULL

実践例:オブジェクトで返ってくる API レスポンスを読む

?-> がいちばん効くのは、階層があって、どこが欠けるか分からないデータを扱うときです。典型例が、外部 API の JSON レスポンスをオブジェクトとして受け取る場合です。json_decode() は第 2 引数を省略すると stdClass のオブジェクトを返すので、存在しないキーに触れると警告や致命的エラーになります。

api-response.php
<?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 が返り得ます。テンプレート側で表示するときは、次のように書けます。

article-view.php
<?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 を返し、その先が普通の -> になっていると、そこで落ちます。

mixed-chain.php
$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 ではないのでチェーンは止まらず、通常どおりアクセスが試みられて警告が出ます。

not-null.php
$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 の警告はそのまま出ます。

undefined.php
// $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 のレスポンスを読む箇所から試してみてください。

参考ページ