一切福田,不離方寸,從心而覓,感無不通。

PHP中一些通用和易混淆技术点的最佳编程实践

我们使用的是哪个 PHP 版本?

带有 Suhosin-补丁的PHP 5.3.10-1ubuntu3.6, 安装于 Ubuntu 12.04 LTS.

PHP如同是网络世界的百年老龟。它的外壳上刻有一个丰富的,令人费解的,粗糙的历史。在一个共享主机的环境下,它的配置可能会限制你能做什么事情。

为了保留一丝明智,我们需要只专注于一个版本的PHP。截至2013年4月30,该版本是 带有Suhosin补丁的PHP5.3.10-1ubuntu3.6 。如果你使用apt-get从一个Ubuntu12.04 LTS服务器来安装PHP的话,你所得到的版本就是这个。换句话说,许多人在默认情况下已经很明智地使用了它。

您可能会发现本文这些解决方案能工作于不同或更旧版本的PHP。如果是这样的话,就要由你来研究在这些旧版本中的细微错误或安全问题的影响了。

保存密码

使用 phpass 库计算密码的哈希值进行比较。

phpass 0.3 进行的测试。

散列化是在把用户密码保存进数据库之前对其进行保护的标准方法。许多常见的散列算法,如MD5,乃至SHA1,用于存储密码都是不安全的,因为黑客可以使用这些散列算法轻松破解密码

要散列化密码最安全的方法是使用bcrypt算法。开源的phpass 库用一个易于使用的类来提供这个功能。

例子:

01 <?php
02 // 包含phpass库
03 require_once('phpass-0.3/PasswordHash.php');
04
05 // 初始化散列器为不可移植(这样更安全)
06 $hasher= newPasswordHash(8, false);
07
08 // 计算密码哈希值。$hashedPassword 将会是一长为60个字符的字符串.
09 $hashedPassword= $hasher->HashPassword('my super cool password');
10
11 // 你现在可以安全地保存$hashedPassword到数据库中!
12
13 // 通过比较用户输入内容(产生的哈希值)和我们之前计算出的哈希值,来判断用户是否输入了正确的密码
14 $hasher->CheckPassword('the wrong password', $hashedPassword); // 返回假
15
16 $hasher->CheckPassword('my super cool password', $hashedPassword); // 返回真
17 ?>

陷阱

  • 很多来源会建议你在计算密码的哈希值之前先给密码加点“作料”。这是个好主意,phpass已经利用HashPassword() 函数中的一部分代码来为你给密码加了作料。 这就意味着你并不需要自己再亲自做这个了。

进一步阅读

连接到并查询MySQL数据库

使用 PDO和它预定义的语句功能.

在PHP中有很多方法连接到一个MySQL数据库。 PDO (PHP Data Objects) 是其中最新最健壮的。对于许多不同类型的数据库,PDO都使用一致性的接口,采用面向对象的方式,并支持较新的数据库的提供的更多特性。

您应该使用PDO预定义语句的功能,以帮助防止SQL注入攻击。使用 bindValue() 函数确保你的SQL对于第一阶的SQL注入攻击是安全的(但这不是100%万无一失的,参考 进一步阅读 获得更详细说明)。在过去,这只能用一些“魔术引号”函数的复杂结合来实现。PDO使所有这些黏糊糊的东西变得不再必要了。

示例

01 <?php
02 try{
03    // Create a new connection.
04    // You'll probably want to replace hostname with localhost in the first parameter.
05    // The PDO options we pass do the following:
06    // \PDO::ATTR_ERRMODE enables exceptions for errors.  This is optional but can be handy.
07    // \PDO::ATTR_PERSISTENT disables persistent connections, which can cause concurrency issues in certain cases.  See "Gotchas".
08    // \PDO::MYSQL_ATTR_INIT_COMMAND alerts the connection that we'll be passing UTF-8 data.  This may not be required depending on your configuration, but it'll save you headaches down the road if you're trying to store Unicode strings in your database.  See "Gotchas".
09    $link= new\PDO(   'mysql:host=your-hostname;dbname=your-db',
10                        'your-username',
11                        'your-password',
12                        array(
13                            \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
14                            \PDO::ATTR_PERSISTENT => false,
15                            \PDO::MYSQL_ATTR_INIT_COMMAND => 'set names utf8mb4'
16                        )
17                    );
18
19    $handle= $link->prepare('select Username from Users where UserId = ? or Username = ? limit ?');
20
21    // PHP bug: if you don't specify PDO::PARAM_INT, PDO may enclose the argument in quotes.  This can mess up some MySQL queries that don't expect integers to be quoted.
22    // See: https://bugs.php.net/bug.php?id=44639
23    // If you're not sure whether the value you're passing is an integer, use the is_int() function.
24    $handle->bindValue(1, 100, PDO::PARAM_INT);
25    $handle->bindValue(2, 'Bilbo Baggins');
26    $handle->bindValue(3, 5, PDO::PARAM_INT);
27
28    $handle->execute();
29
30    // Using the fetchAll() method might be too resource-heavy if you're selecting a truly massive amount of rows.
31    // If that's the case, you can use the fetch() method and loop through each result row one by one.
32    // You can also return arrays and other things instead of objects.  See the PDO documentation for details.
33    $result= $handle->fetchAll(\PDO::FETCH_OBJ);
34
35    foreach($resultas$row){
36        print($row->Username);
37    }
38 }
39 catch(\PDOException $ex){
40    print($ex->getMessage());
41 }
42 ?>

陷阱

  • 当绑定整型变量时,如果不传递PDO::PARAM_INT参数有事可能会导致PDO对数据加引号。这会 搞坏特定的MySQL查询。查看该bug报告

  • 未使用 set names utf8mb4 作为首个查询,可能会导致Unicode数据错误地存储进数据库,这依赖于你的配置。如果你 绝对有把握你的Unicode编码数据不会出问题,那你可以不管这个。

  • 启用持久连接可能会导致怪异的并发相关的问题。这不是一个PHP的问题,而是一个应用层面 的问题。只要你仔细考虑了后果,持久连接一般会是安全的。查看Stack Overfilow这个问题

  • 即使你使用了 set names utf8mb4 ,你也得确认实际的数据库表使用的是utf8mb4字符集!

  • 可以在单个execute()调用中执行多条SQL语句。只需使用分号分隔语句,但注意这个bug,在该文档所针对的PHP版本中还没修复。

进一步阅读

PHP标签

使用<?php ?>.

界定PHP代码块有几种不同方式: <?php ?>, <?= ?>, <? ?>,和 <% %>。虽然更短的方式更便于输入,但能保证在所有PHP服务器上都能工作的只有<?php ?>。如果你计划把你的PHP程序部署到一个你不能控制其配置的服务器上,你必须始终使用 <?php ?>。

如果你有足够权限控制PHP运行环境的配置,你会发现使用更短的标签自然更方便。但要记住,<? ?> 可能与XML声明冲突,而 <% %> 则实际是ASP的风格.

无论你选择哪一种,请确保你保持一致!

陷阱

  • 当在一个纯粹的PHP文件(例如一个只包含一个类定义的文件)中包含一个结束?>标签,确保不要在它后面留下任何尾随的换行符。因为虽然PHP解析器能安全“吃掉”一个关闭标签后面的换行符,其它的换行符却可能输出到浏览器,当你过后想输出任何HTTP头时,这会造成干扰。

  • 当写web程序时,确保不要在任何关闭标签 ?> 和 html <!doctype> 标签之间留下换行符。对正确的HTML来说,<!doctype> 标签必须是文件中的第一行—在它前面有任何空格或换行符都会使其失效。

进一步阅读

自动载入类

spl_autoload_register() 来注册你要自动载入的函数。

PHP提供几种方式来自动载入含有还没被载入的类的文件。较老的方式是使用名为__autoload()的魔术全局函数。但你一次只能使用一个定义的 __autoload() 函数,所以当你包含一个也使用了 __autoload() 函数的库时,就会造成冲突。

解决这个问题的正确方式是把你的自动载入函数命名成一个唯一的名称,然后用 spl_autoload_register() 函数注册。这个函数允许定义多个 __autoload() 函数,这样你就不会踩到其它代码所含的 __autoload() 函数了。

例如:

01 <?php
02 // 首先,定义你的自动载入的函数
03 functionMyAutoload($className){
04    include_once($className. '.php');
05 }
06
07 // 然后注册它.
08 spl_autoload_register('MyAutoload');
09
10 // 试试让它工作!
11 // 因为我们没包含一个定义有MyClass的文件,所以自动加载器会介入并包含MyClass.php.
12 // 对本例来说,假定在MyClass.php文件中定义了MyClass类.
13 $var= newMyClass();
14 ?>

进一步阅读

从性能角度比较单引号和双引号

这并不重要。

关于“定义字符串时应使用单引号还是双引号”,已有很多笔墨评论了。单引号字符串不会进行解析,所以无论你在字符串放了什么,都会原样显示。双引号字符串会被解析,在里面的任何PHP变量都会被求值兑现。另外,对于转义字符(如换行符\n和制表符\t),在单引号和双引号中的差别也是同样的。

因为双引号字符串会在运行时进行解析,理论上能用单引号就用单引号,这应该可以提升性能,因为PHP不需要对单引号字符串额外进行解析。虽然对具有一定规模的应用来说这可能是真的,但对于一般的现实生活中的应用程序来说,效率差距微乎其微,它其实并不重要。因此对于一般的应用程序,你选择什么并不重要(译者注:因此更重要的是,当使用单引号字符串时,应确保它在以后绝无可能加入需要解析的成份,否则你就要在将来多麻烦一下把它改成双引号)。对于非常高负荷的应用程序,它可能有一点影响。选择使用哪种方式,取决于你应用程序的需求,但无论你选择哪种,应该保持一致。

进一步阅读

define() 和 const 的比较

使用 define() ,除非关注“可读性,类常量,微优化”

传统上,在PHP中你会使用define()函数来定义常量。但根据一些意见,PHP也获得了用const关键字声明常量的能力。那么定义常量时,你应该使用哪一个呢?

答案就在于这两种方法之间微小的差异:

  1. define() 是在运行时定义常量,而 const 是在编译时定义常量。这给了const一个很轻微的速度优势,但达不到值得担心的程度,除非你在建立大型软件。

  2. define() 把常量放在全局范围,虽然你可以在你的常量名称中包含命名空间。这意味着你不能用define()来定义类常量。

  3. define() 允许你在常量名称和常量值中都使用表达式,而 const 则都不允许。这使得define() 灵活得多。

  4. define() 可以在一个 if() 块中被使用,而 const 不能.

例子:

01 <?php
02 // 来看看这两种方法如何处理名称空间
03 namespace MiddleEarth\Creatures\Dwarves;
04 constGIMLI_ID = 1;
05 define('MiddleEarth\Creatures\Elves\LEGOLAS_ID', 2);
06
07 echo(\MiddleEarth\Creatures\Dwarves\GIMLI_ID);  // 1
08 echo(\MiddleEarth\Creatures\Elves\LEGOLAS_ID);  // 2; 注意,对此常量,我们是用define()定义的,但也能识别空间。
09
10 // <span>现在让我们来</span><span>声明一些值是</span><span>位</span><span>移运算结果的</span><span>常数来</span><span>代表</span><span>进入</span><span>魔多的方式</span>Mordor.
11 define('TRANSPORT_METHOD_SNEAKING', 1 << 0); // OK!
12 constTRANSPORT_METHOD_WALKING = 1 << 1; //编译错误! const 不允许使用表达式作为值
13
14 // 接下来, 条件常量。
15 define('HOBBITS_FRODO_ID', 1);
16
17 if($isGoingToMordor){
18    define('TRANSPORT_METHOD', TRANSPORT_METHOD_SNEAKING); // OK!
19    constPARTY_LEADER_ID = HOBBITS_FRODO_ID // 编译错误: const 不能用于 if 块中
20 }
21
22 // 最后, 类常量
23 classOneRing{
24    constMELTING_POINT_DEGREES = 1000000; // OK!
25    define('SHOW_ELVISH_DEGREES', 200); // 编译错误: 在类内不能使用define()
26 }
27 ?>

因为define() 最终更为灵活,它是你避免头痛的选择,除非你非常需要类常量。用const可产生更易阅读的代码,但要付出灵活性的代价。

无论你用哪一种,应保持一致!

进一步阅读

缓存PHP操作码(字节码)

使用APC.

在PHP的标准安装环境里,每个PHP脚本在每次它被访问时都会被编译成操作码(字节码)文件并执行。花时间对完全相同的脚本一遍又一遍进行编译,对大型网站来说,势必导致性能问题。

解决方案是操作码缓存。操作码缓存是一种可以记忆每个脚本的编译结果的系统,这样服务器不必浪费时间一遍遍编译。通常它们也很聪明,足以探测到一个脚本已经改变并对其重新编译,所以当你更新你的PHP源文件时,不必非得手动清除缓存。

有几种PHP操作码缓存系统可用,值得一提的是 eaccelerator, xcache, 和 APC. APC 是由PHP项目组官方提供支持的,是最活跃也是最容易安装的。它还提供了一个可选的类似memcached的持久的键-值存储。基于这些理由,它是你应该使用的。

安装APC

通过在终端运行以下命令,可以在Ubuntu 12.04上安装APC:

sudo apt-get install php-apc  

无需另外的配置。

作为一个永久的键值对存储来使用APC

APC也提供类似memcached的功能,对于你的脚本也是显而易见的。相对于使用memcached,最大的优点就是APC集成到了PHP内核中,因此你不需要再的你服务器中保存你的可动部分,PHP开发者主动地在它上面工作。另外一方面,APC不是一个分布式缓存;如果你需要这个特性,你必须使用memcached。

示例

01 <?php
02 // Store some values in the APC cache.  We can optionally pass a time-to-live, but in this example the values will live forever until they're garbage-collected by APC.
03 apc_store('username-1532', 'Frodo Baggins');
04 apc_store('username-958', 'Aragorn');
05 apc_store('username-6389', 'Gandalf');
06
07 // After storing these values, any PHP script can access them, no matter when it's run!
08 $value= apc_fetch('username-958', $success);
09 if($success=== true)
10    print($value); // Aragorn
11
12 $value= apc_fetch('username-1', $success); // $success will be set to boolean false, because this key doesn't exist.
13 if($success!== true) // Note the !==, this checks for true boolean false, not "falsey" values like 0 or empty string.
14    print('Key not found');
15
16 apc_delete('username-958'); // This key will no longer be available.
17 ?>

Gotchas=Got You

  • 如果你不使用PHP-FPM (例如你使用mod_php或者mod_fastcgi),每个PHP进程都将有它独一无二的APC实例,包括键值对存储。如果你不小心的话,在你的应用代码中可能导致同步问题。

进一步阅读

PHP和Memcached

如果你需要一个分布式缓存使用Memcached客户端库。或者使用APC

一个缓存系统通常能够改进你的app的性能。Memcached是一个主流的选择,并且它兼容许多语言,包括PHP。

然而当它从一个PHP脚本访问一个Memcached服务器的时候,你有两个不同的,命名愚蠢的客户端库选择:MemcacheMemcached。它们是不同的库,但是有着近乎相同的名字,并且都用于访问一个Memcached实例。

事实证明Memcached库,是实现Memcached协议最好的方法。它包括一些Memcache库所没有的,有用的特性,看起来是最被积极开发的一款。

然而如果你不需要从一系列分布式服务器中访问一个Memcached实例,则使用APC来代替。APC由PHP项目支持,有很多和Memcached相似的功能, 附加的惊喜就是,她说一个操作码缓存,这能够提升你的PHP脚本的性能。

安装Memached客户端库

在你安装Memcached服务端之后,你需要安装Memcached客户端库。没有这个库,你的PHP脚本将不能和Memcached服务端通信。

通过在终端运行如下命令,你能够安装Memcached客户端库:

sudo apt-get install php5-memcached  

使用APC替代

查看the entry on opcode caches,了解更多关于使用APC作为一个Memcached替代选择。

进一步阅读

PHP和正则

使用PCRE (preg_*)族函数    

PHP有两种不同的方式使用正则表达式:PCRE (Perl-compatible, preg_*)函数和POSIX (POSIX extended, ereg_*)函数。

每一族函数使用轻微不同风格的正则表达式。幸运地,从PHP5.3.0开始,POSIX函数就被弃用了。因为这个,你不应当在新代码中使用POSIX函数。总是勇士PRCE函数,即是preg_*函数。

进一步阅读

配置Web服务器提供PHP服务

使用PHP-FPM

有多种方式来配置一个web服务器以提供PHP服务。传统(并且糟糕的)的方式是使用Apache的 mod_php。Mod_php将PHP 绑定到Apache自身,但是Apache对于该模块功能的管理工作非常糟糕。一旦遇到较大的流量, 就会遭受严重的内存问题。

后来两个新的可选项很快流行起来:mod_fastcgimod_fcgid。两者均保持一定数量的PHP执行进程, Apache将请求发送到这些端口来处理PHP的执行。由于这些库限制了存活的PHP进程的数量, 从而大大减少了内存使用而没有影响性能。

一些聪明的人创建一个fastcgi的实现,专门为真正与PHP工作良好而设计,他们称之为 PHP-FPM。PHP 5.3.0之前,为安装它, 你得跨越许多障碍,但幸运的是,PHP 5.3.3的核心包含了PHP-FPM,因此在Ubuntu 12.04上安装它非常方便。

如下示例是针对Apache 2.2.22的,但PHP-FPM也能用于其他web服务器如Nginx

安装 PHP-FPM 和 Apache

通过在终端中运行命令,在Ubuntu 12.04安装PHP-FPM和Apache:

sudo apt-get install apache2-mpm-worker libapache2-mod-fastcgi php5-fpm sudo a2enmod actions alias fastcgi

注意到我们必须使用apache2-mpm-worker,而不是apache2-mpm-prefork或者apache2-mpm-threaded。

下一步,我们将配置我们的Apache虚拟主机,以便路由PHP请求到PHP-FPM处理中。在你的Apache配置文件中放置如下(在Ubuntu 12.04中,默认的一个路径为/etc/apache2/sites-available/default)。

1 <VirtualHost*:80>
2    AddHandler php5-fcgi .php
3    Action php5-fcgi /php5-fcgi
4    Alias /php5-fcgi /usr/lib/cgi-bin/php5-fcgi
5    FastCgiExternalServer /usr/lib/cgi-bin/php5-fcgi -host 127.0.0.1:9000 -idle-timeout 120 -pass-header Authorization
6 </VirtualHost>

最后,重启Apache和FPM进程:

1 sudoservice apache2 restart
2 sudoservice php5-fpm restart


进一步阅读

发送email

使用PHPMailer.

使用PHPMailer 5.1测试。

PHP提供一个mail()函数,看起来极度简单和容易。不幸的是,就像PHP中的很多事情,它的简单是容易误解的,用表面的值来使用它,容易导致严重的安全问题。

Email是一个协议的集合,具有比PHP更曲折痛苦的历史。满足它也就是说,在发送email的时候有太多的疑惑,就像PHPmail()函数应当给你的感觉一样。

PHPMailer 是一个受欢迎的,完善的开源库,提供安全发送mailis的一个简单接口。它为你处理好疑惑,以便你能够关注更重要的事情。

示例

01 <?php
02 // Include the PHPMailer library
03 require_once('phpmailer-5.1/class.phpmailer.php');
04
05 // Passing 'true' enables exceptions.  This is optional and defaults to false.
06 $mailer= newPHPMailer(true);
07
08 // Send a mail from Bilbo Baggins to Gandalf the Grey
09
10 // Set up to, from, and the message body.  The body doesn't have to be HTML; check the PHPMailer documentation for details.
11 $mailer->Sender = 'bbaggins@example.com';
12 $mailer->AddReplyTo('bbaggins@example.com', 'Bilbo Baggins');
13 $mailer->SetFrom('bbaggins@example.com', 'Bilbo Baggins');
14 $mailer->AddAddress('gandalf@example.com');
15 $mailer->Subject = 'The finest weed in the South Farthing';
16 $mailer->MsgHTML('<p>You really must try it, Gandalf!</p><p>-Bilbo</p>');
17
18 // Set up our connection information.
19 $mailer->IsSMTP();
20 $mailer->SMTPAuth = true;
21 $mailer->SMTPSecure = 'ssl';
22 $mailer->Port = 465;
23 $mailer->Host = 'my smpt host';
24 $mailer->Username = 'my smtp username';
25 $mailer->Password = 'my smtp password';
26
27 // All done!
28 $mailer->Send();
29 ?>

验证email地址

使用filter_var()函数。

你的web应用需要做的一个常见的任务。就是检查一个用户是否输入了一个有效的email地址。你将毫无疑问地在网上找到一堆复杂的表达式,它们声称可以解决此问题,但是最简单的方法就是使用PHP的内建函数filter_var(),它能够检验email地址。

示例

1 <?php
2 filter_var('sgamgee@example.com', FILTER_VALIDATE_EMAIL); // Returns "sgamgee@example.com". This is a valid email address.
3 filter_var('sauron@mordor', FILTER_VALIDATE_EMAIL); // Returns boolean false! This is *not* a valid email address.
4 ?>

进一步阅读

净化HTML输入和输出

对于简单的数据净化,使用htmlentities()函数, 复杂的数据净化则使用HTML Purifier库

经HTML Purifier 4.4.0测试  

在任何wbe应用中展示用户输出时,首先对其进行“净化”去除任何潜在危险的HTML是非常必要的。 一个恶意的用户可以制作某些HTML,若被你的web应用直接输出,对查看它的人来说会很危险。  

虽然可以尝试使用正则表达式来净化HTML,但不要这样做。HTML是一种复杂的语言,试图 使用正则表达式来净化HTML几乎总是失败的。  

你可能会找到建议你使用strip_tags() 函数的观点。虽然strip_tags()从技术上来说是安全的,但如果输入的不合法的HTML(比如, 没有结束标签),它就成了一个“愚蠢”的函数,可能会去除比你期望的更多的内容。由于非技术用户 在通信中经常使用<和>字符,strip_tags()也就不是一个好的选择了。  

如果阅读了  验证邮件地址  一节, 你也许也会考虑使用filter_var() 函数。然而filter_var()函数在遇到断行时会出现问题, 并且需要不直观的配置以接近htmlentities()函数的效果, 因此也不是一个好的选择。  

对于简单需求的净化

如果你的web应用仅需要完全地转义(因此可以无害地呈现,但不是完全去除)HTML,则使用 PHP的内建htmlentities()函数。 这个函数要比HTML Purifier快得多,因为它不对HTML做任何验证—仅转义所有东西。

htmlentities()不同于类似功能的函数htmlspecialchars(), 它会编码所有适用的HTML实体,而不仅仅是一个小的子集。  

示例

01 <?php
02 // 哦哦,用户的提交了一些恶意的html,我们需要将其在web 应用上显示!
03 $evilHtml= '<div onclick="xss();">Mua-ha-ha!  Twiddling my evil mustache…</div>';
04
05 // 用 ENT_QUOTES 确保单双引号被转义.
06 // 用 UTF-8 编码,如果文件被存储为UTF-8格式.
07 // 见本文的 UTF-8 小节
08 $safeHtml= htmlentities($evilHtml, ENT_QUOTES, 'UTF-8');
09 // $safeHtml 已经被完全转移你可以放心的输出显示了!
10 ?>

对于复杂需求的净化

对于很多web应用来说,简单地转义HTML是不够的。你可能想完全去除任何HTML,或者允许 一小部分子集的HTML存在。若是如此,则使用HTML Purifier 库。

HTML Purifier是一个经过充分测试但效率比较低的库。这就是为什么如果你的需求并不复杂 就应使用htmlentities(),因为 它的效率要快得多。

HTML Purifier相比strip_tags() 是有优势的,因为它在净化HTML之前会对其校验。这意味着如果用户输入无效HTML,HTML Purifier相比strip_tags()更能保留HTML的原意。HTML Purifier高度可定制,允许你为HTML的一个子集建立白名单来允许这个HTML子集的实体存在 输出中。

但其缺点就是相当的慢,它要求一些设置,在一个共享主机的环境里可能是不可行的。其文档 通常也复杂而不易理解。以下示例是一个基本的使用配置。查看文档 阅读HTML Purifier提供的更多更高级的特性。

示例

陷阱

  • 以错误的字符编码使用htmlentities()会造成意想不到的输出。在调用该函数时始终确认 指定了一种字符编码,并且该编码与将被净化的字符串的编码相匹配。更多细节请查看 UTF-8一节

  • 使用htmlentities()时,始终包含ENT_QUOTES和字符编码参数。默认情况下,htmlentities() 不会对单引号编码。多愚蠢的默认做法!

  • HTML Purifier对于复杂的HTML效率极其的低。可以考虑设置一个缓存方案如APC来保存经过净化的结果 以备后用。

进一步阅读

PHP and UTF-8

没有一行式解决方案。小心、注意细节,以及一致性。

PHP中的UTF-8糟透了。原谅我的用词。  

目前PHP在低层次上还不支持Unicode。有几种方式可以确保UTF-8字符串能够被正确处理, 但并不容易,需要深入到web应用的所有层面,从HTML,到SQL,到PHP。我们旨在提供一个简洁、 实用的概述。  

PHP层面的UTF-8

基本的字符串操作,如串接 两个字符串、将字符串赋给变量,并不需要任何针对UTF-8的特殊东西。然而,多数 字符串函数,如strpos() 和strlen,就需要特殊的考虑。这些 函数都有一个对应的mb_*函数:例如,mb_strpos() 和mb_strlen()。这些对应的函数 统称为多字节字符串函数。这些多字节字符串 函数是专门为操作Unicode字符串而设计的。  

当你操作Unicode字符串时,必须使用mb_*函数。例如,如果你使用substr() 操作一个UTF-8字符串,其结果就很可能包含一些乱码。正确的函数应该是对应的多字节函数, mb_substr()。  

难的是始终记得使用mb_*函数。即使你仅一次忘了,你的Unicode字符串在接下来的处理中 就可能产生乱码。  

并不是所有的字符串函数都有一个对应的mb_*。如果不存在你想要的那一个,那你就只能 自认倒霉了。  

此外,在每个PHP脚本的顶部(或者在全局包含脚本的顶部)你都应使用 mb_internal_encoding 函数,如果你的脚本会输出到浏览器,那么还得紧跟其后加个mb_http_output() 函数。在每个脚本中显式地定义字符串的编码在以后能为你减少很多令人头疼的事情。  

最后,许多操作字符串的PHP函数都有一个可选参数让你指定字符编码。若有该选项, 你应 始终显式地指明UTF-8编码。例如,htmlentities() 就有一个字符编码方式选项,在处理这样的字符串时应始终指定UTF-8。

MySQL级别的UTF-8

如果你的PHP脚本访问MySQL,你有机会再数据库中,以非UTF-8字符串保存你的字符串,尽管你遵从了上述所有注意事项。

为了确保你的字符串以UTF-8格式,从PHP到MySQL,确保你的数据库和表单都设置为utf8mb4字符集,在争论你的数据库中任何其他查询之前,注意MySQL查询set names utf8mb4。对于一个例子,看看章节connecting to and querying a MySQL database。这是非常重要的。

为了完成UTF-8的支持,注意你必须使用utf8mb4字符集,而不是utf8字符集!查看Further Reading寻找原因。

在浏览器级别上使用UTF-8

使用 mb_http_output() 函数来确保你的PHP 输出给浏览器的文件编码为UTF-8。HTML 页面文件中<head>标签下有字符编码标签( charset <meta> tag )。


01 <?php
02 // Tell PHP that we're using UTF-8 strings until the end of the script
03 mb_internal_encoding('UTF-8');
04
05 // Tell PHP that we'll be outputting UTF-8 to the browser
06 mb_http_output('UTF-8');
07
08 // Our UTF-8 test string
09 $string= 'Aš galiu valgyti stiklą ir jis manęs nežeidžia';
10
11 // Transform the string in some way with a multibyte function
12 $string= mb_substr($string, 0, 10);
13
14 // Connect to a database to store the transformed string
15 // See the PDO example in this document for more information
16 // Note the set names utf8mb4 commmand!
17 $link= new\PDO(   'mysql:host=your-hostname;dbname=your-db',
18                    'your-username',
19                    'your-password',
20                    array(
21                        \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
22                        \PDO::ATTR_PERSISTENT => false,
23                        \PDO::MYSQL_ATTR_INIT_COMMAND => 'set names utf8mb4'
24                    )
25                );
26    
27 // Store our transformed string as UTF-8 in our database
28 // Assume our DB and tables are in the utf8mb4 character set and collation
29 $handle= $link->prepare('insert into Sentences (Id, Body) values (?, ?)');
30 $handle->bindValue(1, 1, PDO::PARAM_INT);
31 $handle->bindValue(2, $string);
32 $handle->execute();
33
34 // Retrieve the string we just stored to prove it was stored correctly
35 $handle= $link->prepare('select * from Sentences where Id = ?');
36 $handle->bindValue(1, 1, PDO::PARAM_INT);
37 $handle->execute();
38    
39 // Store the result into an object that we'll output later in our HTML
40 $result= $handle->fetchAll(\PDO::FETCH_OBJ);
41 ?><!doctype html>
42 <html>
43    <head>
44        <meta http-equiv="Content-Type"content="text/html; charset=UTF-8"/>
45        <title>UTF-8 test page</title>
46    </head>
47    <body>
48        <?php
49        foreach($resultas$row){
50            print($row->Body);  // This should correctly output our transformed UTF-8 string to the browser
51        }
52        ?>
53    </body>
54 </html>

延伸阅读

时间和日期

使用 DateTime 类.

在PHP的早些时候,我们不得不使用关于date()gmdate()date_timezone_set()strtotime() 的一系列眼花缭乱的组合,来完成日期和时间的操作。遗憾的是,你依然能够在网上找到许多这些困难的,旧样式功能的教程。

对于我们幸运的是,我们正在讨论的PHP版本有了更加友好的DateTime类特性。这个类封装了所有的功能,更多旧日期函数到一个易用的类,更令人高兴地是使得时间转换更加容易。在PHP中,总是使用DateTime类来说检查,比较,改变,显示日期。

Example

01 <?php
02 // Construct a new UTC date.  Always specify UTC unless you really know what you're doing!
03 $date= newDateTime('2011-05-04 05:00:00', newDateTimeZone('UTC'));
04
05 // Add ten days to our initial date
06 $date->add(newDateInterval('P10D'));
07
08 echo($date->format('Y-m-d h:i:s')); // 2011-05-14 05:00:00
09
10 // Sadly we don't have a Middle Earth timezone
11 // Convert our UTC date to the PST (or PDT, depending) time zone
12 $date->setTimezone(newDateTimeZone('America/Los_Angeles'));
13
14 // Note that if you run this line yourself, it might differ by an hour depending on daylight savings
15 echo($date->format('Y-m-d h:i:s')); // 2011-05-13 10:00:00
16
17 $later= newDateTime('2012-05-20', newDateTimeZone('UTC'));
18
19 // Compare two dates
20 if($date< $later)
21    echo('Yup, you can compare dates using these easy operators!');
22
23 // Find the difference between two dates
24 $difference= $date->diff($later);
25
26 echo('The 2nd date is '. $difference['days'] . ' later than 1st date.');
27 ?>

Gotchas==Got You

  • 如果你不能指定一个时区,DateTime::__construct()将会设置结果的时区同运行的电脑时区一致。这在后面会导致很大的烦恼。当你创建新的日期的时候,总是会指定UTC时区,除非你知道你在做什么。

  • 如果你在DateTime::__construct()中使用UNIX时间戳,时区将会总是指定为UTC,而不管你在第二个参数中指定的是什么。

  • 传递归零数据(例如"0000-00-00",一个通常由MySQL产生的值,作为一个DateTime行的默认值)到DateTime::__construct(),将会产生一个无法解释的值,而不是"0000-00-00"。

  • 在32位系统中使用 DateTime::getTimestamp()不会显示草果2038的数据。64系统没有此问题。

进一步阅读

检查一个值是null还是false

使用===运算符检查null和false布尔值

PHP宽松的类型系统,提供了许多检查一个变量值的不同方法。然而它也展现了许多问题。使用==去检查一个值是null还是false,如果这个值确实是空字符串或者0,则返回false。isset()检查一个变量是否有值,而不是那个值是null或者false,因此不合适用在这里。

is_null()函数准确检查一个值是否为null,is_bool()函数检查是否为布尔值(比如false),但是有更好的选择:===运算符。===检查值是否一样,但是和PHP宽松类型世界中的equivalent不一样。它也比is_null()和is_bool()微微快一些,而且也被一些人认为比使用一个比较函数更简洁。

Example

01 <?php
02 $x= 0;
03 $y= null;
04
05 // Is $x null?
06 if($x== null)
07    print('Oops! $x is 0, not null!');
08
09 // Is $y null?
10 if(is_null($y))
11    print('Great, but could be faster.');
12
13 if($y=== null)
14    print('Perfect!');
15
16 // Does the string abc contain the character a?
17 if(strpos('abc', 'a'))
18    // GOTCHA!  strpos returns 0, indicating it wishes to return the position of the first character.
19    // But PHP interpretes 0 as false, so we never reach this print statement!
20    print('Found it!');
21
22 //Solution: use !== (the opposite of ===) to see if strpos() returns 0, or boolean false.  
23 if(strpos('abc', 'a') !== false)
24    print('Found it for real this time!');
25 ?>

Gotchas==Got you

  • 当测试一个函数的返回值的时候,函数能够返回0或者布尔值,像strpos()一样,总是使用===和!==,你或许会遇到问题。

进一步阅读

建议和纠正

感谢阅读!如果你还没有弄明白,PHP是复杂的并且也有很多缺陷。既然我也是个人,在这个文档中就可能有错误。

如果你不想通过建议或者纠正来对本文档做贡献,请使用last revised & maintainers部分中的信息联系我。


本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们


转自:http://www.oschina.net/translate/php-best-practices