跳轉至內容

Eloquent:入門

簡介

Laravel 包含了 Eloquent,這是一個物件關係對映器 (ORM),讓你可以愉快地與資料庫進行互動。使用 Eloquent 時,每個資料庫表都有一個對應的“模型”,用於與該表進行互動。除了從資料庫表中檢索記錄外,Eloquent 模型還允許你插入、更新和刪除表中的記錄。

在開始之前,請確保在應用程式的 config/database.php 配置檔案中配置了資料庫連線。有關配置資料庫的更多資訊,請檢視 資料庫配置文件

生成模型類

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

1php artisan make:model Flight

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

1php artisan make:model Flight --migration

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

1# Generate a model and a FlightFactory class...
2php artisan make:model Flight --factory
3php artisan make:model Flight -f
4 
5# Generate a model and a FlightSeeder class...
6php artisan make:model Flight --seed
7php artisan make:model Flight -s
8 
9# Generate a model and a FlightController class...
10php artisan make:model Flight --controller
11php artisan make:model Flight -c
12 
13# Generate a model, FlightController resource class, and form request classes...
14php artisan make:model Flight --controller --resource --requests
15php artisan make:model Flight -crR
16 
17# Generate a model and a FlightPolicy class...
18php artisan make:model Flight --policy
19 
20# Generate a model and a migration, factory, seeder, and controller...
21php artisan make:model Flight -mfsc
22 
23# Shortcut to generate a model, migration, factory, seeder, policy, controller, and form requests...
24php artisan make:model Flight --all
25php artisan make:model Flight -a
26 
27# Generate a pivot model...
28php artisan make:model Member --pivot
29php artisan make:model Member -p

檢查模型

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

1php artisan model:show Flight

Eloquent 模型約定

透過 make:model 命令生成的模型將被放置在 app/Models 目錄中。讓我們檢查一個基礎模型類,並討論一些 Eloquent 的關鍵約定。

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Model;
6 
7class Flight extends Model
8{
9 // ...
10}

表名

瀏覽上面的示例後,你可能注意到我們並沒有告訴 Eloquent Flight 模型對應哪個資料庫表。按照慣例,將使用類名的“蛇形命名法 (snake case)”複數形式作為表名,除非顯式指定了其他名稱。因此,在這種情況下,Eloquent 會假設 Flight 模型將記錄儲存在 flights 表中,而 AirTrafficController 模型會將記錄儲存在 air_traffic_controllers 表中。

如果模型對應的資料庫表不符合此約定,你可以使用 Table 屬性手動指定模型的表名。

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Attributes\Table;
6use Illuminate\Database\Eloquent\Model;
7 
8#[Table('my_flights')]
9class Flight extends Model
10{
11 // ...
12}

主鍵

Eloquent 還會假設每個模型對應的資料庫表都有一個名為 id 的主鍵列。如果需要,你可以使用 Table 屬性上的 key 引數指定作為模型主鍵的列。

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Attributes\Table;
6use Illuminate\Database\Eloquent\Model;
7 
8#[Table(key: 'flight_id')]
9class Flight extends Model
10{
11 // ...
12}

此外,Eloquent 假設主鍵是一個自增的整數值,這意味著 Eloquent 會自動將主鍵轉換為整數。如果你希望使用非自增或非數字主鍵,應該在 Table 屬性上指定 keyTypeincrementing 引數。

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Attributes\Table;
6use Illuminate\Database\Eloquent\Model;
7 
8#[Table(key: 'uuid', keyType: 'string', incrementing: false)]
9class Flight extends Model
10{
11 // ...
12}

“複合”主鍵

Eloquent 要求每個模型至少擁有一個唯一的標識“ID”作為其主鍵。Eloquent 模型不支援“複合”主鍵。不過,除了表的唯一標識主鍵外,你可以自由地在資料庫表中新增額外的多列唯一索引。

UUID 和 ULID 主鍵

你可以選擇使用 UUID 而不是自增整數作為 Eloquent 模型的主鍵。UUID 是 36 個字元長的通用唯一字母數字識別符號。

如果你希望模型使用 UUID 主鍵而不是自增整數主鍵,可以在模型上使用 Illuminate\Database\Eloquent\Concerns\HasUuids 特性。當然,你需要確保模型有一個 UUID 等效的主鍵列

1use Illuminate\Database\Eloquent\Concerns\HasUuids;
2use Illuminate\Database\Eloquent\Model;
3 
4class Article extends Model
5{
6 use HasUuids;
7 
8 // ...
9}
10 
11$article = Article::create(['title' => 'Traveling to Europe']);
12 
13$article->id; // "018f2b5c-6a7f-7b12-9d6f-2f8a4e0c9c11"

預設情況下,HasUuids 特性將為你的模型生成 UUIDv7 識別符號。這些 UUID 對於索引資料庫儲存更高效,因為它們可以按字典順序排序。

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

1use Ramsey\Uuid\Uuid;
2 
3/**
4 * Generate a new UUID for the model.
5 */
6public function newUniqueId(): string
7{
8 return (string) Uuid::uuid4();
9}
10 
11/**
12 * Get the columns that should receive a unique identifier.
13 *
14 * @return array<int, string>
15 */
16public function uniqueIds(): array
17{
18 return ['id', 'discount_code'];
19}

如果你願意,可以選擇使用“ULID”代替 UUID。ULID 與 UUID 類似,但長度僅為 26 個字元。與有序 UUID 一樣,ULID 可按字典順序排序,從而實現高效的資料庫索引。要使用 ULID,你應該在模型上使用 Illuminate\Database\Eloquent\Concerns\HasUlids 特性。你還應該確保模型有一個 ULID 等效的主鍵列

1use Illuminate\Database\Eloquent\Concerns\HasUlids;
2use Illuminate\Database\Eloquent\Model;
3 
4class Article extends Model
5{
6 use HasUlids;
7 
8 // ...
9}
10 
11$article = Article::create(['title' => 'Traveling to Asia']);
12 
13$article->id; // "01gd4d3tgrrfqeda94gdbtdk5c"

時間戳

預設情況下,Eloquent 期望模型對應的資料庫表中存在 created_atupdated_at 列。Eloquent 會在模型建立或更新時自動設定這些列的值。如果你不希望 Eloquent 自動管理這些列,可以在模型的 Table 屬性上將 timestamps 設定為 false

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Attributes\Table;
6use Illuminate\Database\Eloquent\Model;
7 
8#[Table(timestamps: false)]
9class Flight extends Model
10{
11 // ...
12}

如果你需要自定義模型時間戳的格式,可以使用 Table 屬性上的 dateFormat 引數。這決定了日期屬性在資料庫中的儲存方式,以及將模型序列化為陣列或 JSON 時的格式。

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Attributes\Table;
6use Illuminate\Database\Eloquent\Model;
7 
8#[Table(dateFormat: 'U')]
9class Flight extends Model
10{
11 // ...
12}

如果你需要自定義用於儲存時間戳的列名,可以在模型上定義 CREATED_ATUPDATED_AT 常量。

1<?php
2 
3class Flight extends Model
4{
5 /**
6 * The name of the "created at" column.
7 *
8 * @var string|null
9 */
10 public const CREATED_AT = 'creation_date';
11 
12 /**
13 * The name of the "updated at" column.
14 *
15 * @var string|null
16 */
17 public const UPDATED_AT = 'updated_date';
18}

如果你想執行模型操作而不修改模型的 updated_at 時間戳,可以在傳遞給 withoutTimestamps 方法的閉包中操作模型。

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

資料庫連線

預設情況下,所有 Eloquent 模型都將使用為你的應用程式配置的預設資料庫連線。如果你想指定與特定模型互動時使用的不同連線,可以使用 Connection 屬性。

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Attributes\Connection;
6use Illuminate\Database\Eloquent\Model;
7 
8#[Connection('mysql')]
9class Flight extends Model
10{
11 // ...
12}

預設屬性值

預設情況下,新例項化的模型不會包含任何屬性值。如果你想為模型的某些屬性定義預設值,可以在模型上定義一個 $attributes 屬性。放入 $attributes 陣列中的屬性值應為原始的“可儲存”格式,就像它們剛從資料庫中讀取出來一樣。

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Model;
6 
7class Flight extends Model
8{
9 /**
10 * The model's default values for attributes.
11 *
12 * @var array
13 */
14 protected $attributes = [
15 'options' => '[]',
16 'delayed' => false,
17 ];
18}

配置 Eloquent 嚴格模式

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

首先,preventLazyLoading 方法接受一個可選的布林引數,用於指示是否應防止延遲載入。例如,你可能只希望在非生產環境中停用延遲載入,這樣即便生產程式碼中意外出現了延遲載入的關係,生產環境也能繼續正常執行。通常,該方法應在應用程式 AppServiceProviderboot 方法中呼叫。

1use Illuminate\Database\Eloquent\Model;
2 
3/**
4 * Bootstrap any application services.
5 */
6public function boot(): void
7{
8 Model::preventLazyLoading(! $this->app->isProduction());
9}

此外,你可以透過呼叫 preventSilentlyDiscardingAttributes 方法,指示 Laravel 在嘗試填充不可填充屬性時丟擲異常。這有助於在本地開發時,防止因嘗試設定未新增到模型 fillable 陣列中的屬性而導致的意外錯誤。

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

檢索模型

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

1use App\Models\Flight;
2 
3foreach (Flight::all() as $flight) {
4 echo $flight->name;
5}

構建查詢

Eloquent 的 all 方法將返回模型表中的所有結果。然而,由於每個 Eloquent 模型都是一個 查詢構建器,你可以在查詢中新增額外的約束,然後呼叫 get 方法來檢索結果。

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

由於 Eloquent 模型就是查詢構建器,你應該複習 Laravel 查詢構建器 提供的所有方法。在編寫 Eloquent 查詢時,你可以使用這些方法中的任何一個。

重新整理模型

如果你已經擁有從資料庫檢索到的 Eloquent 模型例項,可以使用 freshrefresh 方法“重新整理”模型。fresh 方法將從資料庫中重新檢索模型。現有的模型例項不會受到影響。

1$flight = Flight::where('number', 'FR 900')->first();
2 
3$freshFlight = $flight->fresh();

refresh 方法將使用資料庫中的最新資料重新填充現有模型。此外,其所有已載入的關係也會被重新整理。

1$flight = Flight::where('number', 'FR 900')->first();
2 
3$flight->number = 'FR 456';
4 
5$flight->refresh();
6 
7$flight->number; // "FR 900"

集合 (Collections)

正如我們所見,像 allget 這樣的 Eloquent 方法會從資料庫中檢索多條記錄。然而,這些方法返回的不是普通的 PHP 陣列,而是 Illuminate\Database\Eloquent\Collection 的例項。

Eloquent Collection 類繼承了 Laravel 的基礎 Illuminate\Support\Collection 類,該類為互動資料集合提供了 各種有用的方法。例如,reject 方法可用於根據閉包的執行結果從集合中移除模型。

1$flights = Flight::where('destination', 'Paris')->get();
2 
3$flights = $flights->reject(function (Flight $flight) {
4 return $flight->cancelled;
5});

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

由於 Laravel 的所有集合都實現了 PHP 的迭代介面,你可以像迴圈陣列一樣迴圈遍歷集合。

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

資料分塊 (Chunking)

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

chunk 方法將檢索一部分 Eloquent 模型,並將它們傳遞給一個閉包進行處理。由於每次只檢索當前分塊的 Eloquent 模型,因此在處理大量模型時,chunk 方法可以顯著降低記憶體使用量。

1use App\Models\Flight;
2use Illuminate\Database\Eloquent\Collection;
3 
4Flight::chunk(200, function (Collection $flights) {
5 foreach ($flights as $flight) {
6 // ...
7 }
8});

傳遞給 chunk 方法的第一個引數是你希望每個“分塊”接收的記錄數。作為第二個引數傳遞的閉包將為從資料庫檢索到的每個分塊呼叫。系統將執行資料庫查詢來檢索傳遞給閉包的每一塊記錄。

如果你根據在迭代結果時也會更新的列來過濾 chunk 方法的結果,你應該使用 chunkById 方法。在這種情況下使用 chunk 方法可能會導致意外且不一致的結果。在內部,chunkById 方法將始終檢索 id 列大於前一個分塊中最後一個模型的模型。

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

由於 chunkByIdlazyById 方法會向正在執行的查詢中新增它們自己的“where”條件,因此通常應該將你自己的條件 邏輯分組 在一個閉包內。

1Flight::where(function ($query) {
2 $query->where('delayed', true)->orWhere('cancelled', true);
3})->chunkById(200, function (Collection $flights) {
4 $flights->each->update([
5 'departed' => false,
6 'cancelled' => true
7 ]);
8}, column: 'id');

使用延遲集合進行分塊

lazy 方法的工作原理類似於 chunk 方法,因為它在幕後也是分塊執行查詢的。然而,它不是直接將每個分塊傳遞給回撥,而是返回一個扁平化的 Eloquent 模型 LazyCollection,讓你能夠將結果作為一個單一流進行互動。

1use App\Models\Flight;
2 
3foreach (Flight::lazy() as $flight) {
4 // ...
5}

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

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

你可以使用 lazyByIdDesc 方法按 id 的降序過濾結果。

遊標 (Cursors)

lazy 方法類似,cursor 方法可用於在遍歷成千上萬的 Eloquent 模型記錄時,顯著減少應用程式的記憶體消耗。

cursor 方法只會執行一個單一的資料庫查詢;然而,各個 Eloquent 模型直到真正遍歷到它們時才會被填充。因此,在遍歷遊標時,記憶體中任何時刻只會保留一個 Eloquent 模型。

由於 cursor 方法在任何時候記憶體中只持有一個 Eloquent 模型,它無法進行預載入 (Eager Load) 關係。如果你需要預載入關係,請考慮使用 lazy 方法 代替。

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

1use App\Models\Flight;
2 
3foreach (Flight::where('destination', 'Zurich')->cursor() as $flight) {
4 // ...
5}

cursor 返回一個 Illuminate\Support\LazyCollection 例項。延遲集合 允許你使用普通 Laravel 集合上提供的許多集合方法,同時每次只加載一個模型到記憶體中。

1use App\Models\User;
2 
3$users = User::cursor()->filter(function (User $user) {
4 return $user->id > 500;
5});
6 
7foreach ($users as $user) {
8 echo $user->id;
9}

儘管 cursor 方法使用的記憶體比普通查詢少得多(因為它在記憶體中只持有一個 Eloquent 模型),但它最終仍然會耗盡記憶體。這是 因為 PHP 的 PDO 驅動程式會在其緩衝區中自動快取所有原始查詢結果。如果你處理的是極其大量的 Eloquent 記錄,請考慮使用 lazy 方法 代替。

高階子查詢

子查詢選擇

Eloquent 還提供高階子查詢支援,允許你在單個查詢中從相關表中提取資訊。例如,假設我們有一個航班 destinations(目的地)表和目的地 flights(航班)表。flights 表包含一個 arrived_at 列,用於指示航班到達目的地的時間。

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

1use App\Models\Destination;
2use App\Models\Flight;
3 
4return Destination::addSelect(['last_flight' => Flight::select('name')
5 ->whereColumn('destination_id', 'destinations.id')
6 ->orderByDesc('arrived_at')
7 ->limit(1)
8])->get();

子查詢排序

此外,查詢構建器的 orderBy 函式也支援子查詢。繼續使用我們的航班示例,我們可以利用此功能根據最後一班航班到達該目的地的時間對所有目的地進行排序。同樣,這可以在執行單個數據庫查詢時完成。

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

檢索單個模型 / 聚合資料

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

1use App\Models\Flight;
2 
3// Retrieve a model by its primary key...
4$flight = Flight::find(1);
5 
6// Retrieve the first model matching the query constraints...
7$flight = Flight::where('active', 1)->first();
8 
9// Alternative to retrieving the first model matching the query constraints...
10$flight = Flight::firstWhere('active', 1);

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

1$flight = Flight::findOr(1, function () {
2 // ...
3});
4 
5$flight = Flight::where('legs', '>', 3)->firstOr(function () {
6 // ...
7});

未找到異常

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

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

如果 ModelNotFoundException 沒有被捕獲,將自動向客戶端傳送 404 HTTP 響應。

1use App\Models\Flight;
2 
3Route::get('/api/flights/{id}', function (string $id) {
4 return Flight::findOrFail($id);
5});

檢索或建立模型

firstOrCreate 方法將嘗試使用給定的列/值對查詢資料庫記錄。如果無法在資料庫中找到模型,則會插入一條記錄,該記錄的屬性是第一個陣列引數與可選第二個陣列引數合併的結果。

firstOrNew 方法與 firstOrCreate 一樣,將嘗試查詢資料庫中匹配給定屬性的記錄。但是,如果未找到模型,則返回一個新的模型例項。請注意,firstOrNew 返回的模型尚未持久化到資料庫中。你需要手動呼叫 save 方法將其持久化。

1use App\Models\Flight;
2 
3// Retrieve flight by name or create it if it doesn't exist...
4$flight = Flight::firstOrCreate([
5 'name' => 'London to Paris'
6]);
7 
8// Retrieve flight by name or create it with the name, delayed, and arrival_time attributes...
9$flight = Flight::firstOrCreate(
10 ['name' => 'London to Paris'],
11 ['delayed' => 1, 'arrival_time' => '11:30']
12);
13 
14// Retrieve flight by name or instantiate a new Flight instance...
15$flight = Flight::firstOrNew([
16 'name' => 'London to Paris'
17]);
18 
19// Retrieve flight by name or instantiate with the name, delayed, and arrival_time attributes...
20$flight = Flight::firstOrNew(
21 ['name' => 'Tokyo to Sydney'],
22 ['delayed' => 1, 'arrival_time' => '11:30']
23);

檢索聚合資料

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

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

插入和更新模型

插入

當然,在使用 Eloquent 時,我們不僅需要從資料庫中檢索模型,還需要插入新記錄。值得慶幸的是,Eloquent 使這變得簡單。要將新記錄插入資料庫,你應該例項化一個新模型並設定其屬性。然後,在模型例項上呼叫 save 方法。

1<?php
2 
3namespace App\Http\Controllers;
4 
5use App\Models\Flight;
6use Illuminate\Http\RedirectResponse;
7use Illuminate\Http\Request;
8 
9class FlightController extends Controller
10{
11 /**
12 * Store a new flight in the database.
13 */
14 public function store(Request $request): RedirectResponse
15 {
16 // Validate the request...
17 
18 $flight = new Flight;
19 
20 $flight->name = $request->name;
21 
22 $flight->save();
23 
24 return redirect('/flights');
25 }
26}

在此示例中,我們將傳入的 HTTP 請求中的 name 欄位分配給 App\Models\Flight 模型例項的 name 屬性。當我們呼叫 save 方法時,記錄將被插入到資料庫中。呼叫 save 方法時,模型的 created_atupdated_at 時間戳將自動設定,因此無需手動設定。

或者,你可以使用 create 方法透過單個 PHP 語句“儲存”一個新模型。插入的模型例項將由 create 方法返回。

1use App\Models\Flight;
2 
3$flight = Flight::create([
4 'name' => 'London to Paris',
5]);

但是,在使用 create 方法之前,你需要指定模型類上的 FillableGuarded 屬性。這些屬性是必需的,因為所有 Eloquent 模型預設都會防禦批次賦值漏洞。要了解更多關於批次賦值的資訊,請參閱 批次賦值文件

更新

save 方法也可用於更新資料庫中已存在的模型。要更新模型,你應該先檢索它,然後設定要更新的任何屬性。然後,呼叫模型的 save 方法。同樣,updated_at 時間戳會自動更新,因此無需手動設定其值。

1use App\Models\Flight;
2 
3$flight = Flight::find(1);
4 
5$flight->name = 'Paris to London';
6 
7$flight->save();

有時,你可能需要更新現有模型,或者在沒有匹配模型時建立一個新模型。與 firstOrCreate 方法一樣,updateOrCreate 方法會持久化模型,因此無需手動呼叫 save 方法。

在下面的示例中,如果存在 departure 位置為 Oaklanddestination 位置為 San Diego 的航班,其 pricediscounted 列將被更新。如果不存在此類航班,則會建立一個新的航班,該航班具有將第一個引數陣列與第二個引數數組合並後的屬性。

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

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

1$flight = Flight::updateOrCreate(
2 // ...
3);
4 
5if ($flight->wasRecentlyCreated) {
6 // New flight record was inserted...
7}

批次更新

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

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

update 方法需要一個代表要更新的列和值對的陣列。update 方法返回受影響的行數。

透過 Eloquent 進行批次更新時,不會為更新的模型觸發 savingsavedupdatingupdated 模型事件。這是因為批次更新時模型實際上從未被檢索過。

檢查屬性更改

Eloquent 提供了 isDirtyisCleanwasChanged 方法來檢查模型的內部狀態,並確定其屬性自模型最初被檢索以來發生了什麼變化。

isDirty 方法確定模型的任何屬性自檢索以來是否已更改。你可以將特定的屬性名或屬性陣列傳遞給 isDirty 方法,以確定是否有任何屬性“髒了”(被修改)。isClean 方法將確定自模型檢索以來屬性是否保持不變。此方法也接受一個可選的屬性引數。

1use App\Models\User;
2 
3$user = User::create([
4 'first_name' => 'Taylor',
5 'last_name' => 'Otwell',
6 'title' => 'Developer',
7]);
8 
9$user->title = 'Painter';
10 
11$user->isDirty(); // true
12$user->isDirty('title'); // true
13$user->isDirty('first_name'); // false
14$user->isDirty(['first_name', 'title']); // true
15 
16$user->isClean(); // false
17$user->isClean('title'); // false
18$user->isClean('first_name'); // true
19$user->isClean(['first_name', 'title']); // false
20 
21$user->save();
22 
23$user->isDirty(); // false
24$user->isClean(); // true

wasChanged 方法確定在當前請求週期內上次儲存模型時是否更改了任何屬性。如果需要,你可以傳遞屬性名稱以檢視特定屬性是否已更改。

1$user = User::create([
2 'first_name' => 'Taylor',
3 'last_name' => 'Otwell',
4 'title' => 'Developer',
5]);
6 
7$user->title = 'Painter';
8 
9$user->save();
10 
11$user->wasChanged(); // true
12$user->wasChanged('title'); // true
13$user->wasChanged(['title', 'slug']); // true
14$user->wasChanged('first_name'); // false
15$user->wasChanged(['first_name', 'title']); // true

getOriginal 方法返回一個包含模型原始屬性的陣列,無論自模型檢索以來發生了什麼變化。如果需要,你可以傳遞特定的屬性名稱以獲取特定屬性的原始值。

1$user = User::find(1);
2 
3$user->name; // John
4$user->email; // [email protected]
5 
6$user->name = 'Jack';
7$user->name; // Jack
8 
9$user->getOriginal('name'); // John
10$user->getOriginal(); // Array of original attributes...

getChanges 方法返回一個數組,包含上次儲存模型時更改的屬性,而 getPrevious 方法返回一個數組,包含上次儲存模型之前原始屬性的值。

1$user = User::find(1);
2 
3$user->name; // John
4$user->email; // [email protected]
5 
6$user->update([
7 'name' => 'Jack',
8 'email' => '[email protected]',
9]);
10 
11$user->getChanges();
12 
13/*
14 [
15 'name' => 'Jack',
16 'email' => '[email protected]',
17 ]
18*/
19 
20$user->getPrevious();
21 
22/*
23 [
24 'name' => 'John',
25 'email' => '[email protected]',
26 ]
27*/

批次賦值 (Mass Assignment)

你可以使用 create 方法透過單個 PHP 語句“儲存”一個新模型。插入的模型例項將由該方法返回。

1use App\Models\Flight;
2 
3$flight = Flight::create([
4 'name' => 'London to Paris',
5]);

但是,在使用 create 方法之前,你需要指定模型類上的 FillableGuarded 屬性。這些屬性是必需的,因為所有 Eloquent 模型預設都會防禦批次賦值漏洞。

當用戶傳遞意外的 HTTP 請求欄位,並且該欄位更改了你未預期的資料庫列時,就會發生批次賦值漏洞。例如,惡意使用者可能會透過 HTTP 請求傳送 is_admin 引數,該引數隨後被傳遞給模型的 create 方法,從而允許使用者將自己提升為管理員。

因此,在開始之前,你應該定義想要設為可批次賦值的模型屬性。你可以使用模型上的 Fillable 屬性來執行此操作。例如,讓我們使 Flight 模型的 name 屬性可批次賦值。

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Attributes\Fillable;
6use Illuminate\Database\Eloquent\Model;
7 
8#[Fillable(['name'])]
9class Flight extends Model
10{
11 // ...
12}

一旦指定了哪些屬性可批次賦值,你就可以使用 create 方法在資料庫中插入新記錄。create 方法返回新建立的模型例項。

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

如果你已經擁有一個模型例項,可以使用 fill 方法用屬性陣列填充它。

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

批次賦值與 JSON 列

分配 JSON 列時,必須在模型的 Fillable 屬性中指定每個列的可批次賦值鍵。為了安全起見,在使用 Guarded 屬性時,Laravel 不支援更新巢狀的 JSON 屬性。

1use Illuminate\Database\Eloquent\Attributes\Fillable;
2 
3#[Fillable(['options->enabled'])]
4class Flight extends Model
5{
6 // ...
7}

允許批次賦值

如果你想使所有屬性都可批次賦值,可以在模型上使用 Unguarded 屬性。如果你選擇取消保護模型,則應特別小心,始終手動建立傳遞給 Eloquent 的 fillcreateupdate 方法的陣列。

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Attributes\Unguarded;
6use Illuminate\Database\Eloquent\Model;
7 
8#[Unguarded]
9class Flight extends Model
10{
11 // ...
12}

批次賦值異常

預設情況下,在執行批次賦值操作時,未包含在 Fillable 屬性中的屬性會被靜默丟棄。在生產環境中,這是預期的行為;然而,在本地開發過程中,這可能會導致對模型更改未生效的原因產生困惑。

如果你願意,可以透過呼叫 preventSilentlyDiscardingAttributes 方法,指示 Laravel 在嘗試填充不可填充屬性時丟擲異常。通常,此方法應在應用程式 AppServiceProvider 類的 boot 方法中呼叫。

1use Illuminate\Database\Eloquent\Model;
2 
3/**
4 * Bootstrap any application services.
5 */
6public function boot(): void
7{
8 Model::preventSilentlyDiscardingAttributes($this->app->isLocal());
9}

更新或插入 (Upserts)

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

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

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

刪除模型

要刪除模型,你可以在模型例項上呼叫 delete 方法。

1use App\Models\Flight;
2 
3$flight = Flight::find(1);
4 
5$flight->delete();

按主鍵刪除現有模型

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

1Flight::destroy(1);
2 
3Flight::destroy(1, 2, 3);
4 
5Flight::destroy([1, 2, 3]);
6 
7Flight::destroy(collect([1, 2, 3]));

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

1Flight::forceDestroy(1);

destroy 方法會單獨載入每個模型並呼叫 delete 方法,以便為每個模型正確分發 deletingdeleted 事件。

使用查詢刪除模型

當然,你可以構建 Eloquent 查詢來刪除所有符合查詢條件集模型。在此示例中,我們將刪除所有被標記為非活動的航班。與批次更新一樣,批次刪除不會為被刪除的模型分發模型事件。

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

要刪除表中的所有模型,你應該在不新增任何條件的情況下執行查詢。

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

透過 Eloquent 執行批次刪除語句時,不會為被刪除的模型分發 deletingdeleted 模型事件。這是因為在執行刪除語句時模型實際上從未被檢索過。

軟刪除

除了真正從資料庫中刪除記錄外,Eloquent 還可以“軟刪除”模型。當模型被軟刪除時,它們並沒有真正從資料庫中刪除。相反,模型上設定了 deleted_at 屬性,指示模型被“刪除”的日期和時間。要為模型啟用軟刪除,請將 Illuminate\Database\Eloquent\SoftDeletes 特性新增到模型中。

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Model;
6use Illuminate\Database\Eloquent\SoftDeletes;
7 
8class Flight extends Model
9{
10 use SoftDeletes;
11}

SoftDeletes 特性將自動為你將 deleted_at 屬性轉換為 DateTime / Carbon 例項。

你還應該將 deleted_at 列新增到資料庫表中。Laravel 模式構建器 包含一個用於建立此列的輔助方法。

1use Illuminate\Database\Schema\Blueprint;
2use Illuminate\Support\Facades\Schema;
3 
4Schema::table('flights', function (Blueprint $table) {
5 $table->softDeletes();
6});
7 
8Schema::table('flights', function (Blueprint $table) {
9 $table->dropSoftDeletes();
10});

現在,當你呼叫模型上的 delete 方法時,deleted_at 列將被設定為當前的日期和時間。但是,模型的資料庫記錄將保留在表中。當查詢使用軟刪除的模型時,軟刪除的模型將自動從所有查詢結果中排除。

要確定給定的模型例項是否已被軟刪除,你可以使用 trashed 方法。

1if ($flight->trashed()) {
2 // ...
3}

恢復軟刪除模型

有時你可能希望“取消刪除”軟刪除的模型。要恢復軟刪除的模型,你可以在模型例項上呼叫 restore 方法。restore 方法將把模型的 deleted_at 列設定為 null

1$flight->restore();

你也可以在查詢中使用 restore 方法來恢復多個模型。同樣,與其他“批次”操作一樣,這不會為恢復的模型分發任何模型事件。

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

構建 關係 查詢時也可以使用 restore 方法。

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

永久刪除模型

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

1$flight->forceDelete();

在構建 Eloquent 關係查詢時,也可以使用 forceDelete 方法。

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

查詢軟刪除模型

包含軟刪除模型

如上所述,軟刪除的模型將自動從查詢結果中排除。但是,你可以透過在查詢上呼叫 withTrashed 方法,強制將軟刪除的模型包含在查詢結果中。

1use App\Models\Flight;
2 
3$flights = Flight::withTrashed()
4 ->where('account_id', 1)
5 ->get();

構建 關係 查詢時也可以呼叫 withTrashed 方法。

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

僅檢索軟刪除模型

onlyTrashed 方法將 檢索軟刪除的模型。

1$flights = Flight::onlyTrashed()
2 ->where('airline_id', 1)
3 ->get();

修剪模型 (Pruning Models)

有時你可能想定期刪除不再需要的模型。為此,你可以將 Illuminate\Database\Eloquent\PrunableIlluminate\Database\Eloquent\MassPrunable 特性新增到你想要定期修剪的模型中。將其中一個特性新增到模型後,實現一個 prunable 方法,該方法返回一個解析不再需要的模型的 Eloquent 查詢構建器。

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Builder;
6use Illuminate\Database\Eloquent\Model;
7use Illuminate\Database\Eloquent\Prunable;
8 
9class Flight extends Model
10{
11 use Prunable;
12 
13 /**
14 * Get the prunable model query.
15 */
16 public function prunable(): Builder
17 {
18 return static::where('created_at', '<=', now()->minus(months: 1));
19 }
20}

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

1/**
2 * Prepare the model for pruning.
3 */
4protected function pruning(): void
5{
6 // ...
7}

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

1use Illuminate\Support\Facades\Schedule;
2 
3Schedule::command('model:prune')->daily();

在幕後,model:prune 命令將自動檢測應用程式 app/Models 目錄中的“可修剪”模型。如果你的模型位於不同的位置,可以使用 --model 選項指定模型類名。

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

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

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

你可以透過執行帶有 --pretend 選項的 model:prune 命令來測試你的 prunable 查詢。當假裝執行時,model:prune 命令將簡單地報告如果命令真正執行將會修剪多少記錄。

1php artisan model:prune --pretend

如果軟刪除的模型與可修剪查詢匹配,它們將被永久刪除 (forceDelete)。

批次修剪

當模型被標記為 Illuminate\Database\Eloquent\MassPrunable 特性時,模型將使用批次刪除查詢從資料庫中刪除。因此,pruning 方法不會被呼叫,也不會分發 deletingdeleted 模型事件。這是因為在刪除之前從未真正檢索過這些模型,從而使修剪過程效率更高。

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Builder;
6use Illuminate\Database\Eloquent\Model;
7use Illuminate\Database\Eloquent\MassPrunable;
8 
9class Flight extends Model
10{
11 use MassPrunable;
12 
13 /**
14 * Get the prunable model query.
15 */
16 public function prunable(): Builder
17 {
18 return static::where('created_at', '<=', now()->minus(months: 1));
19 }
20}

複製模型

你可以使用 replicate 方法建立現有模型例項的未儲存副本。當你擁有共享許多相同屬性的模型例項時,此方法特別有用。

1use App\Models\Address;
2 
3$shipping = Address::create([
4 'type' => 'shipping',
5 'line_1' => '123 Example Street',
6 'city' => 'Victorville',
7 'state' => 'CA',
8 'postcode' => '90001',
9]);
10 
11$billing = $shipping->replicate()->fill([
12 'type' => 'billing'
13]);
14 
15$billing->save();

要從新模型中排除一個或多個屬性,你可以將陣列傳遞給 replicate 方法。

1$flight = Flight::create([
2 'destination' => 'LAX',
3 'origin' => 'LHR',
4 'last_flown' => '2020-03-04 11:00:00',
5 'last_pilot_id' => 747,
6]);
7 
8$flight = $flight->replicate([
9 'last_flown',
10 'last_pilot_id'
11]);

查詢作用域 (Query Scopes)

全域性作用域

全域性作用域允許你為給定模型的所有查詢新增約束。Laravel 自己的 軟刪除 功能利用全域性作用域僅從資料庫中檢索“未刪除”的模型。編寫你自己的全域性作用域可以提供一種方便、簡單的方法,確保給定模型的每個查詢都收到某些約束。

生成作用域

要生成新的全域性作用域,可以呼叫 make:scope Artisan 命令,它會將生成的作用域放置在應用程式的 app/Models/Scopes 目錄中。

1php artisan make:scope AncientScope

編寫全域性作用域

編寫全域性作用域很簡單。首先,使用 make:scope 命令生成一個實現 Illuminate\Database\Eloquent\Scope 介面的類。Scope 介面要求你實現一個方法:applyapply 方法可以根據需要向查詢新增 where 約束或其他型別的子句。

1<?php
2 
3namespace App\Models\Scopes;
4 
5use Illuminate\Database\Eloquent\Builder;
6use Illuminate\Database\Eloquent\Model;
7use Illuminate\Database\Eloquent\Scope;
8 
9class AncientScope implements Scope
10{
11 /**
12 * Apply the scope to a given Eloquent query builder.
13 */
14 public function apply(Builder $builder, Model $model): void
15 {
16 $builder->where('created_at', '<', now()->minus(years: 2000));
17 }
18}

如果你的全域性作用域正在向查詢的 select 子句中新增列,你應該使用 addSelect 方法而不是 select。這將防止意外替換查詢現有的 select 子句。

應用全域性作用域

要將全域性作用域分配給模型,只需在模型上放置 ScopedBy 屬性即可。

1<?php
2 
3namespace App\Models;
4 
5use App\Models\Scopes\AncientScope;
6use Illuminate\Database\Eloquent\Attributes\ScopedBy;
7 
8#[ScopedBy([AncientScope::class])]
9class User extends Model
10{
11 //
12}

或者,你可以透過覆蓋模型的 booted 方法並呼叫模型的 addGlobalScope 方法手動註冊全域性作用域。addGlobalScope 方法接受你的作用域例項作為其唯一引數。

1<?php
2 
3namespace App\Models;
4 
5use App\Models\Scopes\AncientScope;
6use Illuminate\Database\Eloquent\Model;
7 
8class User extends Model
9{
10 /**
11 * The "booted" method of the model.
12 */
13 protected static function booted(): void
14 {
15 static::addGlobalScope(new AncientScope);
16 }
17}

在將上面示例中的作用域新增到 App\Models\User 模型後,呼叫 User::all() 方法將執行以下 SQL 查詢:

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

匿名全域性作用域

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

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Builder;
6use Illuminate\Database\Eloquent\Model;
7 
8class User extends Model
9{
10 /**
11 * The "booted" method of the model.
12 */
13 protected static function booted(): void
14 {
15 static::addGlobalScope('ancient', function (Builder $builder) {
16 $builder->where('created_at', '<', now()->minus(years: 2000));
17 });
18 }
19}

移除全域性作用域

如果你想為給定的查詢移除全域性作用域,可以使用 withoutGlobalScope 方法。此方法接受全域性作用域的類名作為其唯一引數。

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

或者,如果你使用閉包定義了全域性作用域,則應該傳遞你分配給全域性作用域的字串名稱。

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

如果你想移除查詢的多個甚至所有全域性作用域,可以使用 withoutGlobalScopeswithoutGlobalScopesExcept 方法。

1// Remove all of the global scopes...
2User::withoutGlobalScopes()->get();
3 
4// Remove some of the global scopes...
5User::withoutGlobalScopes([
6 FirstScope::class, SecondScope::class
7])->get();
8 
9// Remove all global scopes except the given ones...
10User::withoutGlobalScopesExcept([
11 SecondScope::class,
12])->get();

區域性作用域

區域性作用域允許你定義一組通用的查詢約束,你可以在整個應用程式中輕鬆重用它們。例如,你可能需要頻繁檢索被認為是“熱門”的所有使用者。要定義作用域,請將 Scope 屬性新增到 Eloquent 方法。

作用域應始終返回相同的查詢構建器例項或 void

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Attributes\Scope;
6use Illuminate\Database\Eloquent\Builder;
7use Illuminate\Database\Eloquent\Model;
8 
9class User extends Model
10{
11 /**
12 * Scope a query to only include popular users.
13 */
14 #[Scope]
15 protected function popular(Builder $query): void
16 {
17 $query->where('votes', '>', 100);
18 }
19 
20 /**
21 * Scope a query to only include active users.
22 */
23 #[Scope]
24 protected function active(Builder $query): void
25 {
26 $query->where('active', 1);
27 }
28}

利用區域性作用域

定義作用域後,可以在查詢模型時呼叫作用域方法。你甚至可以連結對各種作用域的呼叫。

1use App\Models\User;
2 
3$users = User::popular()->active()->orderBy('created_at')->get();

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

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

但是,由於這可能很麻煩,Laravel 提供了一個“高階” orWhere 方法,允許你流暢地將作用域連結在一起,而無需使用閉包。

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

動態作用域

有時你可能希望定義一個接受引數的作用域。首先,只需將其他引數新增到作用域方法的簽名中。作用域引數應在 $query 引數之後定義。

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Attributes\Scope;
6use Illuminate\Database\Eloquent\Builder;
7use Illuminate\Database\Eloquent\Model;
8 
9class User extends Model
10{
11 /**
12 * Scope a query to only include users of a given type.
13 */
14 #[Scope]
15 protected function ofType(Builder $query, string $type): void
16 {
17 $query->where('type', $type);
18 }
19}

將預期的引數新增到作用域方法的簽名後,你可以在呼叫作用域時傳遞引數。

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

待處理屬性 (Pending Attributes)

如果你想使用作用域來建立具有與用於約束作用域的屬性相同屬性的模型,可以在構建作用域查詢時使用 withAttributes 方法。

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Attributes\Scope;
6use Illuminate\Database\Eloquent\Builder;
7use Illuminate\Database\Eloquent\Model;
8 
9class Post extends Model
10{
11 /**
12 * Scope the query to only include drafts.
13 */
14 #[Scope]
15 protected function draft(Builder $query): void
16 {
17 $query->withAttributes([
18 'hidden' => true,
19 ]);
20 }
21}

withAttributes 方法將使用給定的屬性向查詢新增 where 條件,它還會將給定的屬性新增到透過該作用域建立的任何模型中。

1$draft = Post::draft()->create(['title' => 'In Progress']);
2 
3$draft->hidden; // true

要指示 withAttributes 方法不向查詢新增 where 條件,可以將 asConditions 引數設定為 false

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

比較模型

有時你可能需要確定兩個模型是否“相同”。isisNot 方法可用於快速驗證兩個模型是否具有相同的主鍵、表和資料庫連線。

1if ($post->is($anotherPost)) {
2 // ...
3}
4 
5if ($post->isNot($anotherPost)) {
6 // ...
7}

在使用 belongsTohasOnemorphTomorphOne 關係 時,isisNot 方法也可用。當你想要比較相關模型而不發出查詢來檢索該模型時,此方法特別有用。

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

活動

想將 Eloquent 事件直接廣播到你的客戶端應用程式嗎?請檢視 Laravel 的 模型事件廣播

Eloquent 模型分發多個事件,允許你掛鉤到模型生命週期的以下時刻:retrievedcreatingcreatedupdatingupdatedsavingsaveddeletingdeletedtrashedforceDeletingforceDeletedrestoringrestoredreplicating

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

要開始監聽模型事件,請在你的 Eloquent 模型上定義 $dispatchesEvents 屬性。此屬性將 Eloquent 模型生命週期的各個點對映到你自己的 事件類。每個模型事件類都應透過其建構函式接收受影響模型的例項。

1<?php
2 
3namespace App\Models;
4 
5use App\Events\UserDeleted;
6use App\Events\UserSaved;
7use Illuminate\Foundation\Auth\User as Authenticatable;
8use Illuminate\Notifications\Notifiable;
9 
10class User extends Authenticatable
11{
12 use Notifiable;
13 
14 /**
15 * The event map for the model.
16 *
17 * @var array<string, string>
18 */
19 protected $dispatchesEvents = [
20 'saved' => UserSaved::class,
21 'deleted' => UserDeleted::class,
22 ];
23}

定義並對映 Eloquent 事件後,你可以使用 事件監聽器 來處理這些事件。

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

使用閉包

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

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Model;
6 
7class User extends Model
8{
9 /**
10 * The "booted" method of the model.
11 */
12 protected static function booted(): void
13 {
14 static::created(function (User $user) {
15 // ...
16 });
17 }
18}

如果需要,在註冊模型事件時,你可以利用 可排隊匿名事件監聽器。這將指示 Laravel 使用你的應用程式的 佇列 在後臺執行模型事件監聽器。

1use function Illuminate\Events\queueable;
2 
3static::created(queueable(function (User $user) {
4 // ...
5}));

觀察者 (Observers)

定義觀察者

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

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

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

1<?php
2 
3namespace App\Observers;
4 
5use App\Models\User;
6 
7class UserObserver
8{
9 /**
10 * Handle the User "created" event.
11 */
12 public function created(User $user): void
13 {
14 // ...
15 }
16 
17 /**
18 * Handle the User "updated" event.
19 */
20 public function updated(User $user): void
21 {
22 // ...
23 }
24 
25 /**
26 * Handle the User "deleted" event.
27 */
28 public function deleted(User $user): void
29 {
30 // ...
31 }
32 
33 /**
34 * Handle the User "restored" event.
35 */
36 public function restored(User $user): void
37 {
38 // ...
39 }
40 
41 /**
42 * Handle the User "forceDeleted" event.
43 */
44 public function forceDeleted(User $user): void
45 {
46 // ...
47 }
48}

要註冊觀察者,可以在相應的模型上放置 ObservedBy 屬性。

1use App\Observers\UserObserver;
2use Illuminate\Database\Eloquent\Attributes\ObservedBy;
3 
4#[ObservedBy([UserObserver::class])]
5class User extends Authenticatable
6{
7 //
8}

或者,你可以透過在要觀察的模型上呼叫 observe 方法來手動註冊觀察者。你可以在應用程式 AppServiceProvider 類的 boot 方法中註冊觀察者。

1use App\Models\User;
2use App\Observers\UserObserver;
3 
4/**
5 * Bootstrap any application services.
6 */
7public function boot(): void
8{
9 User::observe(UserObserver::class);
10}

觀察者還可以監聽其他事件,例如 savingretrieved。這些事件在 事件 文件中有所描述。

觀察者與資料庫事務

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

1<?php
2 
3namespace App\Observers;
4 
5use App\Models\User;
6use Illuminate\Contracts\Events\ShouldHandleEventsAfterCommit;
7 
8class UserObserver implements ShouldHandleEventsAfterCommit
9{
10 /**
11 * Handle the User "created" event.
12 */
13 public function created(User $user): void
14 {
15 // ...
16 }
17}

靜默事件

你有時可能需要暫時“靜默”由模型觸發的所有事件。你可以使用 withoutEvents 方法實現這一點。withoutEvents 方法接受閉包作為其唯一引數。在此閉包中執行的任何程式碼都不會分發模型事件,且閉包返回的任何值都將由 withoutEvents 方法返回。

1use App\Models\User;
2 
3$user = User::withoutEvents(function () {
4 User::findOrFail(1)->delete();
5 
6 return User::find(2);
7});

不分發事件儲存單個模型

有時你可能希望“儲存”給定的模型而不分發任何事件。你可以使用 saveQuietly 方法實現這一點。

1$user = User::findOrFail(1);
2 
3$user->name = 'Victoria Faith';
4 
5$user->saveQuietly();

你還可以“更新”、“刪除”、“軟刪除”、“恢復”和“複製”給定的模型而不分發任何事件。

1$user->deleteQuietly();
2$user->forceDeleteQuietly();
3$user->restoreQuietly();