PHP Language Guide

PHP 语言指引

语法特性

命名空间

无命名空间的代码都属于全局命名空间。

<?php
# 声明
namespace online\moha; # 声明命名空间需要在 <?php 后第一行开始。

# 导入命名空间下的类,避免重复输入全名
use Symfony\Component\HttpFoundation\Response;

# 导入并创建别名
use Symfony\Component\HttpFoundation\Response as Res;

# 多重导入
use Symfony\Component\HttpFoundation\Request,
    Symfony\Component\HttpFoundation\Cookie;

# 导入函数 PHP 5.6+
use func online\moha\common\moha_start_with;

# 导入常量 PHP 5.6+
use constant online\moha\common\SECONDS_IN_YEAR;
<?php
# 一个 PHP 文件,多个命名空间
namespace online\moha\wx {

}

namespace online\moha\qr {

}
<?php
namespace online\moha;

# 没有指明命名空间的代码在全局命名空间中。
# 在命名空间下引用全局命名空间下的代码,需要用 \ 限定,否则非限定类 Exception 将被解释为 online\moha\Exception。
throw new \Exception();

接口

使用接口抽象处理流程,使代码依赖抽象定义,而非具体对象,方便扩展。

性状

PHP 5.4 开始支持,用于将共通代码注入多个无关的类以实现代码复用。

<?php

# 定义性状
trait TRAIT_NAME {
  public function FUNCTION_NAME() {

  }
}

# 使用性状,PHP 运行时会复制性状代码到类中。
class CLASS_NAME {
  use TRAIT_NAME;
}

$obj = new CLASS_NAME();
$obj->FUNCTION_NAME();

生成器

PHP 5.5 开始支持。返回一系列值/内容,并配合循环使用的函数。返回内容依次通过关键字 yield 向外传递给调用的循环,省去中间变量的内存开销。

<?php
function getRows($file) {
  $handle = fopen($file, 'rb');
  if ($handle === false) { throw new Exception(); }
  
  while (feof($handle) === false) {
    // 文件内容不会全部加载到内存再返回。
    // yield 会暂停后续处理,直至外部循环执行 1 次。
    yield fgetcsv($handle);
  }

  fclose($handle);
}

foreach (getRows('data.csv') as $row) {
  print_r($row);
}

闭包(匿名函数)

PHP 5.3 开始支持。用关键字 use 传入调用环境的变量,以对象对待,常做为回调函数使用。

array_map(function($value) use ($env){
  return $value + $env;
}, [2,3,4]);

PSR

规范 简介
PSR-1 规范基本代码风格类名遵循,如:CamelCase,方法名遵循 camelCase。
PSR-2 规范严格代码风格,如:缩进用 4 个空格;关键字坚持小写(true, false, null)。
PSR-3 规范日志记录器接口,如:最常见的日志框架 monolog
PSR-4 使用文件系统目录结构和 PHP 命名空间自动加载相应的 PHP 类、接口、性状。常用 Composer 生成的自动加载器。

日期

PHP 5.2 后可使用 DateTimeZone、DateTime 和 DateInternal 类处理日期。

时区

PHP 可以设置默认时区,也可以生成 DateTimeZone 对象,在日期时间相关操作中被引用:

; php.ini 设置默认时区
date.timezone = 'Asia/Shanghai';
# 运行时改变默认时区
date_default_timezone_set('UTC');
# 生成时区对象
new DateTimeZone('Asia/Tokyo');

读取

读取日期字符串,使用不同方法可生成 DateTime 对象:

示例 意义
new DateTime() 当前日期时间
new DateTime('2019-12-31 09:00:00') 可直接识别的日期时间
new DateTime('2019-12-31 09:00:00', $timezone) 参考时区的日期时间
DateTime::createFromFormat('Y-m-d H:i:s', '2019-12-31 09:00:00'); 指定格式的日期时间

计算

DateTime 对象的 add、sub 方法接收 DateInterval 对象进行日期时间计算:

示例 意义
DateInterval::createFromDateString('-2 day') 两天前
DateInterval::createFromDateString('P1WT5H') 一周后(以 P 开始,以 T 分割日期与时间)

DateTime 对象的 setTimezone 方法接收 DateTimeZone 对象调整时区供后续处理或显示。

DatePeriod 类可以根据 DateTime 实例及 DateInterval 实例返回指定个数 DateTime 实例。

$datetime = new DateTime();
$interval = new DateInterval('P1W');  # 间隔 1 周
$period = new DatePeriod($datetime, $interval, 5);
foreach ($period as $next) {
  echo $next->format('Y-m-d H:i:s'), PHP_EOL;
}

数据库

PHP 常用统一接口 PDO(PHP Data Object)和不同数据库交互。PDO 需用数据库对应的连接字符串(Data Source Name)初始化。

用户输入需通过 PDO 的 prepare 方法返回的预处理对象和数据库交互。输入值可通过预处理对象的 bindValue 绑定到 :占位符。

MySQL

# 根据最佳实践,这些配置需从外部文件导入
$dsn = 'mysql:host=localhost;dbname=testdb';
$username = 'username';
$password = 'password';
$options = array(
    PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8',
); 

try {
  $pdo = new PDO($dsn, $username, $password, $options);

  $sql = 'SELECT id FROM users WHERE email = :email';
  $statement = $pdo->prepare($sql);

  $email = filter_value(INPUT_GET, 'email');
  $statement->bindValue(':email', $email, PDO::PARAM_STR);
  $statement->execute();

  while (($result = $statement->fetch(PDO::FETCH_ASSOC)) !== false) {
    // $result['id']
  }
}
catch (PDOException $e) {

}

多字节

PHP 处理多字节字符串需使用 mbstring 扩展提供的函数:

函数 作用
mb_strlen 同 strlen,用于多字节字符串
mb_detect_encoding
mb_convert_encoding

配置 

; php.ini 设置默认字符编码
default_charset = "UTF-8";

流被用来读写不同的资源或内容。流标识符 <schema>://<target> 标识流的协议和目标。流协议通过函数 stream_wrapper_register() 可自定义。

流标识符 作用
file://<PATH> 默认流协议,作文件系统读写函数参数使用
php://stdin

流过滤器

流在读写过程中可以借助流过滤器对内容进行改变。过滤器可以通过函数 stream_filter_append() 添加到某个打开流;也可以在流标识符里直接添加。通过继承 php_user_filter 类,调用函数 stream_filter_register() 可添加自定义流过滤器。

错误

种类 含义
Fatal Error 致命错误
运行时错误
编译错误
启动错误
触发错误;trigger_error() 函数可按需触发错误。

调用会触发错误的函数前加 @ 符号可忽略错误。

相关配置

开发/测试环境

; 错误显示
display_startup_errors = On
display_errors = On

; 报告错误
error_reporting = E_ALL

; 记录错误
log_errors = On

生产环境

; 错误显示
display_startup_errors = Off
display_errors = Off

; 报告错误
error_reporting = E_ALL & ~E_NOTICE

; 记录错误
log_errors = On

自定义错误处理

PHP 可如下自定义错误处理,自己是现实注意 die() 或 exit() 函数的调用,否则 PHP 继续执行后续处理:

// 原生函数实现
set_error_handler(function ($errno, $errstr, $errfile, $errline, $errcontext){

}, E_ALL | E_STRICT);

restore_error_handler();
# 第三方库 filp/whoops
$whoops = new \Whoops\Run;
$whoops->pushHandler(new \Whoops\Handler\PrettyPageHandler);
$whoops->register();

异常

try {
  
}
catch (Exception $e) {
  
}
finally {
  // 始终执行
}

PHP 允许自定义全局异常处理函数,处理未捕捉的异常:

set_exception_handler(function (Exception $e) {

});

// 还原之前异常处理
restore_exception_handler();

安全

转义输出

数据在展示前需要过滤攻击代码,可以用原生代码或 PHP Purifier 库实现;不建议使用正则表达式函数:常用模版引擎,比如:twig、smarty 会自动转义输出:

# 转义 HTML 代码
$input = htmlentities($input, ENT_QUOTES, 'UTF-8');

过滤输入

用户输入的数据需要过滤及验证,可以用原生代码或 AuraSymfony 等库。根据获取的数据操作数据库前要过滤攻击代码,可以借助 PDO 预处理实现。

# 过滤字符串: 只保留电子邮件有效字符
$mail = filter_var($mail, FILTER_SANITIZE_EMAIL);

# 验证字符串:是否为电子邮件
$isMail = filter_var($mail, FILTER_VALIDATE_EMAIL);

# 过滤输入:是否为安全字符串
$isValidPassword = filter_input(INPUT_POST, 'password');

密码

用户密码仅存哈希值,登录验证是否需要重新生成,推荐 bcrypt 算法。PHP 5.5- 版本需要借助 password_compat 库。

# 静态比较哈希值
$hash = password_hash($password, PASSWORD_DEFAULT, ['salt' => 'FIXED_SALT_FIXED_SALT_FIXED_SALT']);

# 动态哈希值匹配
$hash = password_hash($password, PASSWORD_DEFAULT, ['cost' => 12]);
$isMatch = password_verify($password, $hash);

# 存储的哈希值是否需要重新生成
$needRegenerate = password_needs_rehash($hash, PASSWORD_DEFAULT, ['cost' => 20]);

巡查/诊断

内存

# 诊断最大使用内存
echo date('H:i:s') , " Peak memory usage: " , (memory_get_peak_usage(true) / 1024 / 1024) , " MB" , PHP_EOL;

执行时间

# 运行时调整最大执行时间限制
set_time_limit();
Author: njun
njun's picture
Updated: 2020/03/12