跳转至内容

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