本文目录导读:

- 方案一:使用成熟的日志库(推荐,适用于大部分项目)
- 方案二:基于数据库的活动日志(适用于需要查询、展示审计记录的CMS/ERP)
- 方案三:使用AOP(面向切面编程)自动记录(高级,适合框架)
- 最佳实践建议
- 简单演示:最小化文件日志实现(无第三方依赖)
在PHP项目中实现活动日志记录,核心目标是追踪用户行为和系统事件,以便于审计、调试和监控。
根据项目规模和复杂度,有以下几种常见且实用的实现方案:
使用成熟的日志库(推荐,适用于大部分项目)
这是最专业、最灵活的方案,常用的库有 Monolog(PHP社区事实上的标准)。
安装 Monolog
composer require monolog/monolog
创建日志服务类(封装)
<?php
// src/Service/ActivityLogger.php
namespace App\Service;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\RotatingFileHandler;
use Monolog\Formatter\LineFormatter;
class ActivityLogger
{
private Logger $logger;
public function __construct(string $logDir = __DIR__ . '/../../var/log/activity')
{
$this->logger = new Logger('activity');
// 1. 按天轮转的日志文件(推荐,避免单个文件过大)
$handler = new RotatingFileHandler($logDir . '/activity.log', 30, Logger::INFO);
// 2. 自定义日志格式:时间戳 + 通道 + 级别 + 消息 + 上下文
$formatter = new LineFormatter("[%datetime%] %channel%.%level_name%: %message% %context%\n");
$handler->setFormatter($formatter);
$this->logger->pushHandler($handler);
}
/**
* 记录用户活动
* @param string $action 操作类型(如:user_login, order_create, article_edit)
* @param array $context 上下文信息(如:['user_id'=>123, 'target_id'=>456, 'details'=>'...'])
*/
public function log(string $action, array $context = []): void
{
// 自动附加通用信息(如IP、当前用户ID)
$context = array_merge([
'ip' => $_SERVER['REMOTE_ADDR'] ?? 'CLI',
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'CLI',
], $context);
// 使用INFO级别记录活动日志
$this->logger->info($action, $context);
}
}
在代码中使用
// 在控制器或服务中
$activityLogger = new ActivityLogger();
// 记录用户登录
$activityLogger->log('user_login', [
'user_id' => $user->getId(),
'email' => $user->getEmail(),
]);
// 记录创建订单
$activityLogger->log('order_create', [
'user_id' => $user->getId(),
'order_id' => $order->getId(),
'amount' => $order->getTotal(),
]);
输出日志文件示例
[2024-01-15 14:30:00] activity.INFO: user_login {"user_id":1,"email":"a@b.com","ip":"192.168.1.1","user_agent":"Mozilla/5.0"} []
[2024-01-15 14:35:12] activity.INFO: order_create {"user_id":1,"order_id":1001,"amount":99.99,"ip":"192.168.1.1"} []
基于数据库的活动日志(适用于需要查询、展示审计记录的CMS/ERP)
将日志存入数据库,便于前端后台查询和统计。
创建数据表
CREATE TABLE `activity_logs` ( `id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, `user_id` INT UNSIGNED NULL COMMENT '操作用户ID', `action` VARCHAR(100) NOT NULL COMMENT '操作标识', `target_type` VARCHAR(50) NULL COMMENT '操作对象类型(如:order, user, article)', `target_id` INT UNSIGNED NULL COMMENT '操作对象ID', `description` TEXT NULL COMMENT '人类可读的描述', `context` JSON NULL COMMENT '额外上下文信息', `ip_address` VARCHAR(45) NULL, `user_agent` VARCHAR(255) NULL, `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX `idx_user_id` (`user_id`), INDEX `idx_action` (`action`), INDEX `idx_created_at` (`created_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
创建日志模型/Repository
<?php
// src/Repository/ActivityLogRepository.php
use PDO;
class ActivityLogRepository
{
private PDO $pdo;
public function insert(array $data): void
{
$sql = "INSERT INTO activity_logs (user_id, action, target_type, target_id, description, context, ip_address, user_agent)
VALUES (:user_id, :action, :target_type, :target_id, :description, :context, :ip_address, :user_agent)";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([
':user_id' => $data['user_id'] ?? null,
':action' => $data['action'],
':target_type' => $data['target_type'] ?? null,
':target_id' => $data['target_id'] ?? null,
':description' => $data['description'] ?? '',
':context' => json_encode($data['context'] ?? []),
':ip_address' => $_SERVER['REMOTE_ADDR'] ?? null,
':user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? null,
]);
}
}
在业务逻辑中调用
// 在删除文章的服务中
$logRepo = new ActivityLogRepository($pdo);
$logRepo->insert([
'user_id' => getCurrentUserId(),
'action' => 'article_delete',
'target_type' => 'article',
'target_id' => $articleId,
'description' => "用户删除了文章《{$articleTitle》}",
'context' => ['title' => $articleTitle, 'status' => 'deleted']
]);
使用AOP(面向切面编程)自动记录(高级,适合框架)
如果项目使用 Symfony 或 Laravel,可以利用事件系统或中间件自动记录。
使用 Laravel 事件监听(推荐)
Laravel 本身有 Activitylog 包可用,但也可以手动实现:
// app/Listeners/LogUserActivity.php
namespace App\Listeners;
use Illuminate\Auth\Events\Login;
use Illuminate\Events\Dispatcher;
class LogUserActivity
{
public function handle(Login $event): void
{
activity()
->performedOn($event->user)
->causedBy($event->user)
->log('用户登录');
}
public function subscribe(Dispatcher $events): array
{
return [
Login::class => 'handle',
// 注册其他事件...
];
}
}
使用 Symfony EventSubscriber
// src/EventSubscriber/ActivityLogSubscriber.php
namespace App\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
class ActivityLogSubscriber implements EventSubscriberInterface
{
private ActivityLogger $logger;
public function __construct(ActivityLogger $logger)
{
$this->logger = $logger;
}
public static function getSubscribedEvents(): array
{
return [
LoginSuccessEvent::class => 'onLoginSuccess',
];
}
public function onLoginSuccess(LoginSuccessEvent $event): void
{
$user = $event->getUser();
$this->logger->log('user_login', ['user_id' => $user->getId()]);
}
}
最佳实践建议
- 不要记录密码、信用卡等敏感信息,可以在记录前使用
unset()或ArrayUtils::omit()过滤。 - 使用统一日志格式:
[时间] [级别] [操作] [上下文(JSON)],便于后续用grep或日志分析工具(如ELK)处理。 - 异步写入(高并发场景):将日志写入消息队列(如Redis列表、RabbitMQ),由独立消费者批量写入数据库或文件,避免拖慢主请求。
- 日志轮转:使用
RotatingFileHandler(Monolog)或 Linux 的logrotate避免日志无限增长撑爆磁盘。 - 定义规范操作类型:使用
domain_action命名,如user.login、order.create、article.delete,便于统一管理。 - 考虑成本:
- 小型项目:Monolog + 文件日志即可。
- 中型项目:数据库日志(便于后台查询)。
- 大型项目:独立日志服务(如 ELK)、消息队列异步写入。
简单演示:最小化文件日志实现(无第三方依赖)
如果你不想引入任何库,可以快速写一个函数:
function logActivity(string $action, array $data = []) {
$logFile = __DIR__ . '/activity.log';
$entry = [
'time' => date('Y-m-d H:i:s'),
'action' => $action,
'ip' => $_SERVER['REMOTE_ADDR'] ?? 'CLI',
'data' => $data,
];
$line = json_encode($entry) . PHP_EOL;
file_put_contents($logFile, $line, FILE_APPEND | LOCK_EX);
}
// 使用
logActivity('user_register', ['email' => 'test@test.com']);
这个版本最简单,但缺乏轮转、不同级别区分、自定义格式等能力,建议尽快升级到 Monolog。
选择哪种方案取决于你的项目规模、查询需求和开发效率,通常从 Monolog 文件日志 开始,后期需要查询时再增加 数据库日志 支持。