PHP最佳实践(译)

发布时间:2013-06-04

原文: 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库以一个易于使用的类来提供 该功能。

示例

<?php // Include the phpass library require_once('phpass-03/PasswordHash.php') // Initialize the hasher without portable hashes (this is more secure) $hasher = new PasswordHash(8, false); // Hash the password. $hashedPassword will be a 60-character string. $hashedPassword = $hasher->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()函数中已经对你的密码“加盐”了,这意味着你不需要自己“加盐”。

进一步阅读

连接并查询MySQL数据库

使用PDO及其预处理语句功能。

在PHP中,有很多方式来连接到一个MySQL数据库。PDO(PHP数据对象)是其中最新且最健壮的一种。PDO跨多种不同类型数据库有一个一致的接口,使用面向对象的方式,支持更多的新数据库支持的特性。

你应该使用PDO的预处理语句函数来帮助防范SQL注入攻击。使用函数bindValue来确保你的SQL免于一级SQL注入攻击。(虽然并不是100%安全的,查看进一步阅读获取更多细节。)在以前,这必须使用一些“魔术引号(magic quotes)”函数的组合来实现。PDO使得那堆东西不再需要。

示例

<?php try{ // Create a new connection. // You'll probably want to replace hostname with localhost in the first parameter. // The PDO options we pass do the following: // \PDO::ATTR_ERRMODE enables exceptions for errors.  This is optional but can be handy. // \PDO::ATTR_PERSISTENT disables persistent connections, which can cause concurrency issues in certain cases.  See "Gotchas". // \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". $link = new \PDO( 'mysql:host=your-hostname;dbname=your-db', 'your-username', 'your-password', array( \PDO::ATTR_ERRMODE => \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标签

使用 <?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()函数。

示例

<?php // First, define your auto-load function function MyAutoload($className){ include_once($className . '.php'); } // Next, register it with PHP spl_autoload_register('MyAutoload'); // Try it out! // Since we haven't included a file defining the MyClass object, our // auto-loader will kick in and include MyClass.php. // For this example, assume the MyClass class is defined in the MyClass.php // file. $var = new MyClass(); ?> 

进一步阅读

从性能角度来看单引号和双引号

其实并不重要。

已有很多人花费很多笔墨来讨论是使用单引号(')还是双引号(")来定义字符串。 单引号字符串不会被解析,因此放入字符串的任何东西都会以原样显示。双引号字符串会被解析, 字符串中的任何PHP变量都会被求值。另外,转义字符如换行符\n和制表符\t在单引号字符串中 不会被求值,但在双引号字符串中会被求值。

由于双引号字符串在程序运行时要求值,从而理论上使用单引号字符串能提高性能,因为PHP 不会对单引号字符串求值。这对于一定规模的应用来说也许确实如此,但对于现实中一般的应用来说, 区别非常小以至于根本不用在意。因此对于普通应用,你选择哪种字符串并不重要。对于负载 极其高的应用来说,是有点作用的。根据你的应用的需要来做选择,但无论你选择什么,请保持一致。

进一步阅读

define() vs. const

使用define(),除非考虑到可读性、类常量、或关注微优化

习惯上,在PHP中是使用define()函数来定义常量。但从某个时候开始,PHP中也能够使用const 关键字来声明常量了。那么当定义常量时,该使用哪种方式呢?

答案在于这两种方法之间的区别。

  1. define()在执行期定义常量,而const在编译期定义常量。这样const就有轻微的速度优势, 但不值得考虑这个问题,除非你在构建大规模的软件。
  2. define()将常量放入全局作用域,虽然你可以在常量名中包含命名空间。这意味着你不能 使用define()定义类常量。
  3. define()允许你在常量名和常量值中使用表达式,而const则都不允许。这使得define() 更加灵活。
  4. define()可以在if()代码块中调用,但const不行。

示例

<?php // Let's see how the two methods treat namespaces namespace MiddleEarth\Creatures\Dwarves; const GIMLI_ID = 1; define('MiddleEarth\Creatures\Elves\LEGOLAS_ID', 2); echo(\MiddleEarth\Creatures\Dwarves\GIMLI_ID); // 1 echo(\MiddleEarth\Creatures\Elves\LEGOLAS_ID); // 2; note that we used define() // Now let's declare some bit-shifted constants representing ways to enter Mordor. define('TRANSPORT_METHOD_SNEAKING', 1 << 0); // OK! const TRANSPORT_METHOD_WALKING = 1 << 1; //Compile error! const can't use expressions as values // Next, conditional constants. define('HOBBITS_FRODO_ID', 1); if($isGoingToMordor){ define('TRANSPORT_METHOD', TRANSPORT_METHOD_SNEAKING); // OK! const PARTY_LEADER_ID = HOBBITS_FRODO_ID // Compile error: const can't be used in an if block } // Finally, class constants class OneRing{ const MELTING_POINT_DEGREES = 1000000; // OK! define('SHOW_ELVISH_DEGREES', 200); // Compile error: can't use define() within a class } ?> 

因为define()更加灵活,你应该使用它以避免一些令人头疼的事情,除非你明确地需要类 常量。使用const通常会产生更加可读的代码,但是以牺牲灵活性为代价的。

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

进一步阅读

缓存PHP opcode

使用APC

在一个标准的PHP环境中,每次访问PHP脚本时,脚本都会被编译然后执行。一次又一次地花费 时间编译相同的脚本对于大型站点会造成性能问题。

解决方案是采用一个opcode缓存。opcode缓存是一个能够记下每个脚本经过编译的版本,这样 服务器就不需要浪费时间一次又一次地编译了。通常这些opcode缓存系统也能智能地检测到 一个脚本是否发生改变,因此当你升级PHP源码时,并不需要手动清空缓存。

有几个PHP opcode缓存可用,其中值得关注的有eaccelerator, xcache,以及APC。 APC是PHP项目官方支持的,最为活跃,也最容易安装。它也提供一个可选的类memcached 的持久化键-值对存储,因此你应使用它。

安装APC

在Ubuntu 12.04上你可以通过在终端中执行以下命令来安装APC:

[返回上页]