💁‍♂️

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

2024/12/20に公開

概要

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

本題

先に結論

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

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