我开发了一个基于 Beancount 的账本托管服务 HostedBeans,欢迎大家来了解纯文本复式记账并试用我的服务。
标签 #PHP

LightPHP v5: 闲聊PHP框架

展望LightPHP v4是半年前的事情了:http://jyprince.me/program/681.

LightPHP v4算是写出来了,但代码都没经过细致的测试,基本上一边用它写RootPanel一边完善,排Bug, 后来干脆把LightPHP的开发分支丢到RootPanel了。
刚刚统计了一下,不知不觉这么多代码了(不计空行):

  • cli工具集(PHP和部分参杂了PHP的配置文件模版) - 243行
  • 文档(.md) - 251行
  • LightPHP核心部分 - 1806行
  • LightPHP杂项文件 - 51行
  • 配置文件 - 75行
  • 控制器(Handler) - 693行
  • 核心文件 - 426行
  • 国际化(目前仅中文) - 507行
  • 数据模型(Model) - 338行
  • 独立的CSS - 186行
  • 独立的JS - 168行
  • 模版文件(参杂了PHP, CSS, JS的HTML文件) - 1479行
  • 杂项文件 - 15行
  • 引用的其他开源项目:jQuery, Bootstrap, 雅黑工作室的3款探针, 本人的taskinfo.php, Kill-IE6

列得我好有成就感~

总之,写了这么多代码,重构了那么多次,对于PHP的框架,我又有新的体会要说。


正在设计和编写中的LightPHP v5应该说和v4相比变化不算太大,但是根据语义化版本控制(http://semver.org/)的精神,还是要把它命名为v5.

我有提到,我打算认真写一本PHP的书,暂定《PHP进阶——一个论坛系统的实现》。以我这几年折腾的经验,来谈一谈如何利用好PHP的特征,写出优雅的代码,跳过PHP基础,以一个论坛系统的实现为例,这其中当然也隐含了一个PHP框架的实现。

部分草稿可以看下面,不过最后成品肯定会有很大变化的:

这个所谓的“以一个论坛系统的实现为例”, 我打算给它起名 LightTalk. 除此之外我还有另一个论坛系统的项目:JyBBS.
这两个论坛唯的区别恐怕就是LightTalk是随书的示例,是一个完整的论坛,其部分代码也来自LightPHP, 具有宽松的版权,书写好后不再维护。
而JyBBS是基于LightPHP的论坛系统,版权略严格,我打算一直维护。

真是乱呢…


  • 定位

在v4以及之前的版本中,LightPHP虽定位为PHP库,但仍包含了一部分前端模版(v3), 以及Bootstrap, jQuery等东西(v4).
v5中将删去他们,LightPHP将成为一个纯粹的PHP库。

// LightPHP v5的结构

lp-class/  --  LightPHP的核心类
lp-config.php  --  配置文件
lp-load.php  --  用于载入LightPHP
...  --  其他杂项文件,如URL重写规则等
  • 数据库, Model

LightPHP最初就是从一个MySQL封装开始的(模仿自Abreto), 后来数据库封装也一直是LightPHP的核心部分, 在v3中,我力图实现无关SQL的CRUD.
v4中我开始模仿MongoDB的链式调用语法,并且打算实现数据库无关,但我好像失败了。

数据库的差异毕竟是存在的,不可能实现真正的无关,Model不需要处理所有的数据库查询,只要提供最简单的CRUD就够了,复杂的理应交给SQL或具体的数据库API.
Model是我学习了最长时间的概念,现在我可以说我真正地理解了。

另外v5中我用PDO代替了C Style MySQL API, 毕竟是大势所趋。

// 抽象的Model基类,以下是经过精简的成员函数列表
abstract class lpPDOModel implements ArrayAccess
{
    static public function select($if = [], $config = []);
    static public function find($if = [], $config = []);
    static public function count($if = [], $config = []);
    static public function insert($data);
    static public function update($if, $data);
    static public function delete($if);
    static public function install();
}

// 通过继承来“实例化”一个Model, 代码有精简
class rpUserModel extends lpPDOModel
{
    static protected $metaData = null;

    // 具体Model特有的常量成员
    const NO = "no";
    const STD = "std";
    const EXT = "ext";
    const FREE = "free";

    // 为lpPDOModel提供元信息,有了这些信息,lpPDOModel不仅可以检查错误,还可以自动完成数据表的构建
    static protected function metaData()
    {
        if(!self::$metaData) {
            self::$metaData = [
                "db" => f("lpDBDrive"),
                "table" => "user",
                "engine" => "MyISAM",
                "charset" => "utf8",
                self::PRIMARY => "id"
            ];

            self::$metaData["struct"] = [
                "id" => ["type" => self::AI],
                "uname" => ["type" => self::VARCHAR, "length" => 256],
                "passwd" => ["type" => self::TEXT],

                // 支持 JSON 格式哦!

                "settings" => ["type" => self::JSON],
            ];
        }

        return self::$metaData;
    }

    // Model特有的成员函数
    public function isAllowToPanel()
    {
        if($this["type"] != self::NO && !$this->isAdmin())
            return true;
        return false;
    }
}

// 查询
$passwd = rpUserModel::find(["uname" => "jybox"])["passwd"];
// 插入,可以直接插入数组哦,lpPDOModel会将其 json_encode 后储存为字符串,读取时反之
rpUserModel::insert(["uname" => "jybox", "settings" => ["key1" => "value1"]]);
  • 模版

模版也是LightPHP的早期功能之一,我没有选择Smarty之类的专有语言,我直接用PHP编写模版,毕竟PHP本身也算是一个十分方便的模版语言。
在v4中,对模版的解析是用eval, 这不仅存在安全问题,而且显得非常不优雅。知道前一阵,我才发现,我要找到的东西其实就近在眼前!其实只要include就好…
v5中模版类的代码相比于v4有了不少精简,变得更加“一般化”了。

v5中删去了Bootstrap, jQuery等库,我意识到了静态文件至少逻辑上应该位于单独的服务器,不应该和LightPHP混在一起。

一个模版就是一个普通的PHP文件,参数来自 $this:

Hello <?= $this["name"];?>

显示模版:

lpTemplate::outputFile("/path/to/template.php", ["name" => "Jybox"]);
  • 缓存(键/值对储存)

我是从Asp.Net过来的,很喜欢Application类提供的全局键值对储存,但PHP是并发模型的,没有全局的管理器,语言本身没办法实现这种功能,于是我先后调研了使用文件, MySQL, Memcache, APC来实现。
在之前的版本中,一直都有这个功能(虽然Bug成堆), 但v5现在却没有,原因是还没来得及写,我愈发觉得这个功能可能并没有那么重要,大概要等到站点规模大到需要缓存的时候才会用到吧。

  • 登录状态验证

在v4中,我用接口的方式实现了两套验证组件,分别是经典验证和会话式验证,应该说虽然没实现当初设想的全部功能,但它们工作的还不错。

  • 配置文件管理

在v3和v4中LightPHP都没有用于管理配置文件的类,只需定义一个数组,然后在程序启动的时候include一下就好了。
但是当RootPanel需要4个配置文件(虽然后来精简到了3个), 我发现我需要这么一个玩意了。

编写这么一个单独的类我也是为了去掉全局变量,因为在函数中使用全局变量,还要很麻烦地用gloabl声明一下。

  • 调试

v5中我引入了一个自己编写的错误处理器,可以在脚本出错时打印一些有用的调试信息。
同时我还正在编写一个有用的调试日志系统。

关于错误报告可以参见这篇文章:http://jyprince.me/program/1079

  • 构造器

构造器的概念来自于设计模式中的所谓单例模式,在一个PHP脚本中,很多对象需要使用多次,每次都构造新的对象是很浪费的,但如果不使用全局变量又很难实现代码的多个部分共享一个对象。
构造器以“注册——获取”的功能解决了这个问题,在程序开始时,程序向构造器注册用于构造各种对象的函数,但只有当程序需要获取这个对象时,这个函数才会真正地被执行,而且构造一次后,再反复获取,获取到的都是第一次构造的对象的引用。
使用构造器统一构造对象,也省去了接口,LightPHP的类型检查本来就不严格。

// 注册一个构造器
lpFactory::register("lpLocale", function() {
    $path = rpROOT . "/locale";
    return new lpLocale($path, lpLocale::judegeLanguage($path, c("DefaultLanguage")));
});

// 获取一个对象
$rpL = lpFactory::get("lpLocale");

目前该构造器存在一个问题就是无法获得IDE的类型提示,不过目前部分IDE正在着手开发针对于这类构造器的类型提示API.

  • 国际化

v5首次加入了国际化支持,目前使用的是一种非常简单的,基于数组的专有翻译文件格式,我正在设法改进。

  • 路由和分发器

路由在v4的基础上仅做了一丁点优化。

例如请求URL是 /user/show/jybox
那么默认将会创建user类的一个实例, 以jybox为参数, 调用它的show函数。
当然,这个过程中会对类名做一些修饰,这些都是可控的,你可以编写自己的分发器。

  • 工具

v5包含了更多工具组件:分页,锁,Smtp等。

  • 插件

在v4设计之处我就设法加入插件系统,但我查阅了一些项目的设计之后发现,除了在代码中加入大量的hook, 似乎没啥有效的办法了,不喜欢这种简单粗暴的方式。

  • 短函数名

之前看ThinkPHP, 看它用了很多一个字母的短函数名,感觉很不优雅,但现在为了折中代码长度,我也不得不定义了四个短函数名。

c() - 配置文件选项
d() - 数据库连接
f() - 构造器
l() - 本地化翻译

LightPHP到底是个啥定位呢,除了README中(https://github.com/jybox/LightPHP/blob/master/README.md)写的那些精神上的东西。

LightPHP是我逐渐摸索出来的,后期参考了一些其他框架,抽取了他们最核心,最基本的功能。
换句话说,我用20%的代码,实现了他们80%的功能,至于另外比较难实现的20%, 我干脆不要了~

我对LightPHP的定位是一个库,并非框架,这一点可以体现在LightPHP对全局命名空间几乎没有污染,LightPHP的各个部分也比较松散,可以单独使用。
LightPHP也从未规定应用的文件如何组织,没对应用的代码做什么假设。

为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 &gt;= eDefault)
    error_reporting(E_ERROR | E_PARSE);
// 如果是调试模式,打开全部错误提示,并启用修改建议提示
if(eRunMode &gt;= 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-&gt;severity = $severity;
        $this-&gt;file = $filename;
        $this-&gt;line = $lineno;
        $this-&gt;varList = $varList;

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

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

    public function getVarList()
    {
        return $this-&gt;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 &lt;= 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-&gt;getMessage()
        );

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

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

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

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

        // 打印运行栈
        foreach ($trace as $k =&gt; $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-&gt;getFile(),
            $exception-&gt;getLine()
        );

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

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

            print "\n^ Code:\n";

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

            // 为代码添加行号
            $line = $s + 1;
            foreach($code as &amp;$v)
            {
                $l = $line++;
                if(strlen($l) &lt; 4)
                    $l = str_repeat(" ", 4-strlen($l)) . $l;
                if($exception-&gt;getLine() == $l)
                    $v = "{$l}-&gt;{$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` =&gt; `array_keys`
`arg` =&gt; `1234`

^ Code:
 183      throw new MyException;
 184  }
 185
 186  function call($func, $arg)
 187  {
 188-&gt;    $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

PHP进阶:2.PHP中的新特征(1)

这篇文章的最新版本位于 PHP 自 5.2 到 5.6 中新增的功能详解,该页面已停止维护 —— 2015.5.29

截至目前(2014.2), PHP 的最新稳定版本是 PHP5.5, 但有差不多一半的用户仍在使用已经不在维护 [注] 的 PHP5.2, 其余的一半用户在使用 PHP5.3 [注].
因为 PHP 那“集百家之长”的蛋疼语法,加上社区氛围不好,很多人对新版本,新特征并无兴趣。
本文将会介绍自 PHP5.2 起,直至 PHP5.6 中增加的新特征。

  • PHP5.2 以前:autoload, PDO 和 MySQLi, 类型约束
  • PHP5.2:JSON 支持
  • PHP5.3:弃用的功能,匿名函数,新增魔术方法,命名空间,后期静态绑定,Heredoc 和 Nowdoc, const, 三元运算符,Phar
  • PHP5.4:Short Open Tag, 数组简写形式,Traits, 内置 Web 服务器,细节修改
  • PHP5.5:yield, list() 用于 foreach, 细节修改
  • PHP5.6: 常量增强,可变函数参数,命名空间增强

注:已于2011年1月停止支持: http://www.php.net/eol.php
注:http://w3techs.com/technologies/details/pl-php/5/all

PHP5.2以前

(2006前)
顺便介绍一下 PHP5.2 已经出现但值得介绍的特征。

autoload

大家可能都知道 __autoload() 函数,如果定义了该函数,那么当在代码中使用一个未定义的类的时候,该函数就会被调用,你可以在该函数中加载相应的类实现文件,如:

function __autoload($classname)
{
    require_once("{$classname}.php")
}

但该函数已经不被建议使用,原因是一个项目中仅能有一个这样的 __autoload() 函数,因为 PHP 不允许函数重名。但当你使用一些类库的时候,难免会出现多个 autoload 函数的需要,于是 spl_autoload_register() 取而代之:

spl_autoload_register(function($classname)
{
    require_once("{$classname}.php")
});

spl_autoload_register() 会将一个函数注册到 autoload 函数列表中,当出现未定义的类的时候,SPL [注] 会按照注册的倒序逐个调用被注册的 autoload 函数,这意味着你可以使用 spl_autoload_register() 注册多个 autoload 函数.

注:SPL: Standard PHP Library, 标准 PHP 库, 被设计用来解决一些经典问题(如数据结构).

PDO 和 MySQLi

即 PHP Data Object, PHP 数据对象,这是 PHP 的新式数据库访问接口。

按照传统的风格,访问 MySQL 数据库应该是这样子:

// 连接到服务器,选择数据库
$conn = mysql_connect("localhost", "user", "password");
mysql_select_db("database");

// 执行 SQL 查询
$type = $_POST['type'];
$sql = "SELECT * FROM `table` WHERE `type` = {$type}";
$result = mysql_query($sql);

// 打印结果
while($row = mysql_fetch_array($result, MYSQL_ASSOC))
{
    foreach($row as $k => $v)
        print "{$k}: {$v}\n";
}

// 释放结果集,关闭连接
mysql_free_result($result);
mysql_close($conn);

为了能够让代码实现数据库无关,即一段代码同时适用于多种数据库(例如以上代码仅仅适用于MySQL),PHP 官方设计了 PDO.
除此之外,PDO 还提供了更多功能,比如:

  • 面向对象风格的接口
  • SQL预编译(prepare), 占位符语法
  • 更高的执行效率,作为官方推荐,有特别的性能优化
  • 支持大部分SQL数据库,更换数据库无需改动代码

上面的代码用 PDO 实现将会是这样:

// 连接到数据库
$conn = new PDO("mysql:host=localhost;dbname=database", "user", "password");

// 预编译SQL, 绑定参数
$query = $conn->prepare("SELECT * FROM `table` WHERE `type` = :type");
$query->bindParam("type", $_POST['type']);

// 执行查询并打印结果
foreach($query->execute() as $row)
{
    foreach($row as $k => $v)
        print "{$k}: {$v}\n";
}

PDO 是官方推荐的,更为通用的数据库访问方式,如果你没有特殊需求,那么你最好学习和使用 PDO.
但如果你需要使用 MySQL 所特有的高级功能,那么你可能需要尝试一下 MySQLi, 因为 PDO 为了能够同时在多种数据库上使用,不会包含那些 MySQL 独有的功能。

MySQLi 是 MySQL 的增强接口,同时提供面向过程和面向对象接口,也是目前推荐的 MySQL 驱动,旧的C风格 MySQL 接口将会在今后被默认关闭。
MySQLi 的用法和以上两段代码相比,没有太多新概念,在此不再给出示例,可以参见 PHP 官网文档 [注]。

注:http://www.php.net/manual/en/mysqli.quickstart.php

类型约束

通过类型约束可以限制参数的类型,不过这一机制并不完善,目前仅适用于类和 callable(可执行类型) 以及 array(数组), 不适用于 string 和 int.

// 限制第一个参数为 MyClass, 第二个参数为可执行类型,第三个参数为数组
function MyFunction(MyClass $a, callable $b, array $c)
{
    // ...
}

PHP5.2

(2006-2011)

JSON 支持

包括 json_encode(), json_decode() 等函数,JSON 算是在 Web 领域非常常用的数据交换格式,可以被 JS 直接支持,JSON 实际上是 JS 语法的一部分。
JSON 系列函数,可以将 PHP 中的数组结构与 JSON 字符串进行转换:

$array = ["key" => "value", "array" => [1, 2, 3, 4]];
$json = json_encode($array);
echo "{$json}\n";

$object = json_decode($json);
print_r($object);

输出:

{"key":"value","array":[1,2,3,4]}
stdClass Object
(
    [key] => value
    [array] => Array
        (
            [0] => 1
            [1] => 2
            [2] => 3
            [3] => 4
        )
)

值得注意的是 json_decode() 默认会返回一个对象而非数组,如果需要返回数组需要将第二个参数设置为 true.

PHP5.3

(2009-2012)

PHP5.3 算是一个非常大的更新,新增了大量新特征,同时也做了一些不向下兼容的修改。

弃用的功能

以下几个功能被弃用,若在配置文件中启用,则 PHP 会在运行时发出警告。

Register Globals

这是 php.ini 中的一个选项(register_globals), 开启后会将所有表单变量($_GET和$_POST)注册为全局变量.
看下面的例子:

if(isAuth())
    $authorized = true;
if($authorized)
    include("page.php");

这段代码在通过验证时,将 $authorized 设置为 true. 然后根据 $authorized 的值来决定是否显示页面.

但由于并没有事先把 $authorized 初始化为 false, 当 register_globals 打开时,可能访问 /auth.php?authorized=1 来定义该变量值,绕过身份验证。

该特征属于历史遗留问题,在 PHP4.2 中被默认关闭,在 PHP5.4 中被移除。

Magic Quotes

对应 php.ini 中的选项 magic_quotes_gpc, 这个特征同样属于历史遗留问题,已经在 PHP5.4 中移除。

该特征会将所有用户输入进行转义,这看上去不错,在第一章我们提到过要对用户输入进行转义。
但是 PHP 并不知道哪些输入会进入 SQL , 哪些输入会进入 Shell, 哪些输入会被显示为 HTML, 所以很多时候这种转义会引起混乱。

Safe Mode

很多虚拟主机提供商使用 Safe Mode 来隔离多个用户,但 Safe Mode 存在诸多问题,例如某些扩展并不按照 Safe Mode 来进行权限控制。
PHP官方推荐使用操作系统的机制来进行权限隔离,让Web服务器以不同的用户权限来运行PHP解释器,请参见第一章中的最小权限原则.

匿名函数

也叫闭包(Closures), 经常被用来临时性地创建一个无名函数,用于回调函数等用途。

$func = function($arg)
{
    print $arg;
};

$func("Hello World");

以上代码定义了一个匿名函数,并赋值给了 $func.
可以看到定义匿名函数依旧使用 function 关键字,只不过省略了函数名,直接是参数列表。

然后我们又调用了 $func 所储存的匿名函数。

匿名函数还可以用 use 关键字来捕捉外部变量:

function arrayPlus($array, $num)
{
    array_walk($array, function(&$v) use($num){
        $v += $num;
    });
}

上面的代码定义了一个 arrayPlus() 函数(这不是匿名函数), 它会将一个数组($array)中的每一项,加上一个指定的数字($num).

在 arrayPlus() 的实现中,我们使用了 array_walk() 函数,它会为一个数组的每一项执行一个回调函数,即我们定义的匿名函数。
在匿名函数的参数列表后,我们用 use 关键字将匿名函数外的 $num 捕捉到了函数内,以便知道到底应该加上多少。

魔术方法:__invoke(), __callStatic()

PHP 的面向对象体系中,提供了若干“魔术方法”,用于实现类似其他语言中的“重载”,如在访问不存在的属性、方法时触发某个魔术方法。

随着匿名函数的加入,PHP 引入了一个新的魔术方法 __invoke().
该魔术方法会在将一个对象作为函数调用时被调用:

class A
{
    public function __invoke($str)
    {
        print "A::__invoke(): {$str}";
    }
}

$a = new A;
$a("Hello World");

输出毫无疑问是:

A::__invoke(): Hello World

__callStatic() 则会在调用一个不存在的静态方法时被调用。

命名空间

PHP的命名空间有着前无古人后无来者的无比蛋疼的语法:

<?php
// 命名空间的分隔符是反斜杠,该声明语句必须在文件第一行。
// 命名空间中可以包含任意代码,但只有 **类, 函数, 常量** 受命名空间影响。
namespace XXOO\Test;

// 该类的完整限定名是 \XXOO\Test\A , 其中第一个反斜杠表示全局命名空间。
class A{}

// 你还可以在已经文件中定义第二个命名空间,接下来的代码将都位于 \Other\Test2 .
namespace Other\Test2;

// 实例化来自其他命名空间的对象:
$a = new \XXOO\Test\A;
class B{}

// 你还可以用花括号定义第三个命名空间
namespace Other {
    // 实例化来自子命名空间的对象:
    $b = new Test2\B;

    // 导入来自其他命名空间的名称,并重命名,
    // 注意只能导入类,不能用于函数和常量。
    use \XXOO\Test\A as ClassA
}

更多有关命名空间的语法介绍请参见官网 [注].

命名空间时常和 autoload 一同使用,用于自动加载类实现文件:

spl_autoload_register(
    function ($class) {
        spl_autoload(str_replace("\\", "/", $class));
    }
);

当你实例化一个类 \XXOO\Test\A 的时候,这个类的完整限定名会被传递给 autoload 函数,autoload 函数将类名中的命名空间分隔符(反斜杠)替换为斜杠,并包含对应文件。
这样可以实现类定义文件分级储存,按需自动加载。

注:http://www.php.net/manual/zh/language.namespaces.php

后期静态绑定

PHP 的 OPP 机制,具有继承和类似虚函数的功能,例如如下的代码:

class A
{
    public function callFuncXXOO()
    {
        print $this->funcXXOO();
    }

    public function funcXXOO()
    {
        return "A::funcXXOO()";
    }
}

class B extends A
{
    public function funcXXOO()
    {
        return "B::funcXXOO";
    }
}

$b = new B;
$b->callFuncXXOO();

输出是:

B::funcXXOO

可以看到,当在 A 中使用 $this->funcXXOO() 时,体现了“虚函数”的机制,实际调用的是 B::funcXXOO().
然而如果将所有函数都改为静态函数:

class A
{
    static public function callFuncXXOO()
    {
        print self::funcXXOO();
    }

    static public function funcXXOO()
    {
        return "A::funcXXOO()";
    }
}

class B extends A
{
    static public function funcXXOO()
    {
        return "B::funcXXOO";
    }
}

$b = new B;
$b->callFuncXXOO();

情况就没这么乐观了,输出是:

A::funcXXOO()

这是因为 self 的语义本来就是“当前类”,所以 PHP5.3 给 static 关键字赋予了一个新功能:后期静态绑定:

class A
{
    static public function callFuncXXOO()
    {
        print static::funcXXOO();
    }

    // ...
}

// ...

这样就会像预期一样输出了:

B::funcXXOO

Heredoc 和 Nowdoc

PHP5.3 对 Heredoc 以及 Nowdoc 进行了一些改进,它们都用于在 PHP 代码中嵌入大段字符串。

Heredoc 的行为类似于一个双引号字符串:

$name = "MyName";
echo <<< TEXT
My name is "{$name}".
TEXT;

Heredoc 以三个左尖括号开始,后面跟一个标识符(TEXT), 直到一个同样的顶格的标识符(不能缩进)结束。
就像双引号字符串一样,其中可以嵌入变量。

Heredoc 还可以用于函数参数,以及类成员初始化:

var_dump(<<<EOD
Hello World
EOD
);

class A
{
    const xx = <<< EOD
Hello World
EOD;

    public $oo = <<< EOD
Hello World
EOD;
}

Nowdoc 的行为像一个单引号字符串,不能在其中嵌入变量,和 Heredoc 唯一的区别就是,三个左尖括号后的标识符要以单引号括起来:

$name = "MyName";
echo <<< 'TEXT'
My name is "{$name}".
TEXT;

输出:

My name is "{$name}".

用 const 定义常量

PHP5.3 起同时支持在全局命名空间和类中使用 const 定义常量。

旧式风格:

define("XOOO", "Value");

新式风格:

const XXOO = "Value";

const 形式仅适用于常量,不适用于运行时才能求值的表达式:

// 正确
const XXOO = 1234;
// 错误
const XXOO = 2 * 617;

三元运算符简写形式

旧式风格:

echo $a ? $a : "No Value";

可简写成:

echo $a ?: "No Value";

即如果省略三元运算符的第二个部分,会默认用第一个部分代替。

Phar

Phar即PHP Archive, 起初只是Pear中的一个库而已,后来在PHP5.3被重新编写成C扩展并内置到 PHP 中。
Phar用来将多个 .php 脚本打包(也可以打包其他文件)成一个 .phar 的压缩文件(通常是ZIP格式)。
目的在于模仿 Java 的 .jar, 不对,目的是为了让发布PHP应用程序更加方便。同时还提供了数字签名验证等功能。

.phar 文件可以像 .php 文件一样,被PHP引擎解释执行,同时你还可以写出这样的代码来包含(require) .phar 中的代码:

require("xxoo.phar");
require("phar://xxoo.phar/xo/ox.php");

更多信息请参见官网 [注].

注:http://www.php.net/manual/zh/phar.using.intro.php

PHP5.4

(2012-2013)

Short Open Tag

Short Open Tag 自 PHP5.4 起总是可用。
在这里集中讲一下有关 PHP 起止标签的问题。即:

<?php
// Code...
?>

通常就是上面的形式,除此之外还有一种简写形式:

<? /* Code... */ ?>

还可以把

<?php echo $xxoo;?>

简写成:

<?= $xxoo;?>

这种简写形式被称为 Short Open Tag, 在 PHP5.3 起被默认开启,在 PHP5.4 起总是可用。
使用这种简写形式在 HTML 中嵌入 PHP 变量将会非常方便。

对于纯 PHP 文件(如类实现文件), PHP 官方建议顶格写起始标记,同时 省略 结束标记。
这样可以确保整个 PHP 文件都是 PHP 代码,没有任何输出,否则当你包含该文件后,设置 Header 和 Cookie 时会遇到一些麻烦 [注].

注:Header 和 Cookie 必须在输出任何内容之前被发送。

数组简写形式

这是非常方便的一项特征!

// 原来的数组写法
$arr = array("key" => "value", "key2" => "value2");
// 简写形式
$arr = ["key" => "value", "key2" => "value2"];

Traits

所谓Traits就是“构件”,是用来替代继承的一种机制。PHP中无法进行多重继承,但一个类可以包含多个Traits.

// Traits不能被单独实例化,只能被类所包含
trait SayWorld
{
    public function sayHello()
    {
        echo 'World!';
    }
}

class MyHelloWorld
{
    // 将SayWorld中的成员包含进来
    use SayWorld;
}

$xxoo = new MyHelloWorld();
// sayHello() 函数是来自 SayWorld 构件的
$xxoo->sayHello();

Traits还有很多神奇的功能,比如包含多个Traits, 解决冲突,修改访问权限,为函数设置别名等等。
Traits中也同样可以包含Traits. 篇幅有限不能逐个举例,详情参见官网 [注].

注:http://www.php.net/manual/zh/language.oop5.traits.php

内置 Web 服务器

PHP从5.4开始内置一个轻量级的Web服务器,不支持并发,定位是用于开发和调试环境。

在开发环境使用它的确非常方便。

php -S localhost:8000

这样就在当前目录建立起了一个Web服务器,你可以通过 http://localhost:8000/ 来访问。
其中localhost是监听的ip,8000是监听的端口,可以自行修改。

很多应用中,都会进行URL重写,所以PHP提供了一个设置路由脚本的功能:

php -S localhost:8000 index.php

这样一来,所有的请求都会由index.php来处理。

你还可以使用 XDebug 来进行断点调试。

细节修改

PHP5.4 新增了动态访问静态方法的方式:

$func = "funcXXOO";
A::{$func}();

新增在实例化时访问类成员的特征:

(new MyClass)->xxoo();

新增支持对函数返回数组的成员访问解析(这种写法在之前版本是会报错的):

print func()[0];

PHP5.5

(2013起)

yield

yield关键字用于当函数需要返回一个迭代器的时候, 逐个返回值。

function number10()
{
    for($i = 1; $i <= 10; $i += 1)
        yield $i;
}

该函数的返回值是一个数组:

[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

list() 用于 foreach

可以用 list() 在 foreach 中解析嵌套的数组:

$array = [
    [1, 2, 3],
    [4, 5, 6],
];

foreach ($array as list($a, $b, $c))
    echo "{$a} {$b} {$c}\n";

结果:

1 2 3
4 5 6

细节修改

不推荐使用 mysql 函数,推荐使用 PDO 或 MySQLi, 参见前文。
不再支持Windows XP.

可用 MyClass::class 取到一个类的完整限定名(包括命名空间)。

empty() 支持表达式作为参数。

try-catch 结构新增 finally 块。

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

订阅推送

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

王子亭的博客 @ Telegram


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

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