简介
PHP是一门复杂的语言,经过多年折腾,使其不同版本之间高度不一致,有时还有些bug。 每个版本都有自己独有的特性、多余和怪异之处,也很难跟踪哪个版本有哪些问题。这也就 很好理解为什么有时它会遭到那么多的厌恶。
尽管如此,如今它还是Web开发方面最流行的语言。因其悠久的历史,对于实现密码哈希和 数据库访问诸如此类的基本任务你能够找到很多教程。但问题在于,5个教程,你就很有可能 找到5种完全不同的完成任务的方式,那么哪种是“正确”的方式呢?其他方式有难以捉摸的bug 或者陷阱?确实很难搞明白,所以你经常要在互联网上反复查找尝试确认正确的答案。
这也是PHP编程新手频繁地因为丑陋、过时、或不安全的代码而遭到责备的原因之一。如果 Google搜索的第一个结果是一篇4年前的文章,讲述一种5年前的方法,那么PHP新手们也就 很难改变经常遭受责备的现状。
本文档通过为PHP中常见的令人困惑的问题和任务编辑组织一系列被认为最佳实践的基本做法, 来尝试解决上述问题。若一个低层次的任务在PHP中有多种令人困惑的实现方式,本文也会涵盖。
是什么
这是一份指南,在PHP程序员遇到一些常见低层次任务但不明确最佳做法(由于PHP可能提供 了多种解决方案)之时,为其建议最佳实践。例如:连接数据库是一个常见任务,PHP中提供了 大量可行的方案,但并不是所有的都是好的做法,因此,本文也会包含该问题。
本文包含的是一系列简短的、入门性质的方案。涉及的示例在基本设定下就能够运行起来, 你研究一下应该就能把它们变为对你有用的东西。
本文将指出一些我们认为是PHP中最新最好的东西。然而,这意味如果你在使用老版本的PHP, 一些用来实现这些解决方案的特性对你并不可用。
这份文档会一直更新,我会尽我最大努力保持该文档与PHP的发展同步。
不是什么
本文档不是一份PHP教程。你应该在别处学习语言基础和语法。
它也不是一份针对web应用常见问题,如cookie存储、缓存、编程风格、文档等的指南。
它也不是一个安全指南。当本文档触碰到一些安全相关的问题时,也是希望你自己做些研究来 确保你的PHP应用的安全问题。你的代码造成的问题应该都是自己的过错。
该文档也并不是在主张一种特定的编程风格、模式或者框架。
也不是在主张一种特定的方式来完成高层次任务如用户注册、登录系统等。本文档只限于 PHP的悠久历史所造成的一些易混淆或不明确的低层次任务。
它不是一个一劳永逸的解决方案,也不是一个唯一的方案。下面要讲述的一些方法对于你的 特定场景来说也许并不是最好的,存在很多不同的方式来达到同样的目的。特别是,高负载web 应用也许能从更加难懂的方案中获益更多。
我们在使用哪个版本的PHP?
带Suhosin-Patch的PHP 5.3.10-1ubuntu3.6,安装在Ubuntu 12.04 LTS上。
PHP是Web世界里的百年老龟,它的壳上铭刻着一段丰富、复杂、而粗糙的历史。在一个共享 主机的环境里,它的配置可能会限制你能做的事情。
为了保持清晰地叙述,我们将仅针对一个版本的PHP进行讲述。在2013年4月30日时,该版本 为PHP 5.3.10-1ubuntu3.6 with Suhosin-Patch。若你在Ubuntu 12.04 LTS服务器 上使用apt-get进行安装的就是该版本的PHP。
你也许发现这些方案中的一些在其他或者更老版本的PHP上也能工作。如果是这样的话,就由 你来研究在这些更老版本上潜在的难以捉摸的bug或安全问题。
存储密码
使用 。
有几种不同的方式用来区分PHP程序块:, , , 以及。对于打字来说,更短的标签更方便些,但唯一一种在所有PHP服务器上都一定能工作的标签 是。若你计划将你的PHP应用部署到一台上面的PHP配置你无法控制的服务器上,那么你应始终使用 。
若你仅仅是为自己编码,也能控制你将使用的PHP配置,你可能觉得短标签更方便些。但记住 可能会和XML声明冲突,并且实际上是ASP的风格。
无论你选择哪一种,确保一致。
陷阱
- 在一个纯PHP文件(例如,仅包含一个类定义的文件)中包含一个关闭?>标签时,确保其后 不会跟着任何换行。当PHP解析器安全地吃进跟在关闭标签之后的单个换行符时,任何其他的换行 都可能被输出到浏览器,如果之后要输出某些HTTP头,那么可能会造成混淆。
- 编写Web应用时,确保在关闭?>标签与html的标签之间不会留下换行。正确的HTML 文件中,标签必须是文件中的第一样东西—在其之前的任何空格或换行都会使其 无效。
进一步阅读
自动加载类
使用PCRE(preg_*)家族函数
PHP有两种使用不同的方式来使用正则表达式:PCRE(Perl兼容表示法,preg_*)函数 和POSIX(POSIX扩展表示法,ereg_*) 函数。
每个函数家族各自使用一种风格稍微不同的正则表达式。幸运的是,POSIX家族函数从PHP 5.3.0开始就被弃用了。因此,你绝不应该使用POSIX家族函数编写新的代码。始终使用 PRCE家族函数,即preg_*函数。
进一步阅读
配置Web服务器提供PHP服务
–
使用PHPMailer
经PHPMailer 5.1测试
PHP提供了一个mail()函数,看起来很简单易用。 不幸的是,与PHP中的很多东西一样,它的简单性是个幻象,因其虚假的表面使用它会导致 严重的安全问题。
Email是一组网络协议,比PHP的历史还曲折。完全可以说发送邮件中的陷阱与PHP的mail() 函数一样多,这个可能会令你有点“不寒而栗”吧。
PHPMailer是一个流行而 成熟的开源库,为安全地发送邮件提供一个易用的接口。它关注可能陷阱,这样你可以专注 于更重要的事情。
示例
Sender = 'bbaggins@example.com';
$mailer->AddReplyTo('bbaggins@example.com', 'Bilbo Baggins');
$mailer->SetFrom('bbaggins@example.com', 'Bilbo Baggins');
$mailer->AddAddress('gandalf@example.com');
$mailer->Subject = 'The finest weed in the South Farthing';
$mailer->MsgHTML('
You really must try it, Gandalf!
-Bilbo
“`
‘);
// Set up our connection information.
$mailer->IsSMTP();
$mailer->SMTPAuth = true;
$mailer->SMTPSecure = ‘ssl’;
$mailer->Port = 465;
$mailer->Host = ‘my smpt host’;
$mailer->Username = ‘my smtp username’;
$mailer->Password = ‘my smtp password’;
// All done!
$mailer->Send();
?>
“`
验证邮件地址
使用filter_var()函数
Web应用可能需要做的一件常见任务是检测用户是否输入了一个有效的邮件地址。毫无疑问 你可以在网上找到一些声称可以解决该问题的复杂的正则表达式,但是最简单的方法是使用 PHP的内建filter_val()函数。
示例
进一步阅读
净化HTML输入和输出
没有一行式解决方案。小心、注意细节,以及一致性。
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字符串。
确保从PHP到MySQL的字符串为UTF-8编码的,确保你的数据库以及数据表均设置为utf8mb4字符集, 并且在你的数据库中执行任何其他查询之前先执行MySQL查询set names utf8mb4
。这是至关重要的。示例 请查看连接并查询MySQL数据库一节内容。
注意你必须使用utf8mb4
字符集来获得完整的UTF-8支持,而不是utf8
字符集!原因 请查看进一步阅读。
浏览器层面的UTF-8
使用mb_http_output()函数 来确保你的PHP脚本输出UTF-8字符串到浏览器。并且在HTML页面的标签块中包含字符集标签块。
示例
“`
\PDO::ERRMODEEXCEPTION,
\PDO::ATTRPERSISTENT => false,
\PDO::MYSQLATTRINIT_COMMAND => ‘set names utf8mb4’
)
);
// Store our transformed string as UTF-8 in our database
// Assume our DB and tables are in the utf8mb4 character set and collation
$handle = $link->prepare(‘insert into Sentences (Id, Body) values (?, ?)’);
$handle->bindValue(1, 1, PDO::PARAM_INT);
$handle->bindValue(2, $string);
$handle->execute();
// Retrieve the string we just stored to prove it was stored correctly
$handle = $link->prepare(‘select * from Sentences where Id = ?’);
$handle->bindValue(1, 1, PDO::PARAM_INT);
$handle->execute();
// Store the result into an object that we’ll output later in our HTML
$result = $handle->fetchAll(\PDO::FETCH_OBJ);
?>UTF-8 test pageBody);
// This should correctly output our transformed UTF-8 string to the browser
}
?>
“`
进一步阅读
- PHP手册:多字节字符串函数
- PHP UTF-8备忘单
- Stack Overflow: 什么因素致使PHP不兼容Unicode?
- Stack Overflow: PHP与MySQL之间国际化字符串的最佳实践
- 怎样在MySQL数据库中完整支持Unicode
处理日期和时间
–
使用DateTime类。
在PHP糟糕的老时光里,我们必须使用date(), gmdate(), date_timezone_set(), strtotime()等等令人迷惑的 组合来处理日期和时间。悲哀的是现在你仍旧会找到很多在线教程在讲述这些不易使用的老式函数。
幸运的是,我们正在讨论的PHP版本包含友好得多的DateTime类。 该类封装了老式日期函数所有功能,甚至更多,在一个易于使用的类中,并且使得时区转换更加容易。 在PHP中始终使用DateTime类来创建,比较,改变以及展示日期。
示例
“`
add(new DateInterval(‘P10D’));
echo($date->format(‘Y-m-d h:i:s’)); // 2011-05-14 05:00:00
// Sadly we don’t have a Middle Earth timezone
// Convert our UTC date to the PST (or PDT, depending) time zone
$date->setTimezone(new DateTimeZone(‘America/Los_Angeles’));
// Note that if you run this line yourself, it might differ by an
// hour depending on daylight savings
echo($date->format(‘Y-m-d h:i:s’)); // 2011-05-13 10:00:00
$later = new DateTime(‘2012-05-20’, new DateTimeZone(‘UTC’));
// Compare two dates
if($date < $later)
echo(‘Yup, you can compare dates using these easy operators!’);
// Find the difference between two dates
$difference = $date->diff($later);
echo(‘The 2nd date is ‘ . $difference[‘days’] . ‘ later than 1st date.’);
?>
“`
陷阱
- 如果你不指定一个时区,DateTime::__construct() 就会将生成日期的时区设置为正在运行的计算机的时区。之后,这会导致大量令人头疼的事情。 在创建新日期时始终指定UTC时区,除非你确实清楚自己在做的事情。
- 如果你在DateTime::__construct()中使用Unix时间戳,那么时区将始终设置为UTC而不管 第二个参数你指定了什么。
- 向DateTime::__construct()传递零值日期(如:“0000-00-00”,常见MySQL生成该值作为 DateTime类型数据列的默认值)会产生一个无意义的日期,而不是“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宽松类型世界里的相等。它也比is_null()和is_bool()要快一些,并且有些人 认为这比使用函数来做比较更干净些。
示例
陷阱
- 测试一个返回0或布尔false的函数的返回值时,如strpos(),始终使用===和!==,否则 你就会碰到问题。
进一步阅读
建议与指正
感谢阅读!如果你有些地方还不太理解,很正常,PHP是复杂的,并且充斥着陷阱。因为我也 只是一个人,所以本文档中难免存在错误。
如果你想为本文档贡献建议或纠正错误之处,请使用最后修订日期&维护者 一节中的信息联系我。
原文: PHP Best Practices-A short, practical guide for common and confusing PHP tasks
译者:youngsterxyf
主题测试文章,只做测试使用。发布者:eason,转转请注明出处:https://aicodev.cn/2013/06/04/php%e6%9c%80%e4%bd%b3%e5%ae%9e%e8%b7%b5%ef%bc%88%e8%af%91%ef%bc%89/