引言

Laravel 内置了 Eloquent,这是一个对象关系映射器(ORM),让与数据库的交互变得十分便捷。使用 Eloquent 时,每个数据库表都对应一个“模型(Model)”,用于与该表进行交互。除了从数据库表中检索记录外,Eloquent 模型还支持向表中插入、更新和删除记录。

在开始之前,请确保在应用的 config/database.php 配置文件中配置好数据库连接。如需了解数据库配置的更多信息,请查阅 数据库配置文档。

生成模型类

首先,我们来创建一个 Eloquent 模型。模型通常存放在 app\Models 目录下,并继承 Illuminate\Database\Eloquent\Model 类。您可以使用 Artisan 命令make:model 来生成一个新模型:

php artisan make:model Flight

如果您在生成模型的同时希望生成 数据库迁移文件,可以使用 --migration 或 -m 选项:

php artisan make:model Flight --migration

在生成模型时,您还可以生成其他多种类型的类,例如工厂、数据填充器、策略、控制器和表单请求等。此外,这些选项可以组合使用,一次性创建多个类:

# 生成模型和 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

# 生成模型,以及迁移、工厂、填充器和控制器……
php artisan make:model Flight -mfsc

# 快捷方式:生成模型、迁移、工厂、填充器、策略、控制器以及表单请求……
php artisan make:model Flight --all
php artisan make:model Flight -a

# 生成中间表模型……
php artisan make:model Member --pivot
php artisan make:model Member -p

检查模型

有时仅通过浏览代码,很难确定模型的所有可用属性和关系。这时不妨尝试使用 Artisan 命令 model:show,该命令可提供模型所有属性和关系的便捷概览:

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 特性上指定 keyType 和 incrementing 参数:

<?php

namespace App\Models;

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

#[Table(key: 'uuid', keyType: 'string', incrementing: false)]
class Flight extends Model
{
    // ...
}

如果仅需禁用自动递增 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 特性。当然,您应确保模型拥有 对应的 UUID 主键列:

use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Model;

class Article extends Model
{
    use HasUuids;

    // ...
}

$article = Article::create(['title' => 'Traveling to Europe']);

$article->id; // "018f2b5c-6a7f-7b12-9d6f-2f8a4e0c9c11"

默认情况下,HasUuids 特性会为模型生成 UUIDv7 标识符。这些 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 特性。您还应确保模型拥有 对应的 ULID 主键列:

use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Model;

class Article extends Model
{
    use HasUlids;

    // ...
}

$article = Article::create(['title' => 'Traveling to Asia']);

$article->id; // "01gd4d3tgrrfqeda94gdbtdk5c"

时间戳

默认情况下,Eloquent 会期望模型对应的数据库表中存在 created_at 和 updated_at 列。在创建或更新模型时,Eloquent 会自动设置这些列的值。如果您不希望 Eloquent 自动管理这些列,可以在模型的 Table 特性中将 timestamps 设置为 false:

<?php

namespace App\Models;

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

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

如果仅需禁用时间戳,可以使用 WithoutTimestamps 特性:

<?php

namespace App\Models;

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

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

如果你需要自定义模型时间戳的格式,可以使用 Table 特性中的 dateFormat 参数。该参数决定了日期属性在数据库中的存储方式:

<?php

namespace App\Models;

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

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

如果只需定义日期格式,可以使用 DateFormat 特性:

<?php

namespace App\Models;

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

#[DateFormat('U')]
class Flight extends Model
{
    // ...
}

如果需要自定义用于存储时间戳的列名,可以在模型中定义 CREATED_AT 和 UPDATED_AT 常量:

<?php

class Flight extends Model
{
    /**
     * “创建时间”列的名称。
     *
     * @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,使其在模型插入或更新后刷新特定属性。为此,请在模型上定义 Refreshes 特性:

use Illuminate\Database\Eloquent\Attributes\Refreshes;

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

你可以将多个属性指定为数组:

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

在模型写入完成后,将根据配置从数据库刷新这些属性。

配置 Eloquent 严格模式

Laravel 提供了几种方法,允许你在不同场景下配置 Eloquent 的行为与“严格程度”。

首先,preventLazyLoading 方法接受一个可选的布尔参数,用于指示是否应禁止延迟加载。例如,你可能希望仅在非生产环境中禁用延迟加载,这样即使生产代码中意外存在延迟加载的关系,生产环境仍能正常运行。通常,该方法应在应用程序的 AppServiceProvider 的 boot 方法中调用:

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

由于 Eloquent 模型就是查询构造器,你应该查看 Laravel 查询构造器提供的所有方法。编写 Eloquent 查询时可以使用其中任何一种方法。

刷新模型

如果你已经拥有一个从数据库检索出的 Eloquent 模型实例,可以使用 fresh 和 refresh 方法来“刷新”该模型。fresh 方法会从数据库重新检索模型,而现有的模型实例不会受到影响:

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

$freshFlight = $flight->fresh();

refresh 方法会使用数据库中的最新数据重新填充现有模型。此外,模型所有已加载的关系也会随之刷新:

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

$flight->number = 'FR 456';

$flight->refresh();

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

如果你需要在事务中刷新模型并获取悲观锁,可以使用 refreshForUpdate 方法。该方法会使用 FOR UPDATE 锁重新加载模型:

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

    // 更新锁定的模型...
});

集合

正如我们所见,像 all 和 get 这样的 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;
}

分块处理结果

如果尝试通过 all 或 get 方法加载成千上万条 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');

由于 chunkById 和 lazyById 方法会在执行的查询中添加自身的“where”条件,你通常应该在闭包内对自身条件进行逻辑分组:

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

使用延迟集合进行分块

lazy 方法在底层实现上与 chunk 方法类似,同样会分块执行查询。不过,lazy 方法并非像 chunk 方法那样直接将每个数据块传入回调函数,而是返回一个包含 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 方法类似,在遍历数万个 Eloquent 模型记录时,使用 cursor 方法可大幅降低应用程序的内存消耗。

cursor 方法仅会执行一次数据库查询;不过,单个 Eloquent 模型在真正被遍历之前,不会被实例化。因此,在遍历游标的过程中,内存中任何时刻只会保留一个 Eloquent 模型。

由于 cursor 方法在内存中每次仅保留一个 Eloquent 模型,因此它无法实现关联的预加载(eager load)。如果你需要预加载关联,建议改用 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 列,用于记录航班抵达目的地的时间。

利用查询构建器的 select 和 addSelect 方法所提供的子查询功能,我们可以在单次查询中选取所有的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();

检索单个模型 / 聚合结果

除了检索符合给定查询条件的所有记录外,还可以使用 find、first 或 firstWhere 方法检索单条记录。这些方法不会返回模型集合,而是返回单个模型实例:

use App\Models\Flight;

// Retrieve a model by its primary key...
$flight = Flight::find(1);

// Retrieve the first model matching the query constraints...
$flight = Flight::where('active', 1)->first();

// Alternative to retrieving the first model matching the query constraints...
$flight = Flight::firstWhere('active', 1);

有时,你可能希望在未查询到结果时执行其他操作。findOr 和 firstOr 方法会返回单个模型实例;若未查询到结果,则会执行给定的闭包。闭包返回的值将被视为该方法的执行结果:

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

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

未找到异常

有时,你可能希望在未找到模型时抛出异常,这在路由或控制器中尤为实用。findOrFail 和 firstOrFail 方法会检索查询的第一条结果;若未找到结果,则会抛出 Illuminate\Database\Eloquent\ModelNotFoundException 异常:

$flight = Flight::findOrFail(1);

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

如果未捕获 ModelNotFoundException,系统会自动向客户端返回 404 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'
]);

// 按名称检索航班,若不存在则使用 name、delayed 和 arrival_time 属性创建……
$flight = Flight::firstOrCreate(
    ['name' => 'London to Paris'],
    ['delayed' => 1, 'arrival_time' => '11:30']
);

// 按名称检索航班,若不存在则实例化新的 Flight 实例……
$flight = Flight::firstOrNew([
    'name' => 'London to Paris'
]);

// 按名称检索航班,若不存在则使用 name、delayed 和 arrival_time 属性实例化……
$flight = Flight::firstOrNew(
    ['name' => 'Tokyo to Sydney'],
    ['delayed' => 1, 'arrival_time' => '11:30']
);

检索聚合结果

与 Eloquent 模型交互时,你也可以使用 Laravel 查询构造器提供的 count、sum、max 等聚合方法。正如你所预期的,这些方法返回标量值,而非 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 方法时,一条记录将被插入数据库。调用 save 方法时,模型的 created_at 和 updated_at 时间戳会自动设置,因此无需手动设置。

若希望在数据库事务中保存模型,可使用 saveOrFail 方法。若保存过程中抛出异常,事务将自动回滚:

$flight->saveOrFail();

或者,你可以使用 create 方法,通过一条 PHP 语句“保存”新模型。create 方法会将已插入的模型实例返回给你:

use App\Models\Flight;

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

不过,在使用 create 方法前,你需要在模型类中指定 Fillable 或 Guarded 属性。这些属性是必需的,因为默认情况下,所有 Eloquent 模型都会防范批量赋值漏洞。如需了解批量赋值的更多信息,请参阅批量赋值文档。

更新

save 方法也可用于更新数据库中已存在的模型。若要更新模型,应先检索该模型,然后设置需要更新的属性,最后调用模型的 save 方法。同样,updated_at 时间戳会自动更新,无需手动设置其值:

use App\Models\Flight;

$flight = Flight::find(1);

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

$flight->save();

若希望在数据库事务中更新模型,可使用 updateOrFail 方法。若更新过程中抛出异常,事务将自动回滚:

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

有时,你需要更新已存在的模型;若不存在匹配的模型,则创建新模型。与 firstOrCreate 方法类似,updateOrCreate 方法会持久化模型,因此无需手动调用 save 方法。

在以下示例中,若存在出发地点(departure)为 Oakland、目的地(destination)为 San Diego 的航班,其 price 和 discounted 列将被更新。若不存在此类航班,则会创建一个新的航班,其属性为第一个参数数组与第二个参数数组合并后的结果:

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

使用 firstOrCreate 或 updateOrCreate 等方法时,你可能不知道是创建了新模型,还是更新了现有模型。wasRecentlyCreated 属性可表明模型是否在当前生命周期内被创建:

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

if ($flight->wasRecentlyCreated) {
    // 已插入新的航班记录……
}

批量更新

更新操作也可针对符合特定查询条件的模型执行。在此示例中,所有状态为 active 且目的地(destination)为 San Diego 的航班,都将被标记为延误:

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

update 方法需要一个包含列名与对应值的数组,用于指定需更新的列。该方法返回受影响的行数。

通过 Eloquent 执行批量更新时,被更新的模型不会触发 saving、saved、updating 和 updated 模型事件。这是因为执行批量更新时,实际上并未真正检索这些模型。

检查属性变更

Eloquent 提供了 isDirty、isClean 和 wasChanged 方法,用于检查模型的内部状态,并判断模型属性自最初检索以来是否发生变化。

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; //[email protected]

$user->name = 'Jack';
$user->name; // Jack

$user->getOriginal('name'); // John
$user->getOriginal(); // 原始属性数组……

getChanges 方法返回包含模型上次保存时发生变更的属性的数组,而 getPrevious 方法返回包含模型上次保存前原始属性值的数组:

$user = User::find(1);

$user->name; // John
$user->email; //[email protected]

$user->update([
    'name' => 'Jack',
    'email' => '[email protected]',
]);

$user->getChanges();

/*
    [
        'name' => 'Jack',
        'email' => '[email protected]',
    ]
*/

$user->getPrevious();

/*
    [
        'name' => 'John',
        'email' => '[email protected]',
    ]
*/

批量赋值

您可以使用 create 方法,通过一条 PHP 语句“保存”一个新模型。该方法会将插入的模型实例返回给您:

use App\Models\Flight;

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

然而,在使用 create 方法之前,您需要在模型类上指定 Fillable 或 Guarded 属性。这些属性是必需的,因为默认情况下,所有 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 属性中指定。出于安全考虑,Laravel 在使用 Guarded 属性时,不支持更新嵌套的 JSON 属性:

use Illuminate\Database\Eloquent\Attributes\Fillable;

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

允许批量赋值

如果您希望所有属性都可批量赋值,可以在模型上使用 Unguarded 属性。如果您选择对模型取消保护,应格外小心,始终手工构建传递给 Eloquent 的 fill、create 和 update 方法的数组:

<?php

namespace App\Models;

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

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

批量赋值异常

默认情况下,执行批量赋值操作时,不在 Fillable 属性中的属性会被静默丢弃。在生产环境中,这是预期行为;但在本地开发期间,这可能会导致模型更改未生效的困惑。

如果您愿意,可以在尝试填充不可填充的属性时,通过调用 preventSilentlyDiscardingAttributes 方法指示 Laravel 抛出异常。通常,该方法应在应用程序的 AppServiceProvider 类的 boot 方法中调用:

use Illuminate\Database\Eloquent\Model;

/**
 * 初始化任意应用服务。
 */
public function boot(): void
{
    Model::preventSilentlyDiscardingAttributes($this->app->isLocal());
}

Upsert(更新或插入)

Eloquent 的 upsert 方法可用于在单个原子操作中更新或创建记录。该方法的第一个参数是要插入或更新的值,第二个参数列出了在关联表中唯一标识记录的列。该方法的第三个也是最后一个参数是一个数组,包含当数据库中已存在匹配记录时应更新的列。如果模型启用了时间戳,upsert 方法会自动设置 created_at 和 updated_at 时间戳:

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

除 SQL Server 外,所有数据库都要求 upsert 方法第二个参数中的列具有“主”索引或“唯一”索引。此外,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);

destroy 方法会逐个加载模型并调用 delete 方法,从而为每个模型正确触发 deleting 和 deleted 事件。

使用查询删除模型

当然,您可以构建一个 Eloquent 查询,删除符合查询条件的所有模型。在此示例中,我们将删除所有标记为未激活的航班。与批量更新一样,批量删除不会为被删除的模型触发模型事件:

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

要删除表中的所有模型,应执行不带任何条件的查询:

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

当通过 Eloquent 执行批量删除语句时,不会为被删除的模型触发 deleting 和 deleted 模型事件。这是因为执行删除语句时,模型实际上从未被检索出来。

软删除

除了真正从数据库中移除记录外,Eloquent 还能对模型进行“软删除”。模型被软删除时,并不会真正从数据库中移除,而是会在模型上设置一个 deleted_at 属性,用于指示模型被“删除”的日期和时间。要为模型启用软删除,请将 Illuminate\Database\Eloquent\SoftDeletes 特性添加到模型中:

<?php

namespace App\Models;

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

class Flight extends Model
{
    use SoftDeletes;
}

SoftDeletes 特性会自动将 deleted_at 属性转换为 DateTime / Carbon 实例。

您还应在数据库表中添加 deleted_at 列。Laravel 的架构构建器包含一个辅助方法,用于创建此列:

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

Schema::table('flights', function (Blueprint $table) {
    $table->softDeletes();
});

Schema::table('flights', function (Blueprint $table) {
    $table->dropSoftDeletes();
});

现在,当你在模型上调用 delete 方法时,deleted_at 列将被设置为当前日期和时间。然而,该模型的数据库记录仍会保留在表中。在查询使用了软删除的模型时,已软删除的模型会自动从所有查询结果中排除。

要判断给定的模型实例是否已被软删除,你可以使用 trashed 方法:

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

恢复软删除的模型

有时你可能希望“取消删除”一个已软删除的模型。要恢复一个已软删除的模型,你可以在模型实例上调用 restore 方法。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\Prunable 或 Illuminate\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 目录中的“Prunable”模型。如果你的模型位于其他位置,可以使用 --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

如果软删除的模型匹配可清理的查询,它们将被永久删除(forceDelete)。

批量清理

当模型使用 Illuminate\Database\Eloquent\MassPrunable trait 标记时,模型将通过批量删除查询从数据库中删除。因此,pruning 方法不会被调用,也不会派发 deleting 和 deleted 模型事件。这是因为模型在删除前从未被实际检索,从而使清理过程更加高效:

<?php

namespace App\Models;

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

class Flight extends Model
{
    use MassPrunable;

    /**
     * 获取可清理的模型查询。
     */
    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 接口要求你实现一个方法:apply。apply 方法可以根据需要向查询添加 where 约束或其他类型的子句:

<?php

namespace App\Models\Scopes;

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

class AncientScope implements Scope
{
    /**
     * 将作用域应用到给定的 Eloquent 查询构建器。
     */
    public function apply(Builder $builder, Model $model): void
    {
        $builder->where('created_at', '<', now()->minus(years: 2000));
    }
}

如果你的全局作用域正在向查询的 select 子句添加列,你应该使用 addSelect 方法而不是 select。这将防止意外替换查询中已有的 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
{
    /**
     * The "booted" method of the model.
     */
    protected static function booted(): void
    {
        static::addGlobalScope(new AncientScope);
    }
}

将上述示例中的作用域添加到 App\Models\User 模型后,调用 User::all() 方法将执行以下 SQL 查询:

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

匿名全局作用域

Eloquent 还允许您使用闭包定义全局作用域,这对于不需要单独类的简单作用域特别有用。使用闭包定义全局作用域时,您应该将自定义的作用域名称作为 addGlobalScope 方法的第一个参数:

<?php

namespace App\Models;

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

class User extends Model
{
    /**
     * The "booted" method of the model.
     */
    protected static function booted(): void
    {
        static::addGlobalScope('ancient', function (Builder $builder) {
            $builder->where('created_at', '<', now()->minus(years: 2000));
        });
    }
}

移除全局作用域

如果您想为给定查询移除全局作用域,可以使用 withoutGlobalScope 方法。该方法仅接受全局作用域的类名作为参数:

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

或者,如果您使用闭包定义了全局作用域,则应传入您分配给该全局作用域的字符串名称:

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

如果您想移除查询中的多个甚至全部全局作用域,可以使用 withoutGlobalScopes 和 withoutGlobalScopesExcept 方法:

// Remove all of the global scopes...
User::withoutGlobalScopes()->get();

// Remove some of the global scopes...
User::withoutGlobalScopes([
    FirstScope::class, SecondScope::class
])->get();

// Remove all global scopes except the given ones...
User::withoutGlobalScopesExcept([
    SecondScope::class,
])->get();

局部作用域

局部作用域允许您定义可在应用程序中轻松复用的通用查询约束集。例如,您可能需要频繁检索所有被视为“受欢迎”的用户。要定义作用域,请为 Eloquent 方法添加 Scope 特性。

作用域应始终返回相同的查询构建器实例或 void:

<?php

namespace App\Models;

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

class User extends Model
{
    /**
     * Scope a query to only include popular users.
     */
    #[Scope]
    protected function popular(Builder $query): void
    {
        $query->where('votes', '>', 100);
    }

    /**
     * Scope a query to only include active users.
     */
    #[Scope]
    protected function active(Builder $query): void
    {
        $query->where('active', 1);
    }
}

使用局部作用域

定义作用域后,在查询模型时即可调用该作用域方法。您甚至可以链式调用多个作用域:

use App\Models\User;

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

通过 or 查询运算符组合多个 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 a query to only include users of a given type.
     */
    #[Scope]
    protected function ofType(Builder $query, string $type): void
    {
        $query->where('type', $type);
    }
}

在作用域方法的签名中添加预期参数后,调用该作用域时即可传入相应参数:

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

带有作用域特性的方法应为 protected。在模型类内部调用带特性的作用域时,需通过查询构建器实例调用该作用域,例如 static::query()->ofType('admin'),以确保调用通过 Eloquent 的作用域处理逻辑。

待填充属性

如果您想使用作用域创建与作用域约束条件具有相同属性的模型,在构建作用域查询时可以使用 withAttributes 方法:

<?php

namespace App\Models;

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

class Post extends Model
{
    /**
     * Scope the query to only include drafts.
     */
    #[Scope]
    protected function draft(Builder $query): void
    {
        $query->withAttributes([
            'hidden' => true,
        ]);
    }
}

withAttributes 方法会使用给定属性为查询添加 where 条件,同时也会将给定属性添加到通过该作用域创建的任何模型中:

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

$draft->hidden; // true

若要让 withAttributes 方法不为查询添加 where 条件,可将 asConditions 参数设置为 false:

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

比较模型

有时您可能需要判断两个模型是否“相同”。可使用 is 和 isNot 方法快速验证两个模型是否具有相同的主键、表和数据库连接:

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

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

使用 belongsTo、hasOne、morphTo 和 morphOne 关系 时,is 和 isNot 方法同样可用。当您希望比较关联模型却不想发起查询来获取该模型时,此方法尤为实用:

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

事件

想将 Eloquent 事件直接广播到客户端应用吗?请查看 Laravel 的模型事件广播。

Eloquent 模型会派发多个事件,使您能够在模型生命周期的以下节点进行钩子处理:retrieved、creating、created、updating、updated、saving、saved、deleting、deleted、trashed、forceDeleting、forceDeleted、restoring、restored 和 replicating。

从数据库中检索到现有模型时,会派发 retrieved 事件。当新模型首次保存时,会派发 creating 和 created 事件。当现有模型被修改并调用 save 方法时,会派发 updating / updated 事件。无论模型属性是否发生变化,只要模型被创建或更新,就会派发 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 事件后,你可以使用事件监听器来处理这些事件。

当通过 Eloquent 执行批量更新或删除查询时,saved、updated、deleting 和 deleted 模型事件不会针对受影响的模型进行分发。这是因为在进行批量更新或删除操作时,模型实际上从未被检索出来。

使用闭包

你可以注册在各类模型事件分发时执行的闭包,而无需使用自定义事件类。通常,你应该在模型的 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 的“created”事件。
     */
    public function created(User $user): void
    {
        // ...
    }

    /**
     * 处理 User 的“updated”事件。
     */
    public function updated(User $user): void
    {
        // ...
    }

    /**
     * 处理 User 的“deleted”事件。
     */
    public function deleted(User $user): void
    {
        // ...
    }

    /**
     * 处理 User 的“restored”事件。
     */
    public function restored(User $user): void
    {
        // ...
    }

    /**
     * 处理 User 的“forceDeleted”事件。
     */
    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);
}

观察者还可以监听其他事件,例如 saving 和 retrieved。这些事件在事件相关文档中有详细说明。

观察者与数据库事务

当模型在数据库事务中创建时,你可能希望观察者仅在数据库事务提交后才执行其事件处理程序。你可以通过在观察者上实现 ShouldHandleEventsAfterCommit 接口来实现这一点。如果没有正在进行的数据库事务,事件处理程序将立即执行:

<?php

namespace App\Observers;

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

class UserObserver implements ShouldHandleEventsAfterCommit
{
    /**
     * 处理 User 的“created”事件。
     */
    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();