跳到主要內容

Eloquent 入門

📝 此頁面為 Laravel 官方文檔的繁體中文翻譯。查看原始英文版本

Eloquent:開始使用

簡介

Laravel 包含 Eloquent,一個物件關聯映射器(ORM),使與資料庫的互動變得愉快。使用 Eloquent 時,每個資料庫表都有一個對應的「模型」,用於與該表互動。除了從資料庫表中檢索記錄外,Eloquent 模型還允許您插入、更新和刪除表中的記錄。

[!NOTE] 開始之前,請確保在應用程式的 config/database.php 配置檔案中配置了資料庫連接。有關配置資料庫的更多資訊,請查看資料庫配置文檔

生成模型類別

首先,讓我們建立一個 Eloquent 模型。模型通常位於 app\Models 目錄中,並繼承 Illuminate\Database\Eloquent\Model 類別。您可以使用 make:model Artisan 命令來生成新模型:

php artisan make:model Flight

如果您想在生成模型時生成資料庫遷移,可以使用 --migration-m 選項:

php artisan make:model Flight --migration

您可以在生成模型時生成各種其他類型的類別,例如工廠、seeders、策略、控制器和表單請求。此外,這些選項可以組合以一次建立多個類別:

# 生成模型和 FlightFactory 類別...
php artisan make:model Flight --factory
php artisan make:model Flight -f

# 生成模型和 FlightSeeder 類別...
php artisan make:model Flight --seed
php artisan make:model Flight -s

# 生成模型和 FlightController 類別...
php artisan make:model Flight --controller
php artisan make:model Flight -c

# 生成模型、FlightController 資源類別和表單請求類別...
php artisan make:model Flight --controller --resource --requests
php artisan make:model Flight -crR

# 生成模型和 FlightPolicy 類別...
php artisan make:model Flight --policy

# 生成模型、遷移、工廠、seeder 和控制器...
php artisan make:model Flight -mfsc

# 快捷方式:生成模型、遷移、工廠、seeder、策略、控制器和表單請求...
php artisan make:model Flight --all
php artisan make:model Flight -a

# 生成樞紐模型...
php artisan make:model Member --pivot
php artisan make:model Member -p

檢查模型

有時僅透過瀏覽程式碼可能很難確定模型的所有可用屬性和關聯。相反,嘗試使用 model:show Artisan 命令,它提供了模型所有屬性和關聯的便利概述:

php artisan model:show Flight

Eloquent 模型規範

make:model 命令生成的模型將放置在 app/Models 目錄中。讓我們檢查一個基本模型類別並討論 Eloquent 的一些關鍵規範:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Flight extends Model
{
    // ...
}

資料表名稱

查看上面的範例後,您可能已經注意到我們沒有告訴 Eloquent 哪個資料庫表對應我們的 Flight 模型。按照慣例,除非明確指定其他名稱,否則類別的「蛇形命名法」複數名稱將用作資料表名稱。因此,在這種情況下,Eloquent 將假定 Flight 模型將記錄儲存在 flights 表中,而 AirTrafficController 模型將記錄儲存在 air_traffic_controllers 表中。

如果模型對應的資料庫表不符合此慣例,您可以使用 Table 屬性手動指定模型的資料表名稱:

<?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 假定主鍵是一個遞增的整數值,這意味著 Eloquent 會自動將主鍵轉換為整數。如果您希望使用非遞增或非數字主鍵,您應該在 Table 屬性上指定 keyTypeincrementing 參數:

<?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
{
    // ...
}

如果您只需要禁用自動遞增 ID,可以使用 WithoutIncrementing 屬性:

<?php

namespace App\Models;

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

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

「複合」主鍵

Eloquent 要求每個模型都有一個可以用作其主鍵的唯一識別「ID」。Eloquent 模型不支援「複合」主鍵。但是,您可以自由地向資料庫表添加額外的多列唯一索引,除了表的唯一識別主鍵之外。

UUID 和 ULID 金鑰

您可以選擇使用 UUID 代替自動遞增整數作為 Eloquent 模型的主鍵。UUID 是長度為 36 個字元的通用唯一字母數字標識符。

如果您希望模型使用 UUID 金鑰而不是自動遞增整數金鑰,您可以在模型上使用 Illuminate\Database\Eloquent\Concerns\HasUuids trait。當然,您應該確保模型有一個 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 trait 將為您的模型生成 UUIDv7 標識符。這些 UUID 對於索引資料庫儲存更有效,因為它們可以按字典順序排序。

您可以透過在模型上定義 newUniqueId 方法來覆蓋給定模型的 UUID 生成過程。此外,您可以透過在模型上定義 uniqueIds 方法來指定哪些列應接收 UUID:

use Ramsey\Uuid\Uuid;

/**
 * 為模型生成新的 UUID。
 */
public function newUniqueId(): string
{
    return (string) Uuid::uuid4();
}

/**
 * 獲取應接收唯一標識符的列。
 *
 * @return array<int, string>
 */
public function uniqueIds(): array
{
    return ['id', 'discount_code'];
}

如果您願意,您可以選擇使用「ULID」代替 UUID。ULID 與 UUID 類似;但是,它們只有 26 個字元長。像有序 UUID 一樣,ULID 按字典順序可排序,用於高效的資料庫索引。要使用 ULID,您應該在模型上使用 Illuminate\Database\Eloquent\Concerns\HasUlids trait。您還應該確保模型有一個 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_atupdated_at 列。當模型被建立或更新時,Eloquent 會自動設定這些列的值。如果您不希望這些列由 Eloquent 自動管理,可以在模型的 Table 屬性上設定 timestampsfalse

<?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 參數。這決定了日期屬性在資料庫中的儲存方式以及模型序列化為陣列或 JSON 時的格式:

<?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_ATUPDATED_AT 常數:

<?php

class Flight extends Model
{
    /**
     * 「建立時間」列的名稱。
     *
     * @var string|null
     */
    public const CREATED_AT = 'creation_date';

    /**
     * 「更新時間」列的名稱。
     *
     * @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 屬性。放在 $attributes 陣列中的屬性值應採用其原始的「可儲存」格式,就像剛從資料庫中讀取一樣:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Flight extends Model
{
    /**
     * 模型屬性的預設值。
     *
     * @var array<string, mixed>
     */
    protected $attributes = [
        'options' => '[]',
        'delayed' => false,
    ];
}

配置 Eloquent 嚴格性

Laravel 提供了多種方法,允許您在各種情況下配置 Eloquent 的行為和「嚴格性」。

首先,preventLazyLoading 方法接受一個可選的布林參數,表示是否應防止延遲載入。例如,您可能只想在非生產環境中禁用延遲載入,以便即使生產程式碼中意外存在延遲載入的關聯,生產環境仍能正常運行。通常,此方法應在應用程式的 AppServiceProviderboot 方法中調用:

use Illuminate\Database\Eloquent\Model;

/**
 * 引導任何應用程式服務。
 */
public function boot(): void
{
    Model::preventLazyLoading(! $this->app->isProduction());
}

此外,您可以透過調用 preventSilentlyDiscardingAttributes 方法來指示 Laravel 在嘗試填充不可填充的屬性時拋出異常。這有助於在本機開發期間防止嘗試設定未添加到模型 fillable 陣列中的屬性時發生意外錯誤:

Model::preventSilentlyDiscardingAttributes(! $this->app->isProduction());

檢索模型

建立模型和其關聯的資料庫表後,您就可以開始從資料庫中檢索資料了。您可以將每個 Eloquent 模型視為一個強大的查詢建構器,允許您流暢地查詢與模型關聯的資料庫表。模型的 all 方法將從模型關聯的資料庫表中檢索所有記錄:

use App\Models\Flight;

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

建立查詢

Eloquent 的 all 方法將返回模型表中的所有結果。但是,由於每個 Eloquent 模型都作為查詢建構器,您可以向查詢添加額外的條件,然後調用 get 方法來檢索結果:

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

[!NOTE] 由於 Eloquent 模型是查詢建構器,您應該查看 Laravel 查詢建構器提供的所有方法。編寫 Eloquent 查詢時可以使用這些方法中的任何一個。

刷新模型

如果您已經有一個從資料庫中檢索到的 Eloquent 模型實例,您可以使用 freshrefresh 方法「刷新」模型。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"

集合

正如我們所看到的,像 allget 這樣的 Eloquent 方法從資料庫中檢索多條記錄。但是,這些方法不返回普通的 PHP 陣列。相反,它們返回 Illuminate\Database\Eloquent\Collection 的實例。

Eloquent 的 Collection 類別繼承了 Laravel 的基礎 Illuminate\Support\Collection 類別,後者提供了多種有用的輔助方法來與資料集合互動。例如,reject 方法可用於根據調用的閉包的結果從集合中移除模型:

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

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

除了 Laravel 基礎集合類別提供的方法外,Eloquent 集合類別還提供了一些額外的方法,這些方法專門用於與 Eloquent 模型集合互動。

由於 Laravel 的所有集合都實現了 PHP 的可迭代介面,您可以像迭代陣列一樣迭代集合:

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

分塊結果

如果您嘗試透過 allget 方法載入數萬條 Eloquent 記錄,您的應用程式可能會耗盡記憶體。您可以使用 chunk 方法代替這些方法來更有效地處理大量模型。

chunk 方法將檢索 Eloquent 模型的子集,將它們傳遞給閉包進行處理。由於一次只檢索當前區塊的 Eloquent 模型,chunk 方法在處理大量模型時將顯著減少記憶體使用量:

use App\Models\Flight;
use Illuminate\Database\Eloquent\Collection;

Flight::chunk(200, function (Collection $flights) {
    foreach ($flights as $flight) {
        // ...
    }
});

傳遞給 chunk 方法的第一個參數是您希望每個「區塊」接收的記錄數量。作為第二個參數傳遞的閉包將在從資料庫檢索的每個區塊時調用。將執行資料庫查詢以檢索傳遞給閉包的每個區塊的記錄。

如果您根據也會在迭代結果時更新的列來過濾 chunk 方法的結果,您應該使用 chunkById 方法。在這些場景中使用 chunk 方法可能導致意外和不一致的結果。在內部,chunkById 方法將始終檢索 id 列大於上一個區塊中最後一個模型的模型:

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

由於 chunkByIdlazyById 方法向正在執行的查詢添加了自己的「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 方法的工作方式與chunk 方法類似,因為它在底層以區塊形式執行查詢。但是,lazy 方法不是將每個區塊直接傳遞給回呼,而是返回一個扁平化的 Eloquent 模型 LazyCollection,讓您可以將結果作為單個流進行互動:

use App\Models\Flight;

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

如果您根據也會在迭代結果時更新的列來過濾 lazy 方法的結果,您應該使用 lazyById 方法。在內部,lazyById 方法將始終檢索 id 列大於上一個區塊中最後一個模型的模型:

Flight::where('departed', true)
    ->lazyById(200, column: 'id')
    ->each->update(['departed' => false]);

您可以使用 lazyByIdDesc 方法根據 id 的降序過濾結果。

遊標

lazy 類似,cursor 方法可用於在迭代數萬條 Eloquent 模型記錄時顯著減少應用程式的記憶體消耗。

cursor 方法只會執行一個資料庫查詢;但是,單個 Eloquent 模型直到實際迭代時才會被填充。因此,在迭代遊標時,任何給定時間只有一個 Eloquent 模型保存在記憶體中。

[!WARNING] 由於 cursor 方法一次只在記憶體中保存一個 Eloquent 模型,它無法預先載入關聯。如果您需要預先載入關聯,請考慮改用lazy 方法

在內部,cursor 方法使用 PHP 生成器來實現此功能:

use App\Models\Flight;

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

cursor 返回一個 Illuminate\Support\LazyCollection 實例。延遲集合允許您使用典型 Laravel 集合上可用的許多集合方法,同時一次只將一個模型載入記憶體:

use App\Models\User;

$users = User::cursor()->filter(function (User $user) {
    return $user->id > 500;
});

foreach ($users as $user) {
    echo $user->id;
}

儘管 cursor 方法比常規查詢使用更少的記憶體(因為一次只保存一個 Eloquent 模型),但它最終仍會耗盡記憶體。這是因為 PHP 的 PDO 驅動程式在內部將所有原始查詢結果快取在其緩衝區中。如果您正在處理大量的 Eloquent 記錄,請考慮改用lazy 方法

進階子查詢

子查詢選擇

Eloquent 還提供進階子查詢支援,允許您在單個查詢中從關聯表中提取資訊。例如,讓我們假設我們有一個航班 destinations 表和一個飛往目的地的 flights 表。flights 表包含一個 arrived_at 列,表示航班何時到達目的地。

使用查詢建構器的 selectaddSelect 方法可用的子查詢功能,我們可以使用單個查詢選擇所有 destinations 和最近到達該目的地的航班名稱:

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 函數支援子查詢。繼續使用我們的航班範例,我們可以使用此功能根據最後一個航班到達該目的地的時間對所有目的地進行排序。同樣,這可以在執行單個資料庫查詢時完成:

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

檢索單個模型/聚合

除了檢索與給定查詢匹配的所有記錄外,您還可以使用 findfirstfirstWhere 方法檢索單個記錄。這些方法返回一個單一的模型實例,而不是模型集合:

use App\Models\Flight;

// 透過主鍵檢索模型...
$flight = Flight::find(1);

// 檢索符合查詢條件的第一個模型...
$flight = Flight::where('active', 1)->first();

// 檢索符合查詢條件的第一個模型的替代方法...
$flight = Flight::firstWhere('active', 1);

有時您可能希望在未找到結果時執行一些其他操作。findOrfirstOr 方法將返回一個單一的模型實例,或者如果未找到結果,則執行給定的閉包。閉包返回的值將被視為方法的結果:

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

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

未找到異常

有時您可能希望在未找到模型時拋出異常。這在路由或控制器中特別有用。findOrFailfirstOrFail 方法將檢索查詢的第一個結果;但是,如果未找到結果,將拋出 Illuminate\Database\Eloquent\ModelNotFoundException

$flight = Flight::findOrFail(1);

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

如果未捕獲 ModelNotFoundException,將自動向客戶端發送 404 HTTP 響應:

use App\Models\Flight;

Route::get('/api/flights/{id}', function (string $id) {
    return Flight::findOrFail($id);
});

檢索或建立模型

firstOrCreate 方法將嘗試使用給定的列/值對來定位資料庫記錄。如果在資料庫中找不到該模型,則將插入一條記錄,其屬性由第一個陣列參數與可選的第二個陣列參數合併的結果組成。

firstOrNew 方法與 firstOrCreate 類似,將嘗試在資料庫中定位與給定屬性匹配的記錄。但是,如果找不到模型,則返回一個新的模型實例。請注意,firstOrNew 返回的模型尚未持久化到資料庫。您需要手動調用 save 方法來持久化它:

use App\Models\Flight;

// 透過名稱檢索航班,如果不存在則建立它...
$flight = Flight::firstOrCreate([
    'name' => 'London to Paris'
]);

// 透過名稱檢索航班,或使用名稱、delayed 和 arrival_time 屬性建立它...
$flight = Flight::firstOrCreate(
    ['name' => 'London to Paris'],
    ['delayed' => 1, 'arrival_time' => '11:30']
);

// 透過名稱檢索航班,或實例化一個新的 Flight 實例...
$flight = Flight::firstOrNew([
    'name' => 'London to Paris'
]);

// 透過名稱檢索航班,或使用名稱、delayed 和 arrival_time 屬性實例化...
$flight = Flight::firstOrNew(
    ['name' => 'Tokyo to Sydney'],
    ['delayed' => 1, 'arrival_time' => '11:30']
);

檢索聚合

與 Eloquent 模型互動時,您還可以使用 Laravel 查詢建構器提供的 countsummax 和其他聚合方法。正如您所預期的,這些方法返回一個標量值而不是 Eloquent 模型實例:

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

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

插入和更新模型

插入

當然,使用 Eloquent 時,我們不僅需要從資料庫中檢索模型。我們還需要插入新記錄。幸運的是,Eloquent 使這變得簡單。要向資料庫中插入新記錄,您應該實例化一個新的模型實例並在模型上設定屬性。然後,在模型實例上調用 save 方法:

<?php

namespace App\Http\Controllers;

use App\Models\Flight;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;

class FlightController extends Controller
{
    /**
     * 在資料庫中儲存新的航班。
     */
    public function store(Request $request): RedirectResponse
    {
        // 驗證請求...

        $flight = new Flight;

        $flight->name = $request->name;

        $flight->save();

        return redirect('/flights');
    }
}

在此範例中,我們將傳入 HTTP 請求中的 name 欄位賦值給 App\Models\Flight 模型實例的 name 屬性。當我們調用 save 方法時,將向資料庫插入一條記錄。模型的 created_atupdated_at 時間戳將在調用 save 方法時自動設定,因此無需手動設定它們。

如果您想在資料庫事務中儲存模型,可以使用 saveOrFail 方法。如果在儲存期間拋出異常,事務將自動回滾:

$flight->saveOrFail();

或者,您可以使用 create 方法透過單個 PHP 語句「儲存」新模型。create 方法將返回插入的模型實例:

use App\Models\Flight;

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

但是,在使用 create 方法之前,您需要在模型類別上指定 FillableGuarded 屬性。這些屬性是必需的,因為所有 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 位置為 Oaklanddestination 位置為 San Diego 的航班,其 pricediscounted 列將被更新。如果不存在這樣的航班,則將建立一個新的航班,其屬性由第一個參數陣列與第二個參數陣列合併的結果組成:

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

使用 firstOrCreateupdateOrCreate 等方法時,您可能不知道是建立了新模型還是更新了現有模型。wasRecentlyCreated 屬性指示模型是否在其當前生命週期中被建立:

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

if ($flight->wasRecentlyCreated) {
    // 插入了新的航班記錄...
}

批量更新

更新也可以針對與給定查詢匹配的模型執行。在此範例中,所有 activedestinationSan Diego 的航班將被標記為延誤:

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

update 方法期望一個表示應更新的列的列和值對的陣列。update 方法返回受影響的行數。

[!WARNING] 透過 Eloquent 發出批量更新時,更新的模型不會觸發 savingsavedupdatingupdated 模型事件。這是因為在發出批量更新時從未實際檢索模型。

檢查屬性更改

Eloquent 提供了 isDirtyisCleanwasChanged 方法來檢查模型的內部狀態並確定其屬性自模型最初被檢索以來如何更改。

isDirty 方法判斷模型的任何屬性是否已更改。您可以向 isDirty 方法傳遞特定的屬性名稱或屬性陣列來判斷是否有任何屬性是「髒的」。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(); // 原始屬性陣列...

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',
    ]
*/

批量賦值

您可以使用 create 方法透過單個 PHP 語句「儲存」新模型。方法將返回插入的模型實例:

use App\Models\Flight;

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

但是,在使用 create 方法之前,您需要在模型類別上指定 FillableGuarded 屬性。這些屬性是必需的,因為所有 Eloquent 模型預設都受到批量賦值漏洞的保護。

批量賦值漏洞發生在使用者傳遞意外的 HTTP 請求欄位且該欄位更改了資料庫中您未預期的列時。例如,惡意使用者可能透過 HTTP 請求發送 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 方法在資料庫中插入新記錄。create 方法返回新建立的模型實例:

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

如果您已經有一個模型實例,您可以使用 fill 方法用屬性陣列填充它:

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

批量賦值和 JSON 列

賦值 JSON 列時,每個列的可批量賦值金鑰必須在模型的 Fillable 屬性中指定。為了安全性,使用 Guarded 屬性時,Laravel 不支援更新巢狀 JSON 屬性:

use Illuminate\Database\Eloquent\Attributes\Fillable;

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

允許批量賦值

如果您想使所有屬性都可批量賦值,可以在模型上使用 Unguarded 屬性。如果您選擇解除模型的保護,您應該特別注意始終手動建立傳遞給 Eloquent 的 fillcreateupdate 方法的陣列:

<?php

namespace App\Models;

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

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

批量賦值異常

預設情況下,執行批量賦值操作時,未包含在 Fillable 屬性中的屬性會被靜默丟棄。在生產環境中,這是預期的行為;但是,在本機開發期間,它可能導致對模型更改為何不生效的困惑。

如果您願意,您可以透過調用 preventSilentlyDiscardingAttributes 方法來指示 Laravel 在嘗試填充不可填充的屬性時拋出異常。通常,此方法應在應用程式的 AppServiceProvider 類別的 boot 方法中調用:

use Illuminate\Database\Eloquent\Model;

/**
 * 引導任何應用程式服務。
 */
public function boot(): void
{
    Model::preventSilentlyDiscardingAttributes($this->app->isLocal());
}

Upserts

Eloquent 的 upsert 方法可用於在單個原子操作中更新或建立記錄。該方法的第一個參數由要插入或更新的值組成,而第二個參數列出了在關聯表中唯一識別記錄的列。該方法的第三個也是最後一個參數是如果資料庫中已存在匹配記錄時應更新的列的陣列。如果模型上啟用了時間戳,upsert 方法將自動設定 created_atupdated_at 時間戳:

Flight::upsert([
    ['departure' => 'Oakland', 'destination' => 'San Diego', 'price' => 99],
    ['departure' => 'Chicago', 'destination' => 'New York', 'price' => 150]
], uniqueBy: ['departure', 'destination'], update: ['price']);

[!WARNING] 除 SQL Server 外的所有資料庫都要求 upsert 方法第二個參數中的列具有「主鍵」或「唯一」索引。此外,MariaDB 和 MySQL 資料庫驅動程式會忽略 upsert 方法的第二個參數,並始終使用表的「主鍵」和「唯一」索引来偵測現有記錄。

刪除模型

要刪除模型,您可以在模型實例上調用 delete 方法:

use App\Models\Flight;

$flight = Flight::find(1);

$flight->delete();

如果您想在資料庫事務中刪除模型,可以使用 deleteOrFail 方法。如果在刪除期間拋出異常,事務將自動回滾:

$flight->deleteOrFail();

透過主鍵刪除現有模型

在上面的範例中,我們在調用 delete 方法之前從資料庫中檢索模型。但是,如果您知道模型的主鍵,可以透過調用 destroy 方法來刪除模型,而無需明確檢索它。除了接受單個主鍵外,destroy 方法還將接受多個主鍵、主鍵陣列或主鍵集合

Flight::destroy(1);

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

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

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

如果您正在使用軟刪除模型,您可以透過 forceDestroy 方法永久刪除模型:

Flight::forceDestroy(1);

[!WARNING] destroy 方法會單獨載入每個模型並調用 delete 方法,以便為每個模型正確派發 deletingdeleted 事件。

使用查詢刪除模型

當然,您可以建立一個 Eloquent 查詢來刪除與查詢條件匹配的所有模型。在此範例中,我們將刪除所有標記為非活動的航班。像批量更新一樣,批量刪除不會為刪除的模型派發模型事件:

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

要刪除表中的所有模型,您應該執行一個不添加任何條件的查詢:

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

[!WARNING] 透過 Eloquent 執行批量刪除語句時,不會為刪除的模型派發 deletingdeleted 模型事件。這是因為在執行刪除語句時從未實際檢索模型。

軟刪除

除了實際從資料庫中移除記錄外,Eloquent 還可以「軟刪除」模型。當模型被軟刪除時,它們並非實際從資料庫中移除。相反,模型上會設定一個 deleted_at 屬性,指示模型被「刪除」的日期和時間。要為模型啟用軟刪除,請將 Illuminate\Database\Eloquent\SoftDeletes trait 添加到模型中:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Flight extends Model
{
    use SoftDeletes;
}

[!NOTE] SoftDeletes trait 會自動為您將 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 方法。restore 方法將模型的 deleted_at 列設定為 null

$flight->restore();

您也可以在查詢中使用 restore 方法來還原多個模型。同樣,與其他「批量」操作一樣,這不會為還原的模型派發任何模型事件:

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

restore 方法也可在建立關聯查詢時使用:

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

永久刪除模型

有時您可能需要真正從資料庫中移除模型。您可以使用 forceDelete 方法從資料庫表中永久移除軟刪除的模型:

$flight->forceDelete();

您也可以在建立 Eloquent 關聯查詢時使用 forceDelete 方法:

$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();

修剪模型

有時您可能想要定期刪除不再需要的模型。為了實現這一點,您可以將 Illuminate\Database\Eloquent\PrunableIlluminate\Database\Eloquent\MassPrunable trait 添加到您希望定期修剪的模型中。將其中一個 trait 添加到模型後,實現一個 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;

    /**
     * 獲取可修剪的模型查詢。
     */
    public function prunable(): Builder
    {
        return static::where('created_at', '<=', now()->minus(months: 1));
    }
}

將模型標記為 Prunable 時,您還可以在模型上定義一個 pruning 方法。此方法將在模型被刪除之前調用。此方法在從資料庫中永久移除模型之前刪除與模型關聯的任何其他資源(例如儲存的檔案)時非常有用:

/**
 * 為修剪準備模型。
 */
protected function pruning(): void
{
    // ...
}

配置可修剪模型後,您應該在應用程式的 routes/console.php 檔案中排程 model:prune Artisan 命令。您可以自由選擇運行此命令的適當間隔:

use Illuminate\Support\Facades\Schedule;

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

在底層,model:prune 命令將自動偵測應用程式 app/Models 目錄中的「可修剪」模型。如果您的模型位於不同的位置,您可以使用 --model 選項來指定模型類別名稱:

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

如果您希望在修剪所有其他偵測到的模型時排除某些模型,可以使用 --except 選項:

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

您可以透過使用 --pretend 選項執行 model:prune 命令來測試您的 prunable 查詢。假裝時,model:prune 命令將簡單報告如果命令實際運行將有多少記錄被修剪:

php artisan model:prune --pretend

[!WARNING] 如果軟刪除的模型匹配可修剪查詢,則將被永久刪除(forceDelete)。

批量修剪

當模型標記有 Illuminate\Database\Eloquent\MassPrunable trait 時,模型使用批量刪除查詢從資料庫中刪除。因此,pruning 方法不會被調用,deletingdeleted 模型事件也不會被派發。這是因為在刪除之前從未實際檢索模型,因此使修剪過程更加高效:

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

    /**
     * 獲取可修剪的模型查詢。
     */
    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 Artisan 命令,它將生成的作用域放置在應用程式的 app/Models/Scopes 目錄中:

php artisan make:scope AncientScope

編寫全域作用域

編寫全域作用域很簡單。首先,使用 make:scope 命令生成一個實現 Illuminate\Database\Eloquent\Scope 介面的類別。Scope 介面要求您實現一個方法:applyapply 方法可以根據需要向查詢添加 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
{
    /**
     * 將作用域應用於給定的 Eloquent 查詢建構器。
     */
    public function apply(Builder $builder, Model $model): void
    {
        $builder->where('created_at', '<', now()->minus(years: 2000));
    }
}

[!NOTE] 如果您的全域作用域正在向查詢的選擇子句添加列,您應該使用 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 方法接受您的作用域實例作為其唯一參數:

<?php

namespace App\Models;

use App\Models\Scopes\AncientScope;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * 模型的「booted」方法。
     */
    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

匿名全域作用域

Eloquent 還允許您使用閉包定義全域作用域,這對於不需要單獨類別的簡單作用域特別有用。使用閉包定義全域作用域時,您應該提供一個您選擇的作用域名稱作為 addGlobalScope 方法的第一個參數:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * 模型的「booted」方法。
     */
    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();

如果您想移除幾個甚至所有查詢的全域作用域,可以使用 withoutGlobalScopeswithoutGlobalScopesExcept 方法:

// 移除所有全域作用域...
User::withoutGlobalScopes()->get();

// 移除一些全域作用域...
User::withoutGlobalScopes([
    FirstScope::class, SecondScope::class
])->get();

// 移除除給定作用域之外的所有全域作用域...
User::withoutGlobalScopesExcept([
    SecondScope::class,
])->get();

區域作用域

區域作用域允許您定義可輕鬆在應用程式中重用的常用查詢條件集。例如,您可能需要經常檢索所有被認為是「受歡迎」的使用者。要定義作用域,請向 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]
    protected function popular(Builder $query): void
    {
        $query->where('votes', '>', 100);
    }

    /**
     * 作用域查詢以僅包含活動使用者。
     */
    #[Scope]
    protected function active(Builder $query): void
    {
        $query->where('active', 1);
    }
}

使用區域作用域

定義作用域後,您可以在查詢模型時調用作用域方法。您甚至可以鏈接調用各種作用域:

use App\Models\User;

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

透過 or 查詢運算符組合多個 Eloquent 模型作用域可能需要使用閉包來實現正確的邏輯分組

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

但是,由於這可能很繁瑣,Laravel 提供了一個「高階」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]
    protected function ofType(Builder $query, string $type): void
    {
        $query->where('type', $type);
    }
}

將預期的參數添加到作用域方法的簽名後,您可以在調用作用域時傳遞參數:

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

帶有屬性的作用域方法應為 protected。從模型類別內部調用帶有屬性的作用域時,請透過查詢建構器實例調用作用域,例如 static::query()->ofType('admin'),以確保呼叫被路由到 Eloquent 的作用域處理。

待定屬性

如果您想使用作用域來建立具有與用於約束作用域的屬性相同的模型,您可以在建立作用域查詢時使用 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]
    protected function draft(Builder $query): void
    {
        $query->withAttributes([
            'hidden' => true,
        ]);
    }
}

withAttributes 方法將使用給定的屬性向查詢添加 where 條件,並且還會將給定的屬性添加到透過作用域建立的任何模型中:

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

$draft->hidden; // true

要指示 withAttributes 方法不向查詢添加 where 條件,您可以將 asConditions 參數設定為 false

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

比較模型

有時您可能需要判斷兩個模型是否「相同」。isisNot 方法可用於快速驗證兩個模型是否具有相同的主鍵、表和資料庫連接:

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

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

isisNot 方法在使用 belongsTohasOnemorphTomorphOne 關聯時也可用。當您想要比較關聯模型而無需發出查詢來檢索該模型時,此方法特別有幫助:

if ($post->author()->is($user)) {
    // ...
}

事件

[!NOTE] 想要將 Eloquent 事件直接廣播到客戶端應用程式?請查看 Laravel 的模型事件廣播

Eloquent 模型會派發幾個事件,允許您在模型生命週期中的以下時刻加入:retrievedcreatingcreatedupdatingupdatedsavingsaveddeletingdeletedtrashedforceDeletingforceDeletedrestoringrestoredreplicating

retrieved 事件將在從資料庫中檢索現有模型時派發。當新模型第一次被儲存時,creatingcreated 事件將被派發。updating / updated 事件將在修改現有模型並調用 save 方法時派發。saving / saved 事件將在建立或更新模型時派發——即使模型的屬性未被更改。以 -ing 結尾的事件名稱在模型的任何更改被持久化之前派發,而以 -ed 結尾的事件在模型的更改被持久化之後派發。

要開始監聽模型事件,請在 Eloquent 模型上定義 $dispatchesEvents 屬性。此屬性將 Eloquent 模型生命週期的各個點映射到您自己的事件類別。每個模型事件類別應期望透過其構造函數接收受影響模型的實例:

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

    /**
     * 模型的事件映射。
     *
     * @var array<string, string>
     */
    protected $dispatchesEvents = [
        'saved' => UserSaved::class,
        'deleted' => UserDeleted::class,
    ];
}

定義並映射 Eloquent 事件後,您可以使用事件監聽器來處理事件。

[!WARNING] 透過 Eloquent 發出批量更新或刪除查詢時,受影響的模型不會觸發 savedupdateddeletingdeleted 模型事件。這是因為在執行批量更新或刪除時從未實際檢索模型。

使用閉包

您可以註冊在派發各種模型事件時執行的閉包,而不是使用自訂事件類別。通常,您應該在模型的 booted 方法中註冊這些閉包:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * 模型的「booted」方法。
     */
    protected static function booted(): void
    {
        static::created(function (User $user) {
            // ...
        });
    }
}

如有需要,您可以在註冊模型事件時使用可佇列化的匿名事件監聽器。這將指示 Laravel 使用應用程式的佇列在背景中運行模型事件監聽器:

use function Illuminate\Events\queueable;

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

觀察者

定義觀察者

如果您正在監聽給定模型上的許多事件,您可以使用觀察者將所有監聽器分組到一個類別中。觀察者類別的方法名稱反映了您希望監聽的 Eloquent 事件。這些方法中的每一個都接受受影響的模型作為其唯一參數。make:observer Artisan 命令是建立新觀察者類別的最簡單方法:

php artisan make:observer UserObserver --model=User

此命令將新觀察者放置在 app/Observers 目錄中。如果此目錄不存在,Artisan 將為您建立它。您的新觀察者如下所示:

<?php

namespace App\Observers;

use App\Models\User;

class UserObserver
{
    /**
     * 處理 User「已建立」事件。
     */
    public function created(User $user): void
    {
        // ...
    }

    /**
     * 處理 User「已更新」事件。
     */
    public function updated(User $user): void
    {
        // ...
    }

    /**
     * 處理 User「已刪除」事件。
     */
    public function deleted(User $user): void
    {
        // ...
    }

    /**
     * 處理 User「已還原」事件。
     */
    public function restored(User $user): void
    {
        // ...
    }

    /**
     * 處理 User「已強制刪除」事件。
     */
    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;

/**
 * 引導任何應用程式服務。
 */
public function boot(): void
{
    User::observe(UserObserver::class);
}

[!NOTE] 觀察者還可以監聽其他事件,例如 savingretrieved。這些事件在事件文檔中描述。

觀察者和資料庫事務

當模型在資料庫事務內被建立時,您可能希望指示觀察者僅在資料庫事務被提交後執行其事件處理器。您可以透過在觀察者上實現 ShouldHandleEventsAfterCommit 介面來實現這一點。如果資料庫事務不在進行中,事件處理器將立即執行:

<?php

namespace App\Observers;

use App\Models\User;
use Illuminate\Contracts\Events\ShouldHandleEventsAfterCommit;

class UserObserver implements ShouldHandleEventsAfterCommit
{
    /**
     * 處理 User「已建立」事件。
     */
    public function created(User $user): void
    {
        // ...
    }
}

靜音事件

有時您可能需要暫時「靜音」模型觸發的所有事件。您可以使用 withoutEvents 方法來實現這一點。withoutEvents 方法接受一個閉包作為其唯一參數。在此閉包內執行的任何程式碼都不會派發模型事件,閉包返回的任何值都將由 withoutEvents 方法返回:

use App\Models\User;

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

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

儲存單個模型而不觸發事件

有時您可能希望「儲存」給定的模型而不派發任何事件。您可以使用 saveQuietly 方法來實現這一點:

$user = User::findOrFail(1);

$user->name = 'Victoria Faith';

$user->saveQuietly();

您還可以在不派發任何事件的情況下「更新」、「刪除」、「軟刪除」、「還原」和「複製」給定的模型:

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