<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006-2014 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: luofei614 <weibo.com/luofei614>
// +----------------------------------------------------------------------
// $Id$

/**
 * 将Trace信息输出到chrome浏览器的控制器,从而不影响ajax效果和页面的布局。
 * 使用前,你需要先安装 chrome log 这个插件: http://craig.is/writing/chrome-logger。
 * 定义应用的tags.php文件 Application/Common/Conf/tags.php,
 * <code>
 * <?php return array(
 * 'app_end'=>array(
 * 'Behavior\ChromeShowPageTrace'
 * )
 * );
 * </code>
 * 如果trace信息没有正常输出,请查看您的日志。
 * 这是通过http headers和chrome通信,所以要保证在输出trace信息之前不能有
 * headers输出,你可以在入口文件第一行加入代码 ob_start(); 或者配置output_buffering
 */
namespace Behavior;

/**
 * 系统行为扩展 页面Trace显示输出
 */
class ChromeShowPageTraceBehavior
{

    protected $tracePageTabs = array(
        'BASE' => '基本',
        'FILE' => '文件',
        'INFO' => '流程',
        'ERR|NOTIC' => '错误',
        'SQL' => 'SQL',
        'DEBUG' => '调试'
    );
    
    // 行为扩展的执行入口必须是run
    public function run(&$params)
    {
        if (C('SHOW_PAGE_TRACE')) {
            $this->showTrace();
        }
    }

    /**
     * 显示页面Trace信息
     * 
     * @access private
     */
    private function showTrace()
    {
        // 系统默认显示信息
        $files = get_included_files();
        $info = array();
        foreach ($files as $key => $file) {
            $info[] = $file . ' ( ' . number_format(filesize($file) / 1024, 2) . ' KB )';
        }
        $trace = array();
        $base = array(
            '请求信息' => date('Y-m-d H:i:s', $_SERVER['REQUEST_TIME']) . ' ' . $_SERVER['SERVER_PROTOCOL'] . ' ' . $_SERVER['REQUEST_METHOD'] . ' : ' . __SELF__,
            '运行时间' => $this->showTime(),
            '吞吐率' => number_format(1 / G('beginTime', 'viewEndTime'), 2) . 'req/s',
            '内存开销' => MEMORY_LIMIT_ON ? number_format((memory_get_usage() - $GLOBALS['_startUseMems']) / 1024, 2) . ' kb' : '不支持',
            '查询信息' => N('db_query') . ' queries ' . N('db_write') . ' writes ',
            '文件加载' => count(get_included_files()),
            '缓存信息' => N('cache_read') . ' gets ' . N('cache_write') . ' writes ',
            '配置加载' => count(c()),
            '会话信息' => 'SESSION_ID=' . session_id()
        );
        // 读取应用定义的Trace文件
        $traceFile = COMMON_PATH . 'Conf/trace.php';
        if (is_file($traceFile)) {
            $base = array_merge($base, include $traceFile);
        }
        
        $debug = trace();
        $tabs = C('TRACE_PAGE_TABS', null, $this->tracePageTabs);
        foreach ($tabs as $name => $title) {
            switch (strtoupper($name)) {
                case 'BASE': // 基本信息
                    $trace[$title] = $base;
                    break;
                case 'FILE': // 文件信息
                    $trace[$title] = $info;
                    break;
                default: // 调试信息
                    $name = strtoupper($name);
                    if (strpos($name, '|')) { // 多组信息
                        $array = explode('|', $name);
                        $result = array();
                        foreach ($array as $name) {
                            $result += isset($debug[$name]) ? $debug[$name] : array();
                        }
                        $trace[$title] = $result;
                    } else {
                        $trace[$title] = isset($debug[$name]) ? $debug[$name] : '';
                    }
            }
        }
        chrome_debug('TRACE信息:' . __SELF__, 'group');
        // 输出日志
        foreach ($trace as $title => $log) {
            '错误' == $title ? chrome_debug($title, 'group') : chrome_debug($title, 'groupCollapsed');
            foreach ($log as $i => $logstr) {
                chrome_debug($i . '.' . $logstr, 'log');
            }
            chrome_debug('', 'groupEnd');
        }
        chrome_debug('', 'groupEnd');
        $save = C('PAGE_TRACE_SAVE');
        if ($save) {
            // 保存页面Trace日志
            if (is_array($save)) { // 选择选项卡保存
                $tabs = C('TRACE_PAGE_TABS', null, $this->tracePageTabs);
                $array = array();
                foreach ($save as $tab) {
                    $array[] = $tabs[$tab];
                }
            }
            $content = date('[ c ]') . ' ' . get_client_ip() . ' ' . $_SERVER['REQUEST_URI'] . "\r\n";
            foreach ($trace as $key => $val) {
                if (! isset($array) || in_array($key, $array)) {
                    $content .= '[ ' . $key . " ]\r\n";
                    if (is_array($val)) {
                        foreach ($val as $k => $v) {
                            $content .= (! is_numeric($k) ? $k . ':' : '') . print_r($v, true) . "\r\n";
                        }
                    } else {
                        $content .= print_r($val, true) . "\r\n";
                    }
                    $content .= "\r\n";
                }
            }
            error_log(str_replace('<br/>', "\r\n", $content), 3, LOG_PATH . date('y_m_d') . '_trace.log');
        }
        unset($files, $info, $base);
    }

    /**
     * 获取运行时间
     */
    private function showTime()
    {
        // 显示运行时间
        G('beginTime', $GLOBALS['_beginTime']);
        G('viewEndTime');
        
        // 显示详细运行时间
        return G('beginTime', 'viewEndTime') . 's ( Load:' . G('beginTime', 'loadTime') . 's Init:' . G('loadTime', 'initTime') . 's Exec:' . G('initTime', 'viewStartTime') . 's Template:' . G('viewStartTime', 'viewEndTime') . 's )';
    }
}

if (! function_exists('chrome_debug')) {
    // ChromePhp 输出trace的函数
    function chrome_debug($msg, $type = 'trace', $trace_level = 1)
    {
        if ('trace' == $type) {
            ChromePhp::groupCollapsed($msg);
            $traces = debug_backtrace(false);
            $traces = array_reverse($traces);
            $max = count($traces) - $trace_level;
            for ($i = 0; $i < $max; $i ++) {
                $trace = $traces[$i];
                $fun = isset($trace['class']) ? $trace['class'] . '::' . $trace['function'] : $trace['function'];
                $file = isset($trace['file']) ? $trace['file'] : 'unknown file';
                $line = isset($trace['line']) ? $trace['line'] : 'unknown line';
                $trace_msg = '#' . $i . '  ' . $fun . ' called at [' . $file . ':' . $line . ']';
                if (! empty($trace['args'])) {
                    ChromePhp::groupCollapsed($trace_msg);
                    ChromePhp::log($trace['args']);
                    ChromePhp::groupEnd();
                } else {
                    ChromePhp::log($trace_msg);
                }
            }
            ChromePhp::groupEnd();
        } else {
            if (method_exists('Behavior\ChromePhp', $type)) {
                // 支持type trace,warn,log,error,group, groupCollapsed, groupEnd等
                call_user_func(array(
                    'Behavior\ChromePhp',
                    $type
                ), $msg);
            } else {
                // 如果type不为trace,warn,log等,则为log的标签
                call_user_func_array(array(
                    'Behavior\ChromePhp',
                    'log'
                ), func_get_args());
            }
        }
    }

    /**
     * Server Side Chrome PHP debugger class
     * 
     * @package ChromePhp
     * @author Craig Campbell <iamcraigcampbell@gmail.com>
     */
    class ChromePhp
    {

        /**
         *
         * @var string
         */
        const VERSION = '4.1.0';

        /**
         *
         * @var string
         */
        const HEADER_NAME = 'X-ChromeLogger-Data';

        /**
         *
         * @var string
         */
        const BACKTRACE_LEVEL = 'backtrace_level';

        /**
         *
         * @var string
         */
        const LOG = 'log';

        /**
         *
         * @var string
         */
        const WARN = 'warn';

        /**
         *
         * @var string
         */
        const ERROR = 'error';

        /**
         *
         * @var string
         */
        const GROUP = 'group';

        /**
         *
         * @var string
         */
        const INFO = 'info';

        /**
         *
         * @var string
         */
        const GROUP_END = 'groupEnd';

        /**
         *
         * @var string
         */
        const GROUP_COLLAPSED = 'groupCollapsed';

        /**
         *
         * @var string
         */
        const TABLE = 'table';

        /**
         *
         * @var string
         */
        protected $_php_version;

        /**
         *
         * @var int
         */
        protected $_timestamp;

        /**
         *
         * @var array
         */
        protected $_json = array(
            'version' => self::VERSION,
            'columns' => array(
                'log',
                'backtrace',
                'type'
            ),
            'rows' => array()
        );

        /**
         *
         * @var array
         */
        protected $_backtraces = array();

        /**
         *
         * @var bool
         */
        protected $_error_triggered = false;

        /**
         *
         * @var array
         */
        protected $_settings = array(
            self::BACKTRACE_LEVEL => 1
        );

        /**
         *
         * @var ChromePhp
         */
        protected static $_instance;

        /**
         * Prevent recursion when working with objects referring to each other
         * 
         * @var array
         */
        protected $_processed = array();

        /**
         * constructor
         */
        private function __construct()
        {
            $this->_php_version = phpversion();
            $this->_timestamp = $this->_php_version >= 5.1 ? $_SERVER['REQUEST_TIME'] : time();
            $this->_json['request_uri'] = $_SERVER['REQUEST_URI'];
        }

        /**
         * gets instance of this class
         * 
         * @return ChromePhp
         */
        public static function getInstance()
        {
            if (self::$_instance === null) {
                self::$_instance = new self();
            }
            
            return self::$_instance;
        }

        /**
         * logs a variable to the console
         * 
         * @param mixed $data,...
         *            unlimited OPTIONAL number of additional logs [...]
         * @return void
         */
        public static function log()
        {
            $args = func_get_args();
            
            return self::_log('', $args);
        }

        /**
         * logs a warning to the console
         * 
         * @param mixed $data,...
         *            unlimited OPTIONAL number of additional logs [...]
         * @return void
         */
        public static function warn()
        {
            $args = func_get_args();
            
            return self::_log(self::WARN, $args);
        }

        /**
         * logs an error to the console
         * 
         * @param mixed $data,...
         *            unlimited OPTIONAL number of additional logs [...]
         * @return void
         */
        public static function error()
        {
            $args = func_get_args();
            
            return self::_log(self::ERROR, $args);
        }

        /**
         * sends a group log
         * 
         * @param
         *            string value
         */
        public static function group()
        {
            $args = func_get_args();
            
            return self::_log(self::GROUP, $args);
        }

        /**
         * sends an info log
         * 
         * @param mixed $data,...
         *            unlimited OPTIONAL number of additional logs [...]
         * @return void
         */
        public static function info()
        {
            $args = func_get_args();
            
            return self::_log(self::INFO, $args);
        }

        /**
         * sends a collapsed group log
         * 
         * @param
         *            string value
         */
        public static function groupCollapsed()
        {
            $args = func_get_args();
            
            return self::_log(self::GROUP_COLLAPSED, $args);
        }

        /**
         * ends a group log
         * 
         * @param
         *            string value
         */
        public static function groupEnd()
        {
            $args = func_get_args();
            
            return self::_log(self::GROUP_END, $args);
        }

        /**
         * sends a table log
         * 
         * @param
         *            string value
         */
        public static function table()
        {
            $args = func_get_args();
            
            return self::_log(self::TABLE, $args);
        }

        /**
         * internal logging call
         * 
         * @param string $type            
         * @return void
         */
        protected static function _log($type, array $args)
        {
            // nothing passed in, don't do anything
            if (count($args) == 0 && $type != self::GROUP_END) {
                return;
            }
            
            $logger = self::getInstance();
            
            $logger->_processed = array();
            
            $logs = array();
            foreach ($args as $arg) {
                $logs[] = $logger->_convert($arg);
            }
            
            $backtrace = debug_backtrace(false);
            $level = $logger->getSetting(self::BACKTRACE_LEVEL);
            
            $backtrace_message = 'unknown';
            if (isset($backtrace[$level]['file']) && isset($backtrace[$level]['line'])) {
                $backtrace_message = $backtrace[$level]['file'] . ' : ' . $backtrace[$level]['line'];
            }
            
            $logger->_addRow($logs, $backtrace_message, $type);
        }

        /**
         * converts an object to a better format for logging
         * 
         * @param
         *            Object
         * @return array
         */
        protected function _convert($object)
        {
            // if this isn't an object then just return it
            if (! is_object($object)) {
                return $object;
            }
            
            // Mark this object as processed so we don't convert it twice and it
            // Also avoid recursion when objects refer to each other
            $this->_processed[] = $object;
            
            $object_as_array = array();
            
            // first add the class name
            $object_as_array['___class_name'] = get_class($object);
            
            // loop through object vars
            $object_vars = get_object_vars($object);
            foreach ($object_vars as $key => $value) {
                
                // same instance as parent object
                if ($value === $object || in_array($value, $this->_processed, true)) {
                    $value = 'recursion - parent object [' . get_class($value) . ']';
                }
                $object_as_array[$key] = $this->_convert($value);
            }
            
            $reflection = new ReflectionClass($object);
            
            // loop through the properties and add those
            foreach ($reflection->getProperties() as $property) {
                
                // if one of these properties was already added above then ignore it
                if (array_key_exists($property->getName(), $object_vars)) {
                    continue;
                }
                $type = $this->_getPropertyKey($property);
                
                if ($this->_php_version >= 5.3) {
                    $property->setAccessible(true);
                }
                
                try {
                    $value = $property->getValue($object);
                } catch (ReflectionException $e) {
                    $value = 'only PHP 5.3 can access private/protected properties';
                }
                
                // same instance as parent object
                if ($value === $object || in_array($value, $this->_processed, true)) {
                    $value = 'recursion - parent object [' . get_class($value) . ']';
                }
                
                $object_as_array[$type] = $this->_convert($value);
            }
            
            return $object_as_array;
        }

        /**
         * takes a reflection property and returns a nicely formatted key of the property name
         * 
         * @param
         *            ReflectionProperty
         * @return string
         */
        protected function _getPropertyKey(ReflectionProperty $property)
        {
            $static = $property->isStatic() ? ' static' : '';
            if ($property->isPublic()) {
                return 'public' . $static . ' ' . $property->getName();
            }
            
            if ($property->isProtected()) {
                return 'protected' . $static . ' ' . $property->getName();
            }
            
            if ($property->isPrivate()) {
                return 'private' . $static . ' ' . $property->getName();
            }
        }

        /**
         * adds a value to the data array
         * 
         * @var mixed
         * @return void
         */
        protected function _addRow(array $logs, $backtrace, $type)
        {
            // if this is logged on the same line for example in a loop, set it to null to save space
            if (in_array($backtrace, $this->_backtraces)) {
                $backtrace = null;
            }
            
            // for group, groupEnd, and groupCollapsed
            // take out the backtrace since it is not useful
            if ($type == self::GROUP || $type == self::GROUP_END || $type == self::GROUP_COLLAPSED) {
                $backtrace = null;
            }
            
            if ($backtrace !== null) {
                $this->_backtraces[] = $backtrace;
            }
            
            $row = array(
                $logs,
                $backtrace,
                $type
            );
            
            $this->_json['rows'][] = $row;
            $this->_writeHeader($this->_json);
        }

        protected function _writeHeader($data)
        {
            header(self::HEADER_NAME . ': ' . $this->_encode($data));
        }

        /**
         * encodes the data to be sent along with the request
         * 
         * @param array $data            
         * @return string
         */
        protected function _encode($data)
        {
            return base64_encode(utf8_encode(json_encode($data)));
        }

        /**
         * adds a setting
         * 
         * @param
         *            string key
         * @param
         *            mixed value
         * @return void
         */
        public function addSetting($key, $value)
        {
            $this->_settings[$key] = $value;
        }

        /**
         * add ability to set multiple settings in one call
         * 
         * @param array $settings            
         * @return void
         */
        public function addSettings(array $settings)
        {
            foreach ($settings as $key => $value) {
                $this->addSetting($key, $value);
            }
        }

        /**
         * gets a setting
         * 
         * @param
         *            string key
         * @return mixed
         */
        public function getSetting($key)
        {
            if (! isset($this->_settings[$key])) {
                return null;
            }
            
            return $this->_settings[$key];
        }
    }
}