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 协议》,转载必须注明作者和本文链接
遇强则强,太强另说
讨论数量: 2

推荐看 PHP: The Right Way

1年前 评论
竖横山 (楼主) 1年前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!