原文:? php best practices-a short, practical guide for common and confusing php tasks 译者: youngsterxyf 最后修订日期维护者 本文档最后审阅于2013年3月8日。最后修改于2013年5月8日。 由我, alex cabal,维护该文档。我编写php程序已有很长一段时
原文:? php best practices-a short, practical guide for common and confusing php tasks
译者: youngsterxyf
最后修订日期&维护者 本文档最后审阅于2013年3月8日。最后修改于2013年5月8日。
由我, alex cabal,维护该文档。我编写php程序已有很长一段时间了,当前我 经营着 scribophile,由认真作家组成的一个在线写作团体,? writerfolio,为自由职业者提供的一个易用写作工具集,以及? standard ebooks,一个图文并茂、无数字版权管理的公共领域电子书出版商。 有时我是个为吸引我的项目或客户而工作的自由职业者。
如果你认为我在某些事情上能够帮到你,或者对本文档有点建议或纠正存在的错误, 请给我写封邮件。
简介 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或安全问题。
存储密码 使用 phpass库来哈希和比较密码 经phpass 0.3测试
在存入数据库之前进行哈希保护用户密码的标准方式。许多常用的哈希算法如md5,甚至是sha1 对于密码存储都是不安全的,因为 骇客能够使用那些算法轻而易举地破解密码。
对密码进行哈希最安全的方法是使用bcrypt算法。开源的phpass库以一个易于使用的类来提供 该功能。
示例
hashpassword('my super cool password');// you can now safely store the contents of $hashedpassword in your database!// check if a user has provided the correct password by comparing what they// typed with our hash$hasher->checkpassword('the wrong password', $hashedpassword); // false$hasher->checkpassword('my super cool password', $hashedpassword); // true?>
陷阱
许多资源可能推荐你在哈希之前对你的密码“加盐”。想法很好,但phpass在hashpassword()函数中已经对你的密码“加盐”了,这意味着你不需要自己“加盐”。 进一步阅读
phpass 为什么使用md5或sha哈希密码是不安全的 怎样安全地存储密码连接并查询mysql数据库 使用 pdo及其预处理语句功能。 在php中,有很多方式来连接到一个mysql数据库。pdo(php数据对象)是其中最新且最健壮的一种。pdo跨多种不同类型数据库有一个一致的接口,使用面向对象的方式,支持更多的新数据库支持的特性。
你应该使用pdo的预处理语句函数来帮助防范sql注入攻击。使用函数 bindvalue来确保你的sql免于一级sql注入攻击。(虽然并不是100%安全的,查看进一步阅读获取更多细节。)在以前,这必须使用一些“魔术引号(magic quotes)”函数的组合来实现。pdo使得那堆东西不再需要。
示例
\pdo::errmode_exception, \pdo::attr_persistent => false, \pdo::mysql_attr_init_command => 'set names utf8mb4' ) ); $handle = $link->prepare('select username from users where userid = ? or username = ? limit ?'); // 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. // see: https://bugs.php.net/bug.php?id=44639 // if you're not sure whether the value you're passing is an integer, use the is_int() function. $handle->bindvalue(1, 100, pdo::param_int); $handle->bindvalue(2, 'bilbo baggins'); $handle->bindvalue(3, 5, pdo::param_int); $handle->execute(); // using the fetchall() method might be too resource-heavy if you're selecting a truly massive amount of rows. // if that's the case, you can use the fetch() method and loop through each result row one by one. // you can also return arrays and other things instead of objects. see the pdo documentation for details. $result = $handle->fetchall(\pdo::fetch_obj); foreach($result as $row){ print($row->username); }}catch(\pdoexception $ex){ 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手册:pdo 为什么你应该使用php的pdo访问数据库 stack overflow: php pdo vs 普通的mysql_connect stack overflow: pdo预处理语句足以防范sql注入吗? stack overflow: 在mysql中使用set names utf8?php标签 使用? ?。 有几种不同的方式用来区分php程序块: ,? = ?>,? ?>, 以及 。对于打字来说,更短的标签更方便些,但唯一一种在所有php服务器上都一定能工作的标签 是 。若你计划将你的php应用部署到一台上面的php配置你无法控制的服务器上,那么你应始终使用? 。
若你仅仅是为自己编码,也能控制你将使用的php配置,你可能觉得短标签更方便些。但记住? ?>可能会和xml声明冲突,并且 ?>实际上是asp的风格。
无论你选择哪一种,确保一致。
陷阱
在一个纯php文件(例如,仅包含一个类定义的文件)中包含一个关闭 ?>标签时,确保其后 不会跟着任何换行。当php解析器安全地吃进跟在关闭标签之后的单个换行符时,任何其他的换行 都可能被输出到浏览器,如果之后要输出某些http头,那么可能会造成混淆。 编写web应用时,确保在关闭 ?>标签与html的 标签之间不会留下换行。正确的html 文件中, 标签必须是文件中的第一样东西—在其之前的任何空格或换行都会使其 无效。 进一步阅读
stack overflow: 可以使用php短标签吗?自动加载类 使用 spl_autoload_register()来注册你的自动加载函数。 php提供了若干方式来自动加载包含还未加载的类的文件。老的方法是使用名为 __autoload()魔术全局函数。然而你一次仅能定义一个 __autoload()函数,因此如果你的程序 包含一个也使用了 __autoload()函数的库,就会发生冲突。
处理这个问题的正确方法是唯一地命名你的自动加载函数,然后使用 spl_autoload_register()函数 来注册它。该函数允许定义多个 __autoload()这样的函数,因此你不必担心其他代码的 __autoload()函数。
示例
进一步阅读
php手册:spl_autoload_register() stack overflow: 高效的php自动加载和命名策略从性能角度来看单引号和双引号 其实并不重要。 已有很多人花费很多笔墨来讨论是使用单引号( ')还是双引号( )来定义字符串。 单引号字符串不会被解析,因此放入字符串的任何东西都会以原样显示。双引号字符串会被解析, 字符串中的任何php变量都会被求值。另外,转义字符如换行符 \n和制表符 \t在单引号字符串中 不会被求值,但在双引号字符串中会被求值。
由于双引号字符串在程序运行时要求值,从而理论上使用单引号字符串能提高性能,因为php 不会对单引号字符串求值。这对于一定规模的应用来说也许确实如此,但对于现实中一般的应用来说, 区别非常小以至于根本不用在意。因此对于普通应用,你选择哪种字符串并不重要。对于负载 极其高的应用来说,是有点作用的。根据你的应用的需要来做选择,但无论你选择什么,请保持一致。
进一步阅读
php手册:字符串 php基准(向下滚动到引号类型(quote types)) stack overflow: php中单引号字符串相比双引号字符串有性能优势么?define() vs. const 使用 define(),除非考虑到可读性、类常量、或关注微优化 习惯上,在php中是使用define()函数来定义常量。但从某个时候开始,php中也能够使用 const?关键字来声明常量了。那么当定义常量时,该使用哪种方式呢?
答案在于这两种方法之间的区别。
define()在执行期定义常量,而const在编译期定义常量。这样const就有轻微的速度优势, 但不值得考虑这个问题,除非你在构建大规模的软件。 define()将常量放入全局作用域,虽然你可以在常量名中包含命名空间。这意味着你不能 使用define()定义类常量。 define()允许你在常量名和常量值中使用表达式,而const则都不允许。这使得define() 更加灵活。 define()可以在if()代码块中调用,但const不行。 示例
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()函数。
示例
进一步阅读
php手册:filter_var() php手册:过滤器的类型净化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实体,而不仅仅是一个小的子集。
示例
对于复杂需求的净化
对于很多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提供的更多更高级的特性。
示例
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 html净化工具对比 stack overflow: 使用strip_tags()来防止xss? stack overflow: php中净化用户输入的最佳方法是什么? stack overflow: 断行时的filter_sanitize_special_chars问题php与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字符串。
确保从php到mysql的字符串为utf-8编码的,确保你的数据库以及数据表均设置为utf8mb4字符集, 并且在你的数据库中执行任何其他查询之前先执行mysql查询`set names utf8mb4`。这是至关重要的。示例 请查看 连接并查询mysql数据库一节内容。
注意你必须使用`utf8mb4`字符集来获得完整的utf-8支持,而不是`utf8`字符集!原因 请查看 进一步阅读。
浏览器层面的utf-8
使用 mb_http_output()函数 来确保你的php脚本输出utf-8字符串到浏览器。并且在html页面的 标签块中包含? 字符集 标签块。
示例
\pdo::errmode_exception, \pdo::attr_persistent => false, \pdo::mysql_attr_init_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 page body); // 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 savingsecho($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 datesif($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位系统则没有问题。 进一步阅读
php手册:datetime类 stack overflow: 访问超出2038的日期检测一个值是否为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手册:比较操作符 stack overflow: is_null() vs ===建议与指正 感谢阅读!如果你有些地方还不太理解,很正常,php是复杂的,并且充斥着陷阱。因为我也 只是一个人,所以本文档中难免存在错误。
如果你想为本文档贡献建议或纠正错误之处,请使用 最后修订日期&维护者?一节中的信息联系我。
作者:guobin_lu 发表于2013-6-9 21:43:42 原文链接
阅读:89 评论:0 查看评论
原文地址:php最佳实践(译), 感谢原作者分享。