- 后端
- Web框架
- 微服务
- RPC框架
- 异步编程
【免费下载链接】hyperf
🚀 A coroutine framework that focuses on hyperspeed and flexibility. Building microservice or middleware with ease.
本篇文章以 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\QueryExecuted | Query 语句执行后 |
Hyperf\Database\Events\StatementPrepared | SQL 语句 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 日志监听器最常用的事件,构造时携带以下公开属性:
| 属性 | 类型 | 说明 |
|---|---|---|
$sql | string | 实际执行的 SQL 语句 |
$bindings | array | 查询参数绑定数组 |
$time | ?float | 查询执行耗时(毫秒) |
$connection | ConnectionInterface | 数据库连接实例 |
$connectionName | string | 连接名称,构造时自动从连接获取 |
$result | mixed | 查询结果,默认为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.
相关推荐
Hyperf 数据库模型事件机制详解:ORM 运行事件、钩子函数与观察者实战
Hyperf 数据库模型事件机制详解:ORM 运行事件、钩子函数与观察者实战 导读 本文围绕 Hyperf 的数据库(ORM)事件体系展开,聚焦 docs/zh
后端微服务Hyperf 数据库事件机制全解:ORM 运行事件、模型钩子与观察者模式实战
Hyperf 数据库事件机制全解:ORM 运行事件、模型钩子与观察者模式实战 导读 本篇技术指南围绕 Hyperf 数据库(Database)组件的事件体系展开
后端微服务Hyperf 数据库事件与模型事件指南:从 SQL 日志监听器到 ORM 生命周期钩子
Hyperf 数据库事件与模型事件指南:从 SQL 日志监听器到 ORM 生命周期钩子 本篇指南以 docs/en/db/event.md https://li
后端微服务
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考