laravel
Eloquent

Laravel の Eloquent ORM で update() 実行時にミューテータ(setXxxAttribute())が動かない

確認環境

  • Laravel 5.6
  • PHP 7.1

先に結論

hasOne 等の Relationship を利用しているとき、

  • $user->contact()->update([/*...*/]) のような形だとミューテータは動かない
  • $user->contact->update(/*...*/) のような形だとミューテータは動く

事象

例として、ユーザ(user)と連絡先(contact) の1対1関係があり、電話番号は永続化の際にミューテータを介して暗号化する、というケースを考える。

app/User.php
<php

namespace App;

use Illuminate\Database\Eloquent\Model as Eloquent;

class User extends Eloquent
{
    public function contact()
    {
        $this->hasOne('App\Contact');
    }
}
app/Contact.php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model as Eloquent;

class Contact extends Eloquent
{
    protected $fillable = [
        'user_id',
        'telephone_number',
    ];

    // 電話番号は暗号化して保存したい

    public function setTelephoneNumberAttribute($value)
    {
        $this->attributes['telephone_number'] = encrypt($value);
    }

    public function getTelephoneNumberAttribute($value)
    {
        return decrypt($value);
    }
}

このとき $user->contact()->update() による更新ではミューテータによる暗号化が行われない。

tinker
>>> $user = App\User::first();
=> App\User {
    id: 1
}

>>> $user->contact()->create(['telephone_number' => '00-0000-0000'])
=> App\Contact {
    id: 1,
    user_id: 1,
    // 暗号化されている
    telephone_number: "eyJpdiI6ImpKdGZLMXhvZTRGdzRwekNGM3lXN0E9PSIsInZhbHVlIjoiOTJwMTQxY1JHRk5ueWYrMGRBVGNVNVhtZzJFMTk2a0l3ajlkTGRQT29wZz0iLCJtYWMiOiJlNWM4ZTJiZjFlMTg4MWRjOTMzMWVkZDIwNTU2NmY4OTIwMzU0YzM4NGIxMzYyNjAxYTMxM2MzYTlhYzJmZmNhIn0="
}

>>> $user->contact()->update(['telephone_number' => '11-1111-1111'])
=> 1
>>> $user->contact
=> App\Contact {
    id: 1,
    user_id: 1,
    // 暗号化されていない!
    telephone_number: "11-1111-1111"
}

ちなみに、attribute を再代入して save() する場合はミューテータによる暗号化が行われる。

tinker(続き)
>>> $user->contact->telephone_number = '11-1111-1111'
=> '11-1111-1111'
>>> $user->contact->save()
=> App\Contact {
    id: 1,
    user_id: 1,
    // 暗号化されている
    telephone_number: "eyJpdiI6IkhuQVo0MlpKajZoV0MxWmUxUDNlSEE9PSIsInZhbHVlIjoiMEo2TjNxN3AyREpQK2xhVVFqNWtFeGRyU01Feng2YVROdTBYWTFRSWFLdz0iLCJtYWMiOiI2YjY2NWFjNTc5YzBmZDdkZjYwZGM4YjdiODMyYmFhYmFhMGZmZjI1NTQxNjE5N2VlOGI2YWVmOWQ4MmZkMWMwIn0="
}

さらに調べた結果、$user->contact->update() (※)だとミューテータによる暗号化が行われることがわかった。
contact() の有無の違い

tinker(続き)
>>> $user->contact->update(['telephone_number' => '22-2222-2222'])
=> 1
>>> $user->contact
=> App\Contact {
    id: 1,
    user_id: 1,
    // 暗号化されている
    telephone_number: "eyJpdiI6InlxNzFVMk9lWXJsODU4VzJpUG1QUXc9PSIsInZhbHVlIjoiTHNmU3BSQjdJR2s1SVc3NCt0MXRKVURPeGtQMmJVMXpLTFpZbUdxWjg0Zz0iLCJtYWMiOiI0MDk0OTFhODlkMWMxYjVkNTVjMDA2ZTI3MzljMGMyNDRmMjUxMDVjZGM3ODViNmIxMTdiNWI2ZTQxNWY0MmJkIn0="
}

なぜそうなるか?

$user->contact()$user->contact で返ってくるオブジェクトが異なる。

tinker
>>> $user->contact()
=> Illuminate\Database\Eloquent\Relations\HasOne {}
>>> $user->contact
=> App\Contact {
    id: 1,
    user_id: 1,
    telephone_number: "eyJpdiI6InlxNzFVMk9lWXJsODU4VzJpUG1QUXc9PSIsInZhbHVlIjoiTHNmU3BSQjdJR2s1SVc3NCt0MXRKVURPeGtQMmJVMXpLTFpZbUdxWjg0Zz0iLCJtYWMiOiI0MDk0OTFhODlkMWMxYjVkNTVjMDA2ZTI3MzljMGMyNDRmMjUxMDVjZGM3ODViNmIxMTdiNWI2ZTQxNWY0MmJkIn0="
}

前者は Query Builder に対する操作、後者は Eloquent ORM (App\Contact) に対する操作となるため、このような差が起こる。

知らないと嫌なタイミングで引っかかる罠だ。