Eloquent の基本
データベースの表を PHP のクラス(モデル)として扱う Eloquent の基本を、モデルの作り方、取得・追加・更新・削除、スコープ、イベントまで説明します。
Eloquent は、データベースの表を、PHP のクラスとして扱えるようにする Laravel のしくみです。ORM(オブジェクトリレーショナルマッパー。表の行を PHP のオブジェクトとして扱う道具)と呼ばれます。表1つにつき、「モデル」というクラスが1つ対応します。モデルを使うと、表からデータを取り出すだけでなく、追加・更新・削除もできます。
補足
始める前に、config/database.php に、データベースの接続を設定しておいてください。設定のしかたはデータベースの基本のページにあります。
モデルのクラスを作る#
モデルは、ふつう app/Models フォルダに置き、Illuminate\Database\Eloquent\Model クラスを受け継ぎます。make:model という Artisan コマンド(php artisan で動かす Laravel のコマンド)で作れます。
php artisan make:model Flight
モデルを作るとき、マイグレーション(データベースの表を作ったり変えたりする手順書)も一緒に作りたいときは、--migration か -m を付けます。
php artisan make:model Flight --migration
モデルと一緒に、ファクトリ、シーダー、ポリシー、コントローラー、フォームリクエストなど、いろいろなクラスも作れます。オプションを組み合わせれば、まとめて作れます。
# Generate a model and a FlightFactory class...
php artisan make:model Flight --factory
php artisan make:model Flight -f
# Generate a model and a FlightSeeder class...
php artisan make:model Flight --seed
php artisan make:model Flight -s
# Generate a model and a FlightController class...
php artisan make:model Flight --controller
php artisan make:model Flight -c
# Generate a model, FlightController resource class, and form request classes...
php artisan make:model Flight --controller --resource --requests
php artisan make:model Flight -crR
# Generate a model and a FlightPolicy class...
php artisan make:model Flight --policy
# Generate a model and a migration, factory, seeder, and controller...
php artisan make:model Flight -mfsc
# Shortcut to generate a model, migration, factory, seeder, policy, controller, and form requests...
php artisan make:model Flight --all
php artisan make:model Flight -a
# Generate a pivot model...
php artisan make:model Member --pivot
php artisan make:model Member -p
| オプション | 一緒に作るもの |
|---|---|
--migration / -m |
マイグレーション |
--factory / -f |
ファクトリ |
--seed / -s |
シーダー |
--controller / -c |
コントローラー |
--resource / -r |
コントローラーをリソースコントローラーにする(公式の例では --controller と並べて使う) |
--requests / -R |
フォームリクエスト |
--policy |
ポリシー |
--all / -a |
モデル、マイグレーション、ファクトリ、シーダー、ポリシー、コントローラー、フォームリクエストの全部 |
--pivot / -p |
中間表のモデル(pivot) |
モデルの中身を見る#
モデルのコードを眺めても、使える属性(カラムの値)やリレーション(表どうしのつながり)が全部は分からないことがあります。model:show コマンドを使うと、一覧で見られます。
php artisan model:show Flight
モデルの決まり#
make:model で作ったモデルは、app/Models に置かれます。基本のモデルのクラスを見て、Eloquent の主な決まりを説明していきます。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
// ...
}
表の名前#
上の例では、Flight モデルがどの表に対応するか、書いていません。何も書かないと、クラスの名前を「スネークケース」(小文字を _ でつなぐ書き方)の複数形にした名前が、表の名前になります。Flight は flights 表、AirTrafficController は air_traffic_controllers 表に対応します。
表の名前が、この決まりに合わないときは、Table 属性(PHP の属性。クラスの前に書く印)で、名前を決められます。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Table;
use Illuminate\Database\Eloquent\Model;
#[Table('my_flights')]
class Flight extends Model
{
// ...
}
主キー#
Eloquent は、どの表にも、id という名前の主キー(行を区別する番号のカラム)があると考えます。別のカラムを主キーにしたいときは、Table 属性の key 引数に書きます。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Table;
use Illuminate\Database\Eloquent\Model;
#[Table(key: 'flight_id')]
class Flight extends Model
{
// ...
}
また、Eloquent は、主キーが自動で増える整数だと考えて、主キーを整数に変えて扱います。増えない主キーや、数字でない主キーを使うときは、Table 属性の keyType と incrementing に書きます。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Table;
use Illuminate\Database\Eloquent\Model;
#[Table(key: 'uuid', keyType: 'string', incrementing: false)]
class Flight extends Model
{
// ...
}
自動で増えるのをやめたいだけなら、WithoutIncrementing 属性が使えます。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\WithoutIncrementing;
use Illuminate\Database\Eloquent\Model;
#[WithoutIncrementing]
class Flight extends Model
{
// ...
}
複合主キーは使えない#
Eloquent のモデルには、1つで行を区別できる「ID」が、最低でも1つ必要です。複数のカラムを組み合わせた「複合」主キーには、対応していません。ただし、主キーとは別に、複数のカラムのユニークなインデックス(重ならない値の目印)を、表に足すことはできます。
UUID と ULID のキー#
主キーを、自動で増える整数でなく、UUID にしたいことがあります。UUID は、世界中で重ならない、36文字の英数字の ID です。
モデルで UUID を使うには、Illuminate\Database\Eloquent\Concerns\HasUuids トレイト(クラスに機能を足す部品)を使います。表にも、UUID に合う主キーのカラムが必要です。
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Model;
class Article extends Model
{
use HasUuids;
// ...
}
$article = Article::create(['title' => 'Traveling to Europe']);
$article->id; // "018f2b5c-6a7f-7b12-9d6f-2f8a4e0c9c11"
HasUuids は、既定で UUIDv7 の ID を作ります。UUIDv7 は、文字の順に並べられるので、インデックスをつけたデータベースで効率がよくなります。
モデルに newUniqueId メソッドを書くと、UUID の作り方を変えられます。また、uniqueIds メソッドを書くと、UUID を入れるカラムを決められます。
use Ramsey\Uuid\Uuid;
/**
* Generate a new UUID for the model.
*/
public function newUniqueId(): string
{
return (string) Uuid::uuid4();
}
/**
* Get the columns that should receive a unique identifier.
*
* @return array<int, string>
*/
public function uniqueIds(): array
{
return ['id', 'discount_code'];
}
UUID の代わりに「ULID」も使えます。ULID は UUID に似ていますが、26文字と短く、順番に並べられるので、インデックスの効率もよくなります。ULID を使うには、Illuminate\Database\Eloquent\Concerns\HasUlids トレイトを使い、表にULID に合う主キーのカラムを用意します。
use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Model;
class Article extends Model
{
use HasUlids;
// ...
}
$article = Article::create(['title' => 'Traveling to Asia']);
$article->id; // "01gd4d3tgrrfqeda94gdbtdk5c"
タイムスタンプ#
既定では、Eloquent は、モデルの表に created_at と updated_at のカラムがあると考えます。モデルが作られたり更新されたりすると、Eloquent がこの2つの値を自動で入れます。自動で入れてほしくないときは、Table 属性の timestamps を false にします。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Table;
use Illuminate\Database\Eloquent\Model;
#[Table(timestamps: false)]
class Flight extends Model
{
// ...
}
タイムスタンプをやめるだけなら、WithoutTimestamps 属性が使えます。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\WithoutTimestamps;
use Illuminate\Database\Eloquent\Model;
#[WithoutTimestamps]
class Flight extends Model
{
// ...
}
日付の保存の形式を変えたいときは、Table 属性の dateFormat 引数を使います。日付の属性が、データベースにどう保存されるかが決まります。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Table;
use Illuminate\Database\Eloquent\Model;
#[Table(dateFormat: 'U')]
class Flight extends Model
{
// ...
}
日付の形式だけを決めるなら、DateFormat 属性が使えます。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\DateFormat;
use Illuminate\Database\Eloquent\Model;
#[DateFormat('U')]
class Flight extends Model
{
// ...
}
タイムスタンプを入れるカラムの名前を変えたいときは、モデルに CREATED_AT と UPDATED_AT の定数を書きます。
<?php
class Flight extends Model
{
/**
* The name of the "created at" column.
*
* @var string|null
*/
public const CREATED_AT = 'creation_date';
/**
* The name of the "updated at" column.
*
* @var string|null
*/
public const UPDATED_AT = 'updated_date';
}
updated_at を変えずに、モデルを操作したいときは、withoutTimestamps に渡すクロージャ(名前のない関数)の中で操作します。
Model::withoutTimestamps(fn () => $post->increment('reads'));
データベース接続#
既定では、すべての Eloquent モデルは、アプリの既定のデータベース接続を使います。特定のモデルだけ別の接続を使いたいときは、Connection 属性を使います。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Connection;
use Illuminate\Database\Eloquent\Model;
#[Connection('mysql')]
class Flight extends Model
{
// ...
}
属性の既定の値#
新しく作ったモデルは、最初は属性の値を持っていません。いくつかの属性に、既定の値を持たせたいときは、モデルに $attributes プロパティ(クラスの中の変数)を書きます。値は、データベースから読んだときと同じ、保存できる形で書きます。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
/**
* The model's default values for attributes.
*
* @var array<string, mixed>
*/
protected $attributes = [
'options' => '[]',
'delayed' => false,
];
}
書いたあとに属性を読み直す#
データベースに生成カラム(ほかのカラムから自動で計算されるカラム)があるときは、モデルを追加・更新したあとに、特定の属性を読み直すように決められます。モデルに Refreshes 属性を書きます。
use Illuminate\Database\Eloquent\Attributes\Refreshes;
#[Refreshes('name')]
class User extends Model
{
// ...
}
配列で、複数の属性も書けます。
#[Refreshes(['name', 'slug'])]
モデルを書き込んだあと、決めた属性が、データベースから読み直されます。
Eloquent を厳しくする#
Eloquent の動きを「厳しく」するメソッドがいくつかあります。
まず、preventLazyLoading は、レイジーロード(関連するデータを、使う時点になってから読み込むこと)を禁止するかを、真偽値で決めます。たとえば、本番以外でだけ禁止しておくとします。すると、レイジーロードがうっかり本番のコードに残っていても、本番は止まらずに動き続けます。ふつうは、アプリの AppServiceProvider の boot メソッドで呼びます。
use Illuminate\Database\Eloquent\Model;
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Model::preventLazyLoading(! $this->app->isProduction());
}
また、preventSilentlyDiscardingAttributes を呼ぶと、入れてよいと決めていない属性に値を入れようとしたときに、例外(エラー)を投げさせられます。ふつうは、そうした値は黙って捨てられます。モデルの fillable の配列に書き忘れた属性に値を入れても気づけず、あとで思わぬエラーになります。手元で開発している間に、これを防げます。
Model::preventSilentlyDiscardingAttributes(! $this->app->isProduction());
| メソッド | 働き |
|---|---|
Model::preventLazyLoading |
レイジーロードを禁止する(例外を投げる) |
Model::preventSilentlyDiscardingAttributes |
入れられない属性に値を入れようとしたら、例外を投げる |
モデルを取り出す#
モデルと、それに対応する表を作ったら、データベースからデータを取り出せます。Eloquent のモデルは、1つ1つが強力なクエリビルダ(SQL を書かずに問い合わせるしくみ)だと考えられます。モデルの all は、表の全部の行を取り出します。
use App\Models\Flight;
foreach (Flight::all() as $flight) {
echo $flight->name;
}
問い合わせを組み立てる#
all は、表の全部の行を返します。モデルはクエリビルダなので、条件を足して、最後に get で結果を取り出すこともできます。
$flights = Flight::where('active', 1)
->orderBy('name')
->limit(10)
->get();
補足
Eloquent のモデルはクエリビルダなので、クエリビルダのメソッドは、すべて Eloquent の問い合わせでも使えます。
モデルを読み直す#
すでにデータベースから取り出したモデルは、fresh と refresh で「読み直せ」ます。fresh は、データベースから、もう一度モデルを取り出します。いまのモデルは変わりません。
$flight = Flight::where('number', 'FR 900')->first();
$freshFlight = $flight->fresh();
refresh は、いまのモデルを、データベースの新しいデータで作り直します。読み込み済みのリレーションも、読み直されます。
$flight = Flight::where('number', 'FR 900')->first();
$flight->number = 'FR 456';
$flight->refresh();
$flight->number; // "FR 900"
トランザクション(ひとまとめの処理)の中で、モデルを読み直しながら、悲観的ロック(ほかから書き換えられないようにする鍵)も取りたいときは、refreshForUpdate を使います。FOR UPDATE のロックで、モデルを読み込み直します。
DB::transaction(function () use ($flight) {
$flight->refreshForUpdate();
// Update the locked model...
});
コレクション#
all や get は、複数の行を取り出しますが、ふつうの PHP の配列ではなく、Illuminate\Database\Eloquent\Collection を返します。
この Collection は、Laravel の基本の Illuminate\Support\Collection を受け継いでいて、データを扱う便利なメソッドがたくさんあります。たとえば、reject は、クロージャの結果で、モデルをコレクションから取り除きます。
$flights = Flight::where('destination', 'Paris')->get();
$flights = $flights->reject(function (Flight $flight) {
return $flight->cancelled;
});
基本のコレクションのメソッドに加えて、Eloquent のコレクションには、モデルを扱うための特別なメソッドもあります。
Laravel のコレクションは、どれも PHP の iterable(くり返せるもの)のしくみを持っているので、配列のように、ループで回せます。
foreach ($flights as $flight) {
echo $flight->name;
}
少しずつ取り出す(チャンク)#
何万もの行を、all や get で読み込むと、メモリが足りなくなることがあります。そのときは、chunk を使うと、たくさんのモデルを効率よく扱えます。
chunk は、モデルの一部を取り出して、クロージャに渡します。いまの分だけをメモリに読むので、メモリの使い方が大きく減ります。
use App\Models\Flight;
use Illuminate\Database\Eloquent\Collection;
Flight::chunk(200, function (Collection $flights) {
foreach ($flights as $flight) {
// ...
}
});
chunk の1つ目の引数は、1回に受け取る行の数です。2つ目のクロージャは、取り出すたびに呼ばれます。クロージャに渡す行を取り出すたびに、データベースへの問い合わせが1回動きます。
絞りこみの条件に使っているカラムを、くり返しの途中で書き換えるなら、chunkById を使います。このとき chunk を使うと、結果が食いちがうなど、思わぬことが起きます。chunkById は、いつも「前の塊の最後のモデルより id が大きいもの」を取り出します。
Flight::where('departed', true)
->chunkById(200, function (Collection $flights) {
$flights->each->update(['departed' => false]);
}, column: 'id');
chunkById と lazyById は、取り出すために、問い合わせへ自分で where の条件を足します。そのため、こちらで書く条件は、クロージャでかっこにまとめておくのがふつうです。
Flight::where(function ($query) {
$query->where('delayed', true)->orWhere('cancelled', true);
})->chunkById(200, function (Collection $flights) {
$flights->each->update([
'departed' => false,
'cancelled' => true
]);
}, column: 'id');
lazy で少しずつ取り出す#
lazy も、chunk と同じく、裏では少しずつ問い合わせを動かします。ちがうのは、塊ごとにクロージャへ渡さないことです。代わりに、塊をつなげて1本にした LazyCollection(少しずつ読むコレクション)を返します。そのため、結果の全体を1本の流れとして扱えます。
use App\Models\Flight;
foreach (Flight::lazy() as $flight) {
// ...
}
絞りこみの条件に使っているカラムを、くり返しの途中で書き換えるなら、lazyById を使います。こちらも、いつも「前の塊の最後のモデルより id が大きいもの」を取り出します。
Flight::where('departed', true)
->lazyById(200, column: 'id')
->each->update(['departed' => false]);
id の大きい順で取り出したいときは、lazyByIdDesc を使います。
カーソル#
cursor も、lazy と同じく、何万ものモデルをくり返すときのメモリの使い方を、大きく減らします。
cursor は、データベースへの問い合わせを1回だけ動かします。ただし、1つ1つのモデルは、実際にくり返しで読まれるまで作られません。そのため、いつでもメモリにあるモデルは1つだけです。
注意
cursor は、メモリに1つのモデルしか持たないので、リレーションの先読み(eager load)ができません。先読みしたいときは、代わりに lazy を使ってください。
cursor は、中で PHP のジェネレータ(少しずつ値を返すしくみ)を使っています。
use App\Models\Flight;
foreach (Flight::where('destination', 'Zurich')->cursor() as $flight) {
// ...
}
cursor は Illuminate\Support\LazyCollection を返します。LazyCollection では、ふつうのコレクションのメソッドの多くが使えて、しかもメモリにはモデルが1つしか入りません。
use App\Models\User;
$users = User::cursor()->filter(function (User $user) {
return $user->id > 500;
});
foreach ($users as $user) {
echo $user->id;
}
cursor は、ふつうの問い合わせよりずっと少ないメモリで済みますが、それでも、いつかはメモリが足りなくなります。PHP の PDO ドライバが、問い合わせの結果を、内部のバッファ(一時置き場)に全部ためるためです。とても多くの行を扱うときは、代わりに lazy を使うことを考えてください。
取り出しのメソッドの違いは、次のとおりです。
| メソッド | 特徴 |
|---|---|
all |
全部の行を、一度に取り出す |
get |
条件に合う行を、一度に取り出す |
chunk / chunkById |
少しずつ取り出して、クロージャに渡す |
lazy / lazyById / lazyByIdDesc |
少しずつ取り出して、1本の流れとして扱う |
cursor |
問い合わせは1回。モデルは1つずつ作る(先読みはできない) |
くわしいサブクエリ#
Eloquent は、サブクエリ(問い合わせの中の問い合わせ)にも対応していて、関連する表の情報を、1回の問い合わせで引き出せます。たとえば、行き先の表 destinations と、その行き先への便の表 flights があるとします。flights には、到着した日時を表す arrived_at があります。
サブクエリで選ぶ#
クエリビルダの select と addSelect のサブクエリの機能を使うと、全部の行き先と、その行き先にいちばん最近着いた便の名前を、1回の問い合わせで取れます。
use App\Models\Destination;
use App\Models\Flight;
return Destination::addSelect(['last_flight' => Flight::select('name')
->whereColumn('destination_id', 'destinations.id')
->orderByDesc('arrived_at')
->limit(1)
])->get();
サブクエリで並べる#
クエリビルダの orderBy も、サブクエリに対応しています。同じ例で、全部の行き先を、最後の便が着いた時刻の順に並べられます。これも1回の問い合わせです。
return Destination::orderByDesc(
Flight::select('arrived_at')
->whereColumn('destination_id', 'destinations.id')
->orderByDesc('arrived_at')
->limit(1)
)->get();
1つのモデルや集計を取り出す#
条件に合う行を全部取り出すだけでなく、find・first・firstWhere で、1つのモデルだけを取り出せます。コレクションではなく、モデルが1つ返ります。
use App\Models\Flight;
// Retrieve a model by its primary key...
$flight = Flight::find(1);
// Retrieve the first model matching the query constraints...
$flight = Flight::where('active', 1)->first();
// Alternative to retrieving the first model matching the query constraints...
$flight = Flight::firstWhere('active', 1);
結果がないときに、ほかのことをしたいなら、findOr と firstOr を使います。モデルが1つ返りますが、なければ、渡したクロージャが動き、その戻り値が結果になります。
$flight = Flight::findOr(1, function () {
// ...
});
$flight = Flight::where('legs', '>', 3)->firstOr(function () {
// ...
});
見つからないときに例外を投げる#
モデルが見つからないとき、例外を投げたいことがあります。ルートやコントローラーで便利です。findOrFail と firstOrFail は、最初の結果を取り出しますが、なければ Illuminate\Database\Eloquent\ModelNotFoundException を投げます。
$flight = Flight::findOrFail(1);
$flight = Flight::where('legs', '>', 3)->firstOrFail();
ModelNotFoundException を受け止めないと、404 のレスポンスが自動で返ります。
use App\Models\Flight;
Route::get('/api/flights/{id}', function (string $id) {
return Flight::findOrFail($id);
});
| メソッド | 働き |
|---|---|
find |
主キーで、モデルを1つ取り出す |
first |
条件に合う最初のモデルを取り出す |
firstWhere |
条件を決めて、最初のモデルを取り出す |
findOr / firstOr |
なければ、渡したクロージャを動かす |
findOrFail / firstOrFail |
なければ、ModelNotFoundException を投げる(受け止めなければ 404) |
取り出すか、作る#
firstOrCreate は、決めたカラムと値で、行を探します。モデルがなければ、1つ目の配列と、省いてもよい2つ目の配列を合わせた値で、行を追加します。
firstOrNew も、同じように行を探しますが、なければ、新しいモデルを返すだけで、データベースには保存しません。保存するには、自分で save を呼びます。
use App\Models\Flight;
// Retrieve flight by name or create it if it doesn't exist...
$flight = Flight::firstOrCreate([
'name' => 'London to Paris'
]);
// Retrieve flight by name or create it with the name, delayed, and arrival_time attributes...
$flight = Flight::firstOrCreate(
['name' => 'London to Paris'],
['delayed' => 1, 'arrival_time' => '11:30']
);
// Retrieve flight by name or instantiate a new Flight instance...
$flight = Flight::firstOrNew([
'name' => 'London to Paris'
]);
// Retrieve flight by name or instantiate with the name, delayed, and arrival_time attributes...
$flight = Flight::firstOrNew(
['name' => 'Tokyo to Sydney'],
['delayed' => 1, 'arrival_time' => '11:30']
);
集計する#
Eloquent のモデルでも、count・sum・max など、クエリビルダの集計のメソッドが使えます。モデルではなく、1つの値が返ります。
$count = Flight::where('active', 1)->count();
$max = Flight::where('active', 1)->max('price');
モデルを追加・更新する#
追加する#
Eloquent では、データベースからモデルを取り出すだけでなく、新しい行も追加します。新しいモデルを作り、属性に値を入れて、save を呼びます。
<?php
namespace App\Http\Controllers;
use App\Models\Flight;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class FlightController extends Controller
{
/**
* Store a new flight in the database.
*/
public function store(Request $request): RedirectResponse
{
// Validate the request...
$flight = new Flight;
$flight->name = $request->name;
$flight->save();
return redirect('/flights');
}
}
この例では、リクエストの name を、App\Models\Flight の name 属性に入れています。save を呼ぶと、行がデータベースに追加されます。created_at と updated_at は save のときに自動で入るので、自分で入れる必要はありません。
トランザクションの中で保存したいときは、saveOrFail を使います。保存中に例外が起きると、トランザクションが自動で元に戻ります。
$flight->saveOrFail();
create を使えば、PHP の1つの文で、新しいモデルを「保存」できます。追加されたモデルが返ります。
use App\Models\Flight;
$flight = Flight::create([
'name' => 'London to Paris',
]);
ただし、create を使う前に、モデルのクラスに、Fillable か Guarded の属性を書く必要があります。Eloquent のモデルは、既定で、マスアサインメント(配列でまとめて値を入れること)の弱点をつかれないように守られているからです。くわしくは、あとのマスアサインメントの節を見てください。
更新する#
すでにあるモデルの更新にも save を使えます。モデルを取り出し、更新したい属性を書き換えて、save を呼びます。updated_at も自動で更新されます。
use App\Models\Flight;
$flight = Flight::find(1);
$flight->name = 'Paris to London';
$flight->save();
トランザクションの中で更新したいときは、updateOrFail を使います。更新中に例外が起きると、トランザクションが自動で元に戻ります。
$flight->updateOrFail(['name' => 'Paris to London']);
すでにあるモデルを更新し、なければ新しく作りたいことがあります。firstOrCreate と同じく、updateOrCreate は保存までするので、save を呼ぶ必要はありません。
次の例では、departure が Oakland で destination が San Diego の便があれば、price と discounted を更新します。なければ、1つ目と2つ目の配列を合わせた値で、新しい便を作ります。
$flight = Flight::updateOrCreate(
['departure' => 'Oakland', 'destination' => 'San Diego'],
['price' => 99, 'discounted' => 1]
);
firstOrCreate や updateOrCreate を使うと、新しく作られたのか、すでにあるものが更新されたのか、分からないことがあります。wasRecentlyCreated プロパティを見ると、そのモデルがいま新しく作られたものかが分かります。
$flight = Flight::updateOrCreate(
// ...
);
if ($flight->wasRecentlyCreated) {
// New flight record was inserted...
}
まとめて更新する#
問い合わせの条件に合うモデルを、まとめて更新することもできます。次の例では、active が 1 で、行き先が San Diego の便に、すべて「遅れ」の印を付けます。
Flight::where('active', 1)
->where('destination', 'San Diego')
->update(['delayed' => 1]);
update には、更新するカラムと値の配列を渡します。更新した行の数が返ります。
注意
Eloquent でまとめて更新すると、更新されるモデルの saving・saved・updating・updated のイベントは出ません。まとめて更新するときは、モデルを実際には取り出さないからです。
属性の変化を調べる#
Eloquent には、isDirty・isClean・wasChanged があり、モデルを取り出したときから、属性がどう変わったかを調べられます。
isDirty は、モデルを取り出してから、属性が変わったかを調べます。属性の名前か名前の配列を渡して、その属性が「dirty」(変わった)かを調べることもできます。isClean は、取り出してから変わっていないかを調べます。こちらも属性を渡せます。
use App\Models\User;
$user = User::create([
'first_name' => 'Taylor',
'last_name' => 'Otwell',
'title' => 'Developer',
]);
$user->title = 'Painter';
$user->isDirty(); // true
$user->isDirty('title'); // true
$user->isDirty('first_name'); // false
$user->isDirty(['first_name', 'title']); // true
$user->isClean(); // false
$user->isClean('title'); // false
$user->isClean('first_name'); // true
$user->isClean(['first_name', 'title']); // false
$user->save();
$user->isDirty(); // false
$user->isClean(); // true
wasChanged は、いまのリクエストの中で、最後に保存したときに、属性が変わったかを調べます。属性の名前を渡して、その属性が変わったかも調べられます。
$user = User::create([
'first_name' => 'Taylor',
'last_name' => 'Otwell',
'title' => 'Developer',
]);
$user->title = 'Painter';
$user->save();
$user->wasChanged(); // true
$user->wasChanged('title'); // true
$user->wasChanged(['title', 'slug']); // true
$user->wasChanged('first_name'); // false
$user->wasChanged(['first_name', 'title']); // true
getOriginal は、モデルを取り出したあとに何が変わっても、元の属性の配列を返します。属性の名前を渡すと、その属性の元の値が返ります。
$user = User::find(1);
$user->name; // John
$user->email; // john@example.com
$user->name = 'Jack';
$user->name; // Jack
$user->getOriginal('name'); // John
$user->getOriginal(); // Array of original attributes...
getChanges は、最後に保存したとき変わった属性の配列を返します。getPrevious は、最後に保存する前の、元の属性の値の配列を返します。
$user = User::find(1);
$user->name; // John
$user->email; // john@example.com
$user->update([
'name' => 'Jack',
'email' => 'jack@example.com',
]);
$user->getChanges();
/*
[
'name' => 'Jack',
'email' => 'jack@example.com',
]
*/
$user->getPrevious();
/*
[
'name' => 'John',
'email' => 'john@example.com',
]
*/
| メソッド | 働き |
|---|---|
isDirty |
取り出してから、属性が変わったか |
isClean |
取り出してから、属性が変わっていないか |
wasChanged |
最後に保存したとき、属性が変わったか |
getOriginal |
元の属性の値を返す |
getChanges |
最後に保存したとき変わった属性を返す |
getPrevious |
最後に保存する前の、元の値を返す |
マスアサインメント(一括代入)#
マスアサインメントは、配列でまとめて値を入れることです。create を使えば、PHP の1つの文で、新しいモデルを「保存」できます。追加されたモデルが返ります。
use App\Models\Flight;
$flight = Flight::create([
'name' => 'London to Paris',
]);
ただし、create を使う前に、モデルのクラスに、Fillable か Guarded の属性を書く必要があります。Eloquent のモデルは、既定で、マスアサインメントの脆弱性(弱点)から守られているからです。
マスアサインメントの弱点とは、こういうことです。使う人が、こちらの思っていない項目をリクエストに入れて送ります。その項目が、書き換えるつもりのなかったカラムまで書き換えてしまいます。たとえば、悪い人が is_admin を送り、それがモデルの create に渡されると、自分を管理者にできてしまいます。
そこで、まず、一括で入れてよい属性を決めます。モデルの Fillable 属性を使います。たとえば、Flight の name を一括で入れてよいことにします。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Model;
#[Fillable(['name'])]
class Flight extends Model
{
// ...
}
一括で入れてよい属性を決めたら、create で新しい行を追加できます。新しく作ったモデルが返ります。
$flight = Flight::create(['name' => 'London to Paris']);
すでにモデルがあるときは、fill で、属性の配列を入れられます。
$flight->fill(['name' => 'Amsterdam to Frankfurt']);
JSON のカラムへの一括代入#
JSON のカラム(JSON 形式で値をしまうカラム)に入れるときは、一括で入れてよいキーを、モデルの Fillable 属性に1つずつ書く必要があります。Guarded 属性を使っているときは、安全のため、JSON の中の値をまとめて更新することはできません。
use Illuminate\Database\Eloquent\Attributes\Fillable;
#[Fillable(['options->enabled'])]
class Flight extends Model
{
// ...
}
全部を一括で入れてよくする#
全部の属性を、一括で入れてよくしたいなら、モデルに Unguarded 属性を付けます。守りを外したときは、Eloquent の fill・create・update に渡す配列を、入れてよい値だけでいつも自分で組み立てるように、とくに気をつけます。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Unguarded;
use Illuminate\Database\Eloquent\Model;
#[Unguarded]
class Flight extends Model
{
// ...
}
一括代入の例外#
既定では、Fillable 属性にない属性は、一括代入のとき、黙って捨てられます。本番では、それが期待どおりの動きですが、手元での開発では、モデルの変更が効かない理由が分からなくなって、混乱することがあります。
必要なら、preventSilentlyDiscardingAttributes を呼んでおくと、入れてよいと決めていない属性に値を入れようとしたときに、例外を投げさせられます。ふつうは、アプリの AppServiceProvider クラスの boot で呼びます。
use Illuminate\Database\Eloquent\Model;
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Model::preventSilentlyDiscardingAttributes($this->app->isLocal());
}
| 属性 | 働き |
|---|---|
Fillable |
一括で入れてよい属性を決める |
Guarded |
一括で入れてはいけない属性を決める |
Unguarded |
全部の属性を一括で入れてよくする |
アップサート#
Eloquent の upsert は、「なければ追加・あれば更新」を、途中で割り込まれない1回の操作(アトミック)でします。引数は3つです。1つ目は、追加または更新する値です。2つ目は、行を1つに見分けるためのカラムです。3つ目は、行がすでにあったときに更新するカラムの配列です。モデルでタイムスタンプが有効なら、created_at と updated_at も自動で入ります。
Flight::upsert([
['departure' => 'Oakland', 'destination' => 'San Diego', 'price' => 99],
['departure' => 'Chicago', 'destination' => 'New York', 'price' => 150]
], uniqueBy: ['departure', 'destination'], update: ['price']);
注意
SQL Server 以外のデータベースでは、upsert の2つ目の引数のカラムに、「primary」か「unique」のインデックスが必要です。また、MariaDB と MySQL のドライバーは、2つ目の引数を無視して、表の「primary」と「unique」のインデックスで、すでにある行かを見分けます。
モデルを削除する#
モデルを消すには、モデルの delete を呼びます。
use App\Models\Flight;
$flight = Flight::find(1);
$flight->delete();
トランザクションの中で消したいときは、deleteOrFail を使います。削除中に例外が起きると、トランザクションが自動で元に戻ります。
$flight->deleteOrFail();
主キーで消す#
上の例では、delete の前に、モデルを取り出しています。主キーが分かっているなら、destroy で、取り出さずに消せます。destroy は、主キー1つのほか、複数の主キー、主キーの配列、主キーのコレクションも受け取ります。
Flight::destroy(1);
Flight::destroy(1, 2, 3);
Flight::destroy([1, 2, 3]);
Flight::destroy(collect([1, 2, 3]));
ソフトデリートを使っているモデルを、完全に消したいときは、forceDestroy を使います。
Flight::forceDestroy(1);
注意
destroy は、モデルを1つずつ読み込んで delete を呼ぶので、deleting と deleted のイベントが、モデルごとにきちんと出ます。
問い合わせで消す#
問い合わせで、条件に合うモデルを全部消すこともできます。次の例は、inactive の印がある便を、全部消します。まとめて更新と同じく、まとめて消すと、消したモデルのイベントは出ません。
$deleted = Flight::where('active', 0)->delete();
表の全部のモデルを消すには、条件を付けずに動かします。
$deleted = Flight::query()->delete();
注意
Eloquent でまとめて消すと、消されるモデルの deleting と deleted のイベントは出ません。まとめて消すときは、モデルを実際には取り出さないからです。
ソフトデリート#
データベースから本当に消す代わりに、Eloquent は「ソフトデリート」もできます。ソフトデリートでは、行は実際には消えず、モデルの deleted_at 属性に、「消した」日時が入ります。ソフトデリートを使うには、モデルに Illuminate\Database\Eloquent\SoftDeletes トレイトを足します。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Flight extends Model
{
use SoftDeletes;
}
補足
SoftDeletes トレイトは、deleted_at 属性を、DateTime / Carbon のオブジェクトに、自動で変えて扱います。
表にも、deleted_at カラムが必要です。Laravel のスキーマビルダには、このカラムを作る便利なメソッドがあります。
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
Schema::table('flights', function (Blueprint $table) {
$table->softDeletes();
});
Schema::table('flights', function (Blueprint $table) {
$table->dropSoftDeletes();
});
これで、モデルの delete を呼ぶと、deleted_at に、いまの日時が入ります。ただし、行は表に残ります。ソフトデリートを使うモデルを問い合わせると、ソフトデリートされたモデルは、自動で結果から除かれます。
モデルがソフトデリートされているかは、trashed で調べられます。
if ($flight->trashed()) {
// ...
}
ソフトデリートしたモデルを戻す#
ソフトデリートしたモデルの「消す」を取り消したいときは、モデルの restore を呼びます。deleted_at が null になります。
$flight->restore();
問い合わせの中でも restore を使って、複数のモデルを戻せます。ほかの「まとめて」の操作と同じく、戻したモデルのイベントは出ません。
Flight::withTrashed()
->where('airline_id', 1)
->restore();
restore は、リレーションの問い合わせを作るときにも使えます。
$flight->history()->restore();
完全に消す#
モデルを、データベースから本当に消したいときもあります。ソフトデリートしたモデルを完全に消すには、forceDelete を使います。
$flight->forceDelete();
forceDelete も、Eloquent のリレーションの問い合わせを作るときに使えます。
$flight->history()->forceDelete();
ソフトデリートしたモデルを問い合わせる#
ソフトデリートしたものも含める#
上のとおり、ソフトデリートしたモデルは、結果から自動で除かれます。問い合わせで withTrashed を呼ぶと、ソフトデリートしたモデルも含められます。
use App\Models\Flight;
$flights = Flight::withTrashed()
->where('account_id', 1)
->get();
withTrashed は、リレーションの問い合わせを作るときにも呼べます。
$flight->history()->withTrashed()->get();
ソフトデリートしたものだけを取り出す#
onlyTrashed は、ソフトデリートしたモデルだけを取り出します。
$flights = Flight::onlyTrashed()
->where('airline_id', 1)
->get();
| メソッド | 働き |
|---|---|
trashed |
モデルがソフトデリートされているか調べる |
restore |
ソフトデリートを取り消す |
forceDelete |
完全に消す |
forceDestroy |
主キーで、完全に消す |
withTrashed |
ソフトデリートしたものも含める |
onlyTrashed |
ソフトデリートしたものだけを取り出す |
古いモデルを片づける(プルーニング)#
いらなくなったモデルを、定期的に消したいことがあります。消したいモデルに、Illuminate\Database\Eloquent\Prunable か Illuminate\Database\Eloquent\MassPrunable のトレイトを足します。そのうえで、prunable メソッドを書きます。このメソッドは、いらなくなったモデルを選び出す問い合わせ(Eloquent のクエリビルダ)を返します。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Prunable;
class Flight extends Model
{
use Prunable;
/**
* Get the prunable model query.
*/
public function prunable(): Builder
{
return static::where('created_at', '<=', now()->minus(months: 1));
}
}
Prunable にしたモデルには、pruning メソッドも書けます。モデルが消される前に呼ばれます。モデルが完全に消される前に、保存したファイルなど、関連するものを消すのに便利です。
/**
* Prepare the model for pruning.
*/
protected function pruning(): void
{
// ...
}
片づけるモデルを決めたら、アプリの routes/console.php で、model:prune コマンドをスケジュールに入れます。動かす間隔は自由に選べます。
use Illuminate\Support\Facades\Schedule;
Schedule::command('model:prune')->daily();
model:prune は、アプリの app/Models の中にある「Prunable」のモデルを、自動で見つけます。モデルが別の場所にあるときは、--model で、クラスの名前を決められます。
Schedule::command('model:prune', [
'--model' => [Address::class, Flight::class],
])->daily();
見つけたモデルのうち、一部だけ除きたいときは、--except を使います。
Schedule::command('model:prune', [
'--except' => [Address::class, Flight::class],
])->daily();
prunable の問い合わせを試したいときは、model:prune に --pretend を付けて動かします。実際には消さず、消される行の数だけが報告されます。
php artisan model:prune --pretend
注意
ソフトデリートしたモデルが、prunable の問い合わせに合うと、完全に消されます(forceDelete)。
まとめて片づける#
Illuminate\Database\Eloquent\MassPrunable を付けたモデルは、まとめて消す問い合わせで、データベースから消されます。そのため、pruning は呼ばれず、deleting と deleted のイベントも出ません。消す前にモデルを取り出さないので、片づけがずっと効率よくなります。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\MassPrunable;
class Flight extends Model
{
use MassPrunable;
/**
* Get the prunable model query.
*/
public function prunable(): Builder
{
return static::where('created_at', '<=', now()->minus(months: 1));
}
}
モデルをコピーする#
replicate を使うと、すでにあるモデルの、まだ保存されていないコピーを作れます。ほとんど同じ属性を持つモデルがいくつもあるときに便利です。
use App\Models\Address;
$shipping = Address::create([
'type' => 'shipping',
'line_1' => '123 Example Street',
'city' => 'Victorville',
'state' => 'CA',
'postcode' => '90001',
]);
$billing = $shipping->replicate()->fill([
'type' => 'billing'
]);
$billing->save();
新しいモデルにコピーしたくない属性があるときは、replicate に配列を渡します。
$flight = Flight::create([
'destination' => 'LAX',
'origin' => 'LHR',
'last_flown' => '2020-03-04 11:00:00',
'last_pilot_id' => 747,
]);
$flight = $flight->replicate([
'last_flown',
'last_pilot_id'
]);
クエリスコープ#
スコープは、よく使う問い合わせの条件に名前を付けて、使い回すしくみです。
グローバルスコープ#
グローバルスコープを使うと、あるモデルの、すべての問い合わせに、条件を足せます。Laravel 自身のソフトデリートも、グローバルスコープを使って、「消されていない」モデルだけを取り出しています。自分でグローバルスコープを書くと、あるモデルの問い合わせの全部に、決まった条件を、かんたんに付けられます。
スコープを作る#
新しいグローバルスコープを作るには、make:scope コマンドを使います。作ったスコープは、アプリの app/Models/Scopes に置かれます。
php artisan make:scope AncientScope
グローバルスコープを書く#
グローバルスコープは、かんたんに書けます。まず、make:scope で、Illuminate\Database\Eloquent\Scope インターフェイス(守るべき決まり)を満たすクラスを作ります。Scope では、apply メソッドを1つ書く必要があります。apply は、必要に応じて、where などの条件を、問い合わせに足します。
<?php
namespace App\Models\Scopes;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
class AncientScope implements Scope
{
/**
* Apply the scope to a given Eloquent query builder.
*/
public function apply(Builder $builder, Model $model): void
{
$builder->where('created_at', '<', now()->minus(years: 2000));
}
}
補足
グローバルスコープが、問い合わせの select の部分にカラムを足すときは、select ではなく addSelect を使います。そうすると、問い合わせにすでにある select の部分を、うっかり置き換えずに済みます。
グローバルスコープを付ける#
モデルにグローバルスコープを付けるには、モデルに ScopedBy 属性を付けます。
<?php
namespace App\Models;
use App\Models\Scopes\AncientScope;
use Illuminate\Database\Eloquent\Attributes\ScopedBy;
#[ScopedBy([AncientScope::class])]
class User extends Model
{
//
}
または、モデルの booted メソッドを書き換えて、addGlobalScope を呼び、自分で登録できます。addGlobalScope の引数は、スコープのオブジェクト1つだけです。
<?php
namespace App\Models;
use App\Models\Scopes\AncientScope;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* The "booted" method of the model.
*/
protected static function booted(): void
{
static::addGlobalScope(new AncientScope);
}
}
上の例のスコープを App\Models\User に付けたあとで、User::all() を呼ぶと、次の SQL が動きます。
select * from `users` where `created_at` < 0021-02-18 00:00:00
名前のないグローバルスコープ#
クロージャを使って、グローバルスコープを書くこともできます。専用のクラスを作るほどでもない、かんたんなスコープに便利です。クロージャでグローバルスコープを書くときは、addGlobalScope の1つ目の引数に、自分で選んだスコープの名前を書きます。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* The "booted" method of the model.
*/
protected static function booted(): void
{
static::addGlobalScope('ancient', function (Builder $builder) {
$builder->where('created_at', '<', now()->minus(years: 2000));
});
}
}
グローバルスコープを外す#
ある問い合わせだけ、グローバルスコープを外したいときは、withoutGlobalScope を使います。引数は、グローバルスコープのクラスの名前だけです。
User::withoutGlobalScope(AncientScope::class)->get();
クロージャで書いたグローバルスコープなら、付けた名前の文字列を渡します。
User::withoutGlobalScope('ancient')->get();
複数の、または全部のグローバルスコープを外したいときは、withoutGlobalScopes と withoutGlobalScopesExcept を使います。
// Remove all of the global scopes...
User::withoutGlobalScopes()->get();
// Remove some of the global scopes...
User::withoutGlobalScopes([
FirstScope::class, SecondScope::class
])->get();
// Remove all global scopes except the given ones...
User::withoutGlobalScopesExcept([
SecondScope::class,
])->get();
| メソッド | 働き |
|---|---|
withoutGlobalScope |
決めた1つのグローバルスコープを外す |
withoutGlobalScopes |
全部(または決めたいくつか)のグローバルスコープを外す |
withoutGlobalScopesExcept |
決めたもの以外の、全部のグローバルスコープを外す |
ローカルスコープ#
ローカルスコープは、よく使う問い合わせの条件をひとまとめにして、アプリのあちこちで使い回せるようにしたものです。たとえば、「人気のある」ユーザーをよく取り出すなら、その条件をスコープにしておきます。スコープを作るには、Eloquent のメソッドに Scope 属性を付けます。
スコープは、いつも、同じクエリビルダか void を返します。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* Scope a query to only include popular users.
*/
#[Scope]
protected function popular(Builder $query): void
{
$query->where('votes', '>', 100);
}
/**
* Scope a query to only include active users.
*/
#[Scope]
protected function active(Builder $query): void
{
$query->where('active', 1);
}
}
ローカルスコープを使う#
スコープを書いたら、モデルを問い合わせるとき、スコープのメソッドを呼べます。いくつものスコープを、つなげて呼ぶこともできます。
use App\Models\User;
$users = User::popular()->active()->orderBy('created_at')->get();
複数のスコープを or でつなぐときは、正しいかっこのまとめにするため、クロージャが必要になることがあります。
$users = User::popular()->orWhere(function (Builder $query) {
$query->active();
})->get();
ただ、毎回クロージャを書くのは面倒です。そこで Laravel には、「ハイヤーオーダー」の orWhere があります。これを使うと、クロージャなしで、スコープを orWhere でつないで書けます。
$users = User::popular()->orWhere->active()->get();
引数を取るスコープ#
引数を受け取るスコープを書きたいこともあります。スコープのメソッドの引数に、追加の引数を書くだけです。スコープの引数は、$query のあとに書きます。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* Scope a query to only include users of a given type.
*/
#[Scope]
protected function ofType(Builder $query, string $type): void
{
$query->where('type', $type);
}
}
引数をスコープのメソッドに書いたら、スコープを呼ぶときに、引数を渡せます。
$users = User::ofType('admin')->get();
属性を付けたスコープのメソッドは protected にします。モデルのクラスの中から、属性を付けたスコープを呼ぶときは、static::query()->ofType('admin') のように、クエリビルダ経由で呼びます。そうすると、Eloquent のスコープの処理を通ります。
保留中の属性(pending attributes)#
スコープで絞りこむ条件と同じ値を持ったモデルを、そのスコープから作りたいことがあります。そのときは、スコープの問い合わせの中で withAttributes を使います。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
/**
* Scope the query to only include drafts.
*/
#[Scope]
protected function draft(Builder $query): void
{
$query->withAttributes([
'hidden' => true,
]);
}
}
withAttributes は、渡した属性で、問い合わせに where の条件を足します。さらに、そのスコープで作ったモデルにも、その属性を足します。
$draft = Post::draft()->create(['title' => 'In Progress']);
$draft->hidden; // true
where の条件を問い合わせに足したくないときは、asConditions 引数を false にします。
$query->withAttributes([
'hidden' => true,
], asConditions: false);
モデルを比べる#
2つのモデルが「同じ」かを調べたいときがあります。is と isNot を使うと、主キー・表・データベース接続が同じかどうかを、すばやく調べられます。
if ($post->is($anotherPost)) {
// ...
}
if ($post->isNot($anotherPost)) {
// ...
}
is と isNot は、belongsTo・hasOne・morphTo・morphOne のリレーションでも使えます。関連するモデルを取り出す問い合わせを動かさずに、そのモデルと比べたいときに、とくに便利です。
if ($post->author()->is($user)) {
// ...
}
イベント#
補足
Eloquent のイベントを、そのまま画面側のアプリに送りたいときは、Laravel の「モデルのイベントのブロードキャスト」(リアルタイムに届けるのページにあります)を見てください。
Eloquent のモデルは、いくつかのイベント(「起きた」という知らせ)を出します。モデルの一生のうち、次の瞬間に処理を差し込めます。
| イベント | 出るとき |
|---|---|
retrieved |
すでにあるモデルを、データベースから取り出したとき |
creating / created |
新しいモデルを、はじめて保存するとき(前 / 後) |
updating / updated |
すでにあるモデルを変えて、save を呼んだとき(前 / 後) |
saving / saved |
モデルを追加・更新するとき(前 / 後)。属性が変わっていなくても出る |
deleting / deleted |
モデルを消すとき(前 / 後) |
trashed |
モデルをソフトデリートしたとき |
forceDeleting / forceDeleted |
モデルを完全に消すとき(前 / 後) |
restoring / restored |
ソフトデリートしたモデルを戻すとき(前 / 後) |
replicating |
モデルをコピーするとき |
名前が -ing で終わるイベントは、モデルの変更が保存される前に出ます。-ed で終わるイベントは、保存されたあとに出ます。
モデルのイベントを受け取り始めるには、モデルに $dispatchesEvents プロパティを書きます。モデルの一生の中のいろいろな時点と、自分のイベントのクラスを結びつけます。モデルのイベントのクラスは、コンストラクタ(クラスを作るときに呼ばれるメソッド)で、対象のモデルを受け取るようにします。
<?php
namespace App\Models;
use App\Events\UserDeleted;
use App\Events\UserSaved;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
class User extends Authenticatable
{
use Notifiable;
/**
* The event map for the model.
*
* @var array<string, string>
*/
protected $dispatchesEvents = [
'saved' => UserSaved::class,
'deleted' => UserDeleted::class,
];
}
Eloquent のイベントを決めて結びつけたら、イベントリスナー(知らせを受けて動く処理)で、イベントを処理できます。
注意
Eloquent でまとめて更新や削除をすると、そのモデルの saved・updated・deleting・deleted のイベントは出ません。まとめて更新や削除をするときは、モデルを実際には取り出さないからです。
クロージャを使う#
専用のイベントのクラスの代わりに、モデルのイベントが出たときに動くクロージャを登録できます。ふつうは、モデルの booted メソッドで登録します。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* The "booted" method of the model.
*/
protected static function booted(): void
{
static::created(function (User $user) {
// ...
});
}
}
モデルのイベントを登録するときは、キューに入れられる名前のないリスナー(queueable)も使えます。こうすると、リスナーはその場では動かず、アプリのキュー(時間のかかる仕事の順番待ちの列)を通して、裏で動きます。
use function Illuminate\Events\queueable;
static::created(queueable(function (User $user) {
// ...
}));
オブザーバー#
オブザーバーを作る#
1つのモデルでたくさんのイベントを受け取るなら、オブザーバー(見張り役)のクラス1つに、リスナーをまとめられます。オブザーバーのメソッドの名前は、受け取りたい Eloquent のイベントの名前にします。どのメソッドも、対象のモデルを1つだけ引数に受け取ります。オブザーバーのクラスは、make:observer コマンドで作れます。
php artisan make:observer UserObserver --model=User
このコマンドは、新しいオブザーバーを、アプリの app/Observers に置きます。このフォルダがなければ、Artisan が作ります。作られたてのオブザーバーは、次のようになります。
<?php
namespace App\Observers;
use App\Models\User;
class UserObserver
{
/**
* Handle the User "created" event.
*/
public function created(User $user): void
{
// ...
}
/**
* Handle the User "updated" event.
*/
public function updated(User $user): void
{
// ...
}
/**
* Handle the User "deleted" event.
*/
public function deleted(User $user): void
{
// ...
}
/**
* Handle the User "restored" event.
*/
public function restored(User $user): void
{
// ...
}
/**
* Handle the User "forceDeleted" event.
*/
public function forceDeleted(User $user): void
{
// ...
}
}
オブザーバーを登録するには、対応するモデルに ObservedBy 属性を付けます。
use App\Observers\UserObserver;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
#[ObservedBy([UserObserver::class])]
class User extends Authenticatable
{
//
}
または、見張りたいモデルの observe を呼んで、自分で登録できます。アプリの AppServiceProvider クラスの boot で登録できます。
use App\Models\User;
use App\Observers\UserObserver;
/**
* Bootstrap any application services.
*/
public function boot(): void
{
User::observe(UserObserver::class);
}
補足
オブザーバーが受け取れるイベントには、saving や retrieved など、ほかにもあります。それらは、上のイベントの説明にあります。
オブザーバーとトランザクション#
データベースのトランザクションの中でモデルを作るとき、トランザクションが確定されたあとに、オブザーバーのイベントの処理を動かしたいことがあります。オブザーバーに ShouldHandleEventsAfterCommit インターフェイスを持たせます。トランザクションが動いていないときは、すぐに処理が動きます。
<?php
namespace App\Observers;
use App\Models\User;
use Illuminate\Contracts\Events\ShouldHandleEventsAfterCommit;
class UserObserver implements ShouldHandleEventsAfterCommit
{
/**
* Handle the User "created" event.
*/
public function created(User $user): void
{
// ...
}
}
イベントを止める#
モデルが出すイベントを、一時的に全部「止め」たいことがあります。withoutEvents を使います。引数は、クロージャ1つだけです。クロージャの中で動く処理は、モデルのイベントを出しません。クロージャが返した値は、withoutEvents が返します。
use App\Models\User;
$user = User::withoutEvents(function () {
User::findOrFail(1)->delete();
return User::find(2);
});
1つのモデルだけ、イベントなしで保存する#
あるモデルを、イベントを出さずに「保存」したいこともあります。saveQuietly を使います。
$user = User::findOrFail(1);
$user->name = 'Victoria Faith';
$user->saveQuietly();
「更新」・「削除」・「ソフトデリート」・「戻す」・「コピー」も、イベントなしでできます。
$user->deleteQuietly();
$user->forceDeleteQuietly();
$user->restoreQuietly();
関連するページ#
公式ドキュメント(英語)
2026年10月5日時点の内容をもとに、日本語でまとめています。