10条PHP高级技巧[修正版]

1.使用一个SQL注射备忘单
一个基本的原则就是,永远不要相信用户提交的数据。
另一个规则就是,在你发送或者存储数据时对它进行转义(escape)。
可以总结为:filter input, escape output (FIEO). 输入过滤,输出转义。
通常导致SQL注射漏洞的原因是没有对输入进行过滤,如下语句:

1
2
3
4
<?php
$query = "SELECT *
          FROM   users
          WHERE  name = '{$_GET['name']}'"
;

在这个例子中,$_GET['name']来自用户提交的数据,既没有进行转义,也没有进行过滤~~

对于转义输出,你要记住用于你程序外部的数据需要被转义,否则,它可能被错误地解析。
相反,过滤输入能确保数据在使用前是正确的.
对于过滤输入,你要记住,在你程序外部的原始数据需要被过滤,因为它们是不可信任的。

如下例子演示了输入过滤和输出转义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php

// Initialize arrays for filtered and escaped data, respectively.
$clean = array();
$sql = array();

// Filter the name. (For simplicity, we require alphabetic names.)
if (ctype_alpha($_GET['name'])) {
    $clean['name'] = $_GET['name'];
} else {
    // The name is invalid. Do something here.
}

// Escape the name.
$sql['name'] = mysql_real_escape_string($clean['name']);

// Construct the query.
$query = "SELECT *
          FROM   users
          WHERE  name = '{$sql['name']}'"
;

?>

另一个有效防止SQL注射的方法是使用prepare 语句,如:

1
2
3
4
5
6
7
8
9
10
11
<?php

// Provide the query format.
$query = $db->prepare('SELECT *
                       FROM   users
                       WHERE  name = :name'
);

// Provide the query data and execute the query.
$query->execute(array('name' => $clean['name']));

?>

2.了解比较运算符之间的不同

例如,你使用strpos() 来检测在一个字符串中是否存在一个子串 (如果子串没有找到,函数将返回 FALSE ), 结果可能会导致错误:

1
2
3
4
5
6
7
8
9
<?php
$authors = 'Chris &#038; Sean';

if (strpos($authors, 'Chris')) {
    echo 'Chris is an author.';
} else {
    echo 'Chris is not an author.';
}
?>

上例中,由于子串处于最开始的位置,因此strpos() 函数正确地返回了0,表明子串处于字符串中最开始的位置。然后,因为条件语句会把结果当成Boolean(布尔)类型的,因此 0 就被PHP给计算成了 FALSE,最终导致条件语句判断失败。
当然,这个BUG可以用严格的比较语句来修正:

1
2
3
4
5
6
7
8
9
<?php

if (strpos($authors, 'Chris') !== FALSE) {
    echo 'Chris is an author.';
} else {
    echo 'Chris is not an author.';
}

?>

3.减少else(Shortcut the else)
记住,在你使用变量前总是要先初始化它们。
考虑如下一个用来根据用户名来检测用户是否是管理员的条件语句:

1
2
3
4
5
6
7
8
9
<?php

if (auth($username) == 'admin') {
    $admin = TRUE;
} else {
    $admin = FALSE;
}

?>

这个看起来似乎足够安全,因为看一眼就很容易理解。想象一下有一个更复杂一点的例子,它为name和email同时设置变量,为方便起见:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php

if (auth($username) == 'admin') {
    $name = 'Administrator';
    $email = 'admin@example.org';
    $admin = TRUE;
} else {
    /* Get the name and email from the database. */
    $query = $db->prepare('SELECT name, email
                           FROM   users
                           WHERE  username = :username'
);
    $query->execute(array('username' => $clean['username']));
    $result = $query->fetch(PDO::FETCH_ASSOC);
    $name = $result['name'];
    $email = $result['email'];
    $admin = FALSE;
}

?>

因为 $admin 还是明确地被设置为TRUE or FALSE,似乎一切都完好。但是,如果另一个开发者后来在代码里加了一个elseif语句,很可能他会忘记这回事:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php

if (auth($username) == 'admin') {
    $name = 'Administrator';
    $email = 'admin@example.org';
    $admin = TRUE;
} elseif (auth($username) == 'mod') {
    $name = 'Moderator';
    $email = 'mod@example.org';
    $moderator = TRUE;
} else {
    /* Get the name and email. */
    $query = $db->prepare('SELECT name, email
                           FROM   users
                           WHERE  username = :username'
);
    $query->execute(array('username' => $clean['username']));
    $result = $query->fetch(PDO::FETCH_ASSOC);
    $name = $result['name'];
    $email = $result['email'];
    $admin = FALSE;
    $moderator = FALSE;
}

?>

如果一个用户提供一个能够触发elseif条件的用户名(username), $admin 没有被初始化,这可能会导致不必要的行为,或者更糟糕的情况,一个安全漏洞。另外,一个类似的情况对于 $moderator 变量来说同样存在,它在第一个条件中没有被初始化。

通过初始化$admin 和 $moderator ,这是完全很容易避免这一情况的发生的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php

$admin = FALSE;
$moderator = FALSE;

if (auth($username) == 'admin') {
    $name = 'Administrator';
    $email = 'admin@example.org';
    $admin = TRUE;
} elseif (auth($username) == 'mod') {
    $name = 'Moderator';
    $email = 'mod@example.org';
    $moderator = TRUE;
} else {
    /* Get the name and email. */
    $query = $db->prepare('SELECT name, email
                           FROM   users
                           WHERE  username = :username'
);
    $query->execute(array('username' => $clean['username']));
    $result = $query->fetch(PDO::FETCH_ASSOC);
    $name = $result['name'];
    $email = $result['email'];
}

?>

不管剩下的代码是什么,现在已经明确了 $admin 值 为FALSE ,除非它被显式地设置为其它值。对于 $moderator 也是一样的。最坏的可能发生的情况就是,在任何条件下都没有修改$admin 或 $moderator ,导致某个是administrator 或moderator的人没有被当作相应的administrator 或moderator 。

如果你想 shortcut something ,并且你看到我们的例子有包含有else觉得有点失望。我们有一个bonus tip 你可能会感兴趣的。我们并不确定它可以被认为是a shortcut,但是我们希望它仍然是有帮助的。

考虑一下一个用于检测一个用户是否被授权查看一个特定页面的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php

function authorized($username, $page) {
    if (!isBlacklisted($username)) {
        if (isAdmin($username)) {
            return TRUE;
        } elseif (isAllowed($username, $page)) {
            return TRUE;
        } else {
            return FALSE;
        }
    } else {
        return FALSE;
    }
}

?>

这个例子是相当的简单,因为只有三条规则需要考虑:
administrators 总是被允许访问的,
处于黑名单的永远是禁止访问的,
isAllowed()决定其它人是否有权访问。
(还有一个特例是:当一个administrator 处于黑名单中,但这似乎是不太可能的事,所以我们这里直接忽视这种情况)。
我们使用函数来做这个判断以保持代码的简洁然后集中注意力到业务逻辑上去。
如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php

function authorized($username, $page) {
    if (!isBlacklisted($username)) {
        if (isAdmin($username) || isAllowed($username, $page)) {
            return TRUE;
        } else {
            return FALSE;
        }
    } else {
        return FALSE;
    }
}

?>

事实上,你可以精减整个函数到一个复合条件:

1
2
3
4
5
6
7
8
9
10
11
<?php

function authorized($username, $page) {
    if (!isBlacklisted($username) &#038;&#038; (isAdmin($username) || isAllowed($username, $page)) {
       return TRUE;
    } else {
        return FALSE;
    }
}

?>

最后,这个可以被减少到只有一个return:

1
2
3
4
5
6
7
<?php

function authorized($username, $page) {
    return (!isBlacklisted($username) &#038;&#038; (isAdmin($username) || isAllowed($username, $page));
}

?>

如果你的目标是誊清代码的行数,那么这样你做到的。但是,你要注意到,我们在用isBlacklisted(), isAdmin() 和 isAllowed() ,这取决于参与这些判断的东西,减少代码到只剩下一个复合条件可能不吸引人。

这下说到我们的小技巧上了,一个“立即返回”函数,所以,如果你尽快返回,你可以很简单地表达这些规则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php

function authorized($username, $page) {

    if (isBlacklisted($username)) {
        return FALSE;
    }

    if (isAdmin($username)) {
        return TRUE;
    }

    return isAllowed($username, $page);
}

?>

这个例子使用了更多行数的代码,但是它是非常简单和不惹人注意的。更重要的是,这个方法减少了你必需考虑的上下文的数量。例如,一旦你决定了用户是否处于黑名单里面,你就可以安全地忘掉这件事了。特别是你的逻辑很复杂的时候,这是相当的有帮助的。

4. 总是使用大括号
PS:原谅是“扔掉那些方括号 Drop Those Brackets”

根据本文的内容, 我们相应作者的意思应该是 "braces," 而不是brackets. "Curly brackets" 可能有大括号的意思, 但是"brackets" 通常表示 “方括号”的意思。这个技巧应该被无条件的忽略,因为,没有大括号,可读性和可维护性被破坏了。

举一个简单的例子:

1
2
3
4
5
6
7
8
9
<?php

if (date('d M') == '21 May')
    $birthdays = array('Al Franken',
                       'Chris Shiflett',
                       'Chris Wallace',
                       'Lawrence Tureaud');

?>

If you’re good enough, smart enough, secure enough, notorious enough, or pitied enough, 你可能会想在5月21号参加社交聚会:

1
2
3
4
5
6
7
8
9
10
<?php

if (date('d M') == '21 May')
    $birthdays = array('Al Franken',
                       'Chris Shiflett',
                       'Chris Wallace',
                       'Lawrence Tureaud');
    party(TRUE);

?>

没有大括号,这个简单的条件导致你每天参加社交聚会 :cry: 。也许你有毅力,因此这个错误是一个受欢迎的。希望那个愚蠢的例子并不分散这一的观点,那就是过度狂欢是一种出人意料的副作用。

为了提倡丢掉大括号,先前的文章使用类似下面的简短的语句作为例子:

1
2
3
4
<?php
if ($gollum == 'halfling') $height --;
else $height ++;
?>

因为每个条件被放在单独的一行, 这种错误似乎会较少发生, 但是这将导致另一个问题:代码的不一致和需要更多的时间来阅读和理解。一致性是这样一个重要的特性,以致开发人员经常遵守一个编码标准,即使他们不喜欢编码标准本身。

我们提倡总是使用大括号:

1
2
3
4
5
6
7
8
9
10
11
<?php

if (date('d M') == '21 May') {
    $birthdays = array('Al Franken',
                       'Chris Shiflett',
                       'Chris Wallace',
                       'Lawrence Tureaud');
    party(TRUE);
}

?>

你天天聚会是没关系的,但要保证这是经过思考的,还有,请一定要邀请我们!

5. 尽量用str_replace() 而不是 ereg_replace() 和 preg_replace()
我们讨厌听到的否认的话,但是(原文)这个用于演示误用的小技巧导致了它试图避免的同样的滥用问题。(
We hate to sound disparaging, but this tip demonstrates the sort of misunderstanding that leads to the same misuse it’s trying to prevent.)
很明显字符串函数比正则表达式函数在字符匹配方面更快速高效,但是作者糟糕地试图从失败中得出一个推论:
(FIX ME: It’s an obvious truth that string functions are faster at string matching than regular expression functions, but the author’s attempt to draw a corollary from this fails miserably:)

If you’re using regular expressions, then ereg_replace() and preg_replace() will be much faster than str_replace().

Because str_replace() does not support pattern matching, this statement makes no sense. The choice between string functions and regular expression functions comes down to which is fit for purpose, not which is faster. If you need to match a pattern, use a regular expression function. If you need to match a string, use a string function.

6. 使用三重运算符
三元运算符的好处是值得讨论的. 下面是一行从最近我们进行的审计的代码中取出的:

1
2
3
4
5
<?php

$host = strlen($host) > 0 ? $host : htmlentities($host);

?>

:roll: 啊,作者的真实意愿是如果字符串的长度大于0 就转义 $host , 但是却意外地做了相反的事情。很容易犯的错误是吧?也许吧。在代码审计过程中很容易错过?当然。简洁并不一定能使代码变得很好。

三重运算符对于单行,原型,和模板也行是适合的,但是我们相信一个普通的条件语句总是更好的。PHP是描述性的和详细的,我们认为代码也应该是。

7. Memcached
磁盘访问是慢速的,网络访问也是慢的,数据库通常使用这二者。

内存是很快的。使用本地缓存可以避免网络和磁盘访问的开销。结合这些道理,然后,你想到了memcached,一个“分布式内存对象缓存系统”,最初为基于Perl的博客平台LiveJournal开发的。

如果你的程序不是分布在多个服务器上,你可能并不需要memcached。单的缓存方法——序列化数据然后将它保存在一个临时文件中。例如 - 对每个请求可以消除很多多余的工作。事实上,这是我们考虑帮助我们的客户优化他们的应用程序时,低挂水果的类型。
什么是low-hanging fruit

A fruit-bearing tree often contains some branches low enough for animals and humans to reach without much effort. The fruit contained on these lower branches may be not be as ripe or attractive as the fruit on higher limbs, but it is usually more abundant and easier to harvest. From this we get the popular expression "low hanging fruit", which generally means selecting the easiest targets with the least amount of effort.

一种最简易且最通用的将数据缓存在内存的方式是使用APC中的共享类型辅助方法,APC是一个最初由我们的同事George Schlossnagle开发的缓存系统,考虑如下例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php

$feed = apc_fetch('news');

if ($feed === FALSE) {
    $feed = file_get_contents('http://example.org/news.xml');
    // Store this data in shared memory for five minutes.
    apc_store('news', $feed, 300);
}


// Do something with $feed.

?>

使用这种类型的缓存,你不必在每一次请求时等待远程服务器发送Feed数据。一些延迟产生了 - 在这个例子中上限是五分钟,但可以根据您的应用程序需要调整到接近实时。

8. 使用框架

所有决定都会有结果的,我们喜欢框架——事实上,CakePHP 和 Solar 的主要开发者和我们一起在 OmniTI 工作—— 但是使用一个框架并不会奇迹般地使你在做的东西变得更好。

在十月份,我们的同事Paul Jones为HP Advent写一了篇文章,叫做The Framework as Franchise ,在文章中他将框架与商业专营权相比较。他引用 Michael Gerber “电子神话再现”("The E-Myth Revisited") 一书中的建议:
  格柏指出,运行一个成功的企业,企业家需要像他将要卖掉他的企业作为一个特许经营权的原型一样行动。这是企业拥有者可以不亲自参与每一项决策使企业运营的唯一方法。
( Gerber notes that to run a successful business, the entrepreneur needs to act as if he is going to sell his business as a franchise prototype. It is the only way the business owner can make the business operate without him being personally involved in every decision.)

这是一个好的建议。无论你是打算使用框架或者定义你自己的标签和惯例,从未来开发者的角度来看价值是很重要的。

虽然我们很乐意给你一个放之四海而皆准的真理,延伸这个想法来表明一个框架总是合适的,并不是我们想做的事情。
如果你问我们是否应该使用一个框架,我们可以给出的最好的答案是,“这要看情况。”
9. 正确的使用错误抑制操作符

总是试着避免使用错误抑制操作符号。在前面的文章,作者表明:
  @ 操作符是相当的慢的并且如果你需要写高性能的代码的话它会使得开销很大。

错误抑制慢是因为在执行抑制语句前,PHP动态的改变error_reporting等级到0 ,然后然后立即将其还原。这是要开销的。
更糟糕的是,使用错误抑制符使追踪问题的根本原因很困难。

先前的文章使用如下例子来支持通过引用来给一个变量赋值的做法。。。(这句怎么翻译?我晕~~~ :eek:
The previous article uses the following example to support the practice of assigning a variable by reference when it is unknown if $albus is set:

1
2
3
4
5
<?php

$albert =&#038; $albus;

?>

尽管这样是工作的——对于现在——依靠奇怪的,未定义的行为,而对于为什么这样会工作有一个很好的理解是一个产生BUG的好方法。
因为 $albert 是引用了$albus的,后期对于$albus的修改将会同样影响到$albert .

一个更好的解决方案是使用isset(),加上大括号:

1
2
3
4
5
6
7
<?php

if (!isset($albus)) {
    $albert = NULL;
}

?>

给$albert 赋值NULL和给它赋一个不存在的引用的效果是相同的,但是更加明确了,大大提高了代码的清晰度和避免的两个变量之间的引用关系。

If you inherit code that uses the error suppression operator excessively, we’ve got a bonus tip for you. There is a new PECL extension called Scream that disables error suppression.

10. 使用 isset() 而不是 strlen()

这实际上是一个巧妙的方法,虽然前面的文章完全没有解释这个。下面是补充的例子:

1
2
3
4
5
6
7
<?php

if (isset($username[5])) {
    // The username is at least six characters long.
}

?>

当你把字符串当作一个数组时(荒野无灯:事实上,在C语言里面,字符中通常以数组形式存在),字符串里的每一个字符都是数组的一个元素。通过检测一个特定元素的存在与否,你可以检测这个字符串是否至少有那么多的字符存在。(注意第一个字符是元素0,因此 $username[5] 是 $username中的第6个字符。)

这样使用isset 比strlen稍快的原因是复杂的。简单的解释是,strlen() 是一个函数,而 isset() 是一个语法结构。通常来说,
调用一个函数是比使用语言结构的代价更为昂贵的。

关于作者:
Hi,我们是 Chris Shiflett和 Sean Coates. 我们都在 OmniTI (“the most important web company you’ve never heard of”)工作, blog about PHP and other stuff at shiflett.org and seancoates.com, curate PHP Advent, and do the Twitter thing as @shiflett and @coates.

译 FROM : http://coding.smashingmagazine.com/2009/03/24/10-useful-php-tips-revisited/

更多
5 Responses Post a comment
  1. phoetry

    原来是要写成这样...真不习惯

  2. 荒野无灯

    要加上括号是可以的:

    1
    $s = $a?$x=0:($b?$x=1:($c?$x=2:$x=3) );

    可用以下代码测试:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <?php
    $a=isset($_GET['a'])? $_GET['a'] : 1;

    $b=isset($_GET['b'])? $_GET['b'] : 1;

    $c=isset($_GET['c'])? $_GET['c'] : 1;

    echo '$s = $a?$x=0:($b?$x=1:($c?$x=2:$x=3) ); <br />';
    var_dump($_GET);

    $s = $a?$x=0:($b?$x=1:($c?$x=2:$x=3) );
     
    exit($s);
  3. phoetry

    我的意思是能否把

    1
    <?php if(a)x=0;elseif(b)x=1;elseif(c)x=2;else x=3; ?>

    改写成

    1
    a?x=0:b?x=1:c?x=2:x=3
  4. 荒野无灯

    不知道你的“三元-AND-OR”要表达什么意思,不过,PHP中是可以用三元操作符的,如:

    1
    $words = ( 1 == $passed  ) ? 'congratulations to you' : 'work harder ,son';
  5. phoetry

    问一个, php可以像js那样完全用三元-AND-OR, 这三个去处符来替代条件语句么

Leave a Reply

Note: You may use basic HTML in your comments. Your email address will not be published.

Subscribe to this comment feed via RSS