0

0

Laravel观察者模式?模型观察者如何使用?

煙雲

煙雲

发布时间:2025-09-12 08:25:01

|

590人浏览过

|

来源于php中文网

原创

Laravel模型观察者用于解耦模型生命周期事件处理,通过创建观察者类、定义事件方法(如created、updating)并在AppServiceProvider中注册,实现对模型操作的响应。选择观察者适合处理与模型紧密相关的逻辑,而事件监听器更适合跨模块的解耦场景。saving在保存前执行,可修改数据或阻止操作;saved在保存后执行,宜用于发送通知等副作用。预事件中抛异常可回滚事务,后事件建议异步处理或捕获异常以保障主流程。

laravel观察者模式?模型观察者如何使用?

Laravel的观察者模式,特别是模型观察者(Model Observers),是一种优雅地处理模型生命周期事件的机制。它允许你将这些事件的监听逻辑从模型本身抽离出来,集中管理,从而让你的模型代码更简洁,职责更单一。简单来说,当你的Eloquent模型在创建、更新、删除等操作发生时,观察者会像一个忠实的“管家”一样,自动执行你预设好的操作。这对于维护数据一致性、触发副作用或者进行审计日志等场景,提供了一个非常清晰且低耦合的解决方案。

解决方案

使用Laravel模型观察者来监听和响应Eloquent模型生命周期事件,核心在于创建观察者类,定义事件方法,并将其注册到对应的模型上。

1. 创建观察者类

你可以通过Artisan命令快速生成一个观察者类。例如,如果你想为

User
模型创建一个观察者:

php artisan make:observer UserObserver --model=User

这个命令会在

app/Observers
目录下生成一个
UserObserver.php
文件,并预填充了一些常用的事件方法。

2. 定义事件方法

在生成的

UserObserver
类中,你可以定义多个方法来响应不同的模型事件。这些方法会自动接收受影响的模型实例作为参数。

以下是一些常用的事件方法及其作用:

  • retrieved(User $user)
    : 模型从数据库中获取后触发。
  • creating(User $user)
    : 模型首次保存前触发(
    create()
    方法)。
  • created(User $user)
    : 模型首次保存后触发。
  • updating(User $user)
    : 模型更新前触发(
    update()
    方法)。
  • updated(User $user)
    : 模型更新后触发。
  • saving(User $user)
    : 模型保存(创建或更新)前触发。
  • saved(User $user)
    : 模型保存(创建或更新)后触发。
  • deleting(User $user)
    : 模型删除前触发。
  • deleted(User $user)
    : 模型删除后触发。
  • restoring(User $user)
    : 软删除模型恢复前触发。
  • restored(User $user)
    : 软删除模型恢复后触发。

如果你在

creating
updating
deleting
saving
这些“ing”结尾的方法中返回
false
,Laravel会阻止该模型操作的执行。这在某些需要条件判断才能继续操作的场景下非常有用。

name} ({$user->email}) 已注册。");
        // Mail::to($user->email)->send(new WelcomeEmail($user));
    }

    /**
     * Handle the User "updating" event.
     */
    public function updating(User $user): bool
    {
        // 假设我们不允许用户将邮箱修改为特定域名
        if (str_ends_with($user->email, '@example.com') && $user->isDirty('email')) {
            Log::warning("用户 {$user->id} 尝试将邮箱修改为禁止的域名。");
            return false; // 阻止更新操作
        }
        return true;
    }

    /**
     * Handle the User "deleted" event.
     */
    public function deleted(User $user): void
    {
        // 当用户删除后,清理相关数据或通知管理员
        Log::info("用户 {$user->name} ({$user->id}) 已被删除。");
        // $user->posts()->delete(); // 删除用户所有帖子
    }

    /**
     * Handle the User "retrieved" event.
     */
    public function retrieved(User $user): void
    {
        // 可以在这里对模型进行一些初始化操作,比如设置一个非数据库字段
        $user->is_admin_cached = ($user->role === 'admin');
    }
}

3. 注册观察者

观察者类创建并定义好方法后,需要将其注册到对应的模型上。这通常在

App\Providers\AppServiceProvider
boot
方法中完成。

完成以上步骤后,每当

User
模型执行相应的生命周期操作时,
UserObserver
中定义的方法就会自动被调用。

模型观察者与事件监听器,我该如何选择?

这确实是个让人纠结的问题,毕竟两者都能在模型事件发生时执行逻辑。我个人在实际开发中,会根据业务逻辑的耦合度和事件的广度来做选择。

模型观察者(Model Observers) 更适合那些与特定模型紧密相关的业务逻辑。比如说,一个用户被创建后,你需要给这个用户发送欢迎邮件;一个订单状态更新后,需要同步库存。这些操作通常只关心当前模型本身的变化,并且逻辑相对集中。观察者将这些逻辑封装在一个类中,使得模型本身保持“瘦身”,职责单一,代码看起来也更整洁。在我看来,观察者就像是模型的“专属管家”,只负责管理这个模型自己的事情。它提供的事件钩子非常直观,直接对应模型的生命周期。

事件监听器(Event Listeners) 则更适用于解耦度更高跨多个模块或服务的业务场景。当一个事件发生时,可能有多个完全不相关的模块都需要响应,或者这个事件本身就是一个更抽象的“领域事件”。例如,一个“用户登录成功”的事件,可能需要记录登录日志、更新用户活跃时间、检查是否有未读通知,甚至触发一些第三方服务的调用。这些响应逻辑可能分散在不同的服务或组件中,通过事件分发器(Event Dispatcher)和监听器,可以实现非常松散的耦合。事件监听器就像是一个“广播站”,事件是广播内容,而监听器是收音机,不同的收音机可以根据自己的需求接收和处理广播。

我的选择倾向:

  • 如果逻辑是“当这个模型发生X时,这个模型自身需要做Y”,那我倾向于使用观察者。它简洁、直观,并且逻辑集中。
  • 如果逻辑是“当某个事件发生时(不一定是特定模型),多个不同的模块或服务需要做各自的事情”,那我更倾向于使用事件监听器。它提供了更强的解耦能力和扩展性。

有时候,两者甚至可以结合使用。比如,观察者在模型

created
事件中触发一个更通用的领域事件,然后由多个监听器来响应这个领域事件。关键在于理解它们的侧重点,并根据实际业务场景做出最合适的选择。

模型观察者中的事件钩子,我该用
saving
还是
saved

这是个非常实际的问题,我在写业务逻辑时也经常需要思考。

saving
saved
都涉及到模型的保存操作,但它们触发的时机和能做的事情有本质区别。理解这一点,能帮助你避免很多潜在的问题。

saving(Model $model)

这个钩子是在模型即将被保存到数据库之前触发的。无论是

create()
还是
update()
操作,只要是执行保存动作,
saving
都会被调用。

  • 特点:

    • 在数据库事务开始之前或事务内部(取决于Eloquent如何触发),但肯定在实际的
      INSERT
      UPDATE
      语句执行之前。
    • 你可以修改模型实例的属性。
      saving
      方法中对
      $model
      进行的任何修改,都会在随后的数据库操作中被持久化。这非常适合在数据入库前进行最后的清洗、格式化或默认值填充。
    • 你可以阻止保存操作。 如果
      saving
      方法返回
      false
      ,那么整个保存操作(包括后续的
      created
      updated
      事件)都会被取消。这对于实现条件性保存或复杂的业务规则校验非常有用。
  • 何时使用:

    美图AI开放平台
    美图AI开放平台

    美图推出的AI人脸图像处理平台

    下载
    • 在保存前对数据进行标准化处理(例如,将字符串转换为小写、移除多余空格)。
    • 在保存前生成唯一的标识符或填充默认值(如果这些值不适合在模型构造函数或
      creating
      中处理)。
    • 进行一些前置的业务逻辑校验,不符合条件就阻止保存。

saved(Model $model)

这个钩子是在模型已经成功保存到数据库之后触发的。这意味着

INSERT
UPDATE
语句已经执行完毕,并且数据库已经更新。

  • 特点:

    • 在数据库事务提交之后(通常如此,但具体行为取决于事务边界和事件触发机制)。 这意味着,如果你在
      saved
      中执行了另一个数据库操作,它可能不会与之前的模型保存操作处于同一个事务中,需要注意事务的原子性。
    • 你不应该在这里修改模型属性并期望它们被自动保存。 因为模型已经保存,如果你在这里修改属性,需要再次调用
      $model->save()
      才能持久化这些变更,这可能导致额外的数据库操作甚至循环调用。
    • 主要用于触发副作用。 比如发送通知、记录日志、更新缓存、触发其他服务或任务等。
  • 何时使用:

    • 发送邮件通知(例如,新用户注册成功后发送欢迎邮件)。
    • 更新相关的缓存。
    • 记录操作日志或审计信息。
    • 触发后台任务(例如,图片处理、数据同步到第三方系统)。
    • 需要访问模型保存后的完整状态(包括ID等)。

我的建议:

如果你的逻辑需要在数据入库进行修改或校验,并且有可能阻止操作,请使用

saving
或更具体的
creating
/
updating
。 如果你的逻辑需要在数据入库进行一些“事后处理”,且不影响当前保存操作的成功与否,请使用
saved
或更具体的
created
/
updated

理解这两个钩子的执行时机和能力边界,能让你更精确地控制模型行为,避免不必要的复杂性和潜在的bug。

观察者逻辑出错时,我该如何优雅地处理异常和回滚?

在观察者中处理异常和确保数据一致性,是构建健壮应用的关键。说实话,这块常常被新手忽略,直到生产环境出现奇怪的数据问题才追悔莫及。

1. 预事件(

creating
,
updating
,
deleting
,
saving
)中的异常处理:

这些“ing”结尾的事件方法,因为发生在数据库操作之前,所以它们提供了最好的机会来阻止一个可能导致数据不一致的操作。

  • 抛出异常: 如果在这些方法中检测到严重的业务逻辑错误或数据不合法,直接抛出一个异常是最好的方式。Laravel的Eloquent会捕获这些异常,并且默认会回滚当前正在进行的数据库事务(如果操作是在事务中执行的话)。

    public function creating(User $user): void
    {
        if (User::where('email', $user->email)->exists()) {
            // 抛出自定义异常,或者直接使用通用的异常
            throw new \InvalidArgumentException('该邮箱已被注册。');
        }
    }

    当这个异常被抛出时,

    User::create()
    $user->save()
    调用将会失败,并且不会有任何数据写入数据库。前端或API调用方可以捕获这个异常并返回相应的错误信息。

  • 返回

    false
    如前所述,返回
    false
    会阻止当前操作的继续执行,但不会抛出异常。这适用于一些“软失败”或需要静默阻止的场景。但请注意,返回
    false
    并不会自动回滚任何已经开始的事务,如果观察者之前已经执行了其他数据库操作,它们可能不会被回滚。因此,我个人更倾向于在需要明确失败时抛出异常。

2. 后事件(

created
,
updated
,
deleted
,
saved
)中的异常处理:

这是最需要小心的地方。因为这些事件是在模型已经成功保存到数据库之后触发的。如果在这里抛出异常,模型的数据库操作已经完成并提交。这意味着,即使你的观察者逻辑失败了,模型数据也已经写入了数据库。

  • 异步处理: 对于那些不影响模型核心业务流程,但又可能失败的副作用(如发送邮件、同步到第三方系统),我强烈建议将它们放入队列中异步处理。这样,即使队列任务失败,也不会影响主业务流程的成功。

    public function created(User $user): void
    {
        // 将发送欢迎邮件的任务推送到队列
        SendWelcomeEmail::dispatch($user)->onQueue('emails');
    }

    队列任务有重试机制,并且失败后可以记录日志,让你有时间去修复和处理。

  • 数据库事务的边界: 如果你在后事件中执行了额外的数据库操作,并且希望这些操作与模型的保存操作保持原子性,那么你需要明确地将它们包裹在一个数据库事务中。

    public function created(User $user): void
    {
        \DB::transaction(function () use ($user) {
            // 假设用户创建后,需要自动创建一个默认的个人资料记录
            $user->profile()->create([
                'bio' => '默认个人简介',
                'avatar' => 'default.jpg'
            ]);
    
            // 如果这里出现异常,只会回滚 profile 的创建,user 的创建不会被回滚
            // 如果你需要 user 也回滚,那么整个 user 创建的逻辑也需要包裹在事务中
        });
    }

    但更常见的做法是,如果后事件的逻辑是核心且必须与主模型操作原子化,那么应该将整个主模型操作及其所有相关的原子化逻辑都包裹在一个大的事务中。

  • 异常捕获与日志: 对于那些无法异步化,但又不能阻止主流程的后事件逻辑,至少要做好异常捕获和日志记录。

    public function updated(User $user): void
    {
        try {
            // 尝试同步用户数据到外部CRM系统
            $this->crmService->syncUser($user);
        } catch (\Exception $e) {
            // 记录错误,但不要重新抛出,以免影响主流程
            Log::error("同步用户 {$user->id} 到CRM失败: " . $e->getMessage());
            // 可以通知管理员
            // Mail::to('admin@example.com')->send(new AdminAlert($e));
        }
    }

    这种方式确保了即使副作用失败,主业务流程也能继续,但你需要有监控和告警机制来及时发现并处理这些失败。

总结一下,对于观察者中的异常处理,我的经验是:预事件抛异常,后事件做异步或细致的事务管理,并始终做好日志记录。 明确每个事件钩子的执行时机和事务上下文,是避免数据不一致和系统不稳定的关键。

相关专题

更多
php文件怎么打开
php文件怎么打开

打开php文件步骤:1、选择文本编辑器;2、在选择的文本编辑器中,创建一个新的文件,并将其保存为.php文件;3、在创建的PHP文件中,编写PHP代码;4、要在本地计算机上运行PHP文件,需要设置一个服务器环境;5、安装服务器环境后,需要将PHP文件放入服务器目录中;6、一旦将PHP文件放入服务器目录中,就可以通过浏览器来运行它。

2654

2023.09.01

php怎么取出数组的前几个元素
php怎么取出数组的前几个元素

取出php数组的前几个元素的方法有使用array_slice()函数、使用array_splice()函数、使用循环遍历、使用array_slice()函数和array_values()函数等。本专题为大家提供php数组相关的文章、下载、课程内容,供大家免费下载体验。

1658

2023.10.11

php反序列化失败怎么办
php反序列化失败怎么办

php反序列化失败的解决办法检查序列化数据。检查类定义、检查错误日志、更新PHP版本和应用安全措施等。本专题为大家提供php反序列化相关的文章、下载、课程内容,供大家免费下载体验。

1515

2023.10.11

php怎么连接mssql数据库
php怎么连接mssql数据库

连接方法:1、通过mssql_系列函数;2、通过sqlsrv_系列函数;3、通过odbc方式连接;4、通过PDO方式;5、通过COM方式连接。想了解php怎么连接mssql数据库的详细内容,可以访问下面的文章。

952

2023.10.23

php连接mssql数据库的方法
php连接mssql数据库的方法

php连接mssql数据库的方法有使用PHP的MSSQL扩展、使用PDO等。想了解更多php连接mssql数据库相关内容,可以阅读本专题下面的文章。

1418

2023.10.23

html怎么上传
html怎么上传

html通过使用HTML表单、JavaScript和PHP上传。更多关于html的问题详细请看本专题下面的文章。php中文网欢迎大家前来学习。

1234

2023.11.03

PHP出现乱码怎么解决
PHP出现乱码怎么解决

PHP出现乱码可以通过修改PHP文件头部的字符编码设置、检查PHP文件的编码格式、检查数据库连接设置和检查HTML页面的字符编码设置来解决。更多关于php乱码的问题详情请看本专题下面的文章。php中文网欢迎大家前来学习。

1468

2023.11.09

php文件怎么在手机上打开
php文件怎么在手机上打开

php文件在手机上打开需要在手机上搭建一个能够运行php的服务器环境,并将php文件上传到服务器上。再在手机上的浏览器中输入服务器的IP地址或域名,加上php文件的路径,即可打开php文件并查看其内容。更多关于php相关问题,详情请看本专题下面的文章。php中文网欢迎大家前来学习。

1306

2023.11.13

高德地图升级方法汇总
高德地图升级方法汇总

本专题整合了高德地图升级相关教程,阅读专题下面的文章了解更多详细内容。

72

2026.01.16

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Go语言实战之 GraphQL
Go语言实战之 GraphQL

共10课时 | 0.8万人学习

ThinkPHP6.x API接口--十天技能课堂
ThinkPHP6.x API接口--十天技能课堂

共14课时 | 1.1万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号