- 后端
- 电商
- 前端
【免费下载链接】CRMEB
🔥CRMEB开源商城免费开源多语言商城系统,Tp6框架商城,系统可商用;包含小程序商城、H5商城、公众号商城、PC商城、App,支持分销、拼团、砍价、秒杀、优惠券、积分、会员等级、小程序直播、页面DIY,前后端分离,方便二开,更有详细使用文档、接口文档、数据字典、二开文档/视频教程,欢迎大家提出宝贵意见和建议
本文以 CRMEB 开源商城后端所依赖的 crmeb/vendor/psr/log/README.md 为线索,系统讲解 PSR-3 日志接口规范的核心设计(接口、抽象基类、Trait、NullLogger 与测试组件),并追踪 CRMEB(ThinkPHP 6 框架)中该规范如何落地:日志通道Channel直接实现Psr\Log\LoggerInterface,业务代码通过think\facade\Log门面统一记录。读完本文,你既能掌握在任何 PHP 项目中按 PSR-3 规范编写"日志无关"业务代码的方法,也能理解 CRMEB 日志配置(crmeb/config/log.php)中通道、级别与文件切分参数的含义。
PSR Log 是什么:一个描述"记录器长什么样"的接口包
psr/log是 PHP-FIG 组织发布的 PSR-3(日志接口)参考实现包。它的README.md第一句话就点明了定位:
This repository holds all interfaces/classes/traits related to PSR-3. Note that this is not a logger of its own. It is merely an interface that describes a logger.
也就是说,psr/log本身不提供任何真实日志输出能力,它只定义了一套"记录器"应该具备的方法签名与语义约定。真正把日志写进文件、数据库或发送告警的是具体实现(在 CRMEB 中即 ThinkPHP 的Channel驱动)。这种"面向接口编程"的好处是:业务代码只依赖LoggerInterface,底层日志实现可以随时替换而不影响调用方。
该包在 crmeb/vendor/psr/log/ 目录下共提供如下组件:
| 文件 | 作用 |
|---|---|
| Psr/Log/LoggerInterface.php | 日志器核心接口,定义 8 个级别方法与log() |
| Psr/Log/AbstractLogger.php | 抽象基类,把 8 个级别方法统一转发到log() |
| Psr/Log/LoggerAwareInterface.php | 可注入日志器的对象接口 |
| Psr/Log/LoggerAwareTrait.php | 上述接口的默认实现 Trait |
| Psr/Log/LoggerTrait.php | 给"已继承其他类、无法继承 AbstractLogger"的类使用的 Trait |
| Psr/Log/LogLevel.php | 8 个日志级别常量定义 |
| Psr/Log/NullLogger.php | 什么都不做的空实现 |
| Psr/Log/InvalidArgumentException.php | 传入非法日志级别时抛出的异常 |
| Psr/Log/Test/TestLogger.php | 记录全部日志、便于断言的测试用实现 |
| Psr/Log/Test/LoggerInterfaceTest.php | 供实现方继承的合规性测试基类 |
安装与依赖:版本、环境要求与自动加载
README.md给出的安装方式只有一条 Composer 命令:
composer require psr/log结合仓库内的 crmeb/vendor/psr/log/composer.json 可以确认以下事实:
- 环境要求:
php >= 5.3.0,兼容性极广,老版本 PHP 项目也能使用; - 自动加载:按 PSR-4 规则将
Psr\Log\命名空间映射到Psr/Log/目录,因此引用时只需use Psr\Log\LoggerInterface;; - 许可证:MIT;
- 版本号:CRMEB 通过 Composer 实际锁定的是
psr/log 1.1.2(见 crmeb/composer.lock 中"name": "psr/log", "version": "1.1.2"一段)。
CRMEB 并未在根级composer.json里直接声明psr/log,而是由 ThinkPHP 框架间接依赖(crmeb/vendor/topthink/framework/composer.json 中有"psr/log": "~1.0"),这恰好印证了 PSR-3 的生态价值:框架与组件通过统一接口协同,应用开发者无需关心它从哪来。
快速上手:面向 LoggerInterface 编程
README.md给出了一个完整的构造函数注入示例,这是 PSR-3 最典型的用法——业务类不直接 new 一个日志器,而是接收一个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 } }示例中有两个值得展开的约定:
- 可选注入:构造函数参数默认为
null,调用处先做if ($this->logger)判断再记录。这样日志对业务功能是"锦上添花"而非强依赖——不传日志器功能照常工作,传入后行为被完整记录。 - 异常上下文:捕获异常后,上下文数组必须使用键名
exception携带异常对象。LoggerInterface.php的文档注释明确要求:"如果向context中传入一个异常实例用于生成堆栈追踪,它必须放在名为exception的键中"。这是实现方约定的"事实标准"。
使用方只要持有LoggerInterface,就能在 8 个级别上自由记录(见下节)。而实际运行时的写入行为,由传入的实现决定。
上下文占位符与消息约束
LoggerInterface.php头部注释还定义了另外两条实现方必须遵守的规则:
- 消息类型:
$message必须是字符串,或实现了__toString()的对象; - 占位符插值:消息可以包含
{foo}形式的占位符,实现方应使用context数组中键foo对应的值进行替换。注意占位符只在消息中出现、但在 context 中没有对应键时,应保留原样(不报错、不替换)。
这套约定在 Psr/Log/Test/LoggerInterfaceTest.php 的testContextReplacement中有明确验证:
$logger->info('{Message {nothing} {user} {foo.bar} a}', array('user' => 'Bob', 'foo.bar' => 'Bar')); // 期望输出:'info {Message {nothing} Bob Bar a}'可以看到{user}、{foo.bar}被替换,而未提供的{nothing}原样保留。这套插值规则让日志消息模板化,实现方统一处理格式,调用方无需手工拼接字符串。
8 个日志级别:语义与典型场景
LoggerInterface按严重程度从高到低定义了 8 个方法,其语义注释直接来自接口源码,也是 PSR-3 规范正文的原话:
| 级别 | 接口方法 | 语义 | 典型场景 |
|---|---|---|---|
| emergency | emergency($message, $context) | 系统不可用 | 整站宕机、数据库不可用,应触发短信告警并叫醒值班人员 |
| alert | alert($message, $context) | 必须立即采取行动 | 同上,强调"立刻处理" |
| critical | critical($message, $context) | 严重状况 | 应用组件不可用、未预期的异常 |
| error | error($message, $context) | 运行时错误 | 不需要立即处理,但应记录并监控 |
| warning | warning($message, $context) | 警告 | 使用了废弃 API、API 使用不当、非错误的异常情况 |
| notice | notice($message, $context) | 正常但重要的事件 | 业务里程碑类事件 |
| info | info($message, $context) | 有趣的事件 | 用户登录、SQL 日志等 |
| debug | debug($message, $context) | 详细调试信息 | 开发期最细粒度的诊断 |
这些级别在 Psr/Log/LogLevel.php 中以常量固化:LogLevel::EMERGENCY到LogLevel::DEBUG,值就是小写级别名。CRMEB 的 crmeb/config/log.php 中日志记录级别数组'level' => ['error', 'warning', 'fail', 'success', 'info', 'notice', 'crontab', 'crmeb', 'listener']正是基于这套级别语义并结合业务(成功日志、失败日志、定时任务、监听器、自定义 crmeb 级别)扩展出来的。
任意级别方法 log()
除 8 个具名方法外,接口还定义了通用入口:
public function log($level, $message, array $context = array());它的$level参数接受任意级别名;如果传入的值不是合法级别,必须抛出\Psr\Log\InvalidArgumentException。这是接口注释与 Psr/Log/Test/LoggerInterfaceTest.php 中testThrowsOnInvalidLevel共同强制的行为。
减少样板代码:AbstractLogger 与 LoggerTrait
如果每个实现都要手写 8 个方法就太啰嗦了,psr/log提供两条减负路径:
路径一:继承AbstractLogger(Psr/Log/AbstractLogger.php)。它把emergency到debug全部转发给log(),例如:
public function info($message, array $context = array()) { $this->log(LogLevel::INFO, $message, $context); }于是你只需实现一个log()方法:
class MyLogger extends AbstractLogger { public function log($level, $message, array $context = array()) { // 这里决定消息写到哪、怎么格式化 } }路径二:使用LoggerTrait(Psr/Log/LoggerTrait.php)。当你的类已经继承了别的基类、无法再继承AbstractLogger时,直接use Psr\Log\LoggerTrait;并把log()声明为抽象方法即可,效果完全相同——Trait 内部同样是"逐级转发到log()"。
这一设计使"实现一个 PSR-3 日志器"的成本降到最低:核心只写一个方法。
消灭 null 判断:NullLogger
README.md的示例里到处是if ($this->logger) { ... },而psr/log给出了更优雅的替代方案。NullLogger(crmeb/vendor/psr/log/Psr/Log/NullLogger.php)的注释说得很直白:
This Logger can be used to avoid conditional log calls. ... if no logger is provided to your library creating a NullLogger instance to have something to throw logs at is a good way to avoid littering your code with
if ($this->logger) { }blocks.
它的实现就是一个log()空操作(// noop)。用法上,把"日志器可能为 null"的类改为"默认注入NullLogger",业务方法就能无条件调用$this->logger->info(...),代码更干净且无性能损耗。这一模式同样体现在 PSR-3 生态对"日志必须可选"这一核心价值观的坚持上。
日志器注入:LoggerAwareInterface 与 LoggerAwareTrait
除了构造函数注入,psr/log还定义了二段式注入方案:
// LoggerAwareInterface 只声明一个方法 public function setLogger(LoggerInterface $logger); // LoggerAwareTrait 给出默认实现 protected $logger; public function setLogger(LoggerInterface $logger) { $this->logger = $logger; }相关源码见 Psr/Log/LoggerAwareInterface.php 与 Psr/Log/LoggerAwareTrait.php。这套机制适合容器装配场景:对象创建后由容器(或组装代码)调用setLogger()注入日志器,类本身只负责保存与使用。DI 容器可通过检查LoggerAwareInterface自动完成注入,是许多框架的标配能力。
测试你的实现:TestLogger 与 LoggerInterfaceTest
psr/log附带完整的测试组件,位于 crmeb/vendor/psr/log/Psr/Log/Test/:
TestLogger(TestLogger.php):一个把所有记录按级别存放的AbstractLogger子类,公开$records与$recordsByLevel,并内置hasRecord、hasRecordThatContains、hasRecordThatMatches、hasRecordThatPasses等断言方法;还通过__call魔法方法动态提供hasInfo、hasErrorThatContains、hasErrorThatMatches等便捷调用(见文件头部的大量@method注释),方便单测中"按级别查是否有某条记录"。LoggerInterfaceTest(LoggerInterfaceTest.php):一个抽象 PHPUnit 测试基类,实现方只需继承它并实现getLogger()与getLogs()两个方法,即可自动获得以下合规性验证:testImplements:必须实现LoggerInterface;testLogsAtAllLevels:8 个级别方法 +log()共 16 次调用都要被正确记录,且上下文(如array('user' => 'Bob'))要参与插值;testThrowsOnInvalidLevel:非法级别必须抛InvalidArgumentException;testContextReplacement:占位符按上下文替换,缺失键保留;testObjectCastToString:实现了__toString()的对象可直接作为消息;testContextCanContainAnything:上下文可包含布尔、null、字符串、整数、浮点、嵌套数组、对象、已关闭/未关闭的资源等任意值;testContextExceptionKeyCanBeExceptionOrOtherValues:exception键可以传异常对象也可以传普通字符串。
这一套件是"接口契约"的物化——实现方通过它快速确认自己完全符合 PSR-3,调用方也能据此相信任何 PSR-3 实现行为一致。
CRMEB 中的落地:ThinkPHP 的 Channel 与 Log 门面
在 CRMEB 中,PSR-3 并非停留在"规范"层面,而是被 ThinkPHP 6 框架真实实现并贯穿业务。
框架层实现:ThinkPHP 日志通道类 crmeb/vendor/topthink/framework/src/think/log/Channel.php 第 15 行use Psr\Log\LoggerInterface;,第 21 行class Channel implements LoggerInterface——也就是说,CRMEB 的整个日志通道就是 PSR-3 的一个实现。它会先把日志写入内存(支持lazy延迟写入),在请求结束或触发事件时统一落盘。
门面层调用:业务代码不直接操作Channel,而是通过think\facade\Log门面记录。全仓库大量业务服务都这样使用,例如订单创建服务 crmeb/app/services/order/StoreOrderCreateServices.php 第 516 行、第 539 行在订单商品结算失败时调用:
Log::error('订单商品结算失败,File:' . $e->getFile() . ',Line:' . $e->getLine() . ',Message:' . $e->getMessage());类似的Log::调用还遍布支付回调、微信模板消息、短信、直播、客服、定时任务等模块的服务类中(如 crmeb/app/services/message/notice/SmsService.php、crmeb/app/services/activity/live/LiveRoomServices.php、crmeb/app/jobs/AgentJob.php 等),日志级别以error、info为主,配合File驱动的文件通道落地。
配置层参数:CRMEB 的日志行为由 crmeb/config/log.php 控制,关键配置项可直接对照上文规范理解:
| 配置项 | 值 | 含义 |
|---|---|---|
default | Env::get('log.channel', 'file') | 默认日志通道,未配置环境变量时用file文件驱动 |
level | ['error', 'warning', 'fail', 'success', 'info', 'notice', 'crontab', 'crmeb', 'listener'] | 允许记录的级别白名单(含 CRMEB 自定义的业务级别) |
channels.file.type | File | 文件记录方式 |
channels.file.path | app()->getRuntimePath() . 'log' | 日志保存目录,即crmeb/runtime/log/ |
channels.file.single | false | 按天拆分文件,而非单文件写入 |
channels.file.apart_level | ['error', 'fail', 'success', 'crontab', 'crmeb', 'listener'] | 这些级别单独存放为独立日志文件 |
channels.file.max_files | 60 | 最多保留 60 个日志文件,超出自动清理 |
channels.file.format | '%s\|%s\|%s' | 日志行格式 |
实践上,排查 CRMEB 订单、支付、消息等问题时,通常直接查看crmeb/runtime/log/下当天的日志文件;若某个级别(如crmeb自定义级别)独立成文件,则可快速过滤出业务相关日志。这套机制本质是:规范(PSR-3)保证接口一致,ThinkPHPChannel提供实现,config/log.php决定行为,业务代码只需一行Log::error(...)。
小结
从psr/log的README.md出发可以看到一条清晰的链路:psr/log用最小成本(一个接口、一个抽象基类、两个 Trait、一个空实现和一套测试)定义了 PHP 生态通用的日志契约;CRMEB 则通过 ThinkPHP 的Channel implements LoggerInterface完成实现,业务层以think\facade\Log门面统一调用,最终由config/log.php决定写入文件、级别过滤与文件保留策略。无论你是要在 CRMEB 二次开发时新增业务日志,还是想为其他 PHP 项目自定义一个 PSR-3 日志器,掌握LoggerInterface的 8 个级别、context占位符插值、exception键约定以及AbstractLogger/LoggerTrait/NullLogger的组合用法,都是最高性价比的起点。
- 后端
- 电商
- 前端
【免费下载链接】CRMEB
🔥CRMEB开源商城免费开源多语言商城系统,Tp6框架商城,系统可商用;包含小程序商城、H5商城、公众号商城、PC商城、App,支持分销、拼团、砍价、秒杀、优惠券、积分、会员等级、小程序直播、页面DIY,前后端分离,方便二开,更有详细使用文档、接口文档、数据字典、二开文档/视频教程,欢迎大家提出宝贵意见和建议
相关推荐
Moodle 内置 PSR-3 日志接口(psr/log)完全指南:从接口规范到实践集成
Moodle 内置 PSR 3 日志接口(psr/log)完全指南:从接口规范到实践集成 PSR 3 是 PHP FIG 制定的日志接口规范,它定义了"日志应该
教育后端前端PSR-3 日志接口规范与 psr/log 包实战:解析 ShowDoc 仓库中的 LoggerInterface、NullLogger 与测试体系
PSR 3 日志接口规范与 psr/log 包实战:解析 ShowDoc 仓库中的 LoggerInterface、NullLogger 与测试体系 PSR 3
文档知识库后端前端PSR-3 日志接口规范深度解析:Psr\Log\LoggerInterface 的设计原理与实战实现
PSR 3 日志接口规范深度解析:Psr\Log\LoggerInterface 的设计原理与实战实现 PSR 3(Logger Interface,日志接口)
文档开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考