0x01 前言
本来是这样的,继续是smile 师傅的那篇文章,文章中提到了可以用包含日志, 但是一开始我输入:
http://127.0.0.1/public/index.php/index/index/inde
这种居然没有产生日志文件,一开始以为thinkphp 会按照日志级别来处理,也就是说我设置了 info 的话,error 级别的日志也会记录。后来发现thinkphp 中config.php 中规定了记录哪些类型的日志才会记录这种类型的日志,不是按照级别高低来的。但是后来就算在config 中添加了error ,也不会产生日志。
但是后来这样:
http://127.0.0.1/public/index.php?s=index/think\app/invokefunction&function=call_user_&vars[0]=system&vars[1][]=whoami
(这里调用了一个不存在的方法call_user_() )
会记录到日志中
所以跟一跟源码来看看造成这种情况的原因。
0x02 日志记录
Log::save() 方法:
thinkphp 官方文档也说了,在请求结束的时候会自动调用Log::save()
可以看到152行到154行也确实如此,判断Log::$log 中有没有数据,没有的话,直接return .
然后是看是否Log::driver 为null ,是null 的话,说明还没有初始化Log,这个时候调用Log::init($config=[])来初始化Log ,包括初始化Log::driver ,以及把$config 赋值到 Log::$config 中
这里可以看到如果App::$debug 模式是开启的话,就会record 一个记录
我们看到Log::record 其实就是,把$msg $type ,写到Log::$log 中
然后接着Log::save() 函数,可以看到163 行到 175行,其实日志记录并不是按照优先级高的就也会记录优先级低的,而是一个个匹配,config 中写的是啥type就记录啥type . 将要写的日志记录到$log 变量中。
当然如果config 中的type 为[]的话,就会默认读取全部日志。
然后179行,调用Log::$driver->save($log,true) 方法来真正写入日志: 这里分析 think\log\driver::File 类中的save
首先49行,获取日志文件名,可以看到88 行会判断是否开启single 文件名,然后95行,会判断max_files ,95行到103 还是挺有趣的,利用了glob协议函数,来得到文件数目。
接着可以看到75 调用了 Log::write() 来进行文件写入,跟进Log::write() 函数,发现其实是通过error_log() 函数来写入日志的:
error_log() 函数第三个参数为3 的时候,会把$message 写入到目标文件中。
0x03 错误和异常处理
先提一个和dispatch 有关的:
对于dispatch() 中类名没有找到的话,
可以在index/Error 中自定义控制器,来处理(这个Error,来自于
e
m
p
t
y
,
empty ,
empty,emtpy 是从$config[‘empty_controller’]) 中得到的。
然后正式看看thinkphp 中的异常处理: Error::appException() 所有异常的入口
先看46行,handler:
可以看到handle 可以在config.php 中配置,默认配置是空 (可以看到121行说明可以自定义一handle class ,需要继承\think\excption\Handle 类)官方文档中也提到可以自定义一个handle ,继承Hanle 类,然后需要实现render 方法: https://www.kancloud.cn/manual/thinkphp5/126075
那么就会使用默认的Hanle 类,也就是 \think\exception\Handle 类: [这里的26行可以看到设置了**$ignoreReport** 为 HttpException,这也就是为什么控制器没找到的错误不会记录到日志中]
接着看上面第二个图中,47行调用$handle->report() 函数:
42行,调用了Handler->isIgnoreReport() ,跟进
发现,如果这个$exception 是Hanle::ignoreReport 的类的实例的话,就会返回ture ,也就是不会记录日志。
这也就是为什么 index/inde 不会记录到日志中,而call_user_array_fun_ 会记录到日志中,因为前者是HttpException ,而后者是其他的PHP 内部错误,而且没有错误处理啥的,前面的在Handle 中设置为了IgnoreReport .
再看看怎么抛出异常:
比如App::module() 方法中,模块,控制器,action 没找到都会throw HttpException 对象,这也就是为什么没有记录到日志中的原因
模块不存在的话,也会throw 一个HttpException 对象:
对于控制器不存在,先抛出ClassNotFoundException ,然后catch 之后,也变成throw 一个 HttpException
操作不存在的话,也会throw 一个HtppException 对象
而对于反射中调用不存在的方法名的时候,会抛出 ReflectionException 错误
还有一个问题,为什么发生错误或者异常的时候,会进入think\Error 的appError() 和appException() 函数呢,然后程序结束的时候会调用appShutdown() 函数呢,原来在base.php 中调用了Error::register() 函数来注册错误和异常处理机制;
跟进 ,可以看到具体是通过调用了四个错误异常有关的函数来进行注册的
最后说一说php 中的错误和异常:
php 中任何自身的错误和非正常的代码都会被当作错误来处理,而不是以异常抛出。(这点和java 不一样)
https://segmentfault.com/a/1190000009504337
所以框架中在set_error_handler()中相当于杀了个回马枪,再把错误信息以异常的形式抛出来,这样就可以实现错误以异常的形式抛出。大家要注意:这样做是有缺点的,会受到set_error_handler()函数捕获级别的限制。他能够捕获一部分错误,不能捕获系统级E_ERROR、E_PARSE等错误
对于set_error_handler() 函数会绕过正常的php error_reporting ,直接走用户定义的函数
另外对于 set_error_handler() 函数执行之后不会退出程序,但是set_excption_hanler() 函数执行后会退出程序
set_error_handler:
可以看到程序会继续进行,输出 1
set_exception_handler:
可以看到set_exception_hander 之后,程序并没有输出1 ,而是直接退出了。
此外,在看smile 师傅的wp 中,对于用error_reporting(0) 来 绕过thinkphp 报错不是很理解。
因为set_error_hanlder() 如果没设置第二个参数的话,会忽略error_reporting() 的值,都会走设置好的handler 函数。
而且thinkphp 官方文档中有一段话:
说可以通过error_reporting() 来设置报错级别,
仔细看了看源码之后,恍然大悟,其实64 行,会取出当前 error_reporting()的值,与上$errono ,如果不为0 ,就抛出一个exception ,托管给 Exception 去处理。那么加入托管给了Exception 处理,程序就会终止。而如果结果不会0 ,就走68行 report 日志,这样程序会继续进行不会终止的。
对于 error_reporting() 和 $errono 与的这个操作:
error_reporting() 返回的是当前的设置了level ,有如下级别:
看到其实这些常量 对应的二进制就是:
比如 error_reporting() 设置为 0 的话,显然不管$errno 是多少,都是0;
再比如error_reporting() 设置的是 ERROR_WARING 的话,对于的二进制就是10,如果$errno 是 2 的话,那么与上的结果还是 2 ,不为0 ,这个时候就会就会抛出异常。
0x04 后记
跟完源码后发现,之所以/index/index/inde 没有被记录到日志中有两个原因:
(1) config.php 添加了log level ,但是没有添加error level 。(所以想要记录错误日志,要么就是添加上error ,要么就是使用默认的 [] 。)
(2) 就算开启了error 的日志,因为/index/index/inde 这种方法名不存在(控制器,模块不存在也一样),在thinkphp 中都处理成了 抛出HttpException 异常,对于控制器不存在本身是会抛出ClassNotFoundException,但是thinkphp 中会 catch 这个异常,然后throw 一个 HttpException。而HttpException 在Error::hanle 类中设置在了 $ignoreReport 中.
42行可以看到调用 Error::report() 函数中,对于在$ignoreReport 数组中的Excption 不会进行日志记录。