💁‍♂️

Laravel で json 型のカラムを持つ Eloquent Model でやっておいた方が良いこと

に公開
3

概要

Laravel で json 型のカラムを持つ Eloquent Model を操作する時に、
「型安全に書ければこの修正不要だったなぁ(Laravel がよしなにやってくれたら良いのに...)」
と感じた事に関する記事です。

本題

先に結論

setAttribute を明示的に使用した方が良い。

fuwasegu さんに正しい方法をご教授いただきましたので以下リンクを載せておきます🙏
https://zenn.dev/link/comments/2ad8f197ef083f

json 型のカラムの取得

json 型のカラムを持つ場合、 $casts に json を指定するのはよくある事だと思います。

protected $casts = [
        'json_column' => 'json',
        ...

こうする事で、以下のようにアクセスする事が可能になります。

$hoge = $hoge->json_column['hoge'];

かゆい所に手が届かず...

このような Eloquent Model を操作する場合、 json 型のカラムなので連想配列を保存することになると思うのですが、配列以外にも文字列とかも保存できてしまいます。

// これがエラーにならない
$hoge->json_column = 'json';
$hoge->save();

気をつければ問題ない話ですが、もしも間違えてセットした時にエラーになってくれないのはかなりストレスです。

そもそもなぜエラーにならないのかというと、 Eloquent Model に値をセットする時は以下のような経路を辿って処理されます。

まず、json へキャスト可能かどうかの判定をクリアします。

Illuminate/Database/Eloquent/Concerns/HasAttributes.php
    public function setAttribute($key, $value)
    {
        // 〜省略〜

        // 下記 if 文を通る
        if (! is_null($value) && $this->isJsonCastable($key)) {
            $value = $this->castAttributeAsJson($key, $value);
        }
        // 〜省略〜

次に、 json にキャストされるのですが、

Illuminate/Database/Eloquent/Concerns/HasAttributes.php
    protected function castAttributeAsJson($key, $value)
    {
        $value = $this->asJson($value);

        if ($value === false) {
            throw JsonEncodingException::forAttribute(
                $this, $key, json_last_error_msg()
            );
        }

        return $value;
    }

単に json_encode が実行されるだけとなっています。
よって、配列以外でも保存できてしまいます。

Illuminate/Database/Eloquent/Concerns/HasAttributes.php
    protected function asJson($value)
    {
        return json_encode($value);
    }

解決策

これを解決するために、明示的に attributes にセットする時の型を指定させました。

    public function setJsonColumnAttribute(array $jsonColumn): void
    {
        $this->attributes['json_column'] = json_encode($jsonColumn);
    }

少し面倒ですがこれによって、配列以外は保存しようとするとエラーになってくれるので、型安全にコードを書くことができます。

良かった良かった。

Discussion

ふわせぐふわせぐ

以下のポストで詳細かいてるのですが,ただの文字列も JSON なので,Laravel は間違っていないんですよね.
https://x.com/fuwasegu/status/1900405018395910210

この場合連想配列を強制したい場合は,ミューテタでの解決ではなく CustomCast を作るのが良いと思います!

また,Model の PHPDoc に @property array $foo のように書いておけば,エディタや PHPStan で検出できるのでおすすめです.

参考:
カスタムキャスト: https://laravel.com/docs/12.x/eloquent-mutators#custom-casts
RFC: https://tex2e.github.io/rfc-translater/html/rfc8259.html

hirokita117hirokita117

コメントありがとうございます🙇
また、正攻法についてもご教授いただきありがとうございます🙏
コメントへのリンクを本文に記載させていただきました!