从零开始编写一个PHP框架 系列的《日志模块》
项目地址:terse
前言
在一个系统中,日志模块会记录系统的运行情况,可能是异常,也可能是我们的一些调试信息。
需求分析
- 可以实现记录功能
- 可以指定记录级别
- 可能有多种日志记录方式
- 可以指定记录方式
- 可以指定记录格式
目录结构
这个模块需要实现的东西比较多,涉及多个文件,索性先将目录结构抛出来。
.
├── Adapter
│ └── File.php 文件日志类
├── Adapter.php 日志构抽象类
├── Formatter
│ └── Line.php 行格式化类
└── Formatter.php 格式化抽象类
级别定义及释义
名称 | 类型 | 释义 |
---|---|---|
DEBUG | 调试 | 调试信息 |
INFO | 信息 | 程序输出信息 |
NOTICE | 通知 | 程序可以运行,但是还不够完美的错误 |
WARNING | 警告 | 需要发出警告的错误 |
ERROR | 一般错误 | 一般性错误 |
ALERT | 警戒性错误 | 必须被立即修改的错误 |
CRITICAL | 临界值错误 | 超过临界值的错误 |
EMERG | 严重错误 | 导致系统崩溃无法使用 |
日志抽象类
考虑会有多种记录方式,所以我们需要一个抽象类,来定义一些基本操作。
<?php
abstract class Adapter
{
}
抽象方法
由于日志的多样化,我们需要将数据拼凑的操作放在抽象类里,关于构造函数、写日志和关闭连接的动作要在子类里去操作。
<?php
abstract class Logger
{
/**
* 构造函数
*
* @param string $name
* @param mixed $options
*/
abstract function __construct(string $name, array $options = []);
/**
* 写入日志
*
* @param array $data
* @return void
*/
abstract public function save(array $data);
/**
* 关闭连接
*
* @return void
*/
abstract public function close();
}
属性初始化
上面的表格已经定义了各个级别,并加了相应的释义,在类初始化的时候,会默认初始化各个级别。
<?php
abstract class Adapter
{
/**
* 调试
* 调试信息
*/
const DEBUG = 'DEBUG';
/**
* 信息
* 程序输出信息
*/
const INFO = 'INFO';
/**
* 通知
* 程序可以运行,但是还不够完美的错误
*/
const NOTICE = 'NOTICE';
/**
* 警告
* 需要发出警告的错误
*/
const WARNING = 'WARNING';
/**
* 一般错误
* 一般性错误
*/
const ERROR = 'ERROR';
/**
* 警戒性错误
* 必须被立即修改的错误
*/
const ALERT = 'ALERT';
/**
* 临界值错误
* 超过临界值的错误
*/
const CRITICAL = 'CRITICAL';
/**
* 严重错误
* 导致系统崩溃无法使用
*/
const EMERG = 'EMERG';
}
方法初始化
如果对外只提供一个方法,那我们每次都需要注明需要什么级别的日志,如下:
$logger->log(Logger::DEBUG, $message);
为了语义化和统一,每个类型都会提供一个方法供外界调用,而 log
方法会变为受保护的方法。
<?php
class Adapter
{
...
/**
* 调试
*
* @param string $message
* @return void
*/
public function debug($message) {
$this->log(self::DEBUG, $message);
}
/**
* 信息
*
* @param string $message
* @return void
*/
public function info($message) {
$this->log(self::INFO, $message);
}
/**
* 通知
*
* @param string $message
* @return void
*/
public function notice($message) {
$this->log(self::NOTICE, $message);
}
/**
* 警告
*
* @param string $message
* @return void
*/
public function warning($message) {
$this->log(self::WARNING, $message);
}
/**
* 一般错误
*
* @param string $message
* @return void
*/
public function error($message) {
$this->log(self::ERROR, $message);
}
/**
* 警戒性错误
*
* @param string $message
* @return void
*/
public function alert($message) {
$this->log(self::ALERT, $message);
}
/**
* 临界值错误
*
* @param string $message
* @return void
*/
public function critical($message) {
$this->log(self::CRITICAL, $message);
}
/**
* 严重错误
*
* @param string $message
* @return void
*/
public function emerge($message) {
$this->log(self::EMERG, $message);
}
...
}
事务
有时候,我们需要在一次操作中,写入多次日志。同时,如果出现异常,则回滚之前需要写入的内容。
针对这种情况,我们需要实现一种类似于数据库事务的功能。
然而,这里的事务和数据库的事务有所区别,主要是为了我们解决写入多行日志准备,不过在语义和使用上和数据库事务一致。
<?php
abstract class Logger
{
...
/**
* 日志信息
*
* @var array
*/
protected $_log = [];
/**
* 事务
*
* @var boolean
*/
protected $_transcation = false;
/**
* 开启事务
*
* @return void
*/
public function begin()
{
$transcation = $this->_transcation;
if ($transcation) {
throw new \Exception('不可重复开启事务', -__LINE__);
}
$this->_transcation = true;
}
/**
* 提交事务
*
* @return mixed
*/
public function commit()
{
$transcation = $this->_transcation;
$log = $this->_log;
if (!$transcation || !$log) {
return false;
}
$this->_transcation = false;
foreach ($log as $row) {
$this->save($row);
}
$this->_log = [];
}
/**
* 回滚事务
*
* @return void
*/
public function rollback()
{
$this->_transcation = false;
$this->_log = [];
}
...
}
log 的实现
到这里,我们需要实现 log
方法了。
log
需要辨别两种情况:一种是,不存在事务的时候,直接写入文件。一种是,存在事务时,等 commit
再提交。
<?php
abstract class Logger
{
...
/**
* 记录日志
*
* @param string $level
* @param mixed $message
* @return void
*/
protected function log($level, $message)
{
$data = [
'level' => $level,
'message' => $message,
'time' => time()
];
$transcation = $this->_transcation;
if ($transcation) {
$this->_log[] = $data;
} else {
$this->save($data);
}
}
}
设置格式化类
在写入日志的时候,我们总是需要一些漂亮统一的格式,这时,我们需要一个单独的类来进行处理。
<?php
abstract class Logger
{
...
/**
* 格式化类
*
* @var Formatter
*/
protected $_formatHandler;
/**
* 格式化类
*
* @param Formatter $formatHandler
*/
public function setFormatHandler(Formatter $formatHandler)
{
$this->_formatHandler = $formatHandler;
}
...
}
至此,抽象类结束。
文件日志类
在一般系统日志中,文件日志是最为常见的,这里以文件日志为例,实现上述抽象类。
初始化类
按照日常习惯,我们这个类有打开、写入、关闭的功能,同时,我们需要指定日志地址,或者其它配置。
<?php
class File extends Adapter
{
/**
* 文件句柄
*
* @var resource
*/
protected $_fileHanlder;
/**
* 日志文件地址
*
* @var string
*/
protected $_filePath;
/**
* 额外配置
*
* @var array
*/
protected $_options = [];
/**
* 构造函数
*
* @param string $name
* @param array $options
*/
function __construct(string $name, array $options = [])
{
$this->_filePath = $name;
$this->_options = $options;
$this->open();
}
/**
* 打开文件
*
* @return void
*/
protected function open()
{
}
/**
* 存储文件
*
* @param array $data
* @return mixed
*/
protected function save(array $data)
{
}
/**
* 关闭文件
*
* @return void
*/
public function close()
{
}
/**
* 析构函数
*/
function __destruct()
{
$this->close();
}
}
完善打开文件
我们需要写入日志,当然,指定的文件地址可能不存在,目录也有可能不存在。
logs/test.log # test.log 不存在
logs/2018/09/18/test.log # 2018/09/18/test.log 不存在
所以我们需要考虑所有情况,不存在则创建。
<?php
/**
* 打开文件
*
* @return void
*/
protected function open()
{
$filePath = $this->_filePath;
// 获取文件目录
$dir = dirname($filePath);
// 检测创建目录
if (!is_dir($dir)) {
if (!mkdir($dir, 0666, true)) {
throw new \Exception('文件目录创建失败,目录:' . $dir, -__LINE__);
}
}
if (!is_writable($dir)) {
throw new \Exception('不存在创建目录的权限,请分配后重试,目录:' . $dir, -__LINE__);
}
// 检测创建文件
if (!is_file($filePath)) {
if (!touch($filePath)) {
throw new \Exception('文件创建失败,文件:' . $filePath, -__LINE__);
}
}
// 开启文件,后续可以把 mode 交给配置参数
$fileHanlder = @fopen($filePath, 'a');
$this->_fileHanlder = $fileHanlder;
}
完善保存文件
到这里是真正写入了,上面我们为了能够让日志输出的更加漂亮,添加了一个 Formatter
,它将在这里起作用。
<?php
/**
* 存储文件
*
* @param array $data
* @return mixed
*/
protected function save(array $data)
{
// 文件路径
$filePath = $this->_filePath;
// 格式化句柄
$formatHandler = $this->_formatHandler;
if (!$formatHandler) {
throw new \Exception('未指定格式化类型', -__LINE__);
}
// 文件句柄
$fileHanlder = $this->_fileHanlder;
// 如果不是资源类型,可能在创建目录或者创建文件时失败,也有可能没有写权限
if (!is_resource($fileHanlder)) {
throw new \Exception('文件资源错误', -__LINE__);
}
// 写入文件
fwrite($fileHanlder, $formatHandler->format($data['level'], $data['message'], $data['time']));
}
完善关闭文件
关闭什么面特别的,直接上代码。
<?php
/**
* 关闭文件
*
* @return void
*/
public function close()
{
$fileHanlder = $this->_fileHanlder;
fclose($fileHanlder);
}
格式化抽象类
相对于日志抽象类,格式化抽象类就简单多了。
我们需要让其按照我们想要的格式写入日志,那么我们就需要一个配置格式的参数。由于日期比较特殊,这里也给个配置的参数。
当然,既然是格式化,那当然少不了格式化的方法了。
<?php
abstract class Formatter
{
/**
* 消息体格式化
*
* @var string
*/
protected $_format = '[:date:] [:level:] :message:';
/**
* 日期格式化
*
* @var string
*/
protected $_dateFormat = 'Y-m-d H:i:s';
/**
* 构造函数
*
* @param string $format
* @param string $dateFormat
*/
function __construct(string $format = '', string $dateFormat = '')
{
if ($format) {
$this->_format = $format;
}
if ($dateFormat) {
$this->_dateFormat = $dateFormat;
}
}
/**
* 格式化
*
* @param string $level
* @param string $message
* @param string $time
* @return mixed
*/
abstract public function format(string $level, string $message, string $time);
}
行格式化类
这里以行格式化作为例子。
我们需要按照指定的格式来渲染每一行记录,我在设定配置的时候,个体每个类型,如:date
、level
、message
,两边都添加了 :
,主要用作区分。
这里使用 str_replace
可以批量操作。
<?php
class Line extends Formatter
{
/**
* 格式化
*
* @param string $level
* @param string $message
* @param string $time
* @return string
*/
public function format(string $level, string $message, string $time)
{
$format = $this->_format;
$dateFormat = $this->_dateFormat;
$date = date($dateFormat, $time);
return str_replace([':date:', ':level:', ':message:'], [$date, $level, $message], $format) . PHP_EOL;
}
}
到此,整个日志模块就告一段落了。
完整代码
本来准备放的,但是涉及四个文件,而且代码总数也蛮长的,这里就不放了,有需要的小伙伴可以去我的代码库里看。
总结
本来是想用一个类来解决掉的,也没有想写那么多,但是写写就发现需要更好的设计。
下一篇《事件管理模块》
网友评论