引言:为什么需要模型事件与观察者模式?
在复杂的业务系统中,我们经常需要在数据创建、更新、删除等操作前后执行特定逻辑。传统做法是在控制器中堆叠代码,导致业务逻辑分散、难以维护。ThinkPHP 8.2 提供了完善的模型事件系统,结合观察者模式,能够实现业务逻辑的解耦和复用。本文将通过构建一个用户行为追踪系统,深入讲解这一技术的实战应用。
一、环境准备与项目初始化
确保已安装ThinkPHP 8.2+版本,创建新项目或使用现有项目。我们以用户行为日志记录为例,需要创建以下数据表:
1.1 用户表 (users)
CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL,
`email` varchar(100) NOT NULL,
`login_count` int(11) DEFAULT 0,
`last_login_time` datetime DEFAULT NULL,
`status` tinyint(1) DEFAULT 1,
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
1.2 行为日志表 (user_actions)
CREATE TABLE `user_actions` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL,
`action_type` varchar(50) NOT NULL COMMENT '操作类型:login,update,delete等',
`action_data` json DEFAULT NULL COMMENT '操作数据',
`ip_address` varchar(45) DEFAULT NULL,
`user_agent` text,
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_action_type` (`action_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
二、模型事件基础配置
2.1 创建基础模型
首先创建User模型,定义模型事件监听:
<?php
// app/model/User.php
namespace appmodel;
use thinkModel;
class User extends Model
{
// 定义模型对应表名
protected $table = 'users';
// 自动写入时间戳
protected $autoWriteTimestamp = true;
protected $createTime = 'created_at';
protected $updateTime = 'updated_at';
// 定义模型事件
protected static function init()
{
// 注册模型事件
self::event('before_insert', function ($user) {
// 插入前逻辑
if (empty($user->username)) {
throw new Exception('用户名不能为空');
}
});
self::event('after_insert', function ($user) {
// 记录用户创建行为
ActionLog::record($user->id, 'user_create', [
'username' => $user->username,
'email' => $user->email
]);
});
self::event('before_update', function ($user) {
// 更新前记录旧数据
$user->oldData = $user->getOrigin();
});
self::event('after_update', function ($user) {
// 记录更新行为
$changes = [];
foreach ($user->getChangedData() as $field => $value) {
$changes[$field] = [
'old' => $user->oldData[$field] ?? null,
'new' => $value
];
}
ActionLog::record($user->id, 'user_update', $changes);
});
self::event('after_delete', function ($user) {
// 记录删除行为
ActionLog::record($user->id, 'user_delete', [
'username' => $user->username,
'deleted_at' => date('Y-m-d H:i:s')
]);
});
}
// 登录成功后的处理
public function onLoginSuccess($request)
{
// 增加登录次数
$this->setInc('login_count');
$this->save(['last_login_time' => date('Y-m-d H:i:s')]);
// 记录登录行为
ActionLog::record($this->id, 'user_login', [
'ip' => $request->ip(),
'user_agent' => $request->header('user-agent'),
'login_time' => date('Y-m-d H:i:s')
]);
}
}
三、高级应用:观察者模式实现
对于更复杂的业务逻辑,建议使用观察者模式,将事件处理逻辑从模型中分离。
3.1 创建用户观察者
<?php
// app/observer/UserObserver.php
namespace appobserver;
use appmodelUser;
use appmodelActionLog;
use thinkRequest;
class UserObserver
{
// 监听用户登录事件
public function onLogin(User $user)
{
$request = app(Request::class);
// 更新登录信息
$user->login_count += 1;
$user->last_login_time = date('Y-m-d H:i:s');
$user->save();
// 记录登录日志
ActionLog::create([
'user_id' => $user->id,
'action_type' => 'login',
'action_data' => json_encode([
'ip_address' => $request->ip(),
'user_agent' => substr($request->header('user-agent'), 0, 500),
'login_time' => date('Y-m-d H:i:s')
], JSON_UNESCAPED_UNICODE),
'ip_address' => $request->ip()
]);
// 触发其他相关事件(如发送登录通知)
event('user_logged_in', $user);
}
// 监听用户注册事件
public function onRegister(User $user)
{
// 发送欢迎邮件
$this->sendWelcomeEmail($user);
// 初始化用户资料
$this->initUserProfile($user);
// 记录注册行为
ActionLog::record($user->id, 'register', [
'register_source' => 'web',
'register_time' => date('Y-m-d H:i:s')
]);
}
// 监听用户信息更新
public function onUpdate(User $user, array $oldData)
{
$changes = [];
$changedData = $user->getChangedData();
foreach ($changedData as $field => $newValue) {
$oldValue = $oldData[$field] ?? null;
if ($oldValue != $newValue) {
$changes[$field] = [
'from' => $oldValue,
'to' => $newValue
];
// 特殊字段处理
if ($field === 'email') {
$this->sendEmailChangeNotification($user, $oldValue, $newValue);
}
}
}
if (!empty($changes)) {
ActionLog::record($user->id, 'profile_update', $changes);
}
}
private function sendWelcomeEmail(User $user)
{
// 发送欢迎邮件逻辑
// 这里可以使用ThinkPHP的邮件驱动
}
private function initUserProfile(User $user)
{
// 初始化用户个人资料
}
private function sendEmailChangeNotification(User $user, $oldEmail, $newEmail)
{
// 发送邮箱变更通知
}
}
3.2 注册观察者
<?php
// app/provider.php
return [
// 注册观察者
'thinkmodelobserver' => [
'user' => appobserverUserObserver::class,
],
// 绑定模型事件
'bind' => [
'UserLogin' => appeventUserLogin::class,
'UserRegister' => appeventUserRegister::class,
],
];
3.3 在模型中绑定观察者
<?php
// app/model/User.php 更新部分
class User extends Model
{
// 指定观察者
protected $observerClass = appobserverUserObserver::class;
// 或者使用动态绑定
public static function init()
{
// 绑定观察者方法到模型事件
self::observe(appobserverUserObserver::class);
// 自定义事件绑定
self::event('after_login', [appobserverUserObserver::class, 'onLogin']);
self::event('after_register', [appobserverUserObserver::class, 'onRegister']);
}
}
四、行为日志模型与辅助方法
4.1 创建行为日志模型
<?php
// app/model/ActionLog.php
namespace appmodel;
use thinkModel;
class ActionLog extends Model
{
protected $table = 'user_actions';
protected $autoWriteTimestamp = true;
protected $createTime = 'created_at';
protected $updateTime = false;
// JSON字段自动转换
protected $json = ['action_data'];
// 定义操作类型常量
const TYPE_LOGIN = 'login';
const TYPE_REGISTER = 'register';
const TYPE_UPDATE = 'update';
const TYPE_DELETE = 'delete';
const TYPE_CREATE = 'create';
/**
* 记录用户行为
* @param int $userId 用户ID
* @param string $actionType 操作类型
* @param array $data 操作数据
* @param string|null $ip IP地址
* @return ActionLog
*/
public static function record($userId, $actionType, $data = [], $ip = null)
{
$request = app('request');
return self::create([
'user_id' => $userId,
'action_type' => $actionType,
'action_data' => $data,
'ip_address' => $ip ?: $request->ip(),
'user_agent' => $request->header('user-agent', '')
]);
}
/**
* 获取用户最近的行为记录
* @param int $userId 用户ID
* @param int $limit 限制条数
* @return thinkCollection
*/
public static function getUserRecentActions($userId, $limit = 20)
{
return self::where('user_id', $userId)
->order('created_at', 'desc')
->limit($limit)
->select();
}
/**
* 搜索行为日志
* @param array $conditions 搜索条件
* @param int $page 页码
* @param int $pageSize 每页条数
* @return thinkPaginator
*/
public static function search($conditions = [], $page = 1, $pageSize = 15)
{
$query = self::with(['user' => function($query) {
$query->field('id,username,email');
}]);
if (!empty($conditions['user_id'])) {
$query->where('user_id', $conditions['user_id']);
}
if (!empty($conditions['action_type'])) {
$query->where('action_type', $conditions['action_type']);
}
if (!empty($conditions['start_date'])) {
$query->where('created_at', '>=', $conditions['start_date']);
}
if (!empty($conditions['end_date'])) {
$query->where('created_at', 'whereLike('action_data', '%' . $conditions['keyword'] . '%');
}
return $query->order('created_at', 'desc')
->paginate([
'list_rows' => $pageSize,
'page' => $page
]);
}
// 定义用户关联
public function user()
{
return $this->belongsTo(User::class, 'user_id');
}
}
五、控制器中的使用示例
5.1 用户控制器示例
<?php
// app/controller/UserController.php
namespace appcontroller;
use appBaseController;
use appmodelUser;
use appmodelActionLog;
use thinkfacadeEvent;
class UserController extends BaseController
{
/**
* 用户登录
*/
public function login()
{
$username = $this->request->param('username');
$password = $this->request->param('password');
$user = User::where('username', $username)->find();
if (!$user || !password_verify($password, $user->password)) {
return json(['code' => 401, 'msg' => '用户名或密码错误']);
}
// 触发登录事件(观察者模式)
Event::trigger('UserLogin', $user);
// 或者直接调用模型方法
$user->onLoginSuccess($this->request);
// 生成token等操作...
return json([
'code' => 200,
'msg' => '登录成功',
'data' => [
'user' => $user->hidden(['password']),
'token' => $token
]
]);
}
/**
* 更新用户信息
*/
public function updateProfile()
{
$userId = $this->request->user_id; // 假设从JWT中获取
$data = $this->request->only(['email', 'nickname', 'avatar']);
$user = User::find($userId);
if (!$user) {
return json(['code' => 404, 'msg' => '用户不存在']);
}
// 保存时会自动触发模型事件
$user->save($data);
return json(['code' => 200, 'msg' => '更新成功']);
}
/**
* 获取用户行为日志
*/
public function actionLogs()
{
$userId = $this->request->user_id;
$page = $this->request->param('page', 1);
$pageSize = $this->request->param('page_size', 15);
$logs = ActionLog::where('user_id', $userId)
->order('created_at', 'desc')
->paginate([
'list_rows' => $pageSize,
'page' => $page
]);
return json([
'code' => 200,
'data' => $logs
]);
}
/**
* 管理员查看行为日志
*/
public function adminActionLogs()
{
// 验证管理员权限...
$conditions = [
'user_id' => $this->request->param('user_id'),
'action_type' => $this->request->param('action_type'),
'start_date' => $this->request->param('start_date'),
'end_date' => $this->request->param('end_date'),
'keyword' => $this->request->param('keyword')
];
$page = $this->request->param('page', 1);
$pageSize = $this->request->param('page_size', 15);
$result = ActionLog::search($conditions, $page, $pageSize);
return json([
'code' => 200,
'data' => $result
]);
}
}
六、性能优化与最佳实践
6.1 异步处理耗时操作
对于发送邮件、推送通知等耗时操作,建议使用队列异步处理:
<?php
// 在观察者中使用队列
public function onRegister(User $user)
{
// 立即执行的操作
ActionLog::record($user->id, 'register', [
'register_source' => 'web'
]);
// 异步执行耗时操作
thinkQueue::push(appjobSendWelcomeEmail::class, [
'user_id' => $user->id
]);
thinkQueue::push(appjobInitUserProfile::class, [
'user_id' => $user->id
]);
}
6.2 批量操作的事件处理
<?php
// 批量更新时的事件处理
public function batchUpdateStatus($userIds, $status)
{
// 开始事务
Db::startTrans();
try {
// 获取旧数据
$oldUsers = User::whereIn('id', $userIds)->select();
// 批量更新
User::whereIn('id', $userIds)->update(['status' => $status]);
// 手动记录批量操作日志
foreach ($oldUsers as $user) {
ActionLog::record($user->id, 'batch_update', [
'field' => 'status',
'old_value' => $user->status,
'new_value' => $status,
'batch_id' => uniqid()
]);
}
Db::commit();
return true;
} catch (Exception $e) {
Db::rollback();
throw $e;
}
}
6.3 事件监听器的禁用与启用
<?php
// 临时禁用模型事件
$user = User::withoutEvents(function () use ($userId) {
return User::find($userId);
});
// 或者
$user = User::find($userId);
$user->withEvents(false)->save($data);
// 启用特定事件
$user->withEvents(['after_update'])->save($data);
七、测试与调试
7.1 单元测试示例
<?php
// tests/UserObserverTest.php
namespace tests;
use PHPUnitFrameworkTestCase;
use appmodelUser;
use appobserverUserObserver;
use thinkfacadeDb;
class UserObserverTest extends TestCase
{
protected function setUp(): void
{
// 清空测试数据
Db::name('users')->delete(true);
Db::name('user_actions')->delete(true);
}
public function testLoginEvent()
{
// 创建测试用户
$user = User::create([
'username' => 'testuser',
'email' => 'test@example.com',
'password' => password_hash('123456', PASSWORD_DEFAULT)
]);
$observer = new UserObserver();
$observer->onLogin($user);
// 验证登录次数增加
$user = User::find($user->id);
$this->assertEquals(1, $user->login_count);
// 验证行为日志记录
$log = Db::name('user_actions')
->where('user_id', $user->id)
->where('action_type', 'login')
->find();
$this->assertNotEmpty($log);
}
public function testUpdateEvent()
{
$user = User::create([
'username' => 'updateuser',
'email' => 'old@example.com'
]);
$oldData = $user->toArray();
$user->email = 'new@example.com';
$user->save();
// 验证更新日志
$log = Db::name('user_actions')
->where('user_id', $user->id)
->where('action_type', 'profile_update')
->order('id', 'desc')
->find();
$this->assertNotEmpty($log);
$actionData = json_decode($log['action_data'], true);
$this->assertEquals('old@example.com', $actionData['email']['from']);
$this->assertEquals('new@example.com', $actionData['email']['to']);
}
}
总结
通过本文的实战讲解,我们深入探讨了ThinkPHP 8.2中模型事件与观察者模式的高级应用。构建的用户行为追踪系统展示了如何:
- 利用模型事件实现业务逻辑的自动触发
- 通过观察者模式实现关注点分离
- 构建可扩展的行为日志系统
- 实现异步处理和性能优化
- 编写可测试的代码结构
这种设计模式不仅适用于用户行为追踪,还可以扩展到订单处理、内容审核、数据同步等多种业务场景。掌握ThinkPHP的事件系统,能够显著提升代码的可维护性和系统的可扩展性。
在实际项目中,建议根据业务复杂度选择合适的事件处理方式。简单逻辑可以使用模型事件闭包,复杂业务则推荐使用观察者类。同时注意事件处理的性能影响,对耗时操作采用队列异步处理。

