☰
YOURLS 中的 PSR-3 日志接口:从规范到 YOURLS 的 Logger 实现与调试实践
2026/10/7 5:32:21 网站建设 项目流程
  • 后端

【免费下载链接】YOURLS

🔗 The 𝘥𝘦 𝘧𝘢𝘤𝘵𝘰 standard, self hosted, powerful and customizable, URL shortener in PHP

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

导读

PSR-3(PSR Log)是 PHP-FIG 组织制定的「日志记录器接口」规范,它不提供任何具体的日志实现,只定义了一套描述「日志记录器」的通用接口,让任意 PHP 库都能与 Monolog、Log4php 等日志库无缝协作。本文以 includes/vendor/psr/log/README.md 为骨架,先完整讲解 PSR-3 接口规范的定义、安装方式与使用模式,再深入 YOURLS 仓库源码,揭示这套规范在自托管短链接服务 YOURLS 中是如何被落地实现的——包括YOURLS\Database\Logger的具体实现、8 个日志级别的语义、{placeholder}占位符约定,以及yourls_debug_log()与 SQL 查询日志的完整调用链。读完本文,你既能掌握 PSR-3 的标准用法,也能在 YOURLS 中启用调试模式并读懂其日志输出。

一、PSR-3 是什么:一个只描述「记录器」的接口规范

includes/vendor/psr/log/README.md 开篇就强调了一个关键事实:

This is not a logger of its own. It is merely an interface that describes a logger.

也就是说,psr/log这个包本身不是日志器,它只是描述「日志器长什么样」的接口。真正的日志写入(写文件、发邮件、上报监控系统)由你选择的实现来完成。这套规范的价值在于:只要你的类依赖Psr\Log\LoggerInterface而不是某个具体日志库,就能在运行时自由替换底层日志实现,实现类与日志库的解耦。

YOURLS 正是这样做的:YOURLS 自身的调试日志器 includes/Database/Logger.php 继承了Psr\Log\AbstractLogger,而数据库查询剖析器(Profiler)的日志则来自 Aura SQL 包(基于\Aura\Sql\Profiler\MemoryLogger,见 includes/Database/Logger.php 的注释)。两者通过同一套 PSR-3 接口被统一消费。

二、安装:用 Composer 引入 psr/log

规范文档给出的安装方式只有一条命令:

composer require psr/log

在 YOURLS 仓库中,该包已被安装并固化在 vendor 目录下,包含以下 8 个源文件(见 includes/vendor/psr/log/src):

文件职责
LoggerInterface.php核心接口,定义 9 个日志方法
LogLevel.php定义 8 个日志级别常量
AbstractLogger.php抽象基类,把级别方法委托给log()
LoggerTrait.php与 AbstractLogger 等价的 Trait,供无法继承基类的类使用
LoggerAwareInterface.php描述「可感知日志器」的对象接口
LoggerAwareTrait.phpLoggerAwareInterface的基础实现
NullLogger.php空实现,丢弃所有日志
InvalidArgumentException.php无效日志级别时抛出的异常类

在 YOURLS 根目录的 composer.json 中,psr/log位于require依赖之列,通过 includes/vendor/autoload.php 注册到 PSR-4 自动加载映射(详见 includes/vendor/composer/autoload_psr4.php)。

三、使用:面向 LoggerInterface 编程

README 给出了一个标准的「面向接口」使用示例:构造函数注入一个可选的LoggerInterface,在业务方法里按需记录信息或错误:

<?php use Psr\Log\LoggerInterface; class Foo { private $logger; public function __construct(LoggerInterface $logger = null) { $this->logger = $logger; } public function doSomething() { if ($this->logger) { $this->logger->info('Doing work'); } try { $this->doSomethingElse(); } catch (Exception $exception) { $this->logger->error('Oh no!', array('exception' => $exception)); } // do something useful } }

这个例子体现了三个最佳实践:

  1. 可选注入:$logger允许为null,调用前用if ($this->logger)判空,避免强依赖;
  2. 异常入上下文:捕获异常时把异常对象放进$context['exception']键,这是规范里唯一对上下文键名做的约定;
  3. 无需关心实现:Foo类完全不知道底层是文件日志、还是内存日志。

3.1 如何获得一个可用的 Logger

「You can then pick one of the implementations of the interface to get a logger.」——你只需挑选任意一个实现了LoggerInterface的日志库(如 Monolog),就能把这个Foo类接上真实日志。若暂时没有实现可用,规范还提供了Psr\Log\NullLogger:它是一个log()方法体为// noop(空操作)的实现(见 NullLogger.php)。文档建议用它来消灭if ($this->logger) { }条件块——给对象注入一个 NullLogger 后,就可以直接调用$logger->info(...)而无需判空,日志会被静默丢弃,行为与不记录完全一致。

四、LoggerInterface 与 8 个日志级别

LoggerInterface.php 定义了 9 个方法:8 个级别方法加上 1 个通用方法log($level, $message, $context)。每个级别方法签名统一为方法名(string|\Stringable $message, array $context = []): void。

级别的语义(依据 LoggerInterface.php 的注释与 LogLevel.php 的常量):

级别常量值语义与典型场景
emergencyemergency系统不可用,如整站宕机、数据库连不上,应立即触发短信告警
alertalert必须立刻采取行动,如整个网站挂掉
criticalcritical严重状态,如应用组件不可用、未预期的异常
errorerror运行时错误,不必立即处理但应被记录与监控
warningwarning非错误的异常情况,如使用了废弃 API、API 使用不当
noticenotice正常但重要的事件
infoinfo有趣的事件,如用户登录、SQL 日志
debugdebug详细的调试信息

4.1 消息与上下文的约定

接口注释明确了两条约束:

  • 消息类型:$message必须是字符串或实现了__toString()的对象(源码中类型声明为string|\Stringable);
  • 占位符:消息可包含{foo}形式的占位符,运行时会被$context中键名为foo的值替换;
  • 异常键:$context可含任意数据,但若要携带异常以生成堆栈追踪,必须放在键名为exception的位置。

4.2 无级别方法也能写日志:log()

log($level, $message, $context)允许以任意级别(如LogLevel::WARNING)动态记录,当传入未定义级别时会抛出\Psr\Log\InvalidArgumentException(见 InvalidArgumentException.php)。所有级别方法只是它的「语法糖」。

五、实现接口的两种姿势:AbstractLogger 与 LoggerTrait

README 指出:如果你想自己实现一个日志器,只需require本包并实现Psr\Log\LoggerInterface。规范为此提供了两条减少样板代码的捷径:

5.1 继承 AbstractLogger

AbstractLogger.php 是一个空壳抽象类,它通过use LoggerTrait获得 8 个级别方法的默认实现——所有级别方法都被委托给唯一的抽象方法log()。因此你实现日志器时只需写一个log()方法,级别分发逻辑全部免费:

class MyLogger extends AbstractLogger { public function log($level, string|\Stringable $message, array $context = []): void { // 真正写日志的逻辑 } }

5.2 使用 LoggerTrait

LoggerTrait.php 与 AbstractLogger 逻辑完全一致(每个方法如info()内部执行$this->log(LogLevel::INFO, $message, $context)),但它是 Trait,供已经继承其他类、无法再继承 AbstractLogger的类使用(PHP 单继承限制下的替代方案)。

5.3 LoggerAware:让对象「感知」日志器

配套的 LoggerAwareInterface.php 与 LoggerAwareTrait.php 提供「注入日志器」的规范入口:实现该接口的类拥有setLogger(LoggerInterface $logger): void方法,Trait 内部用protected ?LoggerInterface $logger = null保存实例,方便依赖注入容器统一装配。

六、纵深:PSR-3 在 YOURLS 中的真实落地

下面把镜头切到 YOURLS 仓库本身,看看这套接口在实际项目里如何被消费。这正体现了 PSR-3「只定接口、实现自由」的设计意图。

6.1 YOURLS 自己的 Logger:YOURLS\Database\Logger

includes/Database/Logger.php 定义了class Logger extends AbstractLogger,它是 YOURLS 的定制日志器(自 1.7.10 版本起,注释见文件头部)。它只重写了一个log()方法,核心逻辑分两条路径:

  • 普通日志(如debug级别):直接把$message转为字符串存入$this->messages[]数组;
  • SQL 查询日志($level === 'query'):对来自 Aura SQL Profiler 的上下文做格式化,通过debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 6)回溯调用栈,识别真正发起查询的方法名(fetchAll、fetchOne等,否则回退为perform),最终生成形如SQL fetchAll: SELECT ... (%s s)的可读消息,耗时用number_format($context['duration'], 5)保留 5 位小数。

它还提供两个公开方法:

  • getMessages():返回已收集的所有日志消息数组;
  • pretty_format($statement, $values):用正则/:([^\s;)]*)/把 PDO 命名占位符(如:url)替换为上下文中的实际值,方便人眼阅读。注意注释明确警告:这只是为了可读性的外观替换,替换结果不是合法 SQL(不会正确加引号)。

6.2 调用链:yourls_debug_log() 如何驱动 Logger

includes/functions-debug.php 是调试日志的对外入口:

  • yourls_debug_log(string $msg)(第 17 行起):当处于调试模式时,执行yourls_get_db('read-debug_log')->getProfiler()->getLogger()->log('debug', $msg);
  • yourls_get_db()返回的数据库对象在 includes/Database/YDB.php 中会new Logger()并挂到 Profiler 上——于是yourls_debug_log()的调用最终落到YOURLS\Database\Logger::log('debug', ...)。

实际调用点遍布全库,例如:

  • includes/class-mysql.php:yourls_debug_log( 'Connected to ' . $dsn ),记录数据库连接;
  • includes/functions-auth.php:密码哈希读写失败、Cookie 写入失败等认证环节的多个调试点;
  • includes/functions-http.php:远程请求异常时yourls_debug_log( $e->getMessage() ... )。

6.3 调试模式开关

调试日志默认不工作,需满足两个条件之一:

  • 启动时配置常量YOURLS_DEBUG == true(见 includes/class-mysql.php 的yourls_debug_mode( YOURLS_DEBUG ));
  • 运行期调用yourls_debug_mode(true)动态开启(定义见 includes/functions-debug.php)。

启用后,可通过yourls_get_db('read-get_debug_log')->getProfiler()->getLogger()->getMessages()(includes/functions-debug.php)读取已收集的消息数组,结合 includes/Database/YDB.php 中 Profiler 对查询消息的聚合,即可同时获得应用调试信息与 SQL 查询耗时日志。

6.4 一个「接口解耦」的活例子

在 YOURLS 中,同一个Psr\Log\LoggerInterface名下并存了两类日志来源:

来源级别消息示例
yourls_debug_log()debugConnected to mysql:host=...
Aura SQL Profiler 内部日志query{function} ({duration} seconds): {statement} {backtrace}(原始格式),经Logger::log()重排为SQL fetchAll: SELECT ... (0.00253 s)

YOURLS\Database\Logger::log()通过判断$level区分两者(见 includes/Database/Logger.php),这正是 PSR-3 接口「统一方法入口、自由分发处理」的典型应用——日志框架只需认准log()一个方法。

七、实战小结

  1. 依赖注入:composer require psr/log后,你的类只依赖Psr\Log\LoggerInterface,运行时注入 Monolog 等实现即可;
  2. 级别语义:8 个级别从emergency到debug严重度递减,$context中保留exception键用于异常堆栈;
  3. 实现捷径:继承AbstractLogger或use LoggerTrait,只需实现log()一个方法;不想判空就用NullLogger;
  4. YOURLS 实践:yourls_debug_log()→YDB::getProfiler()->getLogger()→YOURLS\Database\Logger::log()构成完整调用链;配置YOURLS_DEBUG = true即可让调试日志与 SQL 查询日志同时生效。

相关源码入口:psr/log 接口定义、YOURLS Logger 实现、调试函数、数据库与 Profiler 装配、composer 依赖声明。

  • 后端

【免费下载链接】YOURLS

🔗 The 𝘥𝘦 𝘧𝘢𝘤𝘵𝘰 standard, self hosted, powerful and customizable, URL shortener in PHP

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

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

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

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

立即咨询