PHP项目如何实现活动日志记录?

wen PHP项目 4

本文目录导读:

PHP项目如何实现活动日志记录?

  1. 方案一:使用成熟的日志库(推荐,适用于大部分项目)
  2. 方案二:基于数据库的活动日志(适用于需要查询、展示审计记录的CMS/ERP)
  3. 方案三:使用AOP(面向切面编程)自动记录(高级,适合框架)
  4. 最佳实践建议
  5. 简单演示:最小化文件日志实现(无第三方依赖)

在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(面向切面编程)自动记录(高级,适合框架)

如果项目使用 SymfonyLaravel,可以利用事件系统或中间件自动记录。

使用 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()]);
    }
}

最佳实践建议

  1. 不要记录密码、信用卡等敏感信息,可以在记录前使用 unset()ArrayUtils::omit() 过滤。
  2. 使用统一日志格式[时间] [级别] [操作] [上下文(JSON)],便于后续用 grep 或日志分析工具(如ELK)处理。
  3. 异步写入(高并发场景):将日志写入消息队列(如Redis列表、RabbitMQ),由独立消费者批量写入数据库或文件,避免拖慢主请求。
  4. 日志轮转:使用 RotatingFileHandler(Monolog)或 Linux 的 logrotate 避免日志无限增长撑爆磁盘。
  5. 定义规范操作类型:使用 domain_action 命名,如 user.loginorder.createarticle.delete,便于统一管理。
  6. 考虑成本
    • 小型项目: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 文件日志 开始,后期需要查询时再增加 数据库日志 支持。

抱歉,评论功能暂时关闭!