【Laravel】optionalを多重配列の呼出しでもnull安全にする

  • 2022年11月11日
  • PHP

 Laravel ではよくデータを元に web ページを作ります。これは例えば次の様なコードで作られます。

<div>
    <span>ユーザー名</span>
    <span>{{ $user->name }}</span>
</div>
<div>
    <span>ユーザーのアイテムの名前</span>
    <span>{{ $user->item->name }}</span>
</div>

 $user というインスタンスからデータを読み取り、それを HTML にいい感じに出力するテンプレートです。一見正しそうですが、このコードでは user や item が null の場合、PHPが警告を出します。検知するエラーレベルによっては期待通りのHTMLが出力されません。これを簡易に対処できるために optional というヘルパー関数があります。これと PHP 8.0 から使用できる様になった null safe 演算子を用いることで次の様に書けます
 
ヘルパ 9.x Laravel#method-optional
PHP: クラスの基礎 – Manual#nullsafe メソッドとプロパティ

@php
// もし変数 user が null か存在しないならば null を optional 関数に渡す
$user = optional($user ?? null);
@endphp
<div>
    <span>ユーザー名</span>
    {{-- optional からのプロパティ呼出しは null を返し、警告を引き起こさない --}}
    <span>{{ $user->name }}</span>
</div>
<div>
    <span>ユーザーのアイテムの名前</span>
    {{-- オブジェクト外からの null safe 演算子を用いたプロパティ呼出しは警告を引き起こさない --}}
    <span>{{ $user->item?->name }}</span>
</div>

 仮に optional と null safe 演算子を使わないとなると次になります。

<div>
    <span>ユーザー名</span>
    {{-- null 合体演算子で使う予定の値が null でないことを確かめて、問題なければそちらを返す --}}
    <span>{{ $user->name ?? null }}</span>
    <a href="https://www.php.net/manual/ja/language.operators.comparison.php#language.operators.comparison.coalesce">PHP: 比較演算子 - Manual#Null 合体演算子</a>
</div>
<div>
    <span>ユーザーのアイテムの名前</span>
    {{-- null 合体演算子で使う予定の値が null でないことを確かめて、問題なければそちらを返す --}}
    <span>{{ $user->item->name ?? null }}</span>
</div>

 ?? [デフォルト値]が毎回必要なのが面倒です。特に空白でいい場合は?? nullが連続してますます面倒になります。仮に null 合体演算子も使わないとなると次になります。

<div>
    <span>ユーザー名</span>
    {{-- isset で使う予定の値が null でないことを確かめて、三項演算子で振り分ける --}}
    <span>{{ isset($user->name) ? $user->name : null }}</span>
</div>
<div>
    <span>ユーザーのアイテムの名前</span>
    {{-- isset で使う予定の値が null でないことを確かめて、三項演算子で振り分ける --}}
    <span>{{ isset($user->item->name) ? $user->item->name : null }}</span>
</div>

 例で見て分かる通り optional 関数と null safe 演算子はコード簡潔にしてくれます。これらは便利ですがオブジェクトを対象とするのが前提としている様な部分があり、次の様に配列が相手となると既存の optional 関数と null safe 演算子では簡潔に記述することができません

@php
$user = optional($user ?? null);
@endphp
<div>
    <span>ユーザー名</span>
    {{-- 一段目の読み取りは optional がいい感じに null を返してくれる --}}
    <span>{{ $user['name'] }}</span>
</div>
<div>
    <span>ユーザーのアイテムの名前</span>
    {{-- 二段目の読み取りはできない --}}
    {{-- optional が一段目で null を返すため null['name'] となってしまう --}}
    {{-- null safe 演算子は配列相手に使えないので結局分岐で対処するしかない --}}
    <span>{{ $user['item']['name'] ?? null }}</span>
</div>

 これをマシにするために optional の拡張を考えます。

@php
    /**
     * Laravel の Optional クラスを拡張したクラス。
     * Optional で対応できていない配列やプロパティのチェーンに対応する目的で作成
     * 実際に使う時は別ファイルに分けてちゃんと namespace で他とぶつからない様にすることを推奨
     */
    class OptionalForChain extends \Illuminate\Support\Optional implements Stringable {
        /**
         * 配列としてアクセスされた時、インスタンスが保持している値が配列として使えるならばそこから値を取り出す。
         * もし、値が取り出せないならば null を元にしたこのクラスのインスタンスを返す
         */
        public function offsetGet($key): mixed
        {
            return Arr::get($this->value, $key, new OptionalForChain(null));
        }

        /**
         * オブジェクトしてプロパティにアクセスされた時、インスタンスが保持している値がオブジェクトであるならばそこから値を取り出す。
         * もし、値が取り出せないならば null を元にしたこのクラスのインスタンスを返す
         */
        public function __get($key)
        {
            if (is_object($this->value)) {
                return $this->value->{$key} ?? new OptionalForChain(null);
            }
            return new OptionalForChain(null);
        }

        /**
         * 文字列を要求する関数に渡す時や文字列として表示する時、
         * 期待通りにインスタンスが持っている値を表示するために
         * Stringable インターフェースを実装する。
         * @see https://www.php.net/manual/ja/class.stringable.php
         */
        public function __toString(){
            return (string)$this->value;
        }
    }

    $user = new OptionalForChain($user ?? null);
@endphp
<div>
    <span>ユーザー名</span>
    {{-- 存在しない一段目の読み取りは new OptionalForChain(null) が返る --}}
    {{-- OoptionalForChain::__toString() によって (staring)null が出力される --}}
    <span>{{ $user['name'] }}</span>
</div>
<div>
    <span>ユーザーのアイテムの名前</span>
    {{-- 存在しない一段目の読み取りは new OptionalForChain(null) が返る --}}
    {{-- 存在しない二段目の読み取りも new OptionalForChain(null) が返る --}}
    {{-- OoptionalForChain::__toString() によって (staring)null が出力される --}}
    <span>{{ $user['item']['name'] }}</span>
</div>

 Laravel の optional 関数は Laravel が用意した Illuminate\Support\Optional クラスを初期化する関数です。そこで Optional クラスをチェーンに対応した拡張して、それを取り扱うことで楽をします。これを使って説明用のコメントを省くと次の様にすごくすっきりします。

@php
    $user = new \App\Library\LaravelExtends\OptionalForChain($user ?? null);
@endphp
<div>
    <span>ユーザー名</span>
    <span>{{ $user['name'] }}</span>
    <span>{{ $user['name'] ?? '空の時に使うデフォルトネーム' }}</span>
</div>
<div>
    <span>ユーザーのアイテムの名前</span>
    <span>{{ $user['item']['name'] }}</span>
</div>
<div>
    <span>ユーザー名</span>
    <span>{{ $user->name }}</span>
</div>
<div>
    <span>ユーザーのアイテムの名前</span>
    <span>{{ $user->item->name }}</span>
</div>

 この様にして一々空欄のために特殊な記述しなくとも、一度インスタンス化を嚙ますだけで楽にコードを書けます。注意点として結局別オブジェクトで何かをラッピングしているため、ロジック中に使うとかえって混乱を生みやすい点があります。instanseof が使いにくくなるし、ログに残るクラス名も何でもかんでも OptionalForChain でラッピングされて読めなくなったり読みにくかったりでさんざんな目に遭いやすいです。そういった時は律儀に空の場合についての処理を記述した方がわかりやすく使いやすくなります。

>株式会社シーポイントラボ

株式会社シーポイントラボ

TEL:053-543-9889
営業時間:9:00~18:00(月〜金)
住所:〒432-8003
   静岡県浜松市中央区和地山3-1-7
   浜松イノベーションキューブ 315
※ご来社の際はインターホンで「316」をお呼びください

CTR IMG