Edited at

僕がLaravelのEloquentに$fillableでなく$guardedを指定する理由

More than 1 year has passed since last update.


はじめに

みなさん、Laravel捗っていますか?

僕が日々Laravelのコードを書いている上で困っている点の1つが、Eloquent$fillable指定です。

基本的に$guardedを使う方が良いと思っているのでそれについて簡単にまとめます。

もし「$fillableを使うとこういう点で便利だよ!」というのがあればコメントで教えてくださると助かります。


$fillable$guarded


$fillable

所謂ホワイトリストです。$fillableに指定したカラムのみ、create()fill()update()で値が代入されます。


app/User.php

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
protected $fillable = [
'name',
'sex'
];
}



app/Http/Controllers/UsersController.php

<?php

namespace App\Http\Controllers;

use App\User;
use Illuminate\Http\Request;

class UsersController extends Controller
{
public function update(Request $request, User $user)
{
// $fillableに指定したもの以外は入らない
$user->update($request->all());

return redirect()->route('user.edit', $user);
}
}



$guarded

所謂ブラックリストです。$guardedに指定したカラムのみ、create()fill()update()で値が代入されません。


app/User.php

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
protected $guarded = [
'id',
'email',
'password'
];
}



app/Http/Controllers/UsersController.php

<?php

namespace App\Http\Controllers;

use App\User;
use Illuminate\Http\Request;

class UsersController extends Controller
{
public function update(Request $request, User $user)
{
// $guardedに指定していないものは全て入り得る
$user->update($request->all());

return redirect()->route('user.edit', $user);
}
}



Pros & Cons


$fillable


Pros


  • 指定したもの以外は入り得ない


Cons


  • カラムが増えるたびに追加する必要がある


    • 特に横に長いテーブルだとツラい ← これで困ったので今回このPOSTを書きました




$guarded


Pros


  • 基本的に指定は増えない



    • idemailpassword などだけ




Cons


  • 指定していないものは全て入り得る


上記を踏まえた上で $guarded を選んだ理由

そもそも、Modelに指定する$fillable$guardedは、Model・DB単位で予期せぬ代入が起こると困るものを書くべきだと考えました。というのも、複数代入されることを期待しているデータは、モデルごとにあるのではなくアクションごとにあります。

$fillableに指定した全てのカラムが、一度のリクエストで代入されるとは限らず、例えばページごとに、複数代入されるカラムが違う可能性もあります。すると、$fillableには入れているが、このアクションで代入されることは考えていないデータが悪意あるユーザーによって渡されるかもしれません。

しかし、$guardedでもこれは起こり得ます。


ではどのようにするか

フィルタリングの役割分担をします。具体的に言うと、



  • Model・DB単位で書き換えられると困るものや、個別の更新ロジックがあるものはModel$guardedに指定する



    • idemailuniqueなどの制約がかかっているでしょうし、passwordも専用の変更アクションがあるでしょう



  • アクションごとに書き換えられるべきもののみを、FormRequest側で抽出する



    • $request->all()を使わない



こうすることで、指定していないものは全て入り得る現象を回避できます。


コード


app/User.php

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
protected $guarded = [
'id',
'email',
'password'
];
}



app/Http/Controllers/UsersController.php

<?php

namespace App\Http\Controllers;

use App\User;
use Illuminate\Http\Requests\User\UpdateUserRequest;

class UsersController extends Controller
{
public function update(UpdateUserRequest $request, User $user)
{
// UpdateUserRequest@userAttributesで必要なものだけ返す
$user->update($request->userAttributes());

return redirect()->route('user.edit', $user);
}
}



app/Http/Requests/User/UpdateUserRequest.php

<?php

use Illuminate\Foundation\Http\FormRequest;

class UpdateUserRequest extends FormRequest
{
public function authorize()
{
return true;
}

public function rules()
{
return [
'name' => 'required|string',
'sex' => 'required|in:1,2,3',
];
}

public function userAttributes()
{
return $this->only([
'name',
'sex'
]);
}
}


こうすることで、いくつかの利点が得られます。


  • 予期せぬ代入を防げる

  • View側を見なくても、どのカラムが更新されるか分かる

  • バリデーションなどもFormRequestに書くので、バリデーションチェックする値と、代入される値が同じクラスに存在していて保守性が上がる

もちろんFormRequest側をこのようにした上で$fillableを指定しても構いませんが、先述した通り、カラムが増えるたびに追加する必要があることと、役割分担があいまいになってしまうため、$guardedを使うようにしました。

(もともと横長のテーブルが根本的原因であるのかもしれませんが……)


まとめ



  • \$model->fill($request->all())を止めよう


  • ModelFormRequestのそれぞれで期待したデータ以外が入らないようにしよう


  • $fillableでも問題はないが、テーブルが横長になるほどツラくなる


参考サイト