[PHP最佳实践]阅读札记


存储密码, 使用 phpass ^1 库来哈希和比较密码

// Include phpass 库
require_once('phpass-03/PasswordHash.php')

// 初始化散列器为不可移植(这样更安全)
$hasher = new PasswordHash(8, false);

// 计算密码的哈希值。$hashedPassword 是一个长度为 60 个字符的字符串.
$hashedPassword = $hasher->HashPassword('my super cool password');

// 你现在可以安全地将 $hashedPassword 保存到数据库中!

// 通过比较用户输入内容(产生的哈希值)和我们之前计算出的哈希值,来判断用户是否输入了正确的密码
$hasher->CheckPassword('the wrong password', $hashedPassword);  // false

$hasher->CheckPassword('my super cool password', $hashedPassword);  // true

phpass 在 HashPassword() 函数中已经对你的密码“加盐”了,这意味着你不需要自己“加盐”。

PHP 与 MySQL, 使用 PDO 及其预处理语句功能。

try{
    // 新建一个数据库连接
    // 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 查询。
  • 未使用 set names utf8mb4 作为首个查询,可能会导致 Unicode 数据错误地存储进数据库
  • 即使你使用了 set names utf8mb4,你也得确认实际的数据库表使用的是 utf8mb4 字符集!
  • 可以在单个 execute() 调用中执行多条 SQL 语句。 只需使用分号分隔语句, 但是 PDO::nextRowset() after a multi-statement query doesn’t always work ^2

自动加载类, 使用 spl_autoload_register() 来注册你的自动加载函数。

<?php
// 首先,定义你的自动载入的函数
function MyAutoload($className)&#123;
    include_once($className . '.php');
&#125;

// 然后注册它。
spl_autoload_register('MyAutoload');

// Try it out!
// 因为我们没包含一个定义有 MyClass 的文件,所以自动加载器会介入并包含 MyClass.php。
// 在本例中,假定在 MyClass.php 文件中定义了 MyClass 类。
$var = new MyClass();

从性能角度来看单引号和双引号, 其实并不重要

单引号字符串不会被解析,因此放入字符串的任何东西都会以原样显示。 双引号字符串会被解析,字符串中的任何 PHP 变量都会被求值。

define() vs. const

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

  • define() 在执行期定义常量,而 const 在编译期定义常量。这样 const 就有轻微的速度优势, 但不值得考虑这个问题,除非你在构建大规模的软件。
  • define() 将常量放入全局作用域. 不能使用 define() 定义类常量。
  • define() 可以在 if() 代码块中调用,但 const 不行。

缓存 PHP opcode, 使用 APC

<?php
// 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.
apc_store('username-1532', 'Frodo Baggins');
apc_store('username-958', 'Aragorn');
apc_store('username-6389', 'Gandalf');

// After storing these values, any PHP script can access them, no matter when it's run!
$value = apc_fetch('username-958', $success);
if($success === true)
    print($value); // Aragorn

$value = apc_fetch('username-1', $success); // $success will be set to boolean false, because this key doesn't exist.
if($success !== true) // Note the !==, this checks for true boolean false, not "falsey" values like 0 or empty string.
    print('Key not found');

apc_delete('username-958'); // This key will no longer be available.

配置 Web 服务器提供 PHP 服务, 使用 PHP-FPM

发送邮件, 使用 PHPMailer ^3

<?php
// Include the PHPMailer library
require_once('phpmailer-5.1/class.phpmailer.php');

// Passing 'true' enables exceptions.  This is optional and defaults to false.
$mailer = new PHPMailer(true);

// Send a mail from Bilbo Baggins to Gandalf the Grey

// Set up to, from, and the message body.  The body doesn't have to be HTML;
// check the PHPMailer documentation for details.
$mailer->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('<p>You really must try it, Gandalf!</p><p>-Bilbo</p>');

// 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() 函数 ^4

<?php
filter_var('sgamgee@example.com', FILTER_VALIDATE_EMAIL);
//Returns "sgamgee@example.com". This is a valid email address.

filter_var('sauron@mordor', FILTER_VALIDATE_EMAIL);
// Returns boolean false! This is *not* a valid email address.

净化 HTML 输入和输出

  • 对于简单的数据净化,使用 htmlentities() 函数, 复杂的数据净化则使用 HTML Purifier 库
  • 你可能会找到建议你使用 strip_tags() 函数的观点。 虽然 strip_tags() 从技术上来说是安全的,但如果输入的不合法的 HTML(比如, 没有结束标签),它就成了一个「愚蠢」的函数,可能会去除比你期望的更多的内容。
  • 如果你的 web 应用仅需要完全地转义(因此可以无害地呈现,但不是完全去除) HTML, 则使用 PHP 的内建 htmlentities() 函数。
<?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>';

// Use the ENT_QUOTES flag to make sure both single and double quotes are escaped.
// Use the UTF-8 character encoding if you've stored the text as UTF-8 (as you should have).
// See the UTF-8 section in this document for more details.
$safeHtml = htmlentities($evilHtml, ENT_QUOTES, 'UTF-8');
// $safeHtml is now fully escaped HTML.  You can output $safeHtml to your users without fear!

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

<?php
// Include the HTML Purifier library
require_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 = new HTMLPurifier(HTMLPurifier_Config::createDefault());

$safeHtml = $purifier->purify($evilHtml);
// $safeHtml is now sanitized.  You can output $safeHtml to your users without fear!

HTML Purifier 相比 strip_tags() 是有优势的, 因为它在净化 HTML 之前会对其校验。这意味着如果用户输入无效 HTML,HTML Purifier 相比 strip_tags() 更能保留 HTML 的原意。

  • 以错误的字符编码使用 htmlentities() 会造成意想不到的输出。
  • 使用 htmlentities() 时,始终包含 ENT_QUOTES 和字符编码参数。 默认情况下,htmlentities() 不会 对单引号编码。
  • HTML Purifier 对于复杂的 HTML 效率极其的低。可以考虑设置一个缓存方案如APC来保存经过净化的结果以备后用。

PHP 与 UTF-8

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

  • 并不是所有的字符串函数都有一个对应的 mb_*。
  • 在每个 PHP 脚本的顶部(或者在全局包含脚本的顶部)你都应使用 mb_internal_encoding 函数,如果你的脚本会输出到浏览器,那么还得紧跟其后加个mb_http_output() 函数。
  • 许多操作字符串的 PHP 函数都有一个可选参数让你指定字符编码。 若有该选项, 你应始终显式地指明 UTF-8 编码。 例如,htmlentities() 就有一个字符编码方式选项,在处理这样的字符串时应始终指定 UTF-8。

确保从 PHP 到 MySQL 的字符串为 UTF-8 编码的,确保你的数据库以及数据表均设置为 utf8mb4 字符集, 并且在你的数据库中执行任何其他查询之前先执行 MySQL 查询 set names utf8mb4

  • 注意你必须使用 utf8mb4 字符集来获得完整的 UTF-8 支持,而不是 utf8 字符集!

使用 mb_http_output() 函数 来确保你的 PHP 脚本输出 UTF-8 字符串到浏览器。 并且在 HTML 页面的 标签块中包含 字符集 标签块。

<?php
// Tell PHP that we're using UTF-8 strings until the end of the script
mb_internal_encoding('UTF-8');

// Tell PHP that we'll be outputting UTF-8 to the browser
mb_http_output('UTF-8');

// Our UTF-8 test string
$string = 'Aš galiu valgyti stiklą ir jis manęs nežeidžia';

// Transform the string in some way with a multibyte function
$string = mb_substr($string, 0, 10);

// Connect to a database to store the transformed string
// See the PDO example in this document for more information
// Note the `set names utf8mb4` commmand!
$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'
                    )
                );

// 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);
?><!doctype html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <title>UTF-8 test page</title>
    </head>
    <body>
        <?php
        foreach($result as $row)&#123;
            print($row->Body);  // This should correctly output our transformed UTF-8 string to the browser
        &#125;
        ?>
    </body>
</html>

处理日期和时间, 使用DateTime 类。

<?php
// Construct a new UTC date.  Always specify UTC unless you really know what you're doing!
$date = new DateTime('2011-05-04 05:00:00', new DateTimeZone('UTC'));

// Add ten days to our initial date
$date->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 值。

is_null() 函数能准确地检测一个值是否为 null, is_bool 可以检测一个值是否是布尔值(比如 false), 但存在一个更好的选择:=== 操作符。

原文: PHP 最佳实践(译)


参考文档


Author: Itaken
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source Itaken !
  TOC目录