メモ Laravel

LaravelのSeederではModelのguardが無効になることで場合によって存在しないカラムへの挿入でエラーとなる

2024.06.28

こんにちはjunです。Laravelのseederでfactoryのcreate()を使用せず、new Model()からなるモデルメソッドからデータ挿入を行った時に詰まったことがあり、その忘備録です。

結論としてSeederではモデルのguardが無効になり、fill()を使用した挿入で存在しないカラムへのinsertが走ってエラーになることが原因でした。背景から話すので、もしさっさと解決策を知りたい方は「解決方法」のとこまで移動してください。

モデルベースで挿入処理をしたい

Laravelにはダミーデータを挿入するためにseederと呼ばれる挿入スクリプトファイル、factoryというfakerを利用して指定モデルへのダミーデータの定義を作成できます。とりあえずデータを作成してページングや表示の様子を確かめる時に非常に便利です。

ただしこのfacotryで挿入するときは「DBのinsert処理を走らせる」方法です。factoryと連結したモデルのキャストなどは考慮してくれますが、基本的にはDBのinsert文を作成するような感じです。

// ※ Laravel6での記述方法です!

/** @var \Illuminate\Database\Eloquent\Factory $factory */

use App\Models\Article;
use Faker\Generator as Faker;

$factory->define(Article::class, function (Faker $faker) {
    return [
        'title'=>$faker->text(),
        'content'=>$faker->realText(),
        'is_active'=>true, // キャストしてくれる。true => 1
        'author'=> User::inRandomOrder()->first()->id // ID番号を指定しないとダメ。
    ];
});

複雑なリレーションがあったり、モデル内でなんやかんやして他のモデルやデータに変更を与える場合は上記の方法では面倒なことがあります。そのため、facotryで仮のhttpリクエストに相当する連想配列を作成しておき、リクエストボディから挿入処理を行うときのメソッドを経由してデータを作成したい時があります。

例ではこんな感じ..

// Controller.php
public function createData(HogeRequest $request){
    $m = new TargetModel();
    $m->add($request->validated());
    return $m
}


// TargetModel.php
public function add(array $val){
    $this->fill($val);
    $this->save();
    // complex process...
    return $this;
}
// ↑このメソッドを使用したい!


// seeder/insertDummy.php
$f = factory(App\Models\TargetModel.php)->make(); // データの挿入はせず、fakerで作成した連想配列が取得できます..!
$m = new TargetModel();
$m->add($f);

例として、id,title,contentのカラムを持つArticleテーブルとManyToManyのリレーションをもつTagsテーブルがあるとしましょう。作成する際のhttp bodyでは

[
    'title'=>'タイトルだよ',
    'content'=>'文章文章...'
    'tags'=>[1,3]
]

このようにフロントエンドから送信され、対象のArticleが作成されたのちに指定のTagsとのIDが連携されます。

// Article.php
protected $guarded = ['id'];

public function tag(){
    return $this->hasMany(\App\Models\Tags);
}

public function add(array $val){
    $this->fill($val);
    $this->save();
    $this->sync($val['tags'])
    return $this;
}

モデルではguardedを指定して、fillが使用できるようにします。リレーション部分のカラムはlaravelが対応してくれます。そのため、fill()ではtagsというカラムに挿入させるようなSQLは生成されなくなります。

しかしseederでこのメソッドを使用した時に Array to srting convertion というエラーが発生し詰まってしまいました。よくよくエラートレースを見ると、insert文のカラムにtagsが存在しているのがわかりました。「filableが正常に機能していないのか?」と予測をたてて調べたらどうやら「seeder中はfactory通りの内容が挿入されるように、guardedが無効になりfillの値が全て考慮される」とのことでした。fillableがワイルドカードのような状態となり、tagsがArticlesのカラムとして認識されたことが原因です。

解決方法

ではseeder中でもモデルのfillable,guardedを有効にすれば解決します。その専用のメソッドがあります。

use Illuminate\Database\Eloquent\Model;

Model::reguard(); //←これをfillの前に入れる

この静的メソッドを呼び出すことでguardedを再度有効にでき、指定・存在するカラムのみにfillが行われます。

参考

https://stackoverflow.com/questions/67092809/fillable-doesnt-work-when-creating-model-from-seeder

Copyright © 2021 jun. All rights reserved.