本文へ移動
Laravel Tips

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 のコマンド)で作れます。

bash
php artisan make:model Flight

モデルを作るとき、マイグレーション(データベースの表を作ったり変えたりする手順書)も一緒に作りたいときは、--migration か -m を付けます。

bash
php artisan make:model Flight --migration

モデルと一緒に、ファクトリ、シーダー、ポリシー、コントローラー、フォームリクエストなど、いろいろなクラスも作れます。オプションを組み合わせれば、まとめて作れます。

bash
# 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 コマンドを使うと、一覧で見られます。

bash
php artisan model:show Flight

モデルの決まり#

make:model で作ったモデルは、app/Models に置かれます。基本のモデルのクラスを見て、Eloquent の主な決まりを説明していきます。

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Flight extends Model
{
    // ...
}

表の名前#

上の例では、Flight モデルがどの表に対応するか、書いていません。何も書かないと、クラスの名前を「スネークケース」(小文字を _ でつなぐ書き方)の複数形にした名前が、表の名前になります。Flight は flights 表、AirTrafficController は air_traffic_controllers 表に対応します。

表の名前が、この決まりに合わないときは、Table 属性(PHP の属性。クラスの前に書く印)で、名前を決められます。

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
<?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
<?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
<?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 に合う主キーのカラムが必要です。

php
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 を入れるカラムを決められます。

php
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 に合う主キーのカラムを用意します。

php
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
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Attributes\Table;
use Illuminate\Database\Eloquent\Model;

#[Table(timestamps: false)]
class Flight extends Model
{
    // ...
}

タイムスタンプをやめるだけなら、WithoutTimestamps 属性が使えます。

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Attributes\WithoutTimestamps;
use Illuminate\Database\Eloquent\Model;

#[WithoutTimestamps]
class Flight extends Model
{
    // ...
}

日付の保存の形式を変えたいときは、Table 属性の dateFormat 引数を使います。日付の属性が、データベースにどう保存されるかが決まります。

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Attributes\Table;
use Illuminate\Database\Eloquent\Model;

#[Table(dateFormat: 'U')]
class Flight extends Model
{
    // ...
}

日付の形式だけを決めるなら、DateFormat 属性が使えます。

php
<?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
<?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 に渡すクロージャ(名前のない関数)の中で操作します。

php
Model::withoutTimestamps(fn () => $post->increment('reads'));

データベース接続#

既定では、すべての Eloquent モデルは、アプリの既定のデータベース接続を使います。特定のモデルだけ別の接続を使いたいときは、Connection 属性を使います。

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Attributes\Connection;
use Illuminate\Database\Eloquent\Model;

#[Connection('mysql')]
class Flight extends Model
{
    // ...
}

属性の既定の値#

新しく作ったモデルは、最初は属性の値を持っていません。いくつかの属性に、既定の値を持たせたいときは、モデルに $attributes プロパティ(クラスの中の変数)を書きます。値は、データベースから読んだときと同じ、保存できる形で書きます。

php
<?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 属性を書きます。

php
use Illuminate\Database\Eloquent\Attributes\Refreshes;

#[Refreshes('name')]
class User extends Model
{
    // ...
}

配列で、複数の属性も書けます。

php
#[Refreshes(['name', 'slug'])]

モデルを書き込んだあと、決めた属性が、データベースから読み直されます。

Eloquent を厳しくする#

Eloquent の動きを「厳しく」するメソッドがいくつかあります。

まず、preventLazyLoading は、レイジーロード(関連するデータを、使う時点になってから読み込むこと)を禁止するかを、真偽値で決めます。たとえば、本番以外でだけ禁止しておくとします。すると、レイジーロードがうっかり本番のコードに残っていても、本番は止まらずに動き続けます。ふつうは、アプリの AppServiceProvider の boot メソッドで呼びます。

php
use Illuminate\Database\Eloquent\Model;

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    Model::preventLazyLoading(! $this->app->isProduction());
}

また、preventSilentlyDiscardingAttributes を呼ぶと、入れてよいと決めていない属性に値を入れようとしたときに、例外(エラー)を投げさせられます。ふつうは、そうした値は黙って捨てられます。モデルの fillable の配列に書き忘れた属性に値を入れても気づけず、あとで思わぬエラーになります。手元で開発している間に、これを防げます。

php
Model::preventSilentlyDiscardingAttributes(! $this->app->isProduction());
メソッド 働き
Model::preventLazyLoading レイジーロードを禁止する(例外を投げる)
Model::preventSilentlyDiscardingAttributes 入れられない属性に値を入れようとしたら、例外を投げる

モデルを取り出す#

モデルと、それに対応する表を作ったら、データベースからデータを取り出せます。Eloquent のモデルは、1つ1つが強力なクエリビルダ(SQL を書かずに問い合わせるしくみ)だと考えられます。モデルの all は、表の全部の行を取り出します。

php
use App\Models\Flight;

foreach (Flight::all() as $flight) {
    echo $flight->name;
}

問い合わせを組み立てる#

all は、表の全部の行を返します。モデルはクエリビルダなので、条件を足して、最後に get で結果を取り出すこともできます。

php
$flights = Flight::where('active', 1)
    ->orderBy('name')
    ->limit(10)
    ->get();

補足

Eloquent のモデルはクエリビルダなので、クエリビルダのメソッドは、すべて Eloquent の問い合わせでも使えます。

モデルを読み直す#

すでにデータベースから取り出したモデルは、fresh と refresh で「読み直せ」ます。fresh は、データベースから、もう一度モデルを取り出します。いまのモデルは変わりません。

php
$flight = Flight::where('number', 'FR 900')->first();

$freshFlight = $flight->fresh();

refresh は、いまのモデルを、データベースの新しいデータで作り直します。読み込み済みのリレーションも、読み直されます。

php
$flight = Flight::where('number', 'FR 900')->first();

$flight->number = 'FR 456';

$flight->refresh();

$flight->number; // "FR 900"

トランザクション(ひとまとめの処理)の中で、モデルを読み直しながら、悲観的ロック(ほかから書き換えられないようにする鍵)も取りたいときは、refreshForUpdate を使います。FOR UPDATE のロックで、モデルを読み込み直します。

php
DB::transaction(function () use ($flight) {
    $flight->refreshForUpdate();

    // Update the locked model...
});

コレクション#

all や get は、複数の行を取り出しますが、ふつうの PHP の配列ではなく、Illuminate\Database\Eloquent\Collection を返します。

この Collection は、Laravel の基本の Illuminate\Support\Collection を受け継いでいて、データを扱う便利なメソッドがたくさんあります。たとえば、reject は、クロージャの結果で、モデルをコレクションから取り除きます。

php
$flights = Flight::where('destination', 'Paris')->get();

$flights = $flights->reject(function (Flight $flight) {
    return $flight->cancelled;
});

基本のコレクションのメソッドに加えて、Eloquent のコレクションには、モデルを扱うための特別なメソッドもあります。

Laravel のコレクションは、どれも PHP の iterable(くり返せるもの)のしくみを持っているので、配列のように、ループで回せます。

php
foreach ($flights as $flight) {
    echo $flight->name;
}

少しずつ取り出す(チャンク)#

何万もの行を、all や get で読み込むと、メモリが足りなくなることがあります。そのときは、chunk を使うと、たくさんのモデルを効率よく扱えます。

chunk は、モデルの一部を取り出して、クロージャに渡します。いまの分だけをメモリに読むので、メモリの使い方が大きく減ります。

php
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 が大きいもの」を取り出します。

php
Flight::where('departed', true)
    ->chunkById(200, function (Collection $flights) {
        $flights->each->update(['departed' => false]);
    }, column: 'id');

chunkById と lazyById は、取り出すために、問い合わせへ自分で where の条件を足します。そのため、こちらで書く条件は、クロージャでかっこにまとめておくのがふつうです。

php
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本の流れとして扱えます。

php
use App\Models\Flight;

foreach (Flight::lazy() as $flight) {
    // ...
}

絞りこみの条件に使っているカラムを、くり返しの途中で書き換えるなら、lazyById を使います。こちらも、いつも「前の塊の最後のモデルより id が大きいもの」を取り出します。

php
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 のジェネレータ(少しずつ値を返すしくみ)を使っています。

php
use App\Models\Flight;

foreach (Flight::where('destination', 'Zurich')->cursor() as $flight) {
    // ...
}

cursor は Illuminate\Support\LazyCollection を返します。LazyCollection では、ふつうのコレクションのメソッドの多くが使えて、しかもメモリにはモデルが1つしか入りません。

php
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回の問い合わせで取れます。

php
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回の問い合わせです。

php
return Destination::orderByDesc(
    Flight::select('arrived_at')
        ->whereColumn('destination_id', 'destinations.id')
        ->orderByDesc('arrived_at')
        ->limit(1)
)->get();

1つのモデルや集計を取り出す#

条件に合う行を全部取り出すだけでなく、find・first・firstWhere で、1つのモデルだけを取り出せます。コレクションではなく、モデルが1つ返ります。

php
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つ返りますが、なければ、渡したクロージャが動き、その戻り値が結果になります。

php
$flight = Flight::findOr(1, function () {
    // ...
});

$flight = Flight::where('legs', '>', 3)->firstOr(function () {
    // ...
});

見つからないときに例外を投げる#

モデルが見つからないとき、例外を投げたいことがあります。ルートやコントローラーで便利です。findOrFail と firstOrFail は、最初の結果を取り出しますが、なければ Illuminate\Database\Eloquent\ModelNotFoundException を投げます。

php
$flight = Flight::findOrFail(1);

$flight = Flight::where('legs', '>', 3)->firstOrFail();

ModelNotFoundException を受け止めないと、404 のレスポンスが自動で返ります。

php
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 を呼びます。

php
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つの値が返ります。

php
$count = Flight::where('active', 1)->count();

$max = Flight::where('active', 1)->max('price');

モデルを追加・更新する#

追加する#

Eloquent では、データベースからモデルを取り出すだけでなく、新しい行も追加します。新しいモデルを作り、属性に値を入れて、save を呼びます。

php
<?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 を使います。保存中に例外が起きると、トランザクションが自動で元に戻ります。

php
$flight->saveOrFail();

create を使えば、PHP の1つの文で、新しいモデルを「保存」できます。追加されたモデルが返ります。

php
use App\Models\Flight;

$flight = Flight::create([
    'name' => 'London to Paris',
]);

ただし、create を使う前に、モデルのクラスに、Fillable か Guarded の属性を書く必要があります。Eloquent のモデルは、既定で、マスアサインメント(配列でまとめて値を入れること)の弱点をつかれないように守られているからです。くわしくは、あとのマスアサインメントの節を見てください。

更新する#

すでにあるモデルの更新にも save を使えます。モデルを取り出し、更新したい属性を書き換えて、save を呼びます。updated_at も自動で更新されます。

php
use App\Models\Flight;

$flight = Flight::find(1);

$flight->name = 'Paris to London';

$flight->save();

トランザクションの中で更新したいときは、updateOrFail を使います。更新中に例外が起きると、トランザクションが自動で元に戻ります。

php
$flight->updateOrFail(['name' => 'Paris to London']);

すでにあるモデルを更新し、なければ新しく作りたいことがあります。firstOrCreate と同じく、updateOrCreate は保存までするので、save を呼ぶ必要はありません。

次の例では、departure が Oakland で destination が San Diego の便があれば、price と discounted を更新します。なければ、1つ目と2つ目の配列を合わせた値で、新しい便を作ります。

php
$flight = Flight::updateOrCreate(
    ['departure' => 'Oakland', 'destination' => 'San Diego'],
    ['price' => 99, 'discounted' => 1]
);

firstOrCreate や updateOrCreate を使うと、新しく作られたのか、すでにあるものが更新されたのか、分からないことがあります。wasRecentlyCreated プロパティを見ると、そのモデルがいま新しく作られたものかが分かります。

php
$flight = Flight::updateOrCreate(
    // ...
);

if ($flight->wasRecentlyCreated) {
    // New flight record was inserted...
}

まとめて更新する#

問い合わせの条件に合うモデルを、まとめて更新することもできます。次の例では、active が 1 で、行き先が San Diego の便に、すべて「遅れ」の印を付けます。

php
Flight::where('active', 1)
    ->where('destination', 'San Diego')
    ->update(['delayed' => 1]);

update には、更新するカラムと値の配列を渡します。更新した行の数が返ります。

注意

Eloquent でまとめて更新すると、更新されるモデルの saving・saved・updating・updated のイベントは出ません。まとめて更新するときは、モデルを実際には取り出さないからです。

属性の変化を調べる#

Eloquent には、isDirty・isClean・wasChanged があり、モデルを取り出したときから、属性がどう変わったかを調べられます。

isDirty は、モデルを取り出してから、属性が変わったかを調べます。属性の名前か名前の配列を渡して、その属性が「dirty」(変わった)かを調べることもできます。isClean は、取り出してから変わっていないかを調べます。こちらも属性を渡せます。

php
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 は、いまのリクエストの中で、最後に保存したときに、属性が変わったかを調べます。属性の名前を渡して、その属性が変わったかも調べられます。

php
$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 は、モデルを取り出したあとに何が変わっても、元の属性の配列を返します。属性の名前を渡すと、その属性の元の値が返ります。

php
$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 は、最後に保存する前の、元の属性の値の配列を返します。

php
$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つの文で、新しいモデルを「保存」できます。追加されたモデルが返ります。

php
use App\Models\Flight;

$flight = Flight::create([
    'name' => 'London to Paris',
]);

ただし、create を使う前に、モデルのクラスに、Fillable か Guarded の属性を書く必要があります。Eloquent のモデルは、既定で、マスアサインメントの脆弱性(弱点)から守られているからです。

マスアサインメントの弱点とは、こういうことです。使う人が、こちらの思っていない項目をリクエストに入れて送ります。その項目が、書き換えるつもりのなかったカラムまで書き換えてしまいます。たとえば、悪い人が is_admin を送り、それがモデルの create に渡されると、自分を管理者にできてしまいます。

そこで、まず、一括で入れてよい属性を決めます。モデルの Fillable 属性を使います。たとえば、Flight の name を一括で入れてよいことにします。

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Model;

#[Fillable(['name'])]
class Flight extends Model
{
    // ...
}

一括で入れてよい属性を決めたら、create で新しい行を追加できます。新しく作ったモデルが返ります。

php
$flight = Flight::create(['name' => 'London to Paris']);

すでにモデルがあるときは、fill で、属性の配列を入れられます。

php
$flight->fill(['name' => 'Amsterdam to Frankfurt']);

JSON のカラムへの一括代入#

JSON のカラム(JSON 形式で値をしまうカラム)に入れるときは、一括で入れてよいキーを、モデルの Fillable 属性に1つずつ書く必要があります。Guarded 属性を使っているときは、安全のため、JSON の中の値をまとめて更新することはできません。

php
use Illuminate\Database\Eloquent\Attributes\Fillable;

#[Fillable(['options->enabled'])]
class Flight extends Model
{
    // ...
}

全部を一括で入れてよくする#

全部の属性を、一括で入れてよくしたいなら、モデルに Unguarded 属性を付けます。守りを外したときは、Eloquent の fill・create・update に渡す配列を、入れてよい値だけでいつも自分で組み立てるように、とくに気をつけます。

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Attributes\Unguarded;
use Illuminate\Database\Eloquent\Model;

#[Unguarded]
class Flight extends Model
{
    // ...
}

一括代入の例外#

既定では、Fillable 属性にない属性は、一括代入のとき、黙って捨てられます。本番では、それが期待どおりの動きですが、手元での開発では、モデルの変更が効かない理由が分からなくなって、混乱することがあります。

必要なら、preventSilentlyDiscardingAttributes を呼んでおくと、入れてよいと決めていない属性に値を入れようとしたときに、例外を投げさせられます。ふつうは、アプリの AppServiceProvider クラスの boot で呼びます。

php
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 も自動で入ります。

php
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 を呼びます。

php
use App\Models\Flight;

$flight = Flight::find(1);

$flight->delete();

トランザクションの中で消したいときは、deleteOrFail を使います。削除中に例外が起きると、トランザクションが自動で元に戻ります。

php
$flight->deleteOrFail();

主キーで消す#

上の例では、delete の前に、モデルを取り出しています。主キーが分かっているなら、destroy で、取り出さずに消せます。destroy は、主キー1つのほか、複数の主キー、主キーの配列、主キーのコレクションも受け取ります。

php
Flight::destroy(1);

Flight::destroy(1, 2, 3);

Flight::destroy([1, 2, 3]);

Flight::destroy(collect([1, 2, 3]));

ソフトデリートを使っているモデルを、完全に消したいときは、forceDestroy を使います。

php
Flight::forceDestroy(1);

注意

destroy は、モデルを1つずつ読み込んで delete を呼ぶので、deleting と deleted のイベントが、モデルごとにきちんと出ます。

問い合わせで消す#

問い合わせで、条件に合うモデルを全部消すこともできます。次の例は、inactive の印がある便を、全部消します。まとめて更新と同じく、まとめて消すと、消したモデルのイベントは出ません。

php
$deleted = Flight::where('active', 0)->delete();

表の全部のモデルを消すには、条件を付けずに動かします。

php
$deleted = Flight::query()->delete();

注意

Eloquent でまとめて消すと、消されるモデルの deleting と deleted のイベントは出ません。まとめて消すときは、モデルを実際には取り出さないからです。

ソフトデリート#

データベースから本当に消す代わりに、Eloquent は「ソフトデリート」もできます。ソフトデリートでは、行は実際には消えず、モデルの deleted_at 属性に、「消した」日時が入ります。ソフトデリートを使うには、モデルに Illuminate\Database\Eloquent\SoftDeletes トレイトを足します。

php
<?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 のスキーマビルダには、このカラムを作る便利なメソッドがあります。

php
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 で調べられます。

php
if ($flight->trashed()) {
    // ...
}

ソフトデリートしたモデルを戻す#

ソフトデリートしたモデルの「消す」を取り消したいときは、モデルの restore を呼びます。deleted_at が null になります。

php
$flight->restore();

問い合わせの中でも restore を使って、複数のモデルを戻せます。ほかの「まとめて」の操作と同じく、戻したモデルのイベントは出ません。

php
Flight::withTrashed()
    ->where('airline_id', 1)
    ->restore();

restore は、リレーションの問い合わせを作るときにも使えます。

php
$flight->history()->restore();

完全に消す#

モデルを、データベースから本当に消したいときもあります。ソフトデリートしたモデルを完全に消すには、forceDelete を使います。

php
$flight->forceDelete();

forceDelete も、Eloquent のリレーションの問い合わせを作るときに使えます。

php
$flight->history()->forceDelete();

ソフトデリートしたモデルを問い合わせる#

ソフトデリートしたものも含める#

上のとおり、ソフトデリートしたモデルは、結果から自動で除かれます。問い合わせで withTrashed を呼ぶと、ソフトデリートしたモデルも含められます。

php
use App\Models\Flight;

$flights = Flight::withTrashed()
    ->where('account_id', 1)
    ->get();

withTrashed は、リレーションの問い合わせを作るときにも呼べます。

php
$flight->history()->withTrashed()->get();

ソフトデリートしたものだけを取り出す#

onlyTrashed は、ソフトデリートしたモデルだけを取り出します。

php
$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
<?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 メソッドも書けます。モデルが消される前に呼ばれます。モデルが完全に消される前に、保存したファイルなど、関連するものを消すのに便利です。

php
/**
 * Prepare the model for pruning.
 */
protected function pruning(): void
{
    // ...
}

片づけるモデルを決めたら、アプリの routes/console.php で、model:prune コマンドをスケジュールに入れます。動かす間隔は自由に選べます。

php
use Illuminate\Support\Facades\Schedule;

Schedule::command('model:prune')->daily();

model:prune は、アプリの app/Models の中にある「Prunable」のモデルを、自動で見つけます。モデルが別の場所にあるときは、--model で、クラスの名前を決められます。

php
Schedule::command('model:prune', [
    '--model' => [Address::class, Flight::class],
])->daily();

見つけたモデルのうち、一部だけ除きたいときは、--except を使います。

php
Schedule::command('model:prune', [
    '--except' => [Address::class, Flight::class],
])->daily();

prunable の問い合わせを試したいときは、model:prune に --pretend を付けて動かします。実際には消さず、消される行の数だけが報告されます。

bash
php artisan model:prune --pretend

注意

ソフトデリートしたモデルが、prunable の問い合わせに合うと、完全に消されます(forceDelete)。

まとめて片づける#

Illuminate\Database\Eloquent\MassPrunable を付けたモデルは、まとめて消す問い合わせで、データベースから消されます。そのため、pruning は呼ばれず、deleting と deleted のイベントも出ません。消す前にモデルを取り出さないので、片づけがずっと効率よくなります。

php
<?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 を使うと、すでにあるモデルの、まだ保存されていないコピーを作れます。ほとんど同じ属性を持つモデルがいくつもあるときに便利です。

php
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 に配列を渡します。

php
$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 に置かれます。

bash
php artisan make:scope AncientScope

グローバルスコープを書く#

グローバルスコープは、かんたんに書けます。まず、make:scope で、Illuminate\Database\Eloquent\Scope インターフェイス(守るべき決まり)を満たすクラスを作ります。Scope では、apply メソッドを1つ書く必要があります。apply は、必要に応じて、where などの条件を、問い合わせに足します。

php
<?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
<?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
<?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 が動きます。

sql
select * from `users` where `created_at` < 0021-02-18 00:00:00

名前のないグローバルスコープ#

クロージャを使って、グローバルスコープを書くこともできます。専用のクラスを作るほどでもない、かんたんなスコープに便利です。クロージャでグローバルスコープを書くときは、addGlobalScope の1つ目の引数に、自分で選んだスコープの名前を書きます。

php
<?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 を使います。引数は、グローバルスコープのクラスの名前だけです。

php
User::withoutGlobalScope(AncientScope::class)->get();

クロージャで書いたグローバルスコープなら、付けた名前の文字列を渡します。

php
User::withoutGlobalScope('ancient')->get();

複数の、または全部のグローバルスコープを外したいときは、withoutGlobalScopes と withoutGlobalScopesExcept を使います。

php
// 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
<?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);
    }
}

ローカルスコープを使う#

スコープを書いたら、モデルを問い合わせるとき、スコープのメソッドを呼べます。いくつものスコープを、つなげて呼ぶこともできます。

php
use App\Models\User;

$users = User::popular()->active()->orderBy('created_at')->get();

複数のスコープを or でつなぐときは、正しいかっこのまとめにするため、クロージャが必要になることがあります。

php
$users = User::popular()->orWhere(function (Builder $query) {
    $query->active();
})->get();

ただ、毎回クロージャを書くのは面倒です。そこで Laravel には、「ハイヤーオーダー」の orWhere があります。これを使うと、クロージャなしで、スコープを orWhere でつないで書けます。

php
$users = User::popular()->orWhere->active()->get();

引数を取るスコープ#

引数を受け取るスコープを書きたいこともあります。スコープのメソッドの引数に、追加の引数を書くだけです。スコープの引数は、$query のあとに書きます。

php
<?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);
    }
}

引数をスコープのメソッドに書いたら、スコープを呼ぶときに、引数を渡せます。

php
$users = User::ofType('admin')->get();

属性を付けたスコープのメソッドは protected にします。モデルのクラスの中から、属性を付けたスコープを呼ぶときは、static::query()->ofType('admin') のように、クエリビルダ経由で呼びます。そうすると、Eloquent のスコープの処理を通ります。

保留中の属性(pending attributes)#

スコープで絞りこむ条件と同じ値を持ったモデルを、そのスコープから作りたいことがあります。そのときは、スコープの問い合わせの中で withAttributes を使います。

php
<?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 の条件を足します。さらに、そのスコープで作ったモデルにも、その属性を足します。

php
$draft = Post::draft()->create(['title' => 'In Progress']);

$draft->hidden; // true

where の条件を問い合わせに足したくないときは、asConditions 引数を false にします。

php
$query->withAttributes([
    'hidden' => true,
], asConditions: false);

モデルを比べる#

2つのモデルが「同じ」かを調べたいときがあります。is と isNot を使うと、主キー・表・データベース接続が同じかどうかを、すばやく調べられます。

php
if ($post->is($anotherPost)) {
    // ...
}

if ($post->isNot($anotherPost)) {
    // ...
}

is と isNot は、belongsTo・hasOne・morphTo・morphOne のリレーションでも使えます。関連するモデルを取り出す問い合わせを動かさずに、そのモデルと比べたいときに、とくに便利です。

php
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
<?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
<?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)も使えます。こうすると、リスナーはその場では動かず、アプリのキュー(時間のかかる仕事の順番待ちの列)を通して、裏で動きます。

php
use function Illuminate\Events\queueable;

static::created(queueable(function (User $user) {
    // ...
}));

オブザーバー#

オブザーバーを作る#

1つのモデルでたくさんのイベントを受け取るなら、オブザーバー(見張り役)のクラス1つに、リスナーをまとめられます。オブザーバーのメソッドの名前は、受け取りたい Eloquent のイベントの名前にします。どのメソッドも、対象のモデルを1つだけ引数に受け取ります。オブザーバーのクラスは、make:observer コマンドで作れます。

bash
php artisan make:observer UserObserver --model=User

このコマンドは、新しいオブザーバーを、アプリの app/Observers に置きます。このフォルダがなければ、Artisan が作ります。作られたてのオブザーバーは、次のようになります。

php
<?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 属性を付けます。

php
use App\Observers\UserObserver;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;

#[ObservedBy([UserObserver::class])]
class User extends Authenticatable
{
    //
}

または、見張りたいモデルの observe を呼んで、自分で登録できます。アプリの AppServiceProvider クラスの boot で登録できます。

php
use App\Models\User;
use App\Observers\UserObserver;

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    User::observe(UserObserver::class);
}

補足

オブザーバーが受け取れるイベントには、saving や retrieved など、ほかにもあります。それらは、上のイベントの説明にあります。

オブザーバーとトランザクション#

データベースのトランザクションの中でモデルを作るとき、トランザクションが確定されたあとに、オブザーバーのイベントの処理を動かしたいことがあります。オブザーバーに ShouldHandleEventsAfterCommit インターフェイスを持たせます。トランザクションが動いていないときは、すぐに処理が動きます。

php
<?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 が返します。

php
use App\Models\User;

$user = User::withoutEvents(function () {
    User::findOrFail(1)->delete();

    return User::find(2);
});

1つのモデルだけ、イベントなしで保存する#

あるモデルを、イベントを出さずに「保存」したいこともあります。saveQuietly を使います。

php
$user = User::findOrFail(1);

$user->name = 'Victoria Faith';

$user->saveQuietly();

「更新」・「削除」・「ソフトデリート」・「戻す」・「コピー」も、イベントなしでできます。

php
$user->deleteQuietly();
$user->forceDeleteQuietly();
$user->restoreQuietly();

関連するページ#

公式ドキュメント(英語)

2026年10月5日時点の内容をもとに、日本語でまとめています。

ページの一覧