最新改进&维护本文档最后一次于2013年3月8日审核。最后一次修改是在2013年3月8日。 这由我, Alex Cabal维护的。到现在我已经写了很长时间PHP代码了, 目前我运行Scribophile,为严肃作家提供的在线写作小组, Writerfolio, 为自由职业者提供的简单的在线写作文件夹, 和 Standard Ebooks, 有插画的出版物,无数字版权的公共领域电子图书 。 偶尔我会自由的去找那些让我感兴趣的项目和客户。 如果你认为我能够帮你些什么,或者有些关于本文的建议或勘误的话,那么写信给我。
介绍PHP是一门复杂的语言,让你长年承受内心的迂回,弯曲,拉伸和打击。它自相矛盾有时候充满bug。每个版本都有其独特的特性,缺点以及怪癖,而且你很难追踪什么版本有什么问题。不难想见为什么有时候它会招致那么多怨恨。 尽管如此,它是时下web中最流行的语言。由于它的悠久历史,你能找到许多教程,关于一些基本事情的做法,如密码哈希(一次性加密)和数据库访问。问题在于从五篇教程里,关于某件事情你很有可能找到五种完全不同的方法。哪个方法才是“正确”的方法呢?其它的方法有瑕疵或者意想不到的问题吗?确实很难弄清楚,而且你会在网络里到处点击,以试图确定正确的答案。 那也是为什么新的PHP程序员经常会为其丑陋,过时或不安全代码受到批评的原因之一。如果第一次Google搜索结果是一篇传授5年前方法的4年前的文章,他们就不能帮助这些有所改观。 这篇文章试图做这些工作。它尝试将一系列基本的操作提示汇集起来,这些可以被认为是PHP中处理普遍的令人困惑的难题时的最佳实践。如果PHP中一个低等级的任务具有多个和令人困惑的方法,它就属于这里。
它是什么它是在面对PHP程序员可能会遇到的普通的低级任务时,由于PHP提供了 许多选择而不容易了解到的,最佳途径的建议指导。例如:对许许多多可能的PHP解决方案,连接到数据库是一个普通的任务,但这些方案并不都是好的——因此,这个问题包含在这篇文章中。 它是一系列短小,引导式的解决方案。你应该行动起来将例子运行于基础的配置环境,而且你应该自己研究从中找到对自己有用的东西。 它指出了我们所理解的最先进的PHP。但是,这也意味着如果你正使用较老版本的PHP,可能你就没有实现这些方案所需要的一些特性。 这是一篇动态文件,随着PHP的继续演进,我将努力保持相应更新。
它不是什么?这篇文章不是PHP教程。你应该在别的地方学习基础和语法。 它不是普通web应用问题的指南,比如cookie存储,缓存,代码风格,文档等等。 它不是安全向导。当触及一些安全相关的问题时,你要自己研究怎么样才能对你的PHP应用加固。特别的,在着手实施以前,你应该仔细回顾一下这里给出的任何建议方案。你的代码责任在于你。 它不是某种代码风格,模式或者框架的拥护者。 它不是关于如何去做高等级任务,如用户注册,登录系统等等诸如此类任务的特定方法的支持者。这篇文章完全是为低等级任务,是因为PHP的长久历史,可能会令人困惑或者不甚清楚。 它不是终极意义的解决方案,也不是唯一的方案。下面描述的一些方法可能对你实际的情况不是最优的,而且有许多能达到同样目的的不同的方法。特别的,高负载的web应用可能会从更多的针对这些问题的秘密方案中获益。
我们使用的是哪个 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 库用一个易于使用的类来提供这个功能。 例子:03 | require_once ( 'phpass-0.3/PasswordHash.php' ); |
06 | $hasher = new PasswordHash(8, false); |
09 | $hashedPassword = $hasher ->HashPassword( 'my super cool password' ); |
14 | $hasher ->CheckPassword( 'the wrong password' , $hashedPassword ); |
16 | $hasher ->CheckPassword( 'my super cool password' , $hashedPassword ); |
陷阱- 很多来源会建议你在计算密码的哈希值之前先给密码加点“作料”。这是个好主意,phpass已经利用HashPassword() 函数中的一部分代码来为你给密码加了作料。 这就意味着你并不需要自己再亲自做这个了。
进一步阅读连接到并查询MySQL数据库使用 PDO和它预定义的语句功能.在PHP中有很多方法连接到一个MySQL数据库。 PDO (PHP Data Objects) 是其中最新最健壮的。对于许多不同类型的数据库,PDO都使用一致性的接口,采用面向对象的方式,并支持较新的数据库的提供的更多特性。 您应该使用PDO预定义语句的功能,以帮助防止SQL注入攻击。使用 bindValue() 函数确保你的SQL对于第一阶的SQL注入攻击是安全的(但这不是100%万无一失的,参考 进一步阅读 获得更详细说明)。在过去,这只能用一些“魔术引号”函数的复杂结合来实现。PDO使所有这些黏糊糊的东西变得不再必要了。
示例09 | $link = new \PDO( 'mysql:host=your-hostname;dbname=your-db' , |
13 | \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, |
14 | \PDO::ATTR_PERSISTENT => false, |
15 | \PDO::MYSQL_ATTR_INIT_COMMAND => 'set names utf8mb4' |
19 | $handle = $link ->prepare( 'select Username from Users where UserId = ? or Username = ? limit ?' ); |
24 | $handle ->bindValue(1, 100, PDO::PARAM_INT); |
25 | $handle ->bindValue(2, 'Bilbo Baggins' ); |
26 | $handle ->bindValue(3, 5, PDO::PARAM_INT); |
33 | $result = $handle ->fetchAll(\PDO::FETCH_OBJ); |
35 | foreach ( $result as $row ){ |
36 | print( $row ->Username); |
39 | catch(\PDOException $ex ){ |
40 | print( $ex ->getMessage()); |
陷阱当绑定整型变量时,如果不传递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> 标签必须是文件中的第一行—在它前面有任何空格或换行符都会使其失效。
进一步阅读
自动载入类PHP提供几种方式来自动载入含有还没被载入的类的文件。较老的方式是使用名为__autoload()的魔术全局函数。但你一次只能使用一个定义的 __autoload() 函数,所以当你包含一个也使用了 __autoload() 函数的库时,就会造成冲突。 解决这个问题的正确方式是把你的自动载入函数命名成一个唯一的名称,然后用 spl_autoload_register() 函数注册。这个函数允许定义多个 __autoload() 函数,这样你就不会踩到其它代码所含的 __autoload() 函数了。 例如:03 | function MyAutoload( $className ){ |
04 | include_once ( $className . '.php' ); |
08 | spl_autoload_register( 'MyAutoload' ); |
进一步阅读
从性能角度比较单引号和双引号这并不重要。关于“定义字符串时应使用单引号还是双引号”,已有很多笔墨评论了。单引号字符串不会进行解析,所以无论你在字符串放了什么,都会原样显示。双引号字符串会被解析,在里面的任何PHP变量都会被求值兑现。另外,对于转义字符(如换行符\n和制表符\t),在单引号和双引号中的差别也是同样的。 因为双引号字符串会在运行时进行解析,理论上能用单引号就用单引号,这应该可以提升性能,因为PHP不需要对单引号字符串额外进行解析。虽然对具有一定规模的应用来说这可能是真的,但对于一般的现实生活中的应用程序来说,效率差距微乎其微,它其实并不重要。因此对于一般的应用程序,你选择什么并不重要(译者注:因此更重要的是,当使用单引号字符串时,应确保它在以后绝无可能加入需要解析的成份,否则你就要在将来多麻烦一下把它改成双引号)。对于非常高负荷的应用程序,它可能有一点影响。选择使用哪种方式,取决于你应用程序的需求,但无论你选择哪种,应该保持一致。 进一步阅读
define() 和 const 的比较使用 define() ,除非关注“可读性,类常量,微优化”传统上,在PHP中你会使用define()函数来定义常量。但根据一些意见,PHP也获得了用const关键字声明常量的能力。那么定义常量时,你应该使用哪一个呢? 答案就在于这两种方法之间微小的差异: - define() 是在运行时定义常量,而 const 是在编译时定义常量。这给了const一个很轻微的速度优势,但达不到值得担心的程度,除非你在建立大型软件。
- define() 把常量放在全局范围,虽然你可以在你的常量名称中包含命名空间。这意味着你不能用define()来定义类常量。
- define() 允许你在常量名称和常量值中都使用表达式,而 const 则都不允许。这使得define() 灵活得多。
- define() 可以在一个 if() 块中被使用,而 const 不能.
例子:03 | namespace MiddleEarth\Creatures\Dwarves; |
05 | define( 'MiddleEarth\Creatures\Elves\LEGOLAS_ID' , 2); |
07 | echo (\MiddleEarth\Creatures\Dwarves\GIMLI_ID); |
08 | echo (\MiddleEarth\Creatures\Elves\LEGOLAS_ID); |
11 | define( 'TRANSPORT_METHOD_SNEAKING' , 1 << 0); |
12 | const TRANSPORT_METHOD_WALKING = 1 << 1; |
15 | define( 'HOBBITS_FRODO_ID' , 1); |
18 | define( 'TRANSPORT_METHOD' , TRANSPORT_METHOD_SNEAKING); |
19 | const PARTY_LEADER_ID = HOBBITS_FRODO_ID |
24 | const MELTING_POINT_DEGREES = 1000000; |
25 | define( 'SHOW_ELVISH_DEGREES' , 200); |
因为define() 最终更为灵活,它是你避免头痛的选择,除非你非常需要类常量。用const可产生更易阅读的代码,但要付出灵活性的代价。 无论你用哪一种,应保持一致! 进一步阅读
缓存PHP操作码(字节码)在PHP的标准安装环境里,每个PHP脚本在每次它被访问时都会被编译成操作码(字节码)文件并执行。花时间对完全相同的脚本一遍又一遍进行编译,对大型网站来说,势必导致性能问题。 解决方案是操作码缓存。操作码缓存是一种可以记忆每个脚本的编译结果的系统,这样服务器不必浪费时间一遍遍编译。通常它们也很聪明,足以探测到一个脚本已经改变并对其重新编译,所以当你更新你的PHP源文件时,不必非得手动清除缓存。 有几种PHP操作码缓存系统可用,值得一提的是 eaccelerator, xcache, 和 APC. APC 是由PHP项目组官方提供支持的,是最活跃也是最容易安装的。它还提供了一个可选的类似memcached的持久的键-值存储。基于这些理由,它是你应该使用的。
安装APC通过在终端运行以下命令,可以在Ubuntu 12.04上安装APC: sudo apt-get install php-apc无需另外的配置。 作为一个永久的键值对存储来使用APCAPC也提供类似memcached的功能,对于你的脚本也是显而易见的。相对于使用memcached,最大的优点就是APC集成到了PHP内核中,因此你不需要再的你服务器中保存你的可动部分,PHP开发者主动地在它上面工作。另外一方面,APC不是一个分布式缓存;如果你需要这个特性,你必须使用memcached。 示例03 | apc_store( 'username-1532' , 'Frodo Baggins' ); |
04 | apc_store( 'username-958' , 'Aragorn' ); |
05 | apc_store( 'username-6389' , 'Gandalf' ); |
08 | $value = apc_fetch( 'username-958' , $success ); |
12 | $value = apc_fetch( 'username-1' , $success ); |
14 | print( 'Key not found' ); |
16 | apc_delete( 'username-958' ); |
Gotchas=Got You进一步阅读
PHP和Memcached一个缓存系统通常能够改进你的app的性能。Memcached是一个主流的选择,并且它兼容许多语言,包括PHP。 然而当它从一个PHP脚本访问一个Memcached服务器的时候,你有两个不同的,命名愚蠢的客户端库选择:Memcache和Memcached。它们是不同的库,但是有着近乎相同的名字,并且都用于访问一个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服务有多种方式来配置一个web服务器以提供PHP服务。传统(并且糟糕的)的方式是使用Apache的 mod_php。Mod_php将PHP 绑定到Apache自身,但是Apache对于该模块功能的管理工作非常糟糕。一旦遇到较大的流量, 就会遭受严重的内存问题。 后来两个新的可选项很快流行起来:mod_fastcgi 和mod_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)。 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 |
最后,重启Apache和FPM进程: 1 | sudo service apache2 restart |
2 | sudo service php5-fpm restart |
进一步阅读
发送email使用PHPMailer 5.1测试。 PHP提供一个mail()函数,看起来极度简单和容易。不幸的是,就像PHP中的很多事情,它的简单是容易误解的,用表面的值来使用它,容易导致严重的安全问题。 Email是一个协议的集合,具有比PHP更曲折痛苦的历史。满足它也就是说,在发送email的时候有太多的疑惑,就像PHPmail()函数应当给你的感觉一样。 PHPMailer 是一个受欢迎的,完善的开源库,提供安全发送mailis的一个简单接口。它为你处理好疑惑,以便你能够关注更重要的事情。 示例03 | require_once ( 'phpmailer-5.1/class.phpmailer.php' ); |
06 | $mailer = new PHPMailer(true); |
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>' ); |
20 | $mailer ->SMTPAuth = true; |
21 | $mailer ->SMTPSecure = 'ssl' ; |
23 | $mailer ->Host = 'my smpt host' ; |
24 | $mailer ->Username = 'my smtp username' ; |
25 | $mailer ->Password = 'my smtp password' ; |
验证email地址你的web应用需要做的一个常见的任务。就是检查一个用户是否输入了一个有效的email地址。你将毫无疑问地在网上找到一堆复杂的表达式,它们声称可以解决此问题,但是最简单的方法就是使用PHP的内建函数filter_var(),它能够检验email地址。 示例2 | filter_var( 'sgamgee@example.com' , FILTER_VALIDATE_EMAIL); |
3 | filter_var( 'sauron@mordor' , FILTER_VALIDATE_EMAIL); |
进一步阅读
净化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实体,而不仅仅是一个小的子集。 示例03 | $evilHtml = '<div onclick="xss();">Mua-ha-ha! Twiddling my evil mustache...</div>' ; |
08 | $safeHtml = htmlentities( $evilHtml , ENT_QUOTES, 'UTF-8' ); |
对于复杂需求的净化对于很多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提供的更多更高级的特性。 示例 <?php// Include the HTML Purifier libraryrequire_once('htmlpurifier-4.4.0/HTMLPurifier.auto.php');// Oh no! The user has submitted malicious HTML, and we have to display it in our web app!$evilHtml='<div onclick="xss();">Mua-ha-ha! Twiddling my evil mustache...</div>';// Set up the HTML Purifier object with the default configuration.$purifier=newHTMLPurifier(HTMLPurifier_Config::createDefault());$safeHtml=$purifier->purify($evilHtml);// $safeHtml is now sanitized. You can output $safeHtml to your users without fear!?> 陷阱 - 以错误的字符编码使用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 )。 例
03 | mb_internal_encoding( 'UTF-8' ); |
06 | mb_http_output( 'UTF-8' ); |
09 | $string = 'Aš galiu valgyti stiklą ir jis manęs nežeidžia' ; |
12 | $string = mb_substr( $string , 0, 10); |
17 | $link = new \PDO( 'mysql:host=your-hostname;dbname=your-db' , |
21 | \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, |
22 | \PDO::ATTR_PERSISTENT => false, |
23 | \PDO::MYSQL_ATTR_INIT_COMMAND => 'set names utf8mb4' |
29 | $handle = $link ->prepare( 'insert into Sentences (Id, Body) values (?, ?)' ); |
30 | $handle ->bindValue(1, 1, PDO::PARAM_INT); |
31 | $handle ->bindValue(2, $string ); |
35 | $handle = $link ->prepare( 'select * from Sentences where Id = ?' ); |
36 | $handle ->bindValue(1, 1, PDO::PARAM_INT); |
40 | $result = $handle ->fetchAll(\PDO::FETCH_OBJ); |
44 | <meta http-equiv= "Content-Type" content= "text/html; charset=UTF-8" /> |
45 | <title>UTF-8 test page</title> |
49 | foreach ( $result as $row ){ |
延伸阅读
时间和日期在PHP的早些时候,我们不得不使用关于date(),gmdate(),date_timezone_set(),strtotime() 的一系列眼花缭乱的组合,来完成日期和时间的操作。遗憾的是,你依然能够在网上找到许多这些困难的,旧样式功能的教程。 对于我们幸运的是,我们正在讨论的PHP版本有了更加友好的DateTime类特性。这个类封装了所有的功能,更多旧日期函数到一个易用的类,更令人高兴地是使得时间转换更加容易。在PHP中,总是使用DateTime类来说检查,比较,改变,显示日期。 Example03 | $date = new DateTime( '2011-05-04 05:00:00' , new DateTimeZone( 'UTC' )); |
06 | $date ->add( new DateInterval( 'P10D' )); |
08 | echo ( $date ->format( 'Y-m-d h:i:s' )); |
12 | $date ->setTimezone( new DateTimeZone( 'America/Los_Angeles' )); |
15 | echo ( $date ->format( 'Y-m-d h:i:s' )); |
17 | $later = new DateTime( '2012-05-20' , new DateTimeZone( 'UTC' )); |
21 | echo ( 'Yup, you can compare dates using these easy operators!' ); |
24 | $difference = $date ->diff( $later ); |
26 | echo ( 'The 2nd date is ' . $difference [ 'days' ] . ' later than 1st date.' ); |
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()微微快一些,而且也被一些人认为比使用一个比较函数更简洁。 Example07 | print( 'Oops! $x is 0, not null!' ); |
11 | print( 'Great, but could be faster.' ); |
23 | if ( strpos ( 'abc' , 'a' ) !== false) |
24 | print( 'Found it for real this time!' ); |
Gotchas==Got you- 当测试一个函数的返回值的时候,函数能够返回0或者布尔值,像strpos()一样,总是使用===和!==,你或许会遇到问题。
进一步阅读
|