Edited at

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

More than 1 year has passed since last update.


確認環境


  • 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) に対する操作となるため、このような差が起こる。

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