我开发了一个基于 Beancount 的账本托管服务 HostedBeans,欢迎大家来了解纯文本复式记账并试用我的服务。
归档 2013 年 6 月

编程之美

这是一篇文艺型的,科普性的软技术文 …

我所讲的不仅仅是编程,包括整个计算机和互联网领域。

现在,互联网已经遍布了世界的每一个角落,甚至包括地球之外,任何人可以在半秒之内,联结世界上任意两点。

计算机同样每时每刻都出现在我们的生活中。计算机和互联网的发明,可谓人类历史上最重要的科技革命之一。

计算机和互联网同样可以算是由全人类共同构建的,最精密也是最庞大的工程。

每一个芯片都是在纳米级被雕刻而成的,容不得哪怕一粒灰尘的差错。

当你在QQ上发出一条消息时,它会在光纤中穿过大半个中国,经过十几个路由的转发,调用若干台服务器,最后在不到一秒内被对方看到。

无数恐怖分子对互联网虎视眈眈,但它仍健壮地运作。

计算机是完美的,对于最终用户,他们能直接使用着“完美”的软件,而不必关心CPU如何执行指令,也不必关心电子如何在导线中运动。

事实上大部分程序员也不必关心这些。计算机是层层抽象的,每一层都隐去了下一层的丑陋之处。

借助这种抽象,我们可以逃离基于物理定律的现实世界,建立一个全新的,完美的世界。

数学,哲学,文学,都可以构造一个新的世界,但计算机又是以十分直接的方式在影响着现实世界,推动着生产力的提高。

计算机还是一个几乎全新的学科,仅60年历史,而互联网更是仅仅30年。

硬件在以摩尔定律预测的那样每年翻一番,而软件方面则一直没多大进展,没人知道计算机领域今后会发展成怎样。

相比于已经十分稳定的学科,计算机还在快速发展,有更多的变数,也有着更多可能取得突破的机会。

即使仅有60年的历史,计算机领域仍留下了无数的历史遗留问题。不同于自然科学的假设,论证,修正,也不同于数学或哲学的各自为战。

计算机是众人合力搭建的大楼,仅有一次机会,地基中一丁点的错误,都很难被改正,只能以之后更多的努力来尽力弥补。

这样看来,计算机也是不完美的,这就是它的动人之处。

电影:那些年,我们一起追的女孩

题目和正文无关。

我是个胆子很小,很不会说话的人,我喜欢写字,这样没有人打断我。

班主任给我的印象,就是一个没有感情的人,永远戴着面具,极少见她摘下来。不是我如何偏激,这是我见过的唯一一个这样的老师。

当白老师对我说“你这要是在我们班敢这么顶撞老师,同学早上去揍你了”时,为什么我听到了另外一层意思。  

我也试着和她说过一些心里话,但一点用都没有。她就好像一个没有记忆的机器人。“赶紧进入学习状态”,“上操动作都快一点”,“在学校就要穿校服”我不会记差一个字。自诩作为语文老师有很高的文学素养,但骂人除了“恶心”外也只有偶尔的脏字。

班主任的下限随时在刷新。

前一阵,班主任在批评一个同学:“你说你还像一个男子汉么?”,碰巧我对这个同学的人品略有不满,我在等班主任接下来准备讲什么大道理,哪怕是“是男子汉就要争一口气多考几分”也好啊。她说的却是“做人的底线就是按时完成作业”,真让人掉胃口啊。

除了作业,她的做人底线还有“做人的底线就是不迟到”,“做人的底线就是保持安静”,“做人的底线就是天天穿校服”。

说实话,这也不算什么,每个人都有那么几个口头禅,都有点不怎么注意的细节,我也承认老师这个职业是非常非常辛苦的。

电影中,最令我感动的是,当老师怀疑是班级中的同学偷了钱,让同学们写字条互相指认的时候。有不止一个人能够站出来,指出老师不应该这么做,指出自己信任班级里所有的同学。我恨当时这种事发生在自己班上的时候,自己没有这样的胆量,只有在纸上,我才能写出这样一段话。

班主任在上次汽车莫名其妙被划的时候,就像这样让每个人拿一个纸条,写自己怀疑谁,或者其他线索,所有人必须交。在接下来至少一个星期里,整个班级陷入了一种诡异的,互相怀疑的气氛,而且整个事件,到现在也没个结果。

还有一年前,一位同学在交费的时候以十个一毛钱抵一元钱,引起班主任不满。这件小事立刻被上升到道德高度,“这件小事折射出某些同学心灵深处的一些东西”。

接着班主任开始点人,让同学们评价谁对谁错。

今天,班主任见班级纪律混乱,打算制定一番新规定,以及一套不大合情理的惩罚措施。这也没什么,规则要有,惩罚也要有。

但我最不爽的是班主任一定要挨个点同学站起来说该如何惩罚违反规则的人,借同学之口,说班主任她已经想好的话,直到说得满意了,才让人家坐下。

最后好像这些规则惩罚,都是同学们定的一样。最后还不忘要每个同学都写张纸条谈谈对这个规则有什么意见。

试问这种情况下,班级的凝聚力从何而来?作为班主任还有多少威信?有多少同学说的是真心话?

我期待被全班同学揍一顿的那一天。

以上我简单地解释了一下今天我顶撞老师的理由,既然让我写检讨书,就是让我先找理由嘛。

然后我应当反思我的错误。

首先,对于一年前我指正班主任一个微不足道的错误事情,我现在意识到了,是我过于敏感,故意给老师挑毛病。

不指出这个错误也并不影响班主任以不太可靠的论据来教育同学,一直以来我都爱在不值得一提的小事上较真,这确实值得我反思。

而今天的事情,我过于冲动了,我本来可以以略微委婉一些的方式来表达自己的观点,或者直接保持沉默。不应当以非常偏激,讽刺的话来顶撞老师。

从小到大,几乎每个长辈对我的评价无外乎两点,聪明,较真(固执).

我正在写的另一篇日志想说,我并不聪明,也许只是较真而已。不然我不会花那么多那么多的时间,顶着那么大那么大的压力,去读各种老师们所谓的“闲书”。

较真,有利有弊,今后我要尽量做到,对自己,对知识较真。对人对事,能退一步就退一步,用德育处一位老师的话说,“分清理想和现实”,说合适的话。

不得不说,德育处白老师,和我们班主任绝对不是同一等级的,一开始没让我说话,等让我开口的时候,我就知道我已经输了。白老师看人很准,说我较真,固执,幼稚,都十分准确。唯独“缺乏正能量”这点我不能同意,摘我上个月所写的一篇日志[注1]:

“我是个很固执,很天真的人,也许你觉得用天真来形容我很不恰当。但我相信世界总是在向好的方向发展,我相信正义终能战胜邪恶,阴暗面会越来越少,我相信付出总会有收获,我相信会有纯粹的友情和爱情,我相信宪法早晚能够切实落实下去,我相信每个人都有像我一样天真的一面,你相信么?这不是开玩笑,我觉得一个人的世界观,在17岁可以算是初步形成了。”

不管你怎么认为,我的出发点一样是好的,我当然爱这个世界,我的祖国,我的班级。

我的字条:“没(hěn)意(bù)见(shuǎng),借同学之口,说你的话,有意思么”。

为PHP构建可读的错误报告

当年使用ASP.Net时,每当程序出错,ASP.Net均会显示一个“漂亮”的错误页,列出出错所在行附近的代码,调用栈和一些提示信息等等。

相比之下,PHP默认的报错信息十分简陋,仅仅指明了出错的位置。

本文将介绍通过PHP的错误处理相关函数,构造一个类似的,具有丰富的可利用的信息的错误页。

Asp.Net的错误页

提示:本文示例至少需要PHP5.4, 本文使用了PHP5.4中的数组简写语法,和PHP5.3中的匿名函数语法。

应该说很多PHP框架都提供了类似的功能,本文展现的仅仅是其中一种方式。作者我并没有太多地参考其他PHP框架,仅仅是从官网文档下方的注释中精炼出了一些实用的功能。

错误信息并非任何时候都可以展示的,很多情况下,错误信息中包含了很多敏感的数据,容易被攻击者利用。所以显然我们首先要引入一个“运行级别”的概念,该级别将控制我们的错误页展示错误详细信息的程度。

// 为避免污染全局命名空间,已对所有标识符加 `e` 前缀

/**
 *  运行模式
 *  * debug 调试模式, 会开启详细的日志记录和错误提示
 *  * default 默认模式, 会输出错误提示
 *  * production 生产模式, 不会执行任何额外操作, 最高效地执行代码
 */

const eDebug = 2;
const eDefault = 1;
const eProduction = 0;

const eRunMode = eDefault;

PHP5开始,PHP彻底进入OPP时代,但仍留下了不少历史遗留问题。比如直到目前,PHP的内部错误仍然没有以异常的方式实现,绝大部分的内置函数也没有以异常的方式来报告错误,这种内置的错误报告由error_reporting函数进行控制:

// 默认关掉所有错误提示
error_reporting(0);
// 如果是默认模式,打开致命错误和解析错误的报告
if(eRunMode >= eDefault)
    error_reporting(E_ERROR | E_PARSE);
// 如果是调试模式,打开全部错误提示,并启用修改建议提示
if(eRunMode >= eDebug)
    error_reporting(E_ALL | E_STRICT);

为了能够在出现错误的时候,去除已有的输出,以及追加 HTTP 头,我们要开启缓冲:

ob_start();

然后我们需要向PHP引擎申请通过我们自己的函数来接管错误处理的相关流程,这其中有两个关键函数:

  • set_error_handler
  • set_exception_handler

set_error_handler 会注册一个自定义函数用于接管由PHP产生的错误,set_exception_handler 会注册一个自定义函数用于接管在最外层仍未被处理的异常。

在这里,我们会用 error_handler 函数来将PHP产生的错误统一转换为异常,再通过 exception_handler 来实现错误页的展现。

set_error_handler 所注册的自定义函数会被传递5个参数,分别是错误号,错误描述信息,错误文件名和行号,当前上下文的符号表(当前时刻所有变量的值).

PHP文档中推荐我们在自定义函数中重新抛出 ErrorException 类型的异常,这是一个内置的异常类型,但它并没有提供储存符号表的功能,而错误发生时的符号表却是非常有用的,所以这里我选择了自己构造一个异常类型来代替 ErrorException, 以便能将符号表储存下来。

这个工作很简单:

class ePHPException extends Exception
{
    protected $severity;
    protected $varList;

    public function  __construct($message = "", $code = 0, $severity = 1, $filename = __FILE__, $lineno = __LINE__, Exception $previous = null, $varList = [])
    {
        $this->severity = $severity;
        $this->file = $filename;
        $this->line = $lineno;
        $this->varList = $varList;

        // 调用父类的构造函数
        parent::__construct($message, $code, $previous);
    }

    public function getSeverity()
    {
        return $this->severity;
    }

    public function getVarList()
    {
        return $this->varList;
    }
}

然后我们使用 set_error_handler 注册一个自定义函数将PHP报告的错误转换为抛出 ePHPException 类型的异常.

set_error_handler(function($no, $str, $file, $line, $varList) {
    throw new ePHPException($str, 0, $no, $file, $line, null, $varList);
});

然后我们要重新考虑运行级别的问题,在调试模式和默认模式时,我们打算显示错误信息,而在生产模式下,我们不打算显示任何信息:

if(eRunMode <= eProduction)
{
    set_exception_handler(function(Exception $exception) {
        header("Content-Type: text/plant; charset=UTF-8");

        die(header("HTTP/1.1 500 Internal Server Error"));
    });
}

现在我们开始设计真正的错误处理函数,需要显示的信息包括:异常类型名,描述信息,调用栈,符号表,附近代码等等,这些信息都可以轻松地从异常对象上获取到:

else
{
    set_exception_handler(function(Exception $exception) {
        // 暂时我们只打算以纯文本的形式展示信息
        header("Content-Type: text/plant; charset=UTF-8");

        // 头部
        print sprintf(
            "Exception `%s`: %s\n",
            get_class($exception),
            $exception->getMessage()
        );

        // 运行栈
        print "\n^ Call Stack:\n";
        // 从异常对象获取运行栈
        $trace = $exception->getTrace();
        // 如果是 ePHPException 则去除运行栈的第一项,即 error_handler
        if($exception instanceof ePHPException)
            array_shift($trace);

        // 只有在调试模式才会显示参数的值,其他模式下只显示参数类型
        if(eRunMode < eDebug)
            foreach ($trace as $key => $v)
                $trace[$key]["args"] = array_map("gettype", $trace[$key]["args"]);

        // 用于打印参数的函数
        $printArgs = function($a) use(&$printArgs)
        {
            $result = "";
            foreach($a as $k => $v)
            {
                if(is_array($v))
                    $v = "[" . $printArgs($v) . "]";
                else
                    if(is_string($v))
                        $v = "`{$v}`";
                if(!is_int($k))
                    $v = "`$k` => $v";

                $result .= ($result ? ", {$v}" : $v);
            }
            return $result;
        };

        // 打印运行栈
        foreach ($trace as $k => $v)
            print sprintf(
                "#%s %s%s %s(%s)\n",
                $k,
                isset($v["file"]) ? $v["file"] : "",
                isset($v["line"]) ? "({$v["line"]}):" : "",
                $v["function"],
                $printArgs($v["args"])
            );

        print sprintf(
            "#  {main}\n  thrown in %s on line %s\n\n",
            $exception->getFile(),
            $exception->getLine()
        );

        // 如果当前是调试模式,且异常对象是我们构造的 ePHPException 类型,打印符号表和源代码
        if(eRunMode >= eDebug && $exception instanceof ePHPException)
        {
            // 用于打印符号表的函数
            $printVarList = function($a, $tab=0) use(&$printVarList)
            {
                $tabs = str_repeat("   ", $tab);
                foreach($a as $k => $v)
                    if(is_array($v))
                        if(!$v)
                            print "{$tabs}`{$k}` => []\n";
                        else
                            print "{$tabs}`{$k}` => [\n" . $printVarList($v, $tab+1) . "{$tabs}]\n";
                    else
                        print "{$tabs}`{$k}` => `{$v}`\n";
            };

            print "^ Symbol Table:\n";
            $printVarList($exception->getVarList());

            print "\n^ Code:\n";

            // 显示出错附近行的代码
            $code = file($exception->getFile());
            $s = max($exception->getLine()-6, 0);
            $e = min($exception->getLine()+5, count($code));
            $code = array_slice($code, $s, $e - $s);

            // 为代码添加行号
            $line = $s + 1;
            foreach($code as &$v)
            {
                $l = $line++;
                if(strlen($l) < 4)
                    $l = str_repeat(" ", 4-strlen($l)) . $l;
                if($exception->getLine() == $l)
                    $v = "{$l}->{$v}";
                else
                    $v = "{$l}  {$v}";
            }

            print implode("", $code);
        }

    });
}

下面我们编写一个示例来测试一下:

function throwException()
{
    class MyException extends Exception{}
    throw new MyException("Aal izz well");
}

function call($func, $arg)
{
    $func($arg);
}

// 错误1:
call("array_keys", 1234);
// 错误2:
throwException();

下面是错误1在eDefault下的显示:

Exception `ePHPException`: array_keys() expects parameter 1 to be array, integer given

^ Call Stack:
#0 /var/www/test.php(188): array_keys(integer)
#1 /var/www/test.php(192): call(string, integer)
#  {main}
  thrown /var/www/test.php on line 188

很不错!显示了错误信息和运行栈,运行栈中的参数内容都以类型掩去了。

在 eDebug 下:

Exception `ePHPException`: array_keys() expects parameter 1 to be array, integer given

^ Call Stack:
#0 /var/www/test.php(188): array_keys(1234)
#1 /var/www/test.php(192): call(`array_keys`, 1234)
#  {main}
  thrown in /var/www/test.php on line 188

^ Symbol Table:
`func` => `array_keys`
`arg` => `1234`

^ Code:
 183      throw new MyException;
 184  }
 185
 186  function call($func, $arg)
 187  {
 188->    $func($arg);
 189  }
 190
 191  // 错误1:
 192  call("array_keys", 1234);
 193  // 错误2:

也如我们预期一样,在eDebug模式下,在运行栈中会显示参数内容,还会显示符号表和出错附近的代码。

至于eProduction模式,服务器没有返回任何信息,浏览器直接给出了下面的错误提示:

HTTP 错误 500(Internal Server Error):服务器尝试执行请求时遇到了意外情况。

错误2的eDefault模式(其他两个模式不再详细展示):

Exception `MyException`: Aal izz well

^ Call Stack:
#0 /var/www/test.php(194): throwException()
#  {main}
  thrown in /var/www/test.php on line 183

就这样,我们通过百行左右的代码实现了一个具有丰富的可利用信息的PHP错误页,除了我们仅仅是纯文本,已经不逊色于Asp.Net的错误页了。

这将会是非常值得的,会为你的调试工作带来很多方便。

本文全部代码:https://gist.github.com/jybox/5789249

应该说,这个方案是存在硬伤的:PHP并不允许用户使用自定义函数处理致命错误!这包括无法解析的语法,调用未定义的函数等等行为,在发生致命错误时,仍会出现PHP那“丑陋”的错误提示。好在,但凡是致命错误,大都是很容易更正的,反倒是那些乱七八糟的“警告”和“提示”,最难以调试,这也是本文希望解决的问题。

更进一步地,你可以使用PHP的反射机制,获取更多的信息用于调试:http://www.php.net/manual/zh/book.reflection.php

所谓反射,就是在运行时,与PHP的解释器进行交互,获取关于代码的元信息,进行“反向工程”的一种机制,使用反射,你可以深入地挖掘类,函数,扩展的元信息。

比如,获取一个匿名函数的源代码:http://segmentfault.com/q/1010000000160912

精子生于 1995 年,英文 ID jysperm.

订阅推送

通过 Telegram Channel 订阅我的博客日志、产品和项目的动态:

王子亭的博客 @ Telegram


通过邮件订阅订阅我的博客日志、产品和项目的动态(历史邮件):

该博客使用基于  Hexo  的  simpleblock  主题。博客内容使用  CC BY-NC-ND  授权发布。最后生成于 2023-12-20.