☰
Hyperf 数据库与模型事件机制实战:从 SQL 监听、钩子函数到观察者模式
2026/10/9 1:04:55 网站建设 项目流程
  • 后端
  • Web框架
  • 微服务
  • RPC框架
  • 异步编程

【免费下载链接】hyperf

🚀 A coroutine framework that focuses on hyperspeed and flexibility. Building microservice or middleware with ease.

项目地址:https://gitcode.com/hyperf/hyperf
点击查看免费下载

本篇文章以 Hyperf 数据库组件(hyperf/database)为核心,完整梳理 ORM 运行期间触发的事件体系:连接级 SQL 与事务事件、模型生命周期事件(钩子函数与事件监听两种形态),以及基于hyperf/model-listener组件的观察者模式。读完你将掌握如何编写 SQL 执行监听器记录慢查询、在模型saving/created等生命周期中注入业务逻辑,以及如何用注解式 Observer 优雅解耦模型变更处理。

事件体系的整体设计

Hyperf 的数据库与模型事件建立在 PSR-14(psr/event-dispatcher) 接口之上,默认由hyperf/event组件提供事件调度能力。这意味着所有监听器都遵循统一的事件分发契约:监听器实现Hyperf\Event\Contract\ListenerInterface,通过listen()声明关心的事件类,在process()中处理事件。

在仓库源码中可以看到两类事件各自独立:

  • 运行事件(连接级):位于src/database/src/Events/目录,覆盖 SQL 执行、预处理与事务生命周期,属于数据库连接层面的全局事件,与具体模型无关。
  • 模型事件(模型级):位于src/database/src/Model/Events/目录,每个事件对应一个模型生命周期阶段,事件对象内部持有触发它的模型实例。

事件触发与分发的核心实现位于src/event/src/EventDispatcher.php与src/event/src/ListenerProvider.php,配合src/event/src/Annotation/Listener.php注解,Hyperf 在应用启动时通过注解收集自动把带#[Listener]的类注册进事件调度器。

运行事件:连接层面的 SQL 与事务钩子

在 ORM 运行期间,数据库连接会触发以下事件,你可以监听这些事件满足日志、监控、审计等需求:

事件描述
Hyperf\Database\Events\QueryExecutedQuery 语句执行后
Hyperf\Database\Events\StatementPreparedSQL 语句 prepared 后
Hyperf\Database\Events\TransactionBeginning事务开启后
Hyperf\Database\Events\TransactionCommitted事务提交后
Hyperf\Database\Events\TransactionRolledBack事务回滚后

从源码结构看,这五个事件分为两类实现:

  • QueryExecuted与StatementPrepared是独立的普通类,直接携带执行细节;
  • 三个事务事件继承自ConnectionEvent抽象基类(见src/database/src/Events/ConnectionEvent.php),构造时注入ConnectionInterface实例并自动提取connectionName,用于区分不同连接上的事务行为。

QueryExecuted 事件的数据结构

QueryExecuted(见 src/database/src/Events/QueryExecuted.php)是编写 SQL 日志监听器最常用的事件,构造时携带以下公开属性:

属性类型说明
$sqlstring实际执行的 SQL 语句
$bindingsarray查询参数绑定数组
$time?float查询执行耗时(毫秒)
$connectionConnectionInterface数据库连接实例
$connectionNamestring连接名称,构造时自动从连接获取
$resultmixed查询结果,默认为null

StatementPrepared(见 src/database/src/Events/StatementPrepared.php)则在 PDO 预处理完成后触发,公开属性为$connection与$statement(PDOStatement实例),适合在语句真正执行前对 PDOStatement 做额外配置(如绑定自定义游标、设置 fetch 模式等)。

实战:实现一个 SQL 执行监听器

根据上述 ORM 运行事件,接下来实现一个记录 SQL 的监听器,让每次执行 SQL 时把语句记录下来并输出到日志。首先定义DbQueryExecutedListener,实现Hyperf\Event\Contract\ListenerInterface接口并给类加上Hyperf\Event\Annotation\Listener注解,这样 Hyperf 会在应用启动时自动把该监听器注册到事件调度器中,并在事件触发时执行监听逻辑,示例代码如下:

<?php declare(strict_types=1); namespace App\Listener; use Hyperf\Database\Events\QueryExecuted; use Hyperf\Event\Annotation\Listener; use Hyperf\Event\Contract\ListenerInterface; use Hyperf\Logger\LoggerFactory; use Hyperf\Collection\Arr; use Hyperf\Stringable\Str; use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; #[Listener] class DbQueryExecutedListener implements ListenerInterface { private LoggerInterface $logger; public function __construct(ContainerInterface $container) { // 输出到对应名为 sql 的日志 name,如不存在则需自行添加配置 // 这里的 sql 日志 name 不是必须的,只是表达可以将 SQL 执行日志与普通日志区分开 $this->logger = $container->get(LoggerFactory::class)->get('sql'); } public function listen(): array { return [ QueryExecuted::class, ]; } /** * @param QueryExecuted $event */ public function process(object $event) { if ($event instanceof QueryExecuted) { $sql = $event->sql; if (! Arr::isAssoc($event->bindings)) { foreach ($event->bindings as $key => $value) { $sql = Str::replaceFirst('?', "'{$value}'", $sql); } } $this->logger->info(sprintf('[%s] %s', $event->time, $sql)); } } }

几点关键说明:

  • listen()返回数组声明监听的事件类,一个监听器可同时监听多个事件;#[Listener]注解中的priority参数(默认ListenerData::DEFAULT_PRIORITY,见 src/event/src/Annotation/Listener.php)可调整多个监听器之间的执行优先级。
  • process()中做了instanceof二次校验,因为一个监听器可能监听多种事件,需要先确认事件类型再访问对应属性。
  • 绑定参数回填:示例使用Hyperf\Collection\Arr::isAssoc()判断绑定是否为关联数组,非关联数组时用Str::replaceFirst把 SQL 中的?占位符依次替换为实际值,输出更接近真实执行语句;由于time是毫秒耗时,这也能帮助定位慢查询。
  • 日志通道:通过LoggerFactory->get('sql')输出到名为sql的独立日志通道,若config/autoload/logger.php中不存在该通道,需要自行补充相应配置;你也可以改用Hyperf\Utils\ApplicationContext或直接注入容器中已有的默认日志器。

模型事件:钩子函数与事件监听

模型事件与Eloquent ORM不太一致:Eloquent ORM使用Observer监听模型事件,而 Hyperf 提供钩子函数和事件监听两种形式来处理对应的事件,此外还可以基于hyperf/model-listener使用观察者模式(详见下文)。

钩子函数(模型内方法)

事件名触发时机是否阻断备注
booting模型首次加载前否进程生命周期中只会触发一次
booted模型首次加载后否进程生命周期中只会触发一次
retrieved填充数据后否每当模型从 DB 或缓存查询出来后触发
creating数据创建时是
created数据创建后否
updating数据更新时是
updated数据更新后否
saving数据创建或更新时是
saved数据创建或更新后否
restoring软删除数据恢复时是
restored软删除数据恢复后否
deleting数据删除时是
deleted数据删除后否
forceDeleting数据强制删除时是
forceDeleted数据强制删除后否

“是否阻断”一列的含义是:在creating、updating、saving、deleting、restoring、forceDeleting等“进行时”事件中,如果钩子函数返回false(或事件被标记停止传播),对应写入操作会被中断;而created、updated、saved、deleted等“完成后”事件只用于观察结果,无法回退操作。

针对某个模型的事件使用十分简单,只需要在模型中增加对应的方法即可。例如下方保存数据时,触发saving事件,主动覆写created_at字段:

<?php declare(strict_types=1); namespace App\Models; use Hyperf\Database\Model\Events\Saving; /** * @property $id * @property $name * @property $gender * @property $created_at * @property $updated_at */ class User extends Model { /** * The table associated with the model. * * @var string */ protected $table = 'user'; /** * The attributes that are mass assignable. * * @var array */ protected $fillable = ['id', 'name', 'gender', 'created_at', 'updated_at']; protected $casts = ['id' => 'integer', 'gender' => 'integer']; public function saving(Saving $event) { $this->setCreatedAt('2019-01-01'); } }

钩子函数的底层机制可以在 src/database/src/Model/Events/Event.php 中找到:每个模型事件类都继承抽象基类Event(同时实现 PSR-14 的StoppableEventInterface),构造函数接收Model $model与事件方法名,handle()方法会检查模型上是否存在同名方法(方法名由事件类名自动推导,如Saving事件对应saving()方法),存在则调用$model->{$method}($this)并把事件自身作为参数传入。也就是说,模型内的钩子函数本质上也是由事件驱动的。

事件监听(全局 Listener)

当需要监听所有模型的事件时,可以很方便地自定义对应的Listener。比如下方模型缓存的监听器,当模型修改和删除后,会删除对应缓存(该监听器正是hyperf/model-cache组件的真实实现,见 src/model-cache/src/Listener/DeleteCacheListener.php):

<?php declare(strict_types=1); namespace Hyperf\ModelCache\Listener; use Hyperf\Database\Model\Events\Deleted; use Hyperf\Database\Model\Events\Event; use Hyperf\Database\Model\Events\Saved; use Hyperf\Event\Annotation\Listener; use Hyperf\Event\Contract\ListenerInterface; use Hyperf\ModelCache\CacheableInterface; #[Listener] class DeleteCacheListener implements ListenerInterface { public function listen(): array { return [ Deleted::class, Saved::class, ]; } public function process(object $event) { if ($event instanceof Event) { $model = $event->getModel(); if ($model instanceof CacheableInterface) { $model->deleteCache(); } } } }

与“钩子函数”绑定在单个模型类上不同,事件监听是全局的:监听Saved::class会收到所有模型的保存事件。因此这类监听器通常通过$event->getModel()拿到模型实例后,再用接口判断(如CacheableInterface)或instanceof过滤,只处理自己关心的模型,这也是示例中先校验CacheableInterface的原因。

值得留意的是Deleted与Saved的组合:删除操作会触发deleted事件,而保存(创建 + 更新)会触发saved事件,二者合起来覆盖了“数据发生变化”的绝大多数场景,因此缓存失效监听器只需监听这两个事件即可保持缓存一致性。

观察者:基于注解的模型监听

得益于hyperf/model-listener组件(源码位于 src/model-listener),我们也可以使用Observer(观察者)来监听模型事件。通过Hyperf\ModelListener\Annotation\ModelListener注解,可以很方便地定义一个观察者,示例代码如下:

<?php use Hyperf\ModelListener\Annotation\ModelListener; use App\Model\User; use Hyperf\Database\Model\Events\Creating; use Hyperf\Database\Model\Events\Created; /** * 定义一个 UserObserver 观察者,监听 User 模型的事件. * 也可以监听多个模型,只需要在 models 属性中传入多个模型即可 * 需要注意,此类将会被自动注册到容器中成为单例 */ #[ModelListener(models: [ User::class ])] class UserObserver { public function creating(Creating $event) { $user = $event->getModel(); // 创建用户时触发 } public function created(Created $event) { $user = $event->getModel(); // 用户创建完成后触发 } //... 省略其他事件 }

观察者的使用要点:

  • models属性声明观察对象:可以传入多个模型类,例如#[ModelListener(models: [User::class, Order::class])],一个观察者同时服务多个模型。
  • 方法命名与事件对应:观察者内部的方法名与事件同名(如creating、created、updating、deleted等),方法参数接收对应的事件对象,通过$event->getModel()获取触发事件的模型实例。
  • 自动注册为单例:被#[ModelListener]注解标记的类会自动注册到容器中成为单例,因此观察者内部不宜保存跨请求的有状态数据。
  • 底层收集机制:从 src/model-listener/src/Annotation/ModelListener.php 的源码可以看到,注解在collectClass()阶段会遍历models参数,通过ListenerCollector::register($model, $className)把观察者注册到对应模型的监听集合中,应用启动时由组件的ConfigProvider完成装配,无需手动编写注册代码。

三种模型事件处理方式如何选择

方式作用范围适用场景实现成本
钩子函数单个模型类与某个模型强绑定的字段默认值、数据清洗、校验拦截最低,在模型内加一个方法即可
事件监听(Listener)全局所有模型缓存失效、审计日志、数据同步等跨模型横切逻辑中,需实现ListenerInterface并加#[Listener]
观察者(Observer)指定的一个或多个模型按模型聚合多个事件处理逻辑,代码组织更清晰中,加#[ModelListener]注解并定义同名方法

三者可以共存:钩子函数适合“模型自身关心的逻辑”,事件监听适合“与模型无关的系统级逻辑”,观察者则适合“围绕某个模型的多个事件做集中编排”。实际项目中常见组合是——观察者负责业务侧事件处理(如创建用户后发消息),事件监听负责基础设施侧逻辑(如删除模型缓存)。

相关资源

  • 连接级运行事件源码:src/database/src/Events
  • 模型级事件源码:src/database/src/Model/Events
  • 事件调度器与监听器接口:src/event/src
  • 观察者组件源码:src/model-listener
  • 模型缓存监听器实战示例:src/model-cache/src/Listener/DeleteCacheListener.php
  • 组件测试用例:src/database/tests与src/event/tests目录中覆盖了事件类字段与分发行为的单元测试,可作为编写监听器时的参考。
  • 后端
  • Web框架
  • 微服务
  • RPC框架
  • 异步编程

【免费下载链接】hyperf

🚀 A coroutine framework that focuses on hyperspeed and flexibility. Building microservice or middleware with ease.

项目地址:https://gitcode.com/hyperf/hyperf
点击查看免费下载

相关推荐

上一篇:PatreonDownloader:一站式Patreon内容下载解决方案
下一篇:3步轻松解密RPG Maker MV资源:前端素材提取终极指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询