25条PHP开发技巧

1. 不要使用相对路径,定义一个ROOT路径

下面的代码很常见:

require_once('../../lib/some_class.php');

这个方法有诸多缺陷:

  • 它会先在PHP的include路径中查找,接着在当前目录中查找,因此会检查许多目录。
  • 如果脚本由其他目录中的脚本所引用,目录的调整会引发问题。
  • 当以计划任务运行脚本时,以相对路径形式可能无法找到父目录

比较好的办法是采用绝对路径:

define('ROOT' , '/var/www/project/');
require_once(ROOT . '../../lib/some_class.php');

当然, 这里有绝对路径和常量。可以再来改进一下。考虑使用魔术常量,比如__FILE__,看看这样如何:

define('ROOT' , pathinfo(__FILE__, PATHINFO_DIRNAME));
require_once(ROOT . '../../lib/some_class.php');

好了,现在可以迁移你的项目到不同的目录了,比如迁移到在线服务器,无需做任何改动。

2.不再使用require, include, require_once, include_once

你的代码顶部会引用很多,比如类库、文件、小工具以及其他helper函数, 比如这样:

require_once('lib/Database.php');
require_once('lib/Mail.php');
require_once('helpers/utitlity_functions.php');

这样有些原始了。代码需要有弹性。动手写个能更容易引用的helper函数吧。看这个例子:

function load_class($class_name)
{
    //path to the class file
    $path = ROOT . '/lib/' . $class_name . '.php');

    if(file_exists($path))
    {
        require_once( $path );
    }
}

它可以完成以下工作:

  • 在多目录中搜索相同的类文件
  • 当改变引用库目录时会非常容易,而不用到处去修改代码
  • 如果需要引用html内容,稍加修改就成了load_htm

3.建立应用程序中的调试环境

开发过程中遇到问题时,我们可能会输出DB查询、dump变量……,问题解决后我们会注释掉或者删除。其实应该留着它们。

define('ENVIRONMENT' , 'development');

if(! $db->query( $query )
{
    if(ENVIRONMENT == 'development')
    {
        echo "$query failed";
    }
    else
    {
        echo "Database error. Please contact administrator";
    }
}

4. 用Session传送状态消息

在完成一些任务后,系统/应用程序会进行一些消息提示

<?php
if($wrong_username || $wrong_password)
{
    $msg = 'Invalid username or password';
}
?>
<html>
<body>
<?php echo $msg; ?>
<form>
...
</form>

这些代码很常见。但这种方法存在局限性:

  • 不能传递跳转地址(打算用GET参数传递?亲,该吃药……)
  • 消息过多时管理困难

最好的办法是用Session传递,当然,请记得session_start。

function set_flash($msg)
{
    $_SESSION['message'] = $msg;
}

function get_flash()
{
    $msg = $_SESSION['message'];
    unset($_SESSION['message']);
    return $msg;
}
<?php
if($wrong_username || $wrong_password)
{
    set_flash('Invalid username or password');
}
?>
<html>
<body>
Status is : <?php echo get_flash(); ?>
<form>
...
</form>
</body>
</html>

5. 弹性化你的函数

function add_to_cart($item_id , $qty)
{
    $_SESSION['cart'][$item_id] = $qty;
}
add_to_cart( 'IPHONE3' , 2 );

用上面的函数可以添加一个商品。如果需要添加多件,我们又要新建一个函数么?NO。 只需“弹性化”即可,看这个:

function add_to_cart($item_id , $qty)
{
    if(!is_array($item_id))
    {
        $_SESSION['cart'][$item_id] = $qty;
    }
    else
    {
        foreach($item_id as $i_id => $qty)
        {
            $_SESSION['cart'][$i_id] = $qty;
        }
    }
}
add_to_cart( 'IPHONE3' , 2 );
add_to_cart( array('IPHONE3' => 2 , 'IPAD' => 5) );

现在,一个函数可以接受多种类型,此方法在很多地方都可应用。

6. 忽略php的收尾标记

当收尾标记?>之后有额外的字符(比如空格),你此刻需要echo 一个image或pdf,或者玩cookies/sessions, 你会看到”headers already send” error。原因在于额外的字符被显示出来了,你可能需要浪费数小时去寻找这些“额外字符”。

避免此问题的方法就是,请忽略收尾标记?>,好多了吧?

7. 收集所有输出, 再一次输出给浏览器

这玩意是输出缓冲。比如你需要用多个函数输出内容:

function print_header()
{
    echo "<div id='header'>Site Log and Login links</div>";
}
function print_footer()
{
    echo "<div id='footer'>Site was made by me</div>";
}
print_header();
for($i = 0 ; $i < 100; $i++)
{
    echo "I is : $i <br />';
}
print_footer();

考虑这么做:首先收集所有输出到一个地方。可以存在变量里,也可以用ob_start/ob_end_clean. 改改看:

function print_header()
{
    $o = "<div id='header'>Site Log and Login links</div>";
    return $o;
}
function print_footer()
{
    $o = "<div id='footer'>Site was made by me</div>";
    return $o;
}
echo print_header();
for($i = 0 ; $i < 100; $i++)
{
    echo "I is : $i <br />';
}
echo print_footer();

为何需要输出缓冲?

发送给浏览器之前可以改动输出。 比如文本/正则替换,或者加一些额外的html代码,比如 profiler/debugger
同时进行php处理与输出是个坏习惯。

8. 输出非html内容时,通过header发送正确的mime类型

xml:

header("content-type: text/xml");
echo $xml;

Javascript

header("content-type: application/x-javascript");
echo "var a = 10";

CSS

header("content-type: text/css");
echo "#div id { background:#000; }";

9. mysql连接时设置正确的字符编码

如果mysql表以unicode/utf-8正确存储,phpmyadmin中也可正确显示,但读取数据显示在页面时乱码出现,问题则出在mysql连接整理上:

$c = mysqli_connect($host , $username, $password);
mysqli_set_charset ( $c , 'UTF8' );

当连接到数据库时,设置整理字符集是一个好习惯,在开发多语言的项目中尤为重要。

10. 使用htmlentitis设置正确的字符集选项

PHP 5.4之前的默认字符编码是ISO-8859-1,无法显示诸如À â等字符。

$value = htmlentities($this->value , ENT_QUOTES , 'UTF-8');

PHP 5.4起,默认编码是UTF-8,这将解决大部分问题。如果你的应用程序为多语种,请注意这里

11. 不要使用gzip输出,让apache去做这个

考虑用ob_gzhandler? 别这么做,这样没什么意义。不要担心在php上如何优化服务器和浏览器之间的数据传输。在Apache中启用mod_gzip或者mod_deflate来压缩吧。

12. 使用json_encode 在PHP中打印javascript代码

有时需要在PHP中动态生成一些javascript代码:

foreach($images as $image)
{
    $js_code .= "'$image' ,";
}
$js_code = 'var images = [' . $js_code . ']; ';
echo $js_code;
//Output is var images = ['myself.png' ,'friends.png' ,'colleagues.png' ,];

试试json_encode吧:

$images = array(
'myself.png' , 'friends.png' , 'colleagues.png'
);
$js_code = 'var images = ' . json_encode($images);
echo $js_code;
//Output is : var images = ["myself.png","friends.png","colleagues.png"]

13. 在写文件之前请先检查目录是否可写入

写入任何文件之前,请确认该文件所在目录是否可写,如不可写,提示错误信息。这会帮你节省无数“调试”时间。当你在linux下干活时,目录不能被写入、不能读取文件时要首先考虑目录权限问题。

确保你的程序在最短时间内,可以尽量智能化地报告出最重要的错误信息。

$contents = "All the content";
$file_path = "/var/www/project/content.txt";
file_put_contents($file_path , $contents);

代码没问题, 但可能会有些间接问题产生。File_put_contents失败的可能原因如下:

  • 父目录不存在
  • 目录存在,但不可写
  • 文件被锁定

因此,最好在写入文件之前先进行检测。

$contents = "All the content";
$dir = '/var/www/project';
$file_path = $dir . "/content.txt";
if(is_writable($dir))
{
    file_put_contents($file_path , $contents);
}
else
{
    die("Directory $dir is not writable, or does not exist. Please check");
}

这样做的话,当文件写入失败时你会知道准确的信息。

14. 更改您的应用程序创建的文件权限

当你在linux环境下工作时,权限处理会浪费很多时间。因此,当你的应用程序创建文件后,进行chmod以确保外部可以访问。否则会带来很多麻烦。例如,生成的文件由“PHP”用户所创建,而您开发时是另一个用户,系统会禁止您访问或打开文件,之后你可能需要取得root权限再变更文件权限……

// Read and write for owner, read for everybody else
chmod("/somedir/somefile", 0644);
// Everything for owner, read and execute for others
chmod("/somedir/somefile", 0755);

15. 不要通过检查提交按钮的值来判断表单提交

if($_POST['submit'] == 'Save')
{
    //Save the things
}

上面的代码看起来的确没什么错。但,当你的程序是多语言时,就不一定叫Save了,这时怎么判断?所以,不要依赖提交按钮的值了,这么做吧:

if( $_SERVER['REQUEST_METHOD'] == 'POST' and isset($_POST['submit']) )
{
    //Save the things
}

16. 考虑在函数中使用静态变量

//Delay for some time
function delay()
{
    $sync_delay = get_option('sync_delay');
    echo "<br />Delaying for $sync_delay seconds...";
    sleep($sync_delay);
    echo "Done <br />";
}

使用静态变量之后:

//Delay for some time
function delay()
{
    static $sync_delay = null;
    if($sync_delay == null)
    {
        $sync_delay = get_option('sync_delay');
    }
    echo "<br />Delaying for $sync_delay seconds...";
    sleep($sync_delay);
    echo "Done <br />";
}

17. 不要直接使用$_SESSION变量

$_SESSION['username'] = $username;
$username = $_SESSION['username'];

熟悉吧?但这么做有问题。

如果在相同域下运行多个程序,session变量可能会冲突, 2个不同的应用程序可能设置了相同key的session变量。

因此,用wrapper函数指定一下key吧:

define('APP_ID' , 'cichui.com');
//Function to get a session variable
function session_get($key)
{
    $k = APP_ID . '.' . $key;
    if(isset($_SESSION[$k]))
    {
        return $_SESSION[$k];
    }
    return false;
}
//Function set the session variable
function session_set($key , $value)
{
    $k = APP_ID . '.' . $key;
    $_SESSION[$k] = $value;
    return true;
}

18. 将辅助函数(utility helper functions)封装成一个类

你可能有很多像这样的辅助函数:

function utility_a()
{
//This function does a utility thing like string processing
}
function utility_b()
{
//This function does nother utility thing like database processing
}
function utility_c()
{
//This function is ...
}

你可以考虑把他们封装成类的静态方法:

class Utility
{
    public static function utility_a()
    {
    }
    public static function utility_b()
    {
    }
    public static function utility_c()
    {
    }
}
//and call them as
$a = Utility::utility_a();
$b = Utility::utility_b();

这有一个明显的好处是,不会和PHP自带函数命名冲突。另一个角度看,你可以在同一个应用程序内建立多个版本,不会有任何冲突。只是最基本的封装, 没别的。

19. 一些愚蠢的小技巧

  • 用echo代替print
  • 除非绝对必要,请用str_replace代替preg_replace
  • 不要使用短标记( 简单字符串使用单引号
  • 永远记得在header跳转后exit
  • 永远不要在for循环控制行里调用函数
  • isset比strlen快
  • 在循环或if-else代码块中请坚持使用大括号{} (即使一行)。不要尝试通过“吃掉语法”而让你的代码变短,请让你的逻辑更短一些。
  • 使用语法高亮的编辑器,代码高亮有助于帮你减少错误

20. 使用array_map快速处理数组

想清理(trim)一个数组中的所有元素?新手一般会这样:

foreach($arr as $c => $v)
{
    $arr[$c] = trim($v);
}

更清爽的做法是:

$arr = array_map('trim' , $arr);

此函数会将trim应用于所有$arr数组中的元素。另一个类似的函数是array_walk,具体请参见PHP帮助文档。

21. 使用PHP filters扩展验证数据

你用正则做过数据校验吧?比如email, ip地址等等……是得,每个人都做过这些。 现在试试这个——PHP的filters扩展。

if (filter_var($email_a, FILTER_VALIDATE_EMAIL)) {
//…
}
if (filter_var($ip_a, FILTER_VALIDATE_IP)) {
//…
}

除此以外,还有:FILTER_VALIDATE_URL,FILTER_VALIDATE_REGEXP………

22. 强制类型转换

$amount = intval( $_GET['amount'] );
$rate = (int) $_GET['rate'];

强类型转换是个好习惯。

23. 使用set_error_handler() 将PHP错误日志写入文件

set_error_handler()可以用来设置自定义错误。用它把错误日志写入日志文件也是个不错的主意。

24. 小心处理大数组

如果一个变量存有大型数组或者字符串,请小心处理。通常的错误是创建副本然后内存耗尽,得到一个内存超出的致命错误。

$db_records_in_array_format; //1000行*20列,每行至少100 字节 , so total 1000 * 20 * 100 = 2MB
$cc = $db_records_in_array_format; //用掉2MB
some_function($cc); //擦,还要再用2MB ?

上面的代码是普通的CSV文件导入(或导出)。这么干脚本可能会超出内存限值。小规模的当然没有问题,大数组时还是要提防的。

考虑引用传参(by reference)吧, 或者存储到类变量里。

$a = get_large_array();
pass_to_function(&$a);
class A
{
    function first()
    {
        $this->a = get_large_array();
        $this->pass_to_function();
    }
    function pass_to_function()
    {
    //process $this->a
    }
}

大数组变量用毕记得尽快注销掉(unset)。

25. 整个脚本中使用一个数据库连接

连接数据库时,请确保您使用一个连接。开始打开连接并开始使用,直到结束,并在结束时关闭连接。

请不要这么做:

function add_to_cart()
{
    $db = new Database();
    $db->query("INSERT INTO cart .....");
}
function empty_cart()
{
    $db = new Database();
    $db->query("DELETE FROM cart .....");
}

多次数据库连接很糟糕,由于每次连接都需要消耗时间和更多内存,它们会让执行时间变得更慢。

可以考虑使用单件模式(Singleton pattern)进行数据库连接。