PHP最佳实践

前言#

phpbestpractices.org

密码存储#

使用内置的哈希函数进行加密和比较
随着计算机算力的增加,md5 甚至是 sha1 已经不再安全,黑客可以轻松破解大部分 md5/sha1 生成的密码

<?php
# 使用 bcrypt 算法,返回60个字符的哈希值:$2y$10$tiHudceUYpxWK56MqVGQuOXTZ.fCmkLYcX3dAHg/KXjXjg2tUzzci
$hashedPassword = password_hash('123456', PASSWORD_DEFAULT);
# 验证错误返回false
password_verify('1234567', $hashedPassword);
# 验证正确返回true
password_verify('123456', $hashedPassword); 
?>

password_hash 会自动为密码加 salt, 官方也不建议使用自定义的 salt

连接和查询 MySQL 数据库#

PDO (PHP Data Ojects),PDO 在许多不同类型的数据库中具有一致的接口,面向对象,并支持新数据库提供的更多功能,同时也可以防止 SQL 注入攻击

$link = new PDO('mysql:host=your-hostname;dbname=your-db;charset=utf8mb4',
    'your-username',
    'your-password',
    [
        PDO::ATTR_ERRMODE    => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_PERSISTENT => false
    ]
);
$handle = $link->prepare('select Username from Users where UserId = ? or Username = ? limit ?');
$handle->bindValue(1, 100);
$handle->bindValue(2, 'Bilbo Baggins');
$handle->bindValue(3, 5);
$handle->execute();

标签#

PHP 支持标签:

<?php ?>, <?= ?>, <? ?>,<% %>

使用短标签需要修改 php.ini 配置 short_open_tag=on, 默认是 off, 如果你无办法改变全部环境的配置档,建议还是使用 <?php ?>, 另外 <?= ?> 不受 short_open_tag 影响

自动加载#

__autoload() 是 PHP 5 引入的,spl_autoload_register() 是 PHP 5.1.2 引入的。spl_autoload_register()__autoload() 的替代和改进。您一次只能定义一个 __autoload() 函数,因此如果您包含一个也使用 __autoload() 函数的库,那么就会发生冲突。正确方法是给你的自动加载函数起一个独特的名字,然后用 spl_autoload_register () 函数注册它。

function myLoader($class_name) 
{
    require $class_name . '.php';
}
spl_autoload_register('myLoader');

单引号 vs 双引号#

对于一个普通的应用程序,你选择哪个并不重要。对于负载极高的应用程序,也只是有一点影响。

常量 define () vs const#

除非考虑可读性、类常量或微优化,否则使用 define (), 因为 define () 最终更灵活,除非你特别需要类常量。

  1. define () 在运行时定义常量,而 const 在编译时定义常量。
  2. define () 允许您在常量名称和常量值中都使用表达式,const 不允许
  3. define () 可以在 if () 块中调用,而 const 不能
  4. define () 不能定义类常量。
    const GIMLI_ID = 1;     // 成功
    define('TRANSPORT_METHOD_SNEAKING', 1 << 0); // 成功
    const TRANSPORT_METHOD_WALKING = 1 << 1; // 编译失败,const不能使用表达式
    if (1) {
     define('TRANSPORT_METHOD', TRANSPORT_METHOD_SNEAKING); // 成功
     const PARTY_LEADER_ID = 23; // 编译失败,const不能写在if块里
    }
    class OneRing
    {
     const MELTING_POINT_CELSIUS = 1000000;  // 成功
     define('MELTING_POINT_ELVISH_DEGREES', 200); // define() 不能定义类常量
    }

缓存 PHP opcode#

在旧版本的 PHP 中,每次执行脚本时都必须从头开始编译。 Opcode 可以保存以前编译的 PHP 版本,从而加快速度。您可以选择各种风格的缓存。

PHP 和 Memcached#

如果你需要一个分布式的缓存,使用 Memcached (现在一般用 redis), 单机且内存足够的情况下使用 APCu,APCu 原理是操作系统的进程间共享内存,只适合 php-fpm 模式的进程组,因为 php-fpm 模式都有一个共同的父进程,cli 模式每次都是单独启动一个进程,所以不适合

  • Memcache
    您有两种不同且命名非常愚蠢的客户端库选择:Memcache 和 Memcached, 事实证明 Memcached 更好用
    sudo apt-get install php-memcached
  • APCu
    在 Ubuntu 14.04 之前,APC 项目既是一个 opcache,也是一个类似 Memcached 的键值存储。 Ubuntu 14.04 之后 opcache 独立出来,现在只剩下键值存储功能,需要安装 apcu 扩展
    sudo apt-get install php-apcu
    apcu_store('username-6389', 'Gandalf'); //string
    apcu_store('creatures', array('ent', 'dwarf', 'elf')); //数组
    apcu_store('saruman', new Wizard()); //对象
    apcu_fetch('username-958', $success); //查询
    apcu_delete('username-958'); //删除
    其实想想,那么多年都没用到过这个东西是有道理的,生产环境不建议使用这个 APCu

正则表达式#

在 PHP 7 出现之前,PHP 有两种使用正则表达式的不同方式:preg_* 函数和 ereg_* 函数。 ereg_* 函数已在 PHP 7 中被删除,无论什么版本首选 preg_*

$string = 'April 15, 2003';
$pattern = '/(\w+) (\d+), (\d+)/i';
$replacement = '${1}1,$3';
echo preg_replace($pattern, $replacement, $string);

为 Web 服务器提供 PHP#

apache 的 mod_php 早已过时,首选 nginx php-fpm

发送邮件#

PHP 内置 mail 函数看起来挺好用,实际非常不好用,无法设置合适的 header 被认为是 spam 导致投递失败,无法添加附件,无法获得发送结果等,所以不推荐使用
PHPMailer 是一个流行且成熟的开源库

# 安装
composer require phpmailer/phpmailer

以下代码亲测可用

<?php
use PHPMailer\PHPMailer\PHPMailer;
require "../vendor/autoload.php";
$mailer = new PHPMailer(true);
# 发件人
$mailer->Sender = 'no-reply@metaprisebanking.com';
# 抄送?
//$mailer->AddReplyTo('bbaggins@example.com', 'Bilbo Baggins');
# 发件人
$mailer->SetFrom('no-reply@metaprisebanking.com', 'Bilbo Baggins');
# 收件人
$mailer->AddAddress('kun@qq.com');
# 标题
$mailer->Subject = "Let's dance";
# 内容
$mailer->MsgHTML('<p>You are really nice</p>');

$mailer->IsSMTP();
$mailer->SMTPAuth = true;
# @todo 抽空了解一下email协议
//$mailer->SMTPSecure = 'ssl'; 
$mailer->Port = 587;
$mailer->Host = 'email-smtp.us-west-2.amazonaws.com';
$mailer->Username = 'username';
$mailer->Password = 'password';
$mailer->Send();

验证邮箱地址#

使用内置函数 filter_var ()

filter_var('test@example.com', FILTER_VALIDATE_EMAIL); // 返回 test@example.com
filter_var('sauron@mordor', FILTER_VALIDATE_EMAIL); // 返回 false

还可以验证 IP , URL , 整形,使用回调函数验证等

清理 HTML 输入与输出#

不要信任任何来自不受自己控制的数据源中的数据,例如以下这些

$_GET
$_POST
$_REQUEST
$_COOKIE
$argv
php://stdin
php://input
file_get_contents()
远程数据库
远程API
来自客户端的数据

如果输入以下内容,渲染页面的时候不过滤,直接跳转第三方网站

<p>可以可以,一键三连了![狗头]</p>
<script>windows.location.href="https://www.threebody.com";</script>

一般渲染的时候,使用 htmlentities 函数过滤以下就可以了
当用户输入富文本的时候,比如图片、链接这些的时候,因为 htmlentities 没有验证 html 的功能,使用 HTML Purifier 组件实现更专业地,更定制化过滤

在过滤 html 这方面,用上面两个就可以了,不考虑使用 htmlspecialchars,strip_tags,filter_var 等函数

PHP 和 UTF-8#

UTF-8 in PHP sucks.
处理多字节字符串时需要用到 mb_* 函数,需要安装扩展

sudo apt install php-mbstring

处理 utf8 字符串时必须用 mb_* 函数

# 告诉 PHP 我们使用 UTF-8 字符串直到脚本结束
mb_internal_encoding('UTF-8');
# 告诉 PHP 我们将向浏览器输出 UTF-8
mb_http_output('UTF-8');
# 使用mb_substr截取utf8字符串
$string = 'Êl síla erin lû e-govaned vîn.';
$string = mb_substr($string, 0, 15);

时间管理大师#

以前处理时间只能使用下面这些函数

date()
gmdate()
date_timezone_set()
strtotime()

现在我们用 DateTime 类
在 32 位系统 DateTime::getTimestamp () 将不会表示超过 2038 年的日期。64 位系统可以。

# 构造一个新的 UTC 日期。除非您真的知道自己在做什么,否则请始终指定 UTC!
$date = new DateTime('2011-05-04 05:00:00', new DateTimeZone('UTC'));
# 将我们的初始日期增加十天
$date->add(new DateInterval('P10D'));
# 格式化输出日期
print($date->format('Y-m-d h:i:s')); // 2011-05-14 05:00:00
# 设置时区为美国西部时间
$date->setTimezone(new DateTimeZone('America/Los_Angeles'));
# 打印时间戳
$date->getTimestamp();
# 构造一个新的 UTC 日期
$later = new DateTime('2012-05-20', new DateTimeZone('UTC'));
# 直接比较
if($date < $later){
    print('嗯,确实能比较!');
}
# 比较日期差异
$difference = $date->diff($later);
# 打印差异多少天
print('两个时间相隔' . $difference->days . '天!'); // 两个时间相隔371天!

$datetime = new DateTime();
$interval = new DateInterval('P2D');
# 返回一个迭代器 参数分别是:开始事件,间隔,迭代次数,排除开始的时间,从下一个周期开始
$period = new DatePeriod($datetime, $interval, 3, DatePeriod::EXCLUDE_START_DATE);
foreach ($period as $date) {
    echo $date->format('Y-m-d H:i:s'), PHP_EOL;
}

如果你觉得内置类还是不好用,建议使用这个组件,更多处理时间的函数,更全面

composer require nesbot/carbon

检查一个变量是否为 null 或 false#

使用 === , 它也比 is_null () 和 is_bool () 稍微快一点,而且看起来比使用函数进行比较要好,特别是使用 strpos 函数时一定要使用 ===

  • ==
    如果值是空字符串或 0,则使用 == 检查值是否为 null 或 false 可能会返回误报。
  • isset()
    检查变量是否具有不为 null 的值,但不检查布尔值 false
  • is_null()
    只能准确地检查一个值是否为 null
  • is_bool()
    只能准确地检查一个值是否为布尔值

移除重音符号#

这个国外用得比较多,使用 php-intl 扩展替换 iconv 函数

$transliterator = Transliterator::createFromRules(':: Any-Latin; :: Latin-ASCII; :: NFD; :: [:Nonspacing Mark:] Remove; :: NFC;', Transliterator::FORWARD);
print($transliterator->transliterate('Êl síla erin lû e-govaned vîn.'));
?>

代码风格指南#

PHP-FIG 已经提出并批准了一系列风格建议。并不是所有的都与代码风格有关,但是确实与代码风格有关的是 PSR-1, PSR-12 and PSR-4。
你可以使用以下工具之一自动修复代码布局:

  • PHP Coding Standards Fixer
  • PHP Code Beautifier and Fixer 可
phpcs -sw --standard=PSR1 file.php
phpcbf -w --standard=PSR4 file.php
php-cs-fixer fix -v --rules=@PSR1 file.php

代码可读性#

github.com/php-cpm/clean-code-php

错误与异常#

线程的框架 CURD 的时候一般只需要捕获异常即可,特别是那些需要建立连接的代码需要捕获异常,包括不限于 (curl, 第三方软件 sdk, 数据库,composer 组件), 只有在自己写框架,composer 组件,一些公用类,公用方法的时候需要定义异常,抛出异常

日志级别#

monolog

时间管理#

nesbot/carbon

单元测试#

Opcache & JIT#

点击跳转

持续更新 不好意思没写完 一直都在草稿不小心点了发布

本作品采用《CC 协议》,转载必须注明作者和本文链接
遇强则强,太强另说