- 后端
【免费下载链接】YOURLS
🔗 The 𝘥𝘦 𝘧𝘢𝘤𝘵𝘰 standard, self hosted, powerful and customizable, URL shortener in PHP
导读
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.php | LoggerAwareInterface的基础实现 |
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 } }这个例子体现了三个最佳实践:
- 可选注入:
$logger允许为null,调用前用if ($this->logger)判空,避免强依赖; - 异常入上下文:捕获异常时把异常对象放进
$context['exception']键,这是规范里唯一对上下文键名做的约定; - 无需关心实现:
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 的常量):
| 级别 | 常量值 | 语义与典型场景 |
|---|---|---|
emergency | emergency | 系统不可用,如整站宕机、数据库连不上,应立即触发短信告警 |
alert | alert | 必须立刻采取行动,如整个网站挂掉 |
critical | critical | 严重状态,如应用组件不可用、未预期的异常 |
error | error | 运行时错误,不必立即处理但应被记录与监控 |
warning | warning | 非错误的异常情况,如使用了废弃 API、API 使用不当 |
notice | notice | 正常但重要的事件 |
info | info | 有趣的事件,如用户登录、SQL 日志 |
debug | debug | 详细的调试信息 |
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() | debug | Connected 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()一个方法。
七、实战小结
- 依赖注入:
composer require psr/log后,你的类只依赖Psr\Log\LoggerInterface,运行时注入 Monolog 等实现即可; - 级别语义:8 个级别从
emergency到debug严重度递减,$context中保留exception键用于异常堆栈; - 实现捷径:继承
AbstractLogger或use LoggerTrait,只需实现log()一个方法;不想判空就用NullLogger; - 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
相关推荐
Dolibarr 仓库中的 PSR-3 日志接口规范:深入解析 Psr\Log 包的接口、实现与集成实践
Dolibarr 仓库中的 PSR 3 日志接口规范:深入解析 Psr\Log 包的接口、实现与集成实践 PSR 3(PHP Standard Recommen
企业应用后端Hutch完整指南:如何快速构建高效的RabbitMQ消息处理系统
Hutch完整指南:如何快速构建高效的RabbitMQ消息处理系统 Hutch是一个基于Ruby的 高效RabbitMQ消息处理系统 框架,专为构建可靠的消息驱
消息队列后端微服务PSR-3 日志接口规范深度解析:Psr\Log\LoggerInterface 的设计原理与实战实现
PSR 3 日志接口规范深度解析:Psr\Log\LoggerInterface 的设计原理与实战实现 PSR 3(Logger Interface,日志接口)
文档开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考