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

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. 注册观察者

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

AppProvidersAppServiceProvider

boot

方法中完成。

<?phpnamespace AppProviders;use AppModelsUser;use AppObserversUserObserver;use IlluminateSupportServiceProvider;class AppServiceProvider extends ServiceProvider{    /**     * Register any application services.     */    public function register(): void    {        //    }    /**     * Bootstrap any application services.     */    public function boot(): void    {        // 注册User模型的观察者        User::observe(UserObserver::class);        // 如果有其他模型,可以继续注册        // Post::observe(PostObserver::class);    }}

完成以上步骤后,每当

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

事件)都会被取消。这对于实现条件性保存或复杂的业务规则校验非常有用。

何时使用:

在保存前对数据进行标准化处理(例如,将字符串转换为小写、移除多余空格)。在保存前生成唯一的标识符或填充默认值(如果这些值不适合在模型构造函数或

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

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

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

以上就是Laravel观察者模式?模型观察者如何使用?的详细内容,更多请关注php中文网其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/198016.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2025年11月1日 19:31:19
下一篇 2025年11月1日 19:32:20

相关推荐

  • 如何使鼠标滚轮默认横向滚动水平列表?

    横向滚动列表的巧妙方法 问题: 如何使鼠标滚轮默认横向滚动列表?列表内容水平显示,通常需要按住 Shift 键才能横向滚动。 解决方案: 使用 CSS 变换将容器旋转 -90 度,并将子元素旋转 90 度。这样一来,容器将垂直显示,子元素将水平显示,而鼠标滚轮的默认滚动方向将变为水平。 代码示例: …

    2025年12月22日
    000
  • Vue3 如何实现图片自动切换效果?

    vue3 实现图片自动切换效果 对于你给出的网站,可以采用如下思路实现图片自动切换效果: 首先,创建两个容器,例如 标签,并为其设置 animation 类。 然后,在循环中迭代图片列表,为每个图片创建一个 标签并将其添加到容器中。 @@##@@ 使用 v-show 或 v-if 条件渲染图片,控制…

    2025年12月22日
    000
  • 如何让鼠标滚轮默认进行横向滚动?

    横向滚动鼠标滚轮的解决方法 想要让鼠标滚轮默认进行横向滚动,可以采用以下方法: 旋转容器和子元素 将容器元素旋转-90度,然后将子元素旋转90度。这种方法可以实现横向滚动,而无需使用 JavaScript 监听滚轮事件。 具体步骤如下: 在 CSS 中为容器元素设置 transform: rotat…

    2025年12月22日
    000
  • 五子棋机器人代码中如何简化重复的落子逻辑?

    五子棋机器人 问题:如何在代码中简化重复的部分? 答案: // 举例:简化机器人落子逻辑// 创建放置棋子的函数const placePiece = (x, y) => { boxs.value[x][y].place = 2; fourDetial = determineEquare3(4,…

    2025年12月22日
    000
  • 鼠标滚轮默认滚动方向怎么设置成水平?

    如何设置鼠标滚轮默认滚动方向为水平? 在处理水平显示列表时,需要按住 Shift 键才能水平滚动,这会造成不便。要让鼠标滚轮默认水平滚动,可尝试以下方法: 解决方案:旋转容器和子元素 将包含列表的容器旋转 -90 度: .container { transform: rotate(-90deg);}…

    2025年12月22日
    000
  • AI辅助前端开发:哪个工具最适合你?

    ai辅助前端开发:哪个工具最靠谱? 对于这个问题,并没有一个绝对靠谱的答案,选择合适的前端开发AI工具取决于个人需求和偏好。 以下是一些受欢迎的AI辅助前端开发工具,可以根据不同的需求进行尝试: 用于解决特定模块或代码片段: Kite(https://kite.com/)TabNine(https:…

    2025年12月22日
    000
  • Node.js爬取网页编码异常如何解决?

    node中解决使用request爬取网页编码异常的问题 在node环境中使用request爬取网页,响应的body编码异常,导致打印的结果无法正常显示。代码如下: const request = require(‘request’);function getGoods() { request(‘ht…

    2025年12月22日
    000
  • 图文混排中,如何让父元素仅被文字撑开,而不被图片撑开?

    图文混排中如何控制父元素的高度? 现有父容器(红色边框)包含两个子元素(黑色边框),如图所示,其中图片的高度较文字高。如何让父元素仅被文字撑开,而不被图片撑开? 解决方案: 绝对定位(absolute):将图片子元素设置为绝对定位,使其脱离文档流,不再参与父元素高度的计算。 .image { pos…

    2025年12月22日
    000
  • 防抖代码版本 1 和版本 2 执行结果不同的原因是什么?

    防抖代码的不同结果剖析 在提供的一段防抖代码中,开发者发现版本 1 和版本 2 执行结果不同,版本 1 防抖失败。 问题根源:递归 关键点在于递归。版本 1 中,if (notCalled &​​& immediate) result = func.apply(context, ar…

    2025年12月22日
    000
  • 使用 d3.js 在 SVG 中添加路径为何会显示异常?

    d3.js 添加路径到 svg 容器中显示异常的原因 在使用 d3.js 向 SVG 容器中添加路径元素时,如果出现路径无法正确显示的情况,可能有以下原因: 代码示例: function createPath(){ var svgContainer = $(“#svg_container”).get…

    好文分享 2025年12月22日
    000
  • Vue 项目中如何便捷地给 input 元素添加 focus 方法?

    便捷给input施加focus方法 在Vue项目中,经常需要给input元素加上focus方法,使其获得焦点并光标置于右侧。传统的做法是编写自定义方法并绑定到focus事件,这较为冗长。 为了简化这一操作,有以下三种便捷的方法: 1. 全局自定义指令 在main.js文件中添加以下指令: 立即学习“…

    好文分享 2025年12月22日
    000
  • 如何在 HTML 中禁用 Ctrl+滚轮缩放?

    如何在 html 中禁用 ctrl+滚轮缩放 在 HTML 中,您无法通过 resize 事件禁用 Ctrl+滚轮缩放,因为该事件与窗口大小调整有关。以下是如何使用 JavaScript 禁用缩放: PC 端(原生) document.addEventListener(‘mousewheel’, f…

    好文分享 2025年12月22日
    000
  • 前端开发神器:AI工具究竟能解决哪些难题?

    前端开发神器:ai工具推荐 前言 在前端开发过程中,AI工具可以帮助解决不少难题,但这究竟哪一类工具最为可靠?本篇文章将浅谈前端领域中,能够协助开发者的AI工具有哪些。 具体推荐 虽然没有100%可靠的AI工具,但根据个人经验,在以下场景下AI工具能够发挥不俗的功效: 立即学习“前端免费学习笔记(深…

    好文分享 2025年12月22日
    000
  • d3.js 向 SVG 容器添加路径后,为什么路径无法正确显示?

    d3.js 向 svg 容器添加路径后无法正确显示的解决方法 在使用 d3.js 将路径元素添加到 SVG 容器时,如果路径无法正确显示,可能是以下原因造成的: 参考代码: function createPath(){ var svgContainer = $(“#svg_container”).g…

    好文分享 2025年12月22日
    000
  • 如何让兄弟元素跟随最宽元素等宽?

    如何让兄弟元素跟随最宽元素等宽 问题: HTML 代码如下,需要让红色和灰色元素的宽度跟随绿色元素,保持一致: item1item2item3 现有的 CSS 样式: .item1 { background: red;}.item2 { background: gray;}.item3 { min-…

    2025年12月22日
    000
  • 如何实现邮件发送的新需求:前端和后端的职责分配?

    邮件发送的新需求 在现有的前端页面中,需要增加一个通过电子邮件发送页面的功能。在进行实现之前,让我们深入了解一下前端和后端的职责分配。 前端职责 前端负责创建页面布局,包括表格和图表。由于 HTML 电子邮件不支持 HTML5 特性、CSS 限制和 JavaScript,因此需要重新考虑页面的实现方…

    2025年12月22日
    000
  • Vue Element UI 和 Django 如何实现 HTML 富文本邮件?

    发送邮件,如何实现 html 富文本邮件? 在 Vue Element UI 和 Django 技术栈中,要实现 HTML 邮件功能需要协同前端和后端进行开发。 前端任务: 编写一个静态页面,包含 Element UI 表格和 Echarts 图表。将页面内容重写为兼容 HTML 邮件的格式(比如将…

    2025年12月22日
    000
  • 如何禁用 HTML 页面中的 Ctrl+鼠标滚轮缩放功能?

    如何在 html 中禁止使用 ctrl+鼠标滚轮进行缩放 在 HTML 页面中,使用 Ctrl+鼠标滚轮可以方便地进行页面缩放。但是,在某些情况下,您可能需要禁用此功能。以下是如何实现: 本机(PC 端) 对于原生 HTML 文档,使用以下 JavaScript 代码: document.addEv…

    2025年12月22日
    000
  • 前端开发如何利用 AI 工具提升效率?

    写前端代码时,有哪些靠谱的 ai 工具可以助力? 在编写前端 HTML、CSS 和 JS 代码的过程中,AI 工具可以提供有用的辅助,帮助开发者提高效率并减轻工作量。以下是一些值得考虑的靠谱 AI 工具: 解决模块或代码片段 Copilot:为开发人员提供实时代码建议,并可以自动生成代码片段。Tab…

    2025年12月22日
    000
  • 如何在 Vue.js 中便捷地将输入框焦点置于右侧?

    便捷给 input 施加 focus 方法 在 Vue.js 中,需要在输入框获取焦点时将光标置于右侧。以下几种方式可以快速便利地解决此问题: 自定义指令 // main.jsVue.directive(‘focus-right’, { inserted: function (el) { el.ad…

    2025年12月22日
    000

发表回复

登录后才能评论
关注微信