引言
Yii2提供了很灵活的日志处理方式,可以页面打印输出,也可以保存数据库,还可以给负责人发送邮件通知错误异常等, 其实处理方式需要我们自己组件拓展支持,整体与原生框架写法差不多。
记录一次日志异常经历。有一次项目上线后,发现ES中无操作日志,这个问题比较严重。
记录日志的地方使用了Yii::trace()的方式,研究之后才发现,trace方式,在dev开发环境会记录,在生产环境不会记录,原因就是YII_DEBUG的配置决定的。 我们一般会在入口文件中配置YII_DEBUG,在生产环境不用配置:
defined('YII_DEBUG') or define('YII_DEBUG', true);
一直开发业务,框架配置部分忘记了。为什么这里配置了YII_DEBUG为true就会记录日志呢,我们需要分析框架,看一下vendor/yiisoft/BaseYii.php中:
...
defined('YII_DEBUG') or define('YII_DEBUG', false);
defined('YII_ENV') or define('YII_ENV', 'prod');
...
class BaseYii
{
...
private static $_logger;
public static function getLogger()
{
if (self::$_logger !== null) {
return self::$_logger;
} else {
return self::$_logger = static::createObject('yii\log\Logger');
}
}
public static function setLogger($logger)
{
self::$_logger = $logger;
}
public static function trace($message, $category = 'application')
{
if (YII_DEBUG) {
static::getLogger()->log($message, Logger::LEVEL_TRACE, $category);
}
}
public static function error($message, $category = 'application')
{
static::getLogger()->log($message, Logger::LEVEL_ERROR, $category);
}
public static function warning($message, $category = 'application')
{
static::getLogger()->log($message, Logger::LEVEL_WARNING, $category);
}
public static function info($message, $category = 'application')
{
static::getLogger()->log($message, Logger::LEVEL_INFO, $category);
}
public static function beginProfile($token, $category = 'application')
{
static::getLogger()->log($token, Logger::LEVEL_PROFILE_BEGIN, $category);
}
public static function endProfile($token, $category = 'application')
{
static::getLogger()->log($token, Logger::LEVEL_PROFILE_END, $category);
}
...
}
可以看到Yii工具类完整实现了日志功能,在生产环境除了trace型,其他类型日志都会记录,因为在trace()方法中先判断了YII_DEBUG是否为真。
流程追踪
Yii是组件式框架,所有功能都基于组件,组件封装的很完整。
我们先看一下日志从产生到持久化的过程。
当错误产生时,errorHandler组件的函数handleFatalError
、handleError
、handleException
调用Yii::error($exception, $category);
,
Yii::error()
再调用yii\log\Logger
对象的log()
函数,把该条错误信息写入该对象内部的$messages
数组变量中。
当一次又一次的往$messages
数组变量中写入信息(error、waring、info、trace类别等)时,
条数大于$flushInterval
就会调用内部的flush()
函数,
核心日志组件log
对应的类是'log' => ['class' => 'yii\log\Dispatcher'],
,初始化时会把内部的$_logger
变量赋值为yii\log\Logger
对象,
yii\log\Logger
对象的$dispatcher
变量又赋值为log
组件,同时log
日志组件的flushInterval
参数初始化时调用setFlushInterval()
函数,
会把yii\log\Logger
对象的$flushInterval
变量赋值为该参数值,这也就是上面yii\log\Logger
对象的$flushInterval
的来源。
还有一个就是log
日志组件初始化时,会把targets
参数循环写入到内部的$targets
变量数组中。
yii\log\Logger
对象调用内部的flush()
函数,flush()
函数又调用内部的$dispatcher
变量(实际上是log
组件)的dispatch($messages, $final)
方法,
dispatch()
方法再循环调用$targets
变量数组中的Target
对象的collect($messages, $final)
方法,collect()
方法会把flush()
过来的信息,
与该对象内部已存在的$messages
数组数据合并,当数据条数大于$exportInterval
时,会调用继承类的export()
方法,
export()
方法中就是我们写数据持久化逻辑的地方,如写入日志文件、或写入数据库等。
日志组件
yii\base\Application
中有:
public function coreComponents()
{
return [
'log' => ['class' => 'yii\log\Dispatcher'],
'view' => ['class' => 'yii\web\View'],
'formatter' => ['class' => 'yii\i18n\Formatter'],
'i18n' => ['class' => 'yii\i18n\I18N'],
'mailer' => ['class' => 'yii\swiftmailer\Mailer'],
'urlManager' => ['class' => 'yii\web\UrlManager'],
'assetManager' => ['class' => 'yii\web\AssetManager'],
'security' => ['class' => 'yii\base\Security'],
];
}
yii\log\Dispatcher
类:
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\log;
use Yii;
use yii\base\Component;
use yii\base\ErrorHandler;
/**
* Dispatcher manages a set of [[Target|log targets]].
*
* Dispatcher implements the [[dispatch()]]-method that forwards the log messages from a [[Logger]] to
* the registered log [[targets]].
*
* An instance of Dispatcher is registered as a core application component and can be accessed using `Yii::$app->log`.
*
* You may configure the targets in application configuration, like the following:
*
*
* [
* 'components' => [
* 'log' => [
* 'targets' => [
* 'file' => [
* 'class' => 'yii\log\FileTarget',
* 'levels' => ['trace', 'info'],
* 'categories' => ['yii\*'],
* ],
* 'email' => [
* 'class' => 'yii\log\EmailTarget',
* 'levels' => ['error', 'warning'],
* 'message' => [
* 'to' => 'admin@example.com',
* ],
* ],
* ],
* ],
* ],
* ]
*
*
* Each log target can have a name and can be referenced via the [[targets]] property as follows:
*
*
* Yii::$app->log->targets['file']->enabled = false;
*
*
* @property int $flushInterval How many messages should be logged before they are sent to targets. This
* method returns the value of [[Logger::flushInterval]].
* @property Logger $logger The logger. If not set, [[\Yii::getLogger()]] will be used. Note that the type of
* this property differs in getter and setter. See [[getLogger()]] and [[setLogger()]] for details.
* @property int $traceLevel How many application call stacks should be logged together with each message.
* This method returns the value of [[Logger::traceLevel]]. Defaults to 0.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class Dispatcher extends Component
{
/**
* @var array|Target[] the log targets. Each array element represents a single [[Target|log target]] instance
* or the configuration for creating the log target instance.
*/
public $targets = [];
/**
* @var Logger the logger.
*/
private $_logger;
/**
* {@inheritdoc}
*/
public function __construct($config = [])
{
// ensure logger gets set before any other config option
if (isset($config['logger'])) {
$this->setLogger($config['logger']);
unset($config['logger']);
}
// connect logger and dispatcher
$this->getLogger();
parent::__construct($config);
}
/**
* {@inheritdoc}
*/
public function init()
{
parent::init();
foreach ($this->targets as $name => $target) {
if (!$target instanceof Target) {
$this->targets[$name] = Yii::createObject($target);
}
}
}
/**
* Gets the connected logger.
* If not set, [[\Yii::getLogger()]] will be used.
* @property Logger the logger. If not set, [[\Yii::getLogger()]] will be used.
* @return Logger the logger.
*/
public function getLogger()
{
if ($this->_logger === null) {
$this->setLogger(Yii::getLogger());
}
return $this->_logger;
}
/**
* Sets the connected logger.
* @param Logger|string|array $value the logger to be used. This can either be a logger instance
* or a configuration that will be used to create one using [[Yii::createObject()]].
*/
public function setLogger($value)
{
if (is_string($value) || is_array($value)) {
$value = Yii::createObject($value);
}
$this->_logger = $value;
$this->_logger->dispatcher = $this;
}
/**
* @return int how many application call stacks should be logged together with each message.
* This method returns the value of [[Logger::traceLevel]]. Defaults to 0.
*/
public function getTraceLevel()
{
return $this->getLogger()->traceLevel;
}
/**
* @param int $value how many application call stacks should be logged together with each message.
* This method will set the value of [[Logger::traceLevel]]. If the value is greater than 0,
* at most that number of call stacks will be logged. Note that only application call stacks are counted.
* Defaults to 0.
*/
public function setTraceLevel($value)
{
$this->getLogger()->traceLevel = $value;
}
/**
* @return int how many messages should be logged before they are sent to targets.
* This method returns the value of [[Logger::flushInterval]].
*/
public function getFlushInterval()
{
return $this->getLogger()->flushInterval;
}
/**
* @param int $value how many messages should be logged before they are sent to targets.
* This method will set the value of [[Logger::flushInterval]].
* Defaults to 1000, meaning the [[Logger::flush()]] method will be invoked once every 1000 messages logged.
* Set this property to be 0 if you don't want to flush messages until the application terminates.
* This property mainly affects how much memory will be taken by the logged messages.
* A smaller value means less memory, but will increase the execution time due to the overhead of [[Logger::flush()]].
*/
public function setFlushInterval($value)
{
$this->getLogger()->flushInterval = $value;
}
/**
* Dispatches the logged messages to [[targets]].
* @param array $messages the logged messages
* @param bool $final whether this method is called at the end of the current application
*/
public function dispatch($messages, $final)
{
$targetErrors = [];
foreach ($this->targets as $target) {
if (!$target->enabled) {
continue;
}
try {
$target->collect($messages, $final);
} catch (\Throwable $t) {
$target->enabled = false;
$targetErrors[] = $this->generateTargetFailErrorMessage($target, $t, __METHOD__);
} catch (\Exception $e) {
$target->enabled = false;
$targetErrors[] = $this->generateTargetFailErrorMessage($target, $e, __METHOD__);
}
}
if (!empty($targetErrors)) {
$this->dispatch($targetErrors, true);
}
}
/**
* Generate target error message
*
* @param Target $target log target object
* @param \Throwable|\Exception $throwable catched exception
* @param string $method full method path
* @return array generated error message data
* @since 2.0.32
*/
protected function generateTargetFailErrorMessage($target, $throwable, $method)
{
return [
'Unable to send log via ' . get_class($target) . ': ' . ErrorHandler::convertExceptionToVerboseString($throwable),
Logger::LEVEL_WARNING,
$method,
microtime(true),
[],
];
}
}
Target类
log组件默认的Target是FileTarget:
'log' => [
'traceLevel' => YII_DEBUG ? 3 : 0,
'targets' => [
[
'class' => \yii\log\FileTarget::class,
'levels' => ['error', 'warning'],
],
],
],
yii\log\Target
类:
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\log;
use Yii;
use yii\base\Component;
use yii\base\InvalidConfigException;
use yii\helpers\ArrayHelper;
use yii\helpers\StringHelper;
use yii\helpers\VarDumper;
use yii\web\Request;
/**
* Target is the base class for all log target classes.
*
* A log target object will filter the messages logged by [[Logger]] according
* to its [[levels]] and [[categories]] properties. It may also export the filtered
* messages to specific destination defined by the target, such as emails, files.
*
* Level filter and category filter are combinatorial, i.e., only messages
* satisfying both filter conditions will be handled. Additionally, you
* may specify [[except]] to exclude messages of certain categories.
*
* @property bool $enabled Indicates whether this log target is enabled. Defaults to true. Note that the type
* of this property differs in getter and setter. See [[getEnabled()]] and [[setEnabled()]] for details.
* @property int $levels The message levels that this target is interested in. This is a bitmap of level
* values. Defaults to 0, meaning all available levels. Note that the type of this property differs in getter and
* setter. See [[getLevels()]] and [[setLevels()]] for details.
*
* For more details and usage information on Target, see the [guide article on logging & targets](guide:runtime-logging).
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
abstract class Target extends Component
{
/**
* @var array list of message categories that this target is interested in. Defaults to empty, meaning all categories.
* You can use an asterisk at the end of a category so that the category may be used to
* match those categories sharing the same common prefix. For example, 'yii\db\*' will match
* categories starting with 'yii\db\', such as 'yii\db\Connection'.
*/
public $categories = [];
/**
* @var array list of message categories that this target is NOT interested in. Defaults to empty, meaning no uninteresting messages.
* If this property is not empty, then any category listed here will be excluded from [[categories]].
* You can use an asterisk at the end of a category so that the category can be used to
* match those categories sharing the same common prefix. For example, 'yii\db\*' will match
* categories starting with 'yii\db\', such as 'yii\db\Connection'.
* @see categories
*/
public $except = [];
/**
* @var array list of the PHP predefined variables that should be logged in a message.
* Note that a variable must be accessible via `$GLOBALS`. Otherwise it won't be logged.
*
* Defaults to `['_GET', '_POST', '_FILES', '_COOKIE', '_SESSION', '_SERVER']`.
*
* Since version 2.0.9 additional syntax can be used:
* Each element could be specified as one of the following:
*
* - `var` - `var` will be logged.
* - `var.key` - only `var[key]` key will be logged.
* - `!var.key` - `var[key]` key will be excluded.
*
* Note that if you need $_SESSION to logged regardless if session was used you have to open it right at
* the start of your request.
*
* @see \yii\helpers\ArrayHelper::filter()
*/
public $logVars = [
'_GET',
'_POST',
'_FILES',
'_COOKIE',
'_SESSION',
'_SERVER',
];
/**
* @var array list of the PHP predefined variables that should NOT be logged "as is" and should always be replaced
* with a mask `***` before logging, when exist.
*
* Defaults to `[ '_SERVER.HTTP_AUTHORIZATION', '_SERVER.PHP_AUTH_USER', '_SERVER.PHP_AUTH_PW']`
*
* Each element could be specified as one of the following:
*
* - `var` - `var` will be logged as `***`
* - `var.key` - only `var[key]` will be logged as `***`
*
* @since 2.0.16
*/
public $maskVars = [
'_SERVER.HTTP_AUTHORIZATION',
'_SERVER.PHP_AUTH_USER',
'_SERVER.PHP_AUTH_PW',
];
/**
* @var callable a PHP callable that returns a string to be prefixed to every exported message.
*
* If not set, [[getMessagePrefix()]] will be used, which prefixes the message with context information
* such as user IP, user ID and session ID.
*
* The signature of the callable should be `function ($message)`.
*/
public $prefix;
/**
* @var int how many messages should be accumulated before they are exported.
* Defaults to 1000. Note that messages will always be exported when the application terminates.
* Set this property to be 0 if you don't want to export messages until the application terminates.
*/
public $exportInterval = 1000;
/**
* @var array the messages that are retrieved from the logger so far by this log target.
* Please refer to [[Logger::messages]] for the details about the message structure.
*/
public $messages = [];
/**
* @var bool whether to log time with microseconds.
* Defaults to false.
* @since 2.0.13
*/
public $microtime = false;
private $_levels = 0;
private $_enabled = true;
/**
* Exports log [[messages]] to a specific destination.
* Child classes must implement this method.
*/
abstract public function export();
/**
* Processes the given log messages.
* This method will filter the given messages with [[levels]] and [[categories]].
* And if requested, it will also export the filtering result to specific medium (e.g. email).
* @param array $messages log messages to be processed. See [[Logger::messages]] for the structure
* of each message.
* @param bool $final whether this method is called at the end of the current application
*/
public function collect($messages, $final)
{
$this->messages = array_merge($this->messages, static::filterMessages($messages, $this->getLevels(), $this->categories, $this->except));
$count = count($this->messages);
if ($count > 0 && ($final || $this->exportInterval > 0 && $count >= $this->exportInterval)) {
if (($context = $this->getContextMessage()) !== '') {
$this->messages[] = [$context, Logger::LEVEL_INFO, 'application', YII_BEGIN_TIME, [], 0];
}
// set exportInterval to 0 to avoid triggering export again while exporting
$oldExportInterval = $this->exportInterval;
$this->exportInterval = 0;
$this->export();
$this->exportInterval = $oldExportInterval;
$this->messages = [];
}
}
/**
* Generates the context information to be logged.
* The default implementation will dump user information, system variables, etc.
* @return string the context information. If an empty string, it means no context information.
*/
protected function getContextMessage()
{
$context = ArrayHelper::filter($GLOBALS, $this->logVars);
foreach ($this->maskVars as $var) {
if (ArrayHelper::getValue($context, $var) !== null) {
ArrayHelper::setValue($context, $var, '***');
}
}
$result = [];
foreach ($context as $key => $value) {
$result[] = "\${$key} = " . VarDumper::dumpAsString($value);
}
return implode("\n\n", $result);
}
/**
* @return int the message levels that this target is interested in. This is a bitmap of
* level values. Defaults to 0, meaning all available levels.
*/
public function getLevels()
{
return $this->_levels;
}
/**
* Sets the message levels that this target is interested in.
*
* The parameter can be either an array of interested level names or an integer representing
* the bitmap of the interested level values. Valid level names include: 'error',
* 'warning', 'info', 'trace' and 'profile'; valid level values include:
* [[Logger::LEVEL_ERROR]], [[Logger::LEVEL_WARNING]], [[Logger::LEVEL_INFO]],
* [[Logger::LEVEL_TRACE]] and [[Logger::LEVEL_PROFILE]].
*
* For example,
*
* ```php
* ['error', 'warning']
* // which is equivalent to:
* Logger::LEVEL_ERROR | Logger::LEVEL_WARNING
* ```
*
* @param array|int $levels message levels that this target is interested in.
* @throws InvalidConfigException if $levels value is not correct.
*/
public function setLevels($levels)
{
static $levelMap = [
'error' => Logger::LEVEL_ERROR,
'warning' => Logger::LEVEL_WARNING,
'info' => Logger::LEVEL_INFO,
'trace' => Logger::LEVEL_TRACE,
'profile' => Logger::LEVEL_PROFILE,
];
if (is_array($levels)) {
$this->_levels = 0;
foreach ($levels as $level) {
if (isset($levelMap[$level])) {
$this->_levels |= $levelMap[$level];
} else {
throw new InvalidConfigException("Unrecognized level: $level");
}
}
} else {
$bitmapValues = array_reduce($levelMap, function ($carry, $item) {
return $carry | $item;
});
if (!($bitmapValues & $levels) && $levels !== 0) {
throw new InvalidConfigException("Incorrect $levels value");
}
$this->_levels = $levels;
}
}
/**
* Filters the given messages according to their categories and levels.
* @param array $messages messages to be filtered.
* The message structure follows that in [[Logger::messages]].
* @param int $levels the message levels to filter by. This is a bitmap of
* level values. Value 0 means allowing all levels.
* @param array $categories the message categories to filter by. If empty, it means all categories are allowed.
* @param array $except the message categories to exclude. If empty, it means all categories are allowed.
* @return array the filtered messages.
*/
public static function filterMessages($messages, $levels = 0, $categories = [], $except = [])
{
foreach ($messages as $i => $message) {
if ($levels && !($levels & $message[1])) {
unset($messages[$i]);
continue;
}
$matched = empty($categories);
foreach ($categories as $category) {
if ($message[2] === $category || !empty($category) && substr_compare($category, '*', -1, 1) === 0 && strpos($message[2], rtrim($category, '*')) === 0) {
$matched = true;
break;
}
}
if ($matched) {
foreach ($except as $category) {
$prefix = rtrim($category, '*');
if (($message[2] === $category || $prefix !== $category) && strpos($message[2], $prefix) === 0) {
$matched = false;
break;
}
}
}
if (!$matched) {
unset($messages[$i]);
}
}
return $messages;
}
/**
* Formats a log message for display as a string.
* @param array $message the log message to be formatted.
* The message structure follows that in [[Logger::messages]].
* @return string the formatted message
*/
public function formatMessage($message)
{
list($text, $level, $category, $timestamp) = $message;
$level = Logger::getLevelName($level);
if (!is_string($text)) {
// exceptions may not be serializable if in the call stack somewhere is a Closure
if ($text instanceof \Throwable || $text instanceof \Exception) {
$text = (string) $text;
} else {
$text = VarDumper::export($text);
}
}
$traces = [];
if (isset($message[4])) {
foreach ($message[4] as $trace) {
$traces[] = "in {$trace['file']}:{$trace['line']}";
}
}
$prefix = $this->getMessagePrefix($message);
return $this->getTime($timestamp) . " {$prefix}[$level][$category] $text"
. (empty($traces) ? '' : "\n " . implode("\n ", $traces));
}
/**
* Returns a string to be prefixed to the given message.
* If [[prefix]] is configured it will return the result of the callback.
* The default implementation will return user IP, user ID and session ID as a prefix.
* @param array $message the message being exported.
* The message structure follows that in [[Logger::messages]].
* @return string the prefix string
*/
public function getMessagePrefix($message)
{
if ($this->prefix !== null) {
return call_user_func($this->prefix, $message);
}
if (Yii::$app === null) {
return '';
}
$request = Yii::$app->getRequest();
$ip = $request instanceof Request ? $request->getUserIP() : '-';
/* @var $user \yii\web\User */
$user = Yii::$app->has('user', true) ? Yii::$app->get('user') : null;
if ($user && ($identity = $user->getIdentity(false))) {
$userID = $identity->getId();
} else {
$userID = '-';
}
/* @var $session \yii\web\Session */
$session = Yii::$app->has('session', true) ? Yii::$app->get('session') : null;
$sessionID = $session && $session->getIsActive() ? $session->getId() : '-';
return "[$ip][$userID][$sessionID]";
}
/**
* Sets a value indicating whether this log target is enabled.
* @param bool|callable $value a boolean value or a callable to obtain the value from.
* The callable value is available since version 2.0.13.
*
* A callable may be used to determine whether the log target should be enabled in a dynamic way.
* For example, to only enable a log if the current user is logged in you can configure the target
* as follows:
*
* ```php
* 'enabled' => function() {
* return !Yii::$app->user->isGuest;
* }
* ```
*/
public function setEnabled($value)
{
$this->_enabled = $value;
}
/**
* Check whether the log target is enabled.
* @property bool Indicates whether this log target is enabled. Defaults to true.
* @return bool A value indicating whether this log target is enabled.
*/
public function getEnabled()
{
if (is_callable($this->_enabled)) {
return call_user_func($this->_enabled, $this);
}
return $this->_enabled;
}
/**
* Returns formatted ('Y-m-d H:i:s') timestamp for message.
* If [[microtime]] is configured to true it will return format 'Y-m-d H:i:s.u'.
* @param float $timestamp
* @return string
* @since 2.0.13
*/
protected function getTime($timestamp)
{
$parts = explode('.', sprintf('%F', $timestamp));
return date('Y-m-d H:i:s', $parts[0]) . ($this->microtime ? ('.' . $parts[1]) : '');
}
}
FileTarget
yii\log\FileTarget
类:
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\log;
use Yii;
use yii\base\InvalidConfigException;
use yii\helpers\FileHelper;
/**
* FileTarget records log messages in a file.
*
* The log file is specified via [[logFile]]. If the size of the log file exceeds
* [[maxFileSize]] (in kilo-bytes), a rotation will be performed, which renames
* the current log file by suffixing the file name with '.1'. All existing log
* files are moved backwards by one place, i.e., '.2' to '.3', '.1' to '.2', and so on.
* The property [[maxLogFiles]] specifies how many history files to keep.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class FileTarget extends Target
{
/**
* @var string log file path or [path alias](guide:concept-aliases). If not set, it will use the "@runtime/logs/app.log" file.
* The directory containing the log files will be automatically created if not existing.
*/
public $logFile;
/**
* @var bool whether log files should be rotated when they reach a certain [[maxFileSize|maximum size]].
* Log rotation is enabled by default. This property allows you to disable it, when you have configured
* an external tools for log rotation on your server.
* @since 2.0.3
*/
public $enableRotation = true;
/**
* @var int maximum log file size, in kilo-bytes. Defaults to 10240, meaning 10MB.
*/
public $maxFileSize = 10240; // in KB
/**
* @var int number of log files used for rotation. Defaults to 5.
*/
public $maxLogFiles = 5;
/**
* @var int the permission to be set for newly created log files.
* This value will be used by PHP chmod() function. No umask will be applied.
* If not set, the permission will be determined by the current environment.
*/
public $fileMode;
/**
* @var int the permission to be set for newly created directories.
* This value will be used by PHP chmod() function. No umask will be applied.
* Defaults to 0775, meaning the directory is read-writable by owner and group,
* but read-only for other users.
*/
public $dirMode = 0775;
/**
* @var bool Whether to rotate log files by copy and truncate in contrast to rotation by
* renaming files. Defaults to `true` to be more compatible with log tailers and is windows
* systems which do not play well with rename on open files. Rotation by renaming however is
* a bit faster.
*
* The problem with windows systems where the [rename()](https://secure.php.net/manual/en/function.rename.php)
* function does not work with files that are opened by some process is described in a
* [comment by Martin Pelletier](https://secure.php.net/manual/en/function.rename.php#102274) in
* the PHP documentation. By setting rotateByCopy to `true` you can work
* around this problem.
*/
public $rotateByCopy = true;
/**
* Initializes the route.
* This method is invoked after the route is created by the route manager.
*/
public function init()
{
parent::init();
if ($this->logFile === null) {
$this->logFile = Yii::$app->getRuntimePath() . '/logs/app.log';
} else {
$this->logFile = Yii::getAlias($this->logFile);
}
if ($this->maxLogFiles < 1) {
$this->maxLogFiles = 1;
}
if ($this->maxFileSize < 1) {
$this->maxFileSize = 1;
}
}
/**
* Writes log messages to a file.
* Starting from version 2.0.14, this method throws LogRuntimeException in case the log can not be exported.
* @throws InvalidConfigException if unable to open the log file for writing
* @throws LogRuntimeException if unable to write complete log to file
*/
public function export()
{
if (strpos($this->logFile, '://') === false || strncmp($this->logFile, 'file://', 7) === 0) {
$logPath = dirname($this->logFile);
FileHelper::createDirectory($logPath, $this->dirMode, true);
}
$text = implode("\n", array_map([$this, 'formatMessage'], $this->messages)) . "\n";
if (($fp = @fopen($this->logFile, 'a')) === false) {
throw new InvalidConfigException("Unable to append to log file: {$this->logFile}");
}
@flock($fp, LOCK_EX);
if ($this->enableRotation) {
// clear stat cache to ensure getting the real current file size and not a cached one
// this may result in rotating twice when cached file size is used on subsequent calls
clearstatcache();
}
if ($this->enableRotation && @filesize($this->logFile) > $this->maxFileSize * 1024) {
@flock($fp, LOCK_UN);
@fclose($fp);
$this->rotateFiles();
$writeResult = @file_put_contents($this->logFile, $text, FILE_APPEND | LOCK_EX);
if ($writeResult === false) {
$error = error_get_last();
throw new LogRuntimeException("Unable to export log through file!: {$error['message']}");
}
$textSize = strlen($text);
if ($writeResult < $textSize) {
throw new LogRuntimeException("Unable to export whole log through file! Wrote $writeResult out of $textSize bytes.");
}
} else {
$writeResult = @fwrite($fp, $text);
if ($writeResult === false) {
$error = error_get_last();
throw new LogRuntimeException("Unable to export log through file!: {$error['message']}");
}
$textSize = strlen($text);
if ($writeResult < $textSize) {
throw new LogRuntimeException("Unable to export whole log through file! Wrote $writeResult out of $textSize bytes.");
}
@flock($fp, LOCK_UN);
@fclose($fp);
}
if ($this->fileMode !== null) {
@chmod($this->logFile, $this->fileMode);
}
}
/**
* Rotates log files.
*/
protected function rotateFiles()
{
$file = $this->logFile;
for ($i = $this->maxLogFiles; $i >= 0; --$i) {
// $i == 0 is the original log file
$rotateFile = $file . ($i === 0 ? '' : '.' . $i);
if (is_file($rotateFile)) {
// suppress errors because it's possible multiple processes enter into this section
if ($i === $this->maxLogFiles) {
@unlink($rotateFile);
continue;
}
$newFile = $this->logFile . '.' . ($i + 1);
$this->rotateByCopy ? $this->rotateByCopy($rotateFile, $newFile) : $this->rotateByRename($rotateFile, $newFile);
if ($i === 0) {
$this->clearLogFile($rotateFile);
}
}
}
}
/***
* Clear log file without closing any other process open handles
* @param string $rotateFile
*/
private function clearLogFile($rotateFile)
{
if ($filePointer = @fopen($rotateFile, 'a')) {
@ftruncate($filePointer, 0);
@fclose($filePointer);
}
}
/***
* Copy rotated file into new file
* @param string $rotateFile
* @param string $newFile
*/
private function rotateByCopy($rotateFile, $newFile)
{
@copy($rotateFile, $newFile);
if ($this->fileMode !== null) {
@chmod($newFile, $this->fileMode);
}
}
/**
* Renames rotated file into new file
* @param string $rotateFile
* @param string $newFile
*/
private function rotateByRename($rotateFile, $newFile)
{
@rename($rotateFile, $newFile);
}
}
EmailTarget
yii\log\EmailTarget
类:
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\log;
use Yii;
use yii\base\InvalidConfigException;
use yii\di\Instance;
use yii\mail\MailerInterface;
/**
* EmailTarget sends selected log messages to the specified email addresses.
*
* You may configure the email to be sent by setting the [[message]] property, through which
* you can set the target email addresses, subject, etc.:
*
*
* 'components' => [
* 'log' => [
* 'targets' => [
* [
* 'class' => 'yii\log\EmailTarget',
* 'mailer' => 'mailer',
* 'levels' => ['error', 'warning'],
* 'message' => [
* 'from' => ['log@example.com'],
* 'to' => ['developer1@example.com', 'developer2@example.com'],
* 'subject' => 'Log message',
* ],
* ],
* ],
* ],
* ],
* `
*
* In the above `mailer` is ID of the component that sends email and should be already configured.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class EmailTarget extends Target
{
/**
* @var array the configuration array for creating a [[\yii\mail\MessageInterface|message]] object.
* Note that the "to" option must be set, which specifies the destination email address(es).
*/
public $message = [];
/**
* @var MailerInterface|array|string the mailer object or the application component ID of the mailer object.
* After the EmailTarget object is created, if you want to change this property, you should only assign it
* with a mailer object.
* Starting from version 2.0.2, this can also be a configuration array for creating the object.
*/
public $mailer = 'mailer';
/**
* {@inheritdoc}
*/
public function init()
{
parent::init();
if (empty($this->message['to'])) {
throw new InvalidConfigException('The "to" option must be set for EmailTarget::message.');
}
$this->mailer = Instance::ensure($this->mailer, 'yii\mail\MailerInterface');
}
/**
* Sends log messages to specified email addresses.
* Starting from version 2.0.14, this method throws LogRuntimeException in case the log can not be exported.
* @throws LogRuntimeException
*/
public function export()
{
// moved initialization of subject here because of the following issue
// https://github.com/yiisoft/yii2/issues/1446
if (empty($this->message['subject'])) {
$this->message['subject'] = 'Application Log';
}
$messages = array_map([$this, 'formatMessage'], $this->messages);
$body = wordwrap(implode("\n", $messages), 70);
$message = $this->composeMessage($body);
if (!$message->send($this->mailer)) {
throw new LogRuntimeException('Unable to export log through email!');
}
}
/**
* Composes a mail message with the given body content.
* @param string $body the body content
* @return \yii\mail\MessageInterface $message
*/
protected function composeMessage($body)
{
$message = $this->mailer->compose();
Yii::configure($message, $this->message);
$message->setTextBody($body);
return $message;
}
}
DbTarget
yii\log\DbTarget
类:
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\log;
use Yii;
use yii\base\InvalidConfigException;
use yii\db\Connection;
use yii\db\Exception;
use yii\di\Instance;
use yii\helpers\VarDumper;
/**
* DbTarget stores log messages in a database table.
*
* The database connection is specified by [[db]]. Database schema could be initialized by applying migration:
*
*
* yii migrate --migrationPath=@yii/log/migrations/
*
*
* If you don't want to use migration and need SQL instead, files for all databases are in migrations directory.
*
* You may change the name of the table used to store the data by setting [[logTable]].
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class DbTarget extends Target
{
/**
* @var Connection|array|string the DB connection object or the application component ID of the DB connection.
* After the DbTarget object is created, if you want to change this property, you should only assign it
* with a DB connection object.
* Starting from version 2.0.2, this can also be a configuration array for creating the object.
*/
public $db = 'db';
/**
* @var string name of the DB table to store cache content. Defaults to "log".
*/
public $logTable = '';
/**
* Initializes the DbTarget component.
* This method will initialize the [[db]] property to make sure it refers to a valid DB connection.
* @throws InvalidConfigException if [[db]] is invalid.
*/
public function init()
{
parent::init();
$this->db = Instance::ensure($this->db, Connection::className());
}
/**
* Stores log messages to DB.
* Starting from version 2.0.14, this method throws LogRuntimeException in case the log can not be exported.
* @throws Exception
* @throws LogRuntimeException
*/
public function export()
{
if ($this->db->getTransaction()) {
// create new database connection, if there is an open transaction
// to ensure insert statement is not affected by a rollback
$this->db = clone $this->db;
}
$tableName = $this->db->quoteTableName($this->logTable);
$sql = "INSERT INTO $tableName ([[level]], [[category]], [[log_time]], [[prefix]], [[message]])
VALUES (:level, :category, :log_time, :prefix, :message)";
$command = $this->db->createCommand($sql);
foreach ($this->messages as $message) {
list($text, $level, $category, $timestamp) = $message;
if (!is_string($text)) {
// exceptions may not be serializable if in the call stack somewhere is a Closure
if ($text instanceof \Throwable || $text instanceof \Exception) {
$text = (string) $text;
} else {
$text = VarDumper::export($text);
}
}
if ($command->bindValues([
':level' => $level,
':category' => $category,
':log_time' => $timestamp,
':prefix' => $this->getMessagePrefix($message),
':message' => $text,
])->execute() > 0) {
continue;
}
throw new LogRuntimeException('Unable to export log through database!');
}
}
}
SyslogTarget
yii\log\SyslogTarget
类:
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\log;
use Yii;
use yii\helpers\VarDumper;
/**
* SyslogTarget writes log to syslog.
*
* @author miramir <gmiramir@gmail.com>
* @since 2.0
*/
class SyslogTarget extends Target
{
/**
* @var string syslog identity
*/
public $identity;
/**
* @var int syslog facility.
*/
public $facility = LOG_USER;
/**
* @var int openlog options. This is a bitfield passed as the `$option` parameter to [openlog()](https://secure.php.net/openlog).
* Defaults to `null` which means to use the default options `LOG_ODELAY | LOG_PID`.
* @see https://secure.php.net/openlog for available options.
* @since 2.0.11
*/
public $options;
/**
* @var array syslog levels
*/
private $_syslogLevels = [
Logger::LEVEL_TRACE => LOG_DEBUG,
Logger::LEVEL_PROFILE_BEGIN => LOG_DEBUG,
Logger::LEVEL_PROFILE_END => LOG_DEBUG,
Logger::LEVEL_PROFILE => LOG_DEBUG,
Logger::LEVEL_INFO => LOG_INFO,
Logger::LEVEL_WARNING => LOG_WARNING,
Logger::LEVEL_ERROR => LOG_ERR,
];
/**
* {@inheritdoc}
*/
public function init()
{
parent::init();
if ($this->options === null) {
$this->options = LOG_ODELAY | LOG_PID;
}
}
/**
* Writes log messages to syslog.
* Starting from version 2.0.14, this method throws LogRuntimeException in case the log can not be exported.
* @throws LogRuntimeException
*/
public function export()
{
openlog($this->identity, $this->options, $this->facility);
foreach ($this->messages as $message) {
if (syslog($this->_syslogLevels[$message[1]], $this->formatMessage($message)) === false) {
throw new LogRuntimeException('Unable to export log through system log!');
}
}
closelog();
}
/**
* {@inheritdoc}
*/
public function formatMessage($message)
{
list($text, $level, $category, $timestamp) = $message;
$level = Logger::getLevelName($level);
if (!is_string($text)) {
// exceptions may not be serializable if in the call stack somewhere is a Closure
if ($text instanceof \Throwable || $text instanceof \Exception) {
$text = (string) $text;
} else {
$text = VarDumper::export($text);
}
}
$prefix = $this->getMessagePrefix($message);
return "{$prefix}[$level][$category] $text";
}
}
Logger类
yii\log\Logger
类:
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\log;
use Yii;
use yii\base\Component;
/**
* Logger records logged messages in memory and sends them to different targets if [[dispatcher]] is set.
*
* A Logger instance can be accessed via `Yii::getLogger()`. You can call the method [[log()]] to record a single log message.
* For convenience, a set of shortcut methods are provided for logging messages of various severity levels
* via the [[Yii]] class:
*
* - [[Yii::trace()]]
* - [[Yii::error()]]
* - [[Yii::warning()]]
* - [[Yii::info()]]
* - [[Yii::beginProfile()]]
* - [[Yii::endProfile()]]
*
* For more details and usage information on Logger, see the [guide article on logging](guide:runtime-logging).
*
* When the application ends or [[flushInterval]] is reached, Logger will call [[flush()]]
* to send logged messages to different log targets, such as [[FileTarget|file]], [[EmailTarget|email]],
* or [[DbTarget|database]], with the help of the [[dispatcher]].
*
* @property array $dbProfiling The first element indicates the number of SQL statements executed, and the
* second element the total time spent in SQL execution. This property is read-only.
* @property float $elapsedTime The total elapsed time in seconds for current request. This property is
* read-only.
* @property array $profiling The profiling results. Each element is an array consisting of these elements:
* `info`, `category`, `timestamp`, `trace`, `level`, `duration`, `memory`, `memoryDiff`. The `memory` and
* `memoryDiff` values are available since version 2.0.11. This property is read-only.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class Logger extends Component
{
/**
* Error message level. An error message is one that indicates the abnormal termination of the
* application and may require developer's handling.
*/
const LEVEL_ERROR = 0x01;
/**
* Warning message level. A warning message is one that indicates some abnormal happens but
* the application is able to continue to run. Developers should pay attention to this message.
*/
const LEVEL_WARNING = 0x02;
/**
* Informational message level. An informational message is one that includes certain information
* for developers to review.
*/
const LEVEL_INFO = 0x04;
/**
* Tracing message level. An tracing message is one that reveals the code execution flow.
*/
const LEVEL_TRACE = 0x08;
/**
* Profiling message level. This indicates the message is for profiling purpose.
*/
const LEVEL_PROFILE = 0x40;
/**
* Profiling message level. This indicates the message is for profiling purpose. It marks the
* beginning of a profiling block.
*/
const LEVEL_PROFILE_BEGIN = 0x50;
/**
* Profiling message level. This indicates the message is for profiling purpose. It marks the
* end of a profiling block.
*/
const LEVEL_PROFILE_END = 0x60;
/**
* @var array logged messages. This property is managed by [[log()]] and [[flush()]].
* Each log message is of the following structure:
*
* ```
* [
* [0] => message (mixed, can be a string or some complex data, such as an exception object)
* [1] => level (integer)
* [2] => category (string)
* [3] => timestamp (float, obtained by microtime(true))
* [4] => traces (array, debug backtrace, contains the application code call stacks)
* [5] => memory usage in bytes (int, obtained by memory_get_usage()), available since version 2.0.11.
* ]
* ```
*/
public $messages = [];
/**
* @var int how many messages should be logged before they are flushed from memory and sent to targets.
* Defaults to 1000, meaning the [[flush]] method will be invoked once every 1000 messages logged.
* Set this property to be 0 if you don't want to flush messages until the application terminates.
* This property mainly affects how much memory will be taken by the logged messages.
* A smaller value means less memory, but will increase the execution time due to the overhead of [[flush()]].
*/
public $flushInterval = 1000;
/**
* @var int how much call stack information (file name and line number) should be logged for each message.
* If it is greater than 0, at most that number of call stacks will be logged. Note that only application
* call stacks are counted.
*/
public $traceLevel = 0;
/**
* @var Dispatcher the message dispatcher
*/
public $dispatcher;
/**
* Initializes the logger by registering [[flush()]] as a shutdown function.
*/
public function init()
{
parent::init();
register_shutdown_function(function () {
// make regular flush before other shutdown functions, which allows session data collection and so on
$this->flush();
// make sure log entries written by shutdown functions are also flushed
// ensure "flush()" is called last when there are multiple shutdown functions
register_shutdown_function([$this, 'flush'], true);
});
}
/**
* Logs a message with the given type and category.
* If [[traceLevel]] is greater than 0, additional call stack information about
* the application code will be logged as well.
* @param string|array $message the message to be logged. This can be a simple string or a more
* complex data structure that will be handled by a [[Target|log target]].
* @param int $level the level of the message. This must be one of the following:
* `Logger::LEVEL_ERROR`, `Logger::LEVEL_WARNING`, `Logger::LEVEL_INFO`, `Logger::LEVEL_TRACE`,
* `Logger::LEVEL_PROFILE_BEGIN`, `Logger::LEVEL_PROFILE_END`.
* @param string $category the category of the message.
*/
public function log($message, $level, $category = 'application')
{
$time = microtime(true);
$traces = [];
if ($this->traceLevel > 0) {
$count = 0;
$ts = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
array_pop($ts); // remove the last trace since it would be the entry script, not very useful
foreach ($ts as $trace) {
if (isset($trace['file'], $trace['line']) && strpos($trace['file'], YII2_PATH) !== 0) {
unset($trace['object'], $trace['args']);
$traces[] = $trace;
if (++$count >= $this->traceLevel) {
break;
}
}
}
}
$this->messages[] = [$message, $level, $category, $time, $traces, memory_get_usage()];
if ($this->flushInterval > 0 && count($this->messages) >= $this->flushInterval) {
$this->flush();
}
}
/**
* Flushes log messages from memory to targets.
* @param bool $final whether this is a final call during a request.
*/
public function flush($final = false)
{
$messages = $this->messages;
// https://github.com/yiisoft/yii2/issues/5619
// new messages could be logged while the existing ones are being handled by targets
$this->messages = [];
if ($this->dispatcher instanceof Dispatcher) {
$this->dispatcher->dispatch($messages, $final);
}
}
/**
* Returns the total elapsed time since the start of the current request.
* This method calculates the difference between now and the timestamp
* defined by constant `YII_BEGIN_TIME` which is evaluated at the beginning
* of [[\yii\BaseYii]] class file.
* @return float the total elapsed time in seconds for current request.
*/
public function getElapsedTime()
{
return microtime(true) - YII_BEGIN_TIME;
}
/**
* Returns the profiling results.
*
* By default, all profiling results will be returned. You may provide
* `$categories` and `$excludeCategories` as parameters to retrieve the
* results that you are interested in.
*
* @param array $categories list of categories that you are interested in.
* You can use an asterisk at the end of a category to do a prefix match.
* For example, 'yii\db\*' will match categories starting with 'yii\db\',
* such as 'yii\db\Connection'.
* @param array $excludeCategories list of categories that you want to exclude
* @return array the profiling results. Each element is an array consisting of these elements:
* `info`, `category`, `timestamp`, `trace`, `level`, `duration`, `memory`, `memoryDiff`.
* The `memory` and `memoryDiff` values are available since version 2.0.11.
*/
public function getProfiling($categories = [], $excludeCategories = [])
{
$timings = $this->calculateTimings($this->messages);
if (empty($categories) && empty($excludeCategories)) {
return $timings;
}
foreach ($timings as $i => $timing) {
$matched = empty($categories);
foreach ($categories as $category) {
$prefix = rtrim($category, '*');
if (($timing['category'] === $category || $prefix !== $category) && strpos($timing['category'], $prefix) === 0) {
$matched = true;
break;
}
}
if ($matched) {
foreach ($excludeCategories as $category) {
$prefix = rtrim($category, '*');
foreach ($timings as $i => $timing) {
if (($timing['category'] === $category || $prefix !== $category) && strpos($timing['category'], $prefix) === 0) {
$matched = false;
break;
}
}
}
}
if (!$matched) {
unset($timings[$i]);
}
}
return array_values($timings);
}
/**
* Returns the statistical results of DB queries.
* The results returned include the number of SQL statements executed and
* the total time spent.
* @return array the first element indicates the number of SQL statements executed,
* and the second element the total time spent in SQL execution.
*/
public function getDbProfiling()
{
$timings = $this->getProfiling(['yii\db\Command::query', 'yii\db\Command::execute']);
$count = count($timings);
$time = 0;
foreach ($timings as $timing) {
$time += $timing['duration'];
}
return [$count, $time];
}
/**
* Calculates the elapsed time for the given log messages.
* @param array $messages the log messages obtained from profiling
* @return array timings. Each element is an array consisting of these elements:
* `info`, `category`, `timestamp`, `trace`, `level`, `duration`, `memory`, `memoryDiff`.
* The `memory` and `memoryDiff` values are available since version 2.0.11.
*/
public function calculateTimings($messages)
{
$timings = [];
$stack = [];
foreach ($messages as $i => $log) {
list($token, $level, $category, $timestamp, $traces) = $log;
$memory = isset($log[5]) ? $log[5] : 0;
$log[6] = $i;
$hash = md5(json_encode($token));
if ($level == self::LEVEL_PROFILE_BEGIN) {
$stack[$hash] = $log;
} elseif ($level == self::LEVEL_PROFILE_END) {
if (isset($stack[$hash])) {
$timings[$stack[$hash][6]] = [
'info' => $stack[$hash][0],
'category' => $stack[$hash][2],
'timestamp' => $stack[$hash][3],
'trace' => $stack[$hash][4],
'level' => count($stack) - 1,
'duration' => $timestamp - $stack[$hash][3],
'memory' => $memory,
'memoryDiff' => $memory - (isset($stack[$hash][5]) ? $stack[$hash][5] : 0),
];
unset($stack[$hash]);
}
}
}
ksort($timings);
return array_values($timings);
}
/**
* Returns the text display of the specified level.
* @param int $level the message level, e.g. [[LEVEL_ERROR]], [[LEVEL_WARNING]].
* @return string the text display of the level
*/
public static function getLevelName($level)
{
static $levels = [
self::LEVEL_ERROR => 'error',
self::LEVEL_WARNING => 'warning',
self::LEVEL_INFO => 'info',
self::LEVEL_TRACE => 'trace',
self::LEVEL_PROFILE_BEGIN => 'profile begin',
self::LEVEL_PROFILE_END => 'profile end',
self::LEVEL_PROFILE => 'profile',
];
return isset($levels[$level]) ? $levels[$level] : 'unknown';
}
}
errorHandler组件
需要关注这个方法,注册错误、异常处理函数:
public function register()
{
if (!$this->_registered) {
ini_set('display_errors', false);
set_exception_handler([$this, 'handleException']);
if (defined('HHVM_VERSION')) {
set_error_handler([$this, 'handleHhvmError']);
} else {
set_error_handler([$this, 'handleError']);
}
if ($this->memoryReserveSize > 0) {
$this->_memoryReserve = str_repeat('x', $this->memoryReserveSize);
}
register_shutdown_function([$this, 'handleFatalError']);
$this->_registered = true;
}
}
yii\base\ErrorHandler
类:
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\base;
use Yii;
use yii\helpers\VarDumper;
use yii\web\HttpException;
/**
* ErrorHandler handles uncaught PHP errors and exceptions.
*
* ErrorHandler is configured as an application component in [[\yii\base\Application]] by default.
* You can access that instance via `Yii::$app->errorHandler`.
*
* For more details and usage information on ErrorHandler, see the [guide article on handling errors](guide:runtime-handling-errors).
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @author Alexander Makarov <sam@rmcreative.ru>
* @author Carsten Brandt <mail@cebe.cc>
* @since 2.0
*/
abstract class ErrorHandler extends Component
{
/**
* @var bool whether to discard any existing page output before error display. Defaults to true.
*/
public $discardExistingOutput = true;
/**
* @var int the size of the reserved memory. A portion of memory is pre-allocated so that
* when an out-of-memory issue occurs, the error handler is able to handle the error with
* the help of this reserved memory. If you set this value to be 0, no memory will be reserved.
* Defaults to 256KB.
*/
public $memoryReserveSize = 262144;
/**
* @var \Exception|null the exception that is being handled currently.
*/
public $exception;
/**
* @var string Used to reserve memory for fatal error handler.
*/
private $_memoryReserve;
/**
* @var \Exception from HHVM error that stores backtrace
*/
private $_hhvmException;
/**
* @var bool whether this instance has been registered using `register()`
*/
private $_registered = false;
/**
* Register this error handler.
* @since 2.0.32 this will not do anything if the error handler was already registered
*/
public function register()
{
if (!$this->_registered) {
ini_set('display_errors', false);
set_exception_handler([$this, 'handleException']);
if (defined('HHVM_VERSION')) {
set_error_handler([$this, 'handleHhvmError']);
} else {
set_error_handler([$this, 'handleError']);
}
if ($this->memoryReserveSize > 0) {
$this->_memoryReserve = str_repeat('x', $this->memoryReserveSize);
}
register_shutdown_function([$this, 'handleFatalError']);
$this->_registered = true;
}
}
/**
* Unregisters this error handler by restoring the PHP error and exception handlers.
* @since 2.0.32 this will not do anything if the error handler was not registered
*/
public function unregister()
{
if ($this->_registered) {
restore_error_handler();
restore_exception_handler();
$this->_registered = false;
}
}
/**
* Handles uncaught PHP exceptions.
*
* This method is implemented as a PHP exception handler.
*
* @param \Exception $exception the exception that is not caught
*/
public function handleException($exception)
{
if ($exception instanceof ExitException) {
return;
}
$this->exception = $exception;
// disable error capturing to avoid recursive errors while handling exceptions
$this->unregister();
// set preventive HTTP status code to 500 in case error handling somehow fails and headers are sent
// HTTP exceptions will override this value in renderException()
if (PHP_SAPI !== 'cli') {
http_response_code(500);
}
try {
$this->logException($exception);
if ($this->discardExistingOutput) {
$this->clearOutput();
}
$this->renderException($exception);
if (!YII_ENV_TEST) {
\Yii::getLogger()->flush(true);
if (defined('HHVM_VERSION')) {
flush();
}
exit(1);
}
} catch (\Exception $e) {
// an other exception could be thrown while displaying the exception
$this->handleFallbackExceptionMessage($e, $exception);
} catch (\Throwable $e) {
// additional check for \Throwable introduced in PHP 7
$this->handleFallbackExceptionMessage($e, $exception);
}
$this->exception = null;
}
/**
* Handles exception thrown during exception processing in [[handleException()]].
* @param \Exception|\Throwable $exception Exception that was thrown during main exception processing.
* @param \Exception $previousException Main exception processed in [[handleException()]].
* @since 2.0.11
*/
protected function handleFallbackExceptionMessage($exception, $previousException)
{
$msg = "An Error occurred while handling another error:\n";
$msg .= (string) $exception;
$msg .= "\nPrevious exception:\n";
$msg .= (string) $previousException;
if (YII_DEBUG) {
if (PHP_SAPI === 'cli') {
echo $msg . "\n";
} else {
echo '<pre>' . htmlspecialchars($msg, ENT_QUOTES, Yii::$app->charset) . '</pre>';
}
} else {
echo 'An internal server error occurred.';
}
$msg .= "\n\$_SERVER = " . VarDumper::export($_SERVER);
error_log($msg);
if (defined('HHVM_VERSION')) {
flush();
}
exit(1);
}
/**
* Handles HHVM execution errors such as warnings and notices.
*
* This method is used as a HHVM error handler. It will store exception that will
* be used in fatal error handler
*
* @param int $code the level of the error raised.
* @param string $message the error message.
* @param string $file the filename that the error was raised in.
* @param int $line the line number the error was raised at.
* @param mixed $context
* @param mixed $backtrace trace of error
* @return bool whether the normal error handler continues.
*
* @throws ErrorException
* @since 2.0.6
*/
public function handleHhvmError($code, $message, $file, $line, $context, $backtrace)
{
if ($this->handleError($code, $message, $file, $line)) {
return true;
}
if (E_ERROR & $code) {
$exception = new ErrorException($message, $code, $code, $file, $line);
$ref = new \ReflectionProperty('\Exception', 'trace');
$ref->setAccessible(true);
$ref->setValue($exception, $backtrace);
$this->_hhvmException = $exception;
}
return false;
}
/**
* Handles PHP execution errors such as warnings and notices.
*
* This method is used as a PHP error handler. It will simply raise an [[ErrorException]].
*
* @param int $code the level of the error raised.
* @param string $message the error message.
* @param string $file the filename that the error was raised in.
* @param int $line the line number the error was raised at.
* @return bool whether the normal error handler continues.
*
* @throws ErrorException
*/
public function handleError($code, $message, $file, $line)
{
if (error_reporting() & $code) {
// load ErrorException manually here because autoloading them will not work
// when error occurs while autoloading a class
if (!class_exists('yii\\base\\ErrorException', false)) {
require_once __DIR__ . '/ErrorException.php';
}
$exception = new ErrorException($message, $code, $code, $file, $line);
if (PHP_VERSION_ID < 70400) {
// prior to PHP 7.4 we can't throw exceptions inside of __toString() - it will result a fatal error
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
array_shift($trace);
foreach ($trace as $frame) {
if ($frame['function'] === '__toString') {
$this->handleException($exception);
if (defined('HHVM_VERSION')) {
flush();
}
exit(1);
}
}
}
throw $exception;
}
return false;
}
/**
* Handles fatal PHP errors.
*/
public function handleFatalError()
{
unset($this->_memoryReserve);
// load ErrorException manually here because autoloading them will not work
// when error occurs while autoloading a class
if (!class_exists('yii\\base\\ErrorException', false)) {
require_once __DIR__ . '/ErrorException.php';
}
$error = error_get_last();
if (ErrorException::isFatalError($error)) {
if (!empty($this->_hhvmException)) {
$exception = $this->_hhvmException;
} else {
$exception = new ErrorException($error['message'], $error['type'], $error['type'], $error['file'], $error['line']);
}
$this->exception = $exception;
$this->logException($exception);
if ($this->discardExistingOutput) {
$this->clearOutput();
}
$this->renderException($exception);
// need to explicitly flush logs because exit() next will terminate the app immediately
Yii::getLogger()->flush(true);
if (defined('HHVM_VERSION')) {
flush();
}
exit(1);
}
}
/**
* Renders the exception.
* @param \Exception $exception the exception to be rendered.
*/
abstract protected function renderException($exception);
/**
* Logs the given exception.
* @param \Exception $exception the exception to be logged
* @since 2.0.3 this method is now public.
*/
public function logException($exception)
{
$category = get_class($exception);
if ($exception instanceof HttpException) {
$category = 'yii\\web\\HttpException:' . $exception->statusCode;
} elseif ($exception instanceof \ErrorException) {
$category .= ':' . $exception->getSeverity();
}
Yii::error($exception, $category);
}
/**
* Removes all output echoed before calling this method.
*/
public function clearOutput()
{
// the following manual level counting is to deal with zlib.output_compression set to On
for ($level = ob_get_level(); $level > 0; --$level) {
if (!@ob_end_clean()) {
ob_clean();
}
}
}
/**
* Converts an exception into a PHP error.
*
* This method can be used to convert exceptions inside of methods like `__toString()`
* to PHP errors because exceptions cannot be thrown inside of them.
* @param \Exception $exception the exception to convert to a PHP error.
*/
public static function convertExceptionToError($exception)
{
trigger_error(static::convertExceptionToString($exception), E_USER_ERROR);
}
/**
* Converts an exception into a simple string.
* @param \Exception|\Error $exception the exception being converted
* @return string the string representation of the exception.
*/
public static function convertExceptionToString($exception)
{
if ($exception instanceof UserException) {
return "{$exception->getName()}: {$exception->getMessage()}";
}
if (YII_DEBUG) {
return static::convertExceptionToVerboseString($exception);
}
return 'An internal server error occurred.';
}
/**
* Converts an exception into a string that has verbose information about the exception and its trace.
* @param \Exception|\Error $exception the exception being converted
* @return string the string representation of the exception.
*
* @since 2.0.14
*/
public static function convertExceptionToVerboseString($exception)
{
if ($exception instanceof Exception) {
$message = "Exception ({$exception->getName()})";
} elseif ($exception instanceof ErrorException) {
$message = (string)$exception->getName();
} else {
$message = 'Exception';
}
$message .= " '" . get_class($exception) . "' with message '{$exception->getMessage()}' \n\nin "
. $exception->getFile() . ':' . $exception->getLine() . "\n\n"
. "Stack trace:\n" . $exception->getTraceAsString();
return $message;
}
}
console
yii\console\ErrorHandler
类:
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\console;
use Yii;
use yii\base\ErrorException;
use yii\base\UserException;
use yii\helpers\Console;
/**
* ErrorHandler handles uncaught PHP errors and exceptions.
*
* ErrorHandler is configured as an application component in [[\yii\base\Application]] by default.
* You can access that instance via `Yii::$app->errorHandler`.
*
* @author Carsten Brandt <mail@cebe.cc>
* @since 2.0
*/
class ErrorHandler extends \yii\base\ErrorHandler
{
/**
* Renders an exception using ansi format for console output.
* @param \Exception $exception the exception to be rendered.
*/
protected function renderException($exception)
{
if ($exception instanceof UnknownCommandException) {
// display message and suggest alternatives in case of unknown command
$message = $this->formatMessage($exception->getName() . ': ') . $exception->command;
$alternatives = $exception->getSuggestedAlternatives();
if (count($alternatives) === 1) {
$message .= "\n\nDid you mean \"" . reset($alternatives) . '"?';
} elseif (count($alternatives) > 1) {
$message .= "\n\nDid you mean one of these?\n - " . implode("\n - ", $alternatives);
}
} elseif ($exception instanceof Exception && ($exception instanceof UserException || !YII_DEBUG)) {
$message = $this->formatMessage($exception->getName() . ': ') . $exception->getMessage();
} elseif (YII_DEBUG) {
if ($exception instanceof Exception) {
$message = $this->formatMessage("Exception ({$exception->getName()})");
} elseif ($exception instanceof ErrorException) {
$message = $this->formatMessage($exception->getName());
} else {
$message = $this->formatMessage('Exception');
}
$message .= $this->formatMessage(" '" . get_class($exception) . "'", [Console::BOLD, Console::FG_BLUE])
. ' with message ' . $this->formatMessage("'{$exception->getMessage()}'", [Console::BOLD]) //. "\n"
. "\n\nin " . dirname($exception->getFile()) . DIRECTORY_SEPARATOR . $this->formatMessage(basename($exception->getFile()), [Console::BOLD])
. ':' . $this->formatMessage($exception->getLine(), [Console::BOLD, Console::FG_YELLOW]) . "\n";
if ($exception instanceof \yii\db\Exception && !empty($exception->errorInfo)) {
$message .= "\n" . $this->formatMessage("Error Info:\n", [Console::BOLD]) . print_r($exception->errorInfo, true);
}
$message .= "\n" . $this->formatMessage("Stack trace:\n", [Console::BOLD]) . $exception->getTraceAsString();
} else {
$message = $this->formatMessage('Error: ') . $exception->getMessage();
}
if (PHP_SAPI === 'cli') {
Console::stderr($message . "\n");
} else {
echo $message . "\n";
}
}
/**
* Colorizes a message for console output.
* @param string $message the message to colorize.
* @param array $format the message format.
* @return string the colorized message.
* @see Console::ansiFormat() for details on how to specify the message format.
*/
protected function formatMessage($message, $format = [Console::FG_RED, Console::BOLD])
{
$stream = (PHP_SAPI === 'cli') ? \STDERR : \STDOUT;
// try controller first to allow check for --color switch
if (Yii::$app->controller instanceof \yii\console\Controller && Yii::$app->controller->isColorEnabled($stream)
|| Yii::$app instanceof \yii\console\Application && Console::streamSupportsAnsiColors($stream)) {
$message = Console::ansiFormat($message, $format);
}
return $message;
}
}
web
yii\web\ErrorHandler
类:
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\web;
use Yii;
use yii\base\ErrorException;
use yii\base\Exception;
use yii\base\UserException;
use yii\helpers\VarDumper;
/**
* ErrorHandler handles uncaught PHP errors and exceptions.
*
* ErrorHandler displays these errors using appropriate views based on the
* nature of the errors and the mode the application runs at.
*
* ErrorHandler is configured as an application component in [[\yii\base\Application]] by default.
* You can access that instance via `Yii::$app->errorHandler`.
*
* For more details and usage information on ErrorHandler, see the [guide article on handling errors](guide:runtime-handling-errors).
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @author Timur Ruziev <resurtm@gmail.com>
* @since 2.0
*/
class ErrorHandler extends \yii\base\ErrorHandler
{
/**
* @var int maximum number of source code lines to be displayed. Defaults to 19.
*/
public $maxSourceLines = 19;
/**
* @var int maximum number of trace source code lines to be displayed. Defaults to 13.
*/
public $maxTraceSourceLines = 13;
/**
* @var string the route (e.g. `site/error`) to the controller action that will be used
* to display external errors. Inside the action, it can retrieve the error information
* using `Yii::$app->errorHandler->exception`. This property defaults to null, meaning ErrorHandler
* will handle the error display.
*/
public $errorAction;
/**
* @var string the path of the view file for rendering exceptions without call stack information.
*/
public $errorView = '@yii/views/errorHandler/error.php';
/**
* @var string the path of the view file for rendering exceptions.
*/
public $exceptionView = '@yii/views/errorHandler/exception.php';
/**
* @var string the path of the view file for rendering exceptions and errors call stack element.
*/
public $callStackItemView = '@yii/views/errorHandler/callStackItem.php';
/**
* @var string the path of the view file for rendering previous exceptions.
*/
public $previousExceptionView = '@yii/views/errorHandler/previousException.php';
/**
* @var array list of the PHP predefined variables that should be displayed on the error page.
* Note that a variable must be accessible via `$GLOBALS`. Otherwise it won't be displayed.
* Defaults to `['_GET', '_POST', '_FILES', '_COOKIE', '_SESSION']`.
* @see renderRequest()
* @since 2.0.7
*/
public $displayVars = ['_GET', '_POST', '_FILES', '_COOKIE', '_SESSION'];
/**
* @var string trace line with placeholders to be be substituted.
* The placeholders are {file}, {line} and {text} and the string should be as follows.
*
* `File: {file} - Line: {line} - Text: {text}`
*
* @example <a href="ide://open?file={file}&line={line}">{html}</a>
* @see https://github.com/yiisoft/yii2-debug#open-files-in-ide
* @since 2.0.14
*/
public $traceLine = '{html}';
/**
* Renders the exception.
* @param \Exception|\Error $exception the exception to be rendered.
*/
protected function renderException($exception)
{
if (Yii::$app->has('response')) {
$response = Yii::$app->getResponse();
// reset parameters of response to avoid interference with partially created response data
// in case the error occurred while sending the response.
$response->isSent = false;
$response->stream = null;
$response->data = null;
$response->content = null;
} else {
$response = new Response();
}
$response->setStatusCodeByException($exception);
$useErrorView = $response->format === Response::FORMAT_HTML && (!YII_DEBUG || $exception instanceof UserException);
if ($useErrorView && $this->errorAction !== null) {
Yii::$app->view->clear();
$result = Yii::$app->runAction($this->errorAction);
if ($result instanceof Response) {
$response = $result;
} else {
$response->data = $result;
}
} elseif ($response->format === Response::FORMAT_HTML) {
if ($this->shouldRenderSimpleHtml()) {
// AJAX request
$response->data = '<pre>' . $this->htmlEncode(static::convertExceptionToString($exception)) . '</pre>';
} else {
// if there is an error during error rendering it's useful to
// display PHP error in debug mode instead of a blank screen
if (YII_DEBUG) {
ini_set('display_errors', 1);
}
$file = $useErrorView ? $this->errorView : $this->exceptionView;
$response->data = $this->renderFile($file, [
'exception' => $exception,
]);
}
} elseif ($response->format === Response::FORMAT_RAW) {
$response->data = static::convertExceptionToString($exception);
} else {
$response->data = $this->convertExceptionToArray($exception);
}
$response->send();
}
/**
* Converts an exception into an array.
* @param \Exception|\Error $exception the exception being converted
* @return array the array representation of the exception.
*/
protected function convertExceptionToArray($exception)
{
if (!YII_DEBUG && !$exception instanceof UserException && !$exception instanceof HttpException) {
$exception = new HttpException(500, Yii::t('yii', 'An internal server error occurred.'));
}
$array = [
'name' => ($exception instanceof Exception || $exception instanceof ErrorException) ? $exception->getName() : 'Exception',
'message' => $exception->getMessage(),
'code' => $exception->getCode(),
];
if ($exception instanceof HttpException) {
$array['status'] = $exception->statusCode;
}
if (YII_DEBUG) {
$array['type'] = get_class($exception);
if (!$exception instanceof UserException) {
$array['file'] = $exception->getFile();
$array['line'] = $exception->getLine();
$array['stack-trace'] = explode("\n", $exception->getTraceAsString());
if ($exception instanceof \yii\db\Exception) {
$array['error-info'] = $exception->errorInfo;
}
}
}
if (($prev = $exception->getPrevious()) !== null) {
$array['previous'] = $this->convertExceptionToArray($prev);
}
return $array;
}
/**
* Converts special characters to HTML entities.
* @param string $text to encode.
* @return string encoded original text.
*/
public function htmlEncode($text)
{
return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
}
/**
* Adds informational links to the given PHP type/class.
* @param string $code type/class name to be linkified.
* @return string linkified with HTML type/class name.
*/
public function addTypeLinks($code)
{
if (preg_match('/(.*?)::([^(]+)/', $code, $matches)) {
$class = $matches[1];
$method = $matches[2];
$text = $this->htmlEncode($class) . '::' . $this->htmlEncode($method);
} else {
$class = $code;
$method = null;
$text = $this->htmlEncode($class);
}
$url = null;
$shouldGenerateLink = true;
if ($method !== null && substr_compare($method, '{closure}', -9) !== 0) {
$reflection = new \ReflectionClass($class);
if ($reflection->hasMethod($method)) {
$reflectionMethod = $reflection->getMethod($method);
$shouldGenerateLink = $reflectionMethod->isPublic() || $reflectionMethod->isProtected();
} else {
$shouldGenerateLink = false;
}
}
if ($shouldGenerateLink) {
$url = $this->getTypeUrl($class, $method);
}
if ($url === null) {
return $text;
}
return '<a href="' . $url . '" target="_blank">' . $text . '</a>';
}
/**
* Returns the informational link URL for a given PHP type/class.
* @param string $class the type or class name.
* @param string|null $method the method name.
* @return string|null the informational link URL.
* @see addTypeLinks()
*/
protected function getTypeUrl($class, $method)
{
if (strncmp($class, 'yii\\', 4) !== 0) {
return null;
}
$page = $this->htmlEncode(strtolower(str_replace('\\', '-', $class)));
$url = "http://www.yiiframework.com/doc-2.0/$page.html";
if ($method) {
$url .= "#$method()-detail";
}
return $url;
}
/**
* Renders a view file as a PHP script.
* @param string $_file_ the view file.
* @param array $_params_ the parameters (name-value pairs) that will be extracted and made available in the view file.
* @return string the rendering result
*/
public function renderFile($_file_, $_params_)
{
$_params_['handler'] = $this;
if ($this->exception instanceof ErrorException || !Yii::$app->has('view')) {
ob_start();
ob_implicit_flush(false);
extract($_params_, EXTR_OVERWRITE);
require Yii::getAlias($_file_);
return ob_get_clean();
}
$view = Yii::$app->getView();
$view->clear();
return $view->renderFile($_file_, $_params_, $this);
}
/**
* Renders the previous exception stack for a given Exception.
* @param \Exception $exception the exception whose precursors should be rendered.
* @return string HTML content of the rendered previous exceptions.
* Empty string if there are none.
*/
public function renderPreviousExceptions($exception)
{
if (($previous = $exception->getPrevious()) !== null) {
return $this->renderFile($this->previousExceptionView, ['exception' => $previous]);
}
return '';
}
/**
* Renders a single call stack element.
* @param string|null $file name where call has happened.
* @param int|null $line number on which call has happened.
* @param string|null $class called class name.
* @param string|null $method called function/method name.
* @param array $args array of method arguments.
* @param int $index number of the call stack element.
* @return string HTML content of the rendered call stack element.
*/
public function renderCallStackItem($file, $line, $class, $method, $args, $index)
{
$lines = [];
$begin = $end = 0;
if ($file !== null && $line !== null) {
$line--; // adjust line number from one-based to zero-based
$lines = @file($file);
if ($line < 0 || $lines === false || ($lineCount = count($lines)) < $line) {
return '';
}
$half = (int) (($index === 1 ? $this->maxSourceLines : $this->maxTraceSourceLines) / 2);
$begin = $line - $half > 0 ? $line - $half : 0;
$end = $line + $half < $lineCount ? $line + $half : $lineCount - 1;
}
return $this->renderFile($this->callStackItemView, [
'file' => $file,
'line' => $line,
'class' => $class,
'method' => $method,
'index' => $index,
'lines' => $lines,
'begin' => $begin,
'end' => $end,
'args' => $args,
]);
}
/**
* Renders call stack.
* @param \Exception|\ParseError $exception exception to get call stack from
* @return string HTML content of the rendered call stack.
* @since 2.0.12
*/
public function renderCallStack($exception)
{
$out = '<ul>';
$out .= $this->renderCallStackItem($exception->getFile(), $exception->getLine(), null, null, [], 1);
for ($i = 0, $trace = $exception->getTrace(), $length = count($trace); $i < $length; ++$i) {
$file = !empty($trace[$i]['file']) ? $trace[$i]['file'] : null;
$line = !empty($trace[$i]['line']) ? $trace[$i]['line'] : null;
$class = !empty($trace[$i]['class']) ? $trace[$i]['class'] : null;
$function = null;
if (!empty($trace[$i]['function']) && $trace[$i]['function'] !== 'unknown') {
$function = $trace[$i]['function'];
}
$args = !empty($trace[$i]['args']) ? $trace[$i]['args'] : [];
$out .= $this->renderCallStackItem($file, $line, $class, $function, $args, $i + 2);
}
$out .= '</ul>';
return $out;
}
/**
* Renders the global variables of the request.
* List of global variables is defined in [[displayVars]].
* @return string the rendering result
* @see displayVars
*/
public function renderRequest()
{
$request = '';
foreach ($this->displayVars as $name) {
if (!empty($GLOBALS[$name])) {
$request .= '$' . $name . ' = ' . VarDumper::export($GLOBALS[$name]) . ";\n\n";
}
}
return '<pre>' . $this->htmlEncode(rtrim($request, "\n")) . '</pre>';
}
/**
* Determines whether given name of the file belongs to the framework.
* @param string $file name to be checked.
* @return bool whether given name of the file belongs to the framework.
*/
public function isCoreFile($file)
{
return $file === null || strpos(realpath($file), YII2_PATH . DIRECTORY_SEPARATOR) === 0;
}
/**
* Creates HTML containing link to the page with the information on given HTTP status code.
* @param int $statusCode to be used to generate information link.
* @param string $statusDescription Description to display after the the status code.
* @return string generated HTML with HTTP status code information.
*/
public function createHttpStatusLink($statusCode, $statusDescription)
{
return '<a href="http://en.wikipedia.org/wiki/List_of_HTTP_status_codes#' . (int) $statusCode . '" target="_blank">HTTP ' . (int) $statusCode . ' – ' . $statusDescription . '</a>';
}
/**
* Creates string containing HTML link which refers to the home page of determined web-server software
* and its full name.
* @return string server software information hyperlink.
*/
public function createServerInformationLink()
{
$serverUrls = [
'http://httpd.apache.org/' => ['apache'],
'http://nginx.org/' => ['nginx'],
'http://lighttpd.net/' => ['lighttpd'],
'http://gwan.com/' => ['g-wan', 'gwan'],
'http://iis.net/' => ['iis', 'services'],
'https://secure.php.net/manual/en/features.commandline.webserver.php' => ['development'],
];
if (isset($_SERVER['SERVER_SOFTWARE'])) {
foreach ($serverUrls as $url => $keywords) {
foreach ($keywords as $keyword) {
if (stripos($_SERVER['SERVER_SOFTWARE'], $keyword) !== false) {
return '<a href="' . $url . '" target="_blank">' . $this->htmlEncode($_SERVER['SERVER_SOFTWARE']) . '</a>';
}
}
}
}
return '';
}
/**
* Creates string containing HTML link which refers to the page with the current version
* of the framework and version number text.
* @return string framework version information hyperlink.
*/
public function createFrameworkVersionLink()
{
return '<a href="http://github.com/yiisoft/yii2/" target="_blank">' . $this->htmlEncode(Yii::getVersion()) . '</a>';
}
/**
* Converts arguments array to its string representation.
*
* @param array $args arguments array to be converted
* @return string string representation of the arguments array
*/
public function argumentsToString($args)
{
$count = 0;
$isAssoc = $args !== array_values($args);
foreach ($args as $key => $value) {
$count++;
if ($count >= 5) {
if ($count > 5) {
unset($args[$key]);
} else {
$args[$key] = '...';
}
continue;
}
if (is_object($value)) {
$args[$key] = '<span class="title">' . $this->htmlEncode(get_class($value)) . '</span>';
} elseif (is_bool($value)) {
$args[$key] = '<span class="keyword">' . ($value ? 'true' : 'false') . '</span>';
} elseif (is_string($value)) {
$fullValue = $this->htmlEncode($value);
if (mb_strlen($value, 'UTF-8') > 32) {
$displayValue = $this->htmlEncode(mb_substr($value, 0, 32, 'UTF-8')) . '...';
$args[$key] = "<span class=\"string\" title=\"$fullValue\">'$displayValue'</span>";
} else {
$args[$key] = "<span class=\"string\">'$fullValue'</span>";
}
} elseif (is_array($value)) {
$args[$key] = '[' . $this->argumentsToString($value) . ']';
} elseif ($value === null) {
$args[$key] = '<span class="keyword">null</span>';
} elseif (is_resource($value)) {
$args[$key] = '<span class="keyword">resource</span>';
} else {
$args[$key] = '<span class="number">' . $value . '</span>';
}
if (is_string($key)) {
$args[$key] = '<span class="string">\'' . $this->htmlEncode($key) . "'</span> => $args[$key]";
} elseif ($isAssoc) {
$args[$key] = "<span class=\"number\">$key</span> => $args[$key]";
}
}
return implode(', ', $args);
}
/**
* Returns human-readable exception name.
* @param \Exception $exception
* @return string human-readable exception name or null if it cannot be determined
*/
public function getExceptionName($exception)
{
if ($exception instanceof \yii\base\Exception || $exception instanceof \yii\base\InvalidCallException || $exception instanceof \yii\base\InvalidParamException || $exception instanceof \yii\base\UnknownMethodException) {
return $exception->getName();
}
return null;
}
/**
* @return bool if simple HTML should be rendered
* @since 2.0.12
*/
protected function shouldRenderSimpleHtml()
{
return YII_ENV_TEST || Yii::$app->request->getIsAjax();
}
}
基类方法
yii\BaseYii
类:
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii;
use yii\base\InvalidArgumentException;
use yii\base\InvalidConfigException;
use yii\base\UnknownClassException;
use yii\di\Container;
use yii\log\Logger;
/**
* Gets the application start timestamp.
*/
defined('YII_BEGIN_TIME') or define('YII_BEGIN_TIME', microtime(true));
/**
* This constant defines the framework installation directory.
*/
defined('YII2_PATH') or define('YII2_PATH', __DIR__);
/**
* This constant defines whether the application should be in debug mode or not. Defaults to false.
*/
defined('YII_DEBUG') or define('YII_DEBUG', false);
/**
* This constant defines in which environment the application is running. Defaults to 'prod', meaning production environment.
* You may define this constant in the bootstrap script. The value could be 'prod' (production), 'dev' (development), 'test', 'staging', etc.
*/
defined('YII_ENV') or define('YII_ENV', 'prod');
/**
* Whether the the application is running in production environment.
*/
defined('YII_ENV_PROD') or define('YII_ENV_PROD', YII_ENV === 'prod');
/**
* Whether the the application is running in development environment.
*/
defined('YII_ENV_DEV') or define('YII_ENV_DEV', YII_ENV === 'dev');
/**
* Whether the the application is running in testing environment.
*/
defined('YII_ENV_TEST') or define('YII_ENV_TEST', YII_ENV === 'test');
/**
* This constant defines whether error handling should be enabled. Defaults to true.
*/
defined('YII_ENABLE_ERROR_HANDLER') or define('YII_ENABLE_ERROR_HANDLER', true);
/**
* BaseYii is the core helper class for the Yii framework.
*
* Do not use BaseYii directly. Instead, use its child class [[\Yii]] which you can replace to
* customize methods of BaseYii.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class BaseYii
{
/**
* @var array class map used by the Yii autoloading mechanism.
* The array keys are the class names (without leading backslashes), and the array values
* are the corresponding class file paths (or [path aliases](guide:concept-aliases)). This property mainly affects
* how [[autoload()]] works.
* @see autoload()
*/
public static $classMap = [];
/**
* @var \yii\console\Application|\yii\web\Application the application instance
*/
public static $app;
/**
* @var array registered path aliases
* @see getAlias()
* @see setAlias()
*/
public static $aliases = ['@yii' => __DIR__];
/**
* @var Container the dependency injection (DI) container used by [[createObject()]].
* You may use [[Container::set()]] to set up the needed dependencies of classes and
* their initial property values.
* @see createObject()
* @see Container
*/
public static $container;
/**
* Returns a string representing the current version of the Yii framework.
* @return string the version of Yii framework
*/
public static function getVersion()
{
return '2.0.34';
}
/**
* Translates a path alias into an actual path.
*
* The translation is done according to the following procedure:
*
* 1. If the given alias does not start with '@', it is returned back without change;
* 2. Otherwise, look for the longest registered alias that matches the beginning part
* of the given alias. If it exists, replace the matching part of the given alias with
* the corresponding registered path.
* 3. Throw an exception or return false, depending on the `$throwException` parameter.
*
* For example, by default '@yii' is registered as the alias to the Yii framework directory,
* say '/path/to/yii'. The alias '@yii/web' would then be translated into '/path/to/yii/web'.
*
* If you have registered two aliases '@foo' and '@foo/bar'. Then translating '@foo/bar/config'
* would replace the part '@foo/bar' (instead of '@foo') with the corresponding registered path.
* This is because the longest alias takes precedence.
*
* However, if the alias to be translated is '@foo/barbar/config', then '@foo' will be replaced
* instead of '@foo/bar', because '/' serves as the boundary character.
*
* Note, this method does not check if the returned path exists or not.
*
* See the [guide article on aliases](guide:concept-aliases) for more information.
*
* @param string $alias the alias to be translated.
* @param bool $throwException whether to throw an exception if the given alias is invalid.
* If this is false and an invalid alias is given, false will be returned by this method.
* @return string|bool the path corresponding to the alias, false if the root alias is not previously registered.
* @throws InvalidArgumentException if the alias is invalid while $throwException is true.
* @see setAlias()
*/
public static function getAlias($alias, $throwException = true)
{
if (strncmp($alias, '@', 1)) {
// not an alias
return $alias;
}
$pos = strpos($alias, '/');
$root = $pos === false ? $alias : substr($alias, 0, $pos);
if (isset(static::$aliases[$root])) {
if (is_string(static::$aliases[$root])) {
return $pos === false ? static::$aliases[$root] : static::$aliases[$root] . substr($alias, $pos);
}
foreach (static::$aliases[$root] as $name => $path) {
if (strpos($alias . '/', $name . '/') === 0) {
return $path . substr($alias, strlen($name));
}
}
}
if ($throwException) {
throw new InvalidArgumentException("Invalid path alias: $alias");
}
return false;
}
/**
* Returns the root alias part of a given alias.
* A root alias is an alias that has been registered via [[setAlias()]] previously.
* If a given alias matches multiple root aliases, the longest one will be returned.
* @param string $alias the alias
* @return string|bool the root alias, or false if no root alias is found
*/
public static function getRootAlias($alias)
{
$pos = strpos($alias, '/');
$root = $pos === false ? $alias : substr($alias, 0, $pos);
if (isset(static::$aliases[$root])) {
if (is_string(static::$aliases[$root])) {
return $root;
}
foreach (static::$aliases[$root] as $name => $path) {
if (strpos($alias . '/', $name . '/') === 0) {
return $name;
}
}
}
return false;
}
/**
* Registers a path alias.
*
* A path alias is a short name representing a long path (a file path, a URL, etc.)
* For example, we use '@yii' as the alias of the path to the Yii framework directory.
*
* A path alias must start with the character '@' so that it can be easily differentiated
* from non-alias paths.
*
* Note that this method does not check if the given path exists or not. All it does is
* to associate the alias with the path.
*
* Any trailing '/' and '\' characters in the given path will be trimmed.
*
* See the [guide article on aliases](guide:concept-aliases) for more information.
*
* @param string $alias the alias name (e.g. "@yii"). It must start with a '@' character.
* It may contain the forward slash '/' which serves as boundary character when performing
* alias translation by [[getAlias()]].
* @param string $path the path corresponding to the alias. If this is null, the alias will
* be removed. Trailing '/' and '\' characters will be trimmed. This can be
*
* - a directory or a file path (e.g. `/tmp`, `/tmp/main.txt`)
* - a URL (e.g. `http://www.yiiframework.com`)
* - a path alias (e.g. `@yii/base`). In this case, the path alias will be converted into the
* actual path first by calling [[getAlias()]].
*
* @throws InvalidArgumentException if $path is an invalid alias.
* @see getAlias()
*/
public static function setAlias($alias, $path)
{
if (strncmp($alias, '@', 1)) {
$alias = '@' . $alias;
}
$pos = strpos($alias, '/');
$root = $pos === false ? $alias : substr($alias, 0, $pos);
if ($path !== null) {
$path = strncmp($path, '@', 1) ? rtrim($path, '\\/') : static::getAlias($path);
if (!isset(static::$aliases[$root])) {
if ($pos === false) {
static::$aliases[$root] = $path;
} else {
static::$aliases[$root] = [$alias => $path];
}
} elseif (is_string(static::$aliases[$root])) {
if ($pos === false) {
static::$aliases[$root] = $path;
} else {
static::$aliases[$root] = [
$alias => $path,
$root => static::$aliases[$root],
];
}
} else {
static::$aliases[$root][$alias] = $path;
krsort(static::$aliases[$root]);
}
} elseif (isset(static::$aliases[$root])) {
if (is_array(static::$aliases[$root])) {
unset(static::$aliases[$root][$alias]);
} elseif ($pos === false) {
unset(static::$aliases[$root]);
}
}
}
/**
* Class autoload loader.
*
* This method is invoked automatically when PHP sees an unknown class.
* The method will attempt to include the class file according to the following procedure:
*
* 1. Search in [[classMap]];
* 2. If the class is namespaced (e.g. `yii\base\Component`), it will attempt
* to include the file associated with the corresponding path alias
* (e.g. `@yii/base/Component.php`);
*
* This autoloader allows loading classes that follow the [PSR-4 standard](http://www.php-fig.org/psr/psr-4/)
* and have its top-level namespace or sub-namespaces defined as path aliases.
*
* Example: When aliases `@yii` and `@yii/bootstrap` are defined, classes in the `yii\bootstrap` namespace
* will be loaded using the `@yii/bootstrap` alias which points to the directory where bootstrap extension
* files are installed and all classes from other `yii` namespaces will be loaded from the yii framework directory.
*
* Also the [guide section on autoloading](guide:concept-autoloading).
*
* @param string $className the fully qualified class name without a leading backslash "\"
* @throws UnknownClassException if the class does not exist in the class file
*/
public static function autoload($className)
{
if (isset(static::$classMap[$className])) {
$classFile = static::$classMap[$className];
if ($classFile[0] === '@') {
$classFile = static::getAlias($classFile);
}
} elseif (strpos($className, '\\') !== false) {
$classFile = static::getAlias('@' . str_replace('\\', '/', $className) . '.php', false);
if ($classFile === false || !is_file($classFile)) {
return;
}
} else {
return;
}
include $classFile;
if (YII_DEBUG && !class_exists($className, false) && !interface_exists($className, false) && !trait_exists($className, false)) {
throw new UnknownClassException("Unable to find '$className' in file: $classFile. Namespace missing?");
}
}
/**
* Creates a new object using the given configuration.
*
* You may view this method as an enhanced version of the `new` operator.
* The method supports creating an object based on a class name, a configuration array or
* an anonymous function.
*
* Below are some usage examples:
*
* ```php
* // create an object using a class name
* $object = Yii::createObject('yii\db\Connection');
*
* // create an object using a configuration array
* $object = Yii::createObject([
* 'class' => 'yii\db\Connection',
* 'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
* 'username' => 'root',
* 'password' => '',
* 'charset' => 'utf8',
* ]);
*
* // create an object with two constructor parameters
* $object = \Yii::createObject('MyClass', [$param1, $param2]);
* ```
*
* Using [[\yii\di\Container|dependency injection container]], this method can also identify
* dependent objects, instantiate them and inject them into the newly created object.
*
* @param string|array|callable $type the object type. This can be specified in one of the following forms:
*
* - a string: representing the class name of the object to be created
* - a configuration array: the array must contain a `class` element which is treated as the object class,
* and the rest of the name-value pairs will be used to initialize the corresponding object properties
* - a PHP callable: either an anonymous function or an array representing a class method (`[$class or $object, $method]`).
* The callable should return a new instance of the object being created.
*
* @param array $params the constructor parameters
* @return object the created object
* @throws InvalidConfigException if the configuration is invalid.
* @see \yii\di\Container
*/
public static function createObject($type, array $params = [])
{
if (is_string($type)) {
return static::$container->get($type, $params);
}
if (is_callable($type, true)) {
return static::$container->invoke($type, $params);
}
if (!is_array($type)) {
throw new InvalidConfigException('Unsupported configuration type: ' . gettype($type));
}
if (isset($type['__class'])) {
$class = $type['__class'];
unset($type['__class'], $type['class']);
return static::$container->get($class, $params, $type);
}
if (isset($type['class'])) {
$class = $type['class'];
unset($type['class']);
return static::$container->get($class, $params, $type);
}
throw new InvalidConfigException('Object configuration must be an array containing a "class" or "__class" element.');
}
private static $_logger;
/**
* @return Logger message logger
*/
public static function getLogger()
{
if (self::$_logger !== null) {
return self::$_logger;
}
return self::$_logger = static::createObject('yii\log\Logger');
}
/**
* Sets the logger object.
* @param Logger $logger the logger object.
*/
public static function setLogger($logger)
{
self::$_logger = $logger;
}
/**
* Logs a debug message.
* Trace messages are logged mainly for development purpose to see
* the execution work flow of some code. This method will only log
* a message when the application is in debug mode.
* @param string|array $message the message to be logged. This can be a simple string or a more
* complex data structure, such as array.
* @param string $category the category of the message.
* @since 2.0.14
*/
public static function debug($message, $category = 'application')
{
if (YII_DEBUG) {
static::getLogger()->log($message, Logger::LEVEL_TRACE, $category);
}
}
/**
* Alias of [[debug()]].
* @param string|array $message the message to be logged. This can be a simple string or a more
* complex data structure, such as array.
* @param string $category the category of the message.
* @deprecated since 2.0.14. Use [[debug()]] instead.
*/
public static function trace($message, $category = 'application')
{
static::debug($message, $category);
}
/**
* Logs an error message.
* An error message is typically logged when an unrecoverable error occurs
* during the execution of an application.
* @param string|array $message the message to be logged. This can be a simple string or a more
* complex data structure, such as array.
* @param string $category the category of the message.
*/
public static function error($message, $category = 'application')
{
static::getLogger()->log($message, Logger::LEVEL_ERROR, $category);
}
/**
* Logs a warning message.
* A warning message is typically logged when an error occurs while the execution
* can still continue.
* @param string|array $message the message to be logged. This can be a simple string or a more
* complex data structure, such as array.
* @param string $category the category of the message.
*/
public static function warning($message, $category = 'application')
{
static::getLogger()->log($message, Logger::LEVEL_WARNING, $category);
}
/**
* Logs an informative message.
* An informative message is typically logged by an application to keep record of
* something important (e.g. an administrator logs in).
* @param string|array $message the message to be logged. This can be a simple string or a more
* complex data structure, such as array.
* @param string $category the category of the message.
*/
public static function info($message, $category = 'application')
{
static::getLogger()->log($message, Logger::LEVEL_INFO, $category);
}
/**
* Marks the beginning of a code block for profiling.
*
* This has to be matched with a call to [[endProfile]] with the same category name.
* The begin- and end- calls must also be properly nested. For example,
*
* ```php
* \Yii::beginProfile('block1');
* // some code to be profiled
* \Yii::beginProfile('block2');
* // some other code to be profiled
* \Yii::endProfile('block2');
* \Yii::endProfile('block1');
* ```
* @param string $token token for the code block
* @param string $category the category of this log message
* @see endProfile()
*/
public static function beginProfile($token, $category = 'application')
{
static::getLogger()->log($token, Logger::LEVEL_PROFILE_BEGIN, $category);
}
/**
* Marks the end of a code block for profiling.
* This has to be matched with a previous call to [[beginProfile]] with the same category name.
* @param string $token token for the code block
* @param string $category the category of this log message
* @see beginProfile()
*/
public static function endProfile($token, $category = 'application')
{
static::getLogger()->log($token, Logger::LEVEL_PROFILE_END, $category);
}
/**
* Returns an HTML hyperlink that can be displayed on your Web page showing "Powered by Yii Framework" information.
* @return string an HTML hyperlink that can be displayed on your Web page showing "Powered by Yii Framework" information
* @deprecated since 2.0.14, this method will be removed in 2.1.0.
*/
public static function powered()
{
return \Yii::t('yii', 'Powered by {yii}', [
'yii' => '<a href="http://www.yiiframework.com/" rel="external">' . \Yii::t('yii',
'Yii Framework') . '</a>',
]);
}
/**
* Translates a message to the specified language.
*
* This is a shortcut method of [[\yii\i18n\I18N::translate()]].
*
* The translation will be conducted according to the message category and the target language will be used.
*
* You can add parameters to a translation message that will be substituted with the corresponding value after
* translation. The format for this is to use curly brackets around the parameter name as you can see in the following example:
*
* ```php
* $username = 'Alexander';
* echo \Yii::t('app', 'Hello, {username}!', ['username' => $username]);
* ```
*
* Further formatting of message parameters is supported using the [PHP intl extensions](https://secure.php.net/manual/en/intro.intl.php)
* message formatter. See [[\yii\i18n\I18N::translate()]] for more details.
*
* @param string $category the message category.
* @param string $message the message to be translated.
* @param array $params the parameters that will be used to replace the corresponding placeholders in the message.
* @param string $language the language code (e.g. `en-US`, `en`). If this is null, the current
* [[\yii\base\Application::language|application language]] will be used.
* @return string the translated message.
*/
public static function t($category, $message, $params = [], $language = null)
{
if (static::$app !== null) {
return static::$app->getI18n()->translate($category, $message, $params, $language ?: static::$app->language);
}
$placeholders = [];
foreach ((array) $params as $name => $value) {
$placeholders['{' . $name . '}'] = $value;
}
return ($placeholders === []) ? $message : strtr($message, $placeholders);
}
/**
* Configures an object with the initial property values.
* @param object $object the object to be configured
* @param array $properties the property initial values given in terms of name-value pairs.
* @return object the object itself
*/
public static function configure($object, $properties)
{
foreach ($properties as $name => $value) {
$object->$name = $value;
}
return $object;
}
/**
* Returns the public member variables of an object.
* This method is provided such that we can get the public member variables of an object.
* It is different from "get_object_vars()" because the latter will return private
* and protected variables if it is called within the object itself.
* @param object $object the object to be handled
* @return array the public member variables of the object
*/
public static function getObjectVars($object)
{
return get_object_vars($object);
}
}
Yii
类:
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
require __DIR__ . '/BaseYii.php';
/**
* Yii is a helper class serving common framework functionalities.
*
* It extends from [[\yii\BaseYii]] which provides the actual implementation.
* By writing your own Yii class, you can customize some functionalities of [[\yii\BaseYii]].
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class Yii extends \yii\BaseYii
{
}
spl_autoload_register(['Yii', 'autoload'], true, true);
Yii::$classMap = require __DIR__ . '/classes.php';
Yii::$container = new yii\di\Container();
参考资料
Yii 2.0 权威指南 日志 https://www.yiiframework.com/doc/guide/2.0/zh-cn/runtime-logging
Yii2 异常及错误处理 https://ibaiyang.github.io/blog/yii2/2019/08/26/Yii2-异常及错误处理.html