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

相关推荐

  • 如何用dom2img解决网页打印样式不显示的问题?

    用dom2img解决网页打印样式不显示的问题 想将网页以所见即打印的的效果呈现,需要采取一些措施,特别是在使用了bootstrap等大量采用外部css样式的框架时。 问题根源 在常规打印操作中,浏览器通常会忽略css样式等非必要的页面元素,导致打印出的结果与网页显示效果不一致。这是因为打印机制只识别…

    2025年12月24日
    800
  • Uniapp 中如何不拉伸不裁剪地展示图片?

    灵活展示图片:如何不拉伸不裁剪 在界面设计中,常常需要以原尺寸展示用户上传的图片。本文将介绍一种在 uniapp 框架中实现该功能的简单方法。 对于不同尺寸的图片,可以采用以下处理方式: 极端宽高比:撑满屏幕宽度或高度,再等比缩放居中。非极端宽高比:居中显示,若能撑满则撑满。 然而,如果需要不拉伸不…

    2025年12月24日
    400
  • 如何让小说网站控制台显示乱码,同时网页内容正常显示?

    如何在不影响用户界面的情况下实现控制台乱码? 当在小说网站上下载小说时,大家可能会遇到一个问题:网站上的文本在网页内正常显示,但是在控制台中却是乱码。如何实现此类操作,从而在不影响用户界面(UI)的情况下保持控制台乱码呢? 答案在于使用自定义字体。网站可以通过在服务器端配置自定义字体,并通过在客户端…

    2025年12月24日
    800
  • 如何在地图上轻松创建气泡信息框?

    地图上气泡信息框的巧妙生成 地图上气泡信息框是一种常用的交互功能,它简便易用,能够为用户提供额外信息。本文将探讨如何借助地图库的功能轻松创建这一功能。 利用地图库的原生功能 大多数地图库,如高德地图,都提供了现成的信息窗体和右键菜单功能。这些功能可以通过以下途径实现: 高德地图 JS API 参考文…

    2025年12月24日
    400
  • 如何使用 scroll-behavior 属性实现元素scrollLeft变化时的平滑动画?

    如何实现元素scrollleft变化时的平滑动画效果? 在许多网页应用中,滚动容器的水平滚动条(scrollleft)需要频繁使用。为了让滚动动作更加自然,你希望给scrollleft的变化添加动画效果。 解决方案:scroll-behavior 属性 要实现scrollleft变化时的平滑动画效果…

    2025年12月24日
    000
  • 如何为滚动元素添加平滑过渡,使滚动条滑动时更自然流畅?

    给滚动元素平滑过渡 如何在滚动条属性(scrollleft)发生改变时为元素添加平滑的过渡效果? 解决方案:scroll-behavior 属性 为滚动容器设置 scroll-behavior 属性可以实现平滑滚动。 html 代码: click the button to slide right!…

    2025年12月24日
    500
  • 如何选择元素个数不固定的指定类名子元素?

    灵活选择元素个数不固定的指定类名子元素 在网页布局中,有时需要选择特定类名的子元素,但这些元素的数量并不固定。例如,下面这段 html 代码中,activebar 和 item 元素的数量均不固定: *n *n 如果需要选择第一个 item元素,可以使用 css 选择器 :nth-child()。该…

    2025年12月24日
    200
  • 使用 SVG 如何实现自定义宽度、间距和半径的虚线边框?

    使用 svg 实现自定义虚线边框 如何实现一个具有自定义宽度、间距和半径的虚线边框是一个常见的前端开发问题。传统的解决方案通常涉及使用 border-image 引入切片图片,但是这种方法存在引入外部资源、性能低下的缺点。 为了避免上述问题,可以使用 svg(可缩放矢量图形)来创建纯代码实现。一种方…

    2025年12月24日
    100
  • Bootstrap 中如何让文字浮于阴影之上?

    文字浮于阴影之上 文中提到的代码片段中 元素中的文字被阴影元素 所遮挡,如何让文字显示在阴影之上? bootstrap v3和v5在处理此类问题方面存在差异。 解决方法 在bootstrap v5中,给 元素添加以下css样式: .banner-content { position: relativ…

    2025年12月24日
    000
  • 如何让“元素跟随文本高度,而不是撑高父容器?

    如何让 元素跟随文本高度,而不是撑高父容器 在页面布局中,经常遇到父容器高度被子元素撑开的问题。在图例所示的案例中,父容器被较高的图片撑开,而文本的高度没有被考虑。本问答将提供纯css解决方案,让图片跟随文本高度,确保父容器的高度不会被图片影响。 解决方法 为了解决这个问题,需要将图片从文档流中脱离…

    2025年12月24日
    000
  • 为什么 CSS mask 属性未请求指定图片?

    解决 css mask 属性未请求图片的问题 在使用 css mask 属性时,指定了图片地址,但网络面板显示未请求获取该图片,这可能是由于浏览器兼容性问题造成的。 问题 如下代码所示: 立即学习“前端免费学习笔记(深入)”; icon [data-icon=”cloud”] { –icon-cl…

    2025年12月24日
    200
  • 如何利用 CSS 选中激活标签并影响相邻元素的样式?

    如何利用 css 选中激活标签并影响相邻元素? 为了实现激活标签影响相邻元素的样式需求,可以通过 :has 选择器来实现。以下是如何具体操作: 对于激活标签相邻后的元素,可以在 css 中使用以下代码进行设置: li:has(+li.active) { border-radius: 0 0 10px…

    2025年12月24日
    100
  • 如何模拟Windows 10 设置界面中的鼠标悬浮放大效果?

    win10设置界面的鼠标移动显示周边的样式(探照灯效果)的实现方式 在windows设置界面的鼠标悬浮效果中,光标周围会显示一个放大区域。在前端开发中,可以通过多种方式实现类似的效果。 使用css 使用css的transform和box-shadow属性。通过将transform: scale(1.…

    2025年12月24日
    200
  • 为什么我的 Safari 自定义样式表在百度页面上失效了?

    为什么在 Safari 中自定义样式表未能正常工作? 在 Safari 的偏好设置中设置自定义样式表后,您对其进行测试却发现效果不同。在您自己的网页中,样式有效,而在百度页面中却失效。 造成这种情况的原因是,第一个访问的项目使用了文件协议,可以访问本地目录中的图片文件。而第二个访问的百度使用了 ht…

    2025年12月24日
    000
  • Bootstrap 5:如何将文字置于阴影之上?

    文字重叠阴影 在 bootstrap 5 中,将文字置于阴影之上时遇到了困难。在 bootstrap 3 中,此问题并不存在,但升级到 bootstrap 5 后却无法实现。 解决方案 为了解决这个问题,需要给 元素添加以下样式: .banner-content { position: relati…

    2025年12月24日
    400
  • 如何用前端实现 Windows 10 设置界面的鼠标移动探照灯效果?

    如何在前端实现 Windows 10 设置界面中的鼠标移动探照灯效果 想要在前端开发中实现 Windows 10 设置界面中类似的鼠标移动探照灯效果,可以通过以下途径: CSS 解决方案 DEMO 1: Windows 10 网格悬停效果:https://codepen.io/tr4553r7/pe…

    2025年12月24日
    000
  • 使用CSS mask属性指定图片URL时,为什么浏览器无法加载图片?

    css mask属性未能加载图片的解决方法 使用css mask属性指定图片url时,如示例中所示: mask: url(“https://api.iconify.design/mdi:apple-icloud.svg”) center / contain no-repeat; 但是,在网络面板中却…

    2025年12月24日
    000
  • Bootstrap 5 如何将文字置于阴影上方?

    如何在 bootstrap 5 中让文字位于阴影上方? 在将网站从 bootstrap 3 升级到 bootstrap 5 后,用户遇到一个问题:文字内容无法像以前那样置于阴影层之上。 解决方案: 为了将文字置于阴影层上方,需要给 banner-content 元素添加以下 css 样式: .ban…

    2025年12月24日
    100
  • 如何用CSS Paint API为网页元素添加时尚的斑马线边框?

    为元素添加时尚的斑马线边框 在网页设计中,有时我们需要添加时尚的边框来提升元素的视觉效果。其中,斑马线边框是一种既醒目又别致的设计元素。 实现斜向斑马线边框 要实现斜向斑马线间隔圆环,我们可以使用css paint api。该api提供了强大的功能,可以让我们在元素上绘制复杂的图形。 立即学习“前端…

    2025年12月24日
    000
  • 图片如何不撑高父容器?

    如何让图片不撑高父容器? 当父容器包含不同高度的子元素时,父容器的高度通常会被最高元素撑开。如果你希望父容器的高度由文本内容撑开,避免图片对其产生影响,可以通过以下 css 解决方法: 绝对定位元素: .child-image { position: absolute; top: 0; left: …

    2025年12月24日
    000

发表回复

登录后才能评论
关注微信