最近迁移服务器,将 PHP 升级到 8.0.10 之后,且关闭 PHP warning 报错,博客首页、文章页显示“有点尴尬诶!该页无法显示”。

一开始以为数据库导入有问题,文章没有导入。进入后台查看,文章虽然都还在,但查看文章具体页面,均是“有点尴尬诶!该页无法显示”。
恰好点开评论,遇到函数报错,无法打开评论管理界面,猜测是受 PHP 8。

回退到 PHP 7.4.23 之后,文章正常显示。
科技改变生活,编程改变世界。
最近迁移服务器,将 PHP 升级到 8.0.10 之后,且关闭 PHP warning 报错,博客首页、文章页显示“有点尴尬诶!该页无法显示”。
一开始以为数据库导入有问题,文章没有导入。进入后台查看,文章虽然都还在,但查看文章具体页面,均是“有点尴尬诶!该页无法显示”。
恰好点开评论,遇到函数报错,无法打开评论管理界面,猜测是受 PHP 8。
回退到 PHP 7.4.23 之后,文章正常显示。
为了避免在服务器受到攻击,数据库被拖库时,用户的明文密码不被泄露,一般会对密码进行单向不可逆加密——哈希。
常见的方式是:
哈希方式 | 加密密码 |
md5(‘123456’) | e10adc3949ba59abbe56e057f20f883e |
md5(‘123456’ . ($salt = ‘salt’)) | 207acd61a3c1bd506d7e9a4535359f8a |
sha1(‘123456’) | 40位密文 |
hash(‘sha256’, ‘123456’) | 64位密文 |
hash(‘sha512’, ‘123456’) | 128位密文 |
密文越长,在相同机器上,进行撞库消耗的时间越长,相对越安全。
比较常见的哈希方式是 md5 + 盐,避免用户设置简单密码,被轻松破解。
但是,现在要推荐的是 password_hash() 函数,可以轻松对密码实现加盐加密,而且几乎不能破解。
$password = '123456'; var_dump(password_hash($password, PASSWORD_DEFAULT)); var_dump(password_hash($password, PASSWORD_DEFAULT));
password_hash 生成的哈希长度是 PASSWORD_BCRYPT —— 60位,PASSWORD_DEFAULT —— 60位 ~ 255位。PASSWORD_DEFAULT 取值跟 php 版本有关系,会等于其他值,但不影响使用。
每一次 password_hash 运行结果都不一样,因此需要使用 password_verify 函数进行验证。
$password = '123456'; $hash = password_hash($password, PASSWORD_DEFAULT); var_dump(password_verify($password, $hash));
password_hash 会把计算 hash 的所有参数都存储在 hash 结果中,可以使用 password_get_info 获取相关信息。
$password = '123456'; $hash = password_hash($password, PASSWORD_DEFAULT); var_dump(password_get_info($hash));
输出 array(3) { ["algo"]=> int(1) ["algoName"]=> string(6) "bcrypt" ["options"]=> array(1) { ["cost"]=> int(10) } } 注意不包含 salt
可以看出我当前版本的 PHP 使用 PASSWORD_DEFAULT 实际是使用 PASSWORD_BCRYPT。
password_hash($password, $algo, $options) 的第三个参数 $options 支持设置至少 22 位的 salt。但仍然强烈推荐使用 PHP 默认生成的 salt,不要主动设置 salt。
当要更新加密算法和加密选项时,可以通过 password_needs_rehash 判断是否需要重新加密,下面的代码是一段官方示例
$options = array('cost' => 11); // Verify stored hash against plain-text password if (password_verify($password, $hash)) { // Check if a newer hashing algorithm is available // or the cost has changed if (password_needs_rehash($hash, PASSWORD_DEFAULT, $options)) { // If so, create a new hash, and replace the old one $newHash = password_hash($password, PASSWORD_DEFAULT, $options); } // Log user in }
password_needs_rehash 可以理解为比较 $algo + $option 和 password_get_info($hash) 返回值。
password_hash 是出了名的运行慢,也就意味着在相同时间内,密码重试次数少,泄露风险降低。
$password = '123456'; var_dump(microtime(true)); var_dump(password_hash($password, PASSWORD_DEFAULT)); var_dump(microtime(true)); echo "\n"; var_dump(microtime(true)); var_dump(md5($password)); for ($i = 0; $i < 999; $i++) { md5($password); } var_dump(microtime(true));
输出 float(1495594920.7034) string(60) "$2y$10$9ZLvgzqmiZPEkYiIUchT6eUJqebekOAjFQO8/jW/Q6DMrmWNn0PDm" float(1495594920.7818) float(1495594920.7818) string(32) "e10adc3949ba59abbe56e057f20f883e" float(1495594920.7823)
password_hash 运行一次耗时 784 毫秒, md5 运行 1000 次耗时 5 毫秒。这是一个非常粗略的比较,跟运行机器有关,但也可以看出 password_hash 运行确实非常慢。
通过 php curl 请求网页并不能获取到证书信息,此时需要使用 ssl socket 获取证书内容。
// 创建 stream context $context = stream_context_create([ 'ssl' => [ 'capture_peer_cert' => true, 'capture_peer_cert_chain' => true, ], ]); $resource = stream_socket_client("ssl://$domain:$port", $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $context); $cert = stream_context_get_params($resource); $ssl = $cert['options']['ssl']; $resource = $ssl['peer_certificate']; // 网站证书中只有公钥,通过 openssl_pkey_get_details 导出公钥 $ret = [ 'crt' => '', 'pub' => '', ]; $pkey = openssl_pkey_get_public($resource); $ret['pub'] = openssl_pkey_get_details($pkey)['key']; openssl_x509_export($resource, $pem); $ret['crt'] = $pem; foreach ($ssl['peer_certificate_chain'] as $resource) { openssl_x509_export($resource, $pem); $ret['crt'] .= "\n" . $pem; } // 保存 $ret['crt'] 为 domain.crt // 保存 $ret['pub'] 为 domain.pub return $ret;
验证证书中的公钥A是否正确,通过私钥导出公钥B,比较两者发现一致。
$domain = 'blog.zhengxianjun.com'; $port = '443'; // ... $pub_a = $ret['pub']; $private_key_path = '/conf/ssl/blog.zhengxianjun.com.key'; // 证书没有设置密码,$passphrase 为空字符串 $pkey = openssl_pkey_get_private(file_get_content($private_key_path), $passphrase = ''); $pub_b = openssl_pkey_get_details($pkey)['key']; // 两者一致 var_dump($pub_a === $pub_b);
函数 stream_socket_client 还有一个用途是当知道服务器 IP 时,能获取到服务器可能可以使用的域名。
$resource = stream_socket_client("ssl://$ip:$port", $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $context); $cert = stream_context_get_params($resource); // 解析 X.509 格式证书 $info = openssl_x509_parse($cert['options']['ssl']['peer_certificate']); // 获取证书中的可信域名列表 $domain = str_replace('DNS:', '', $info['extensions']['subjectAltName']);
以上可以看到获取网站证书并不能获得私钥。
在一些使用 CDN 的站点,如果使用了 HTTPS 同时又希望使用自有域名,是否需要将自己的私钥提供给 CDN 厂商呢?实际上证书路径与使用者名称(支持 https 的域名)并不需要一致。
也就是使用自有域名并进行 CDN 加速时不需要使用自有的 ssl 证书,只需将自己的 CDN 域名加到厂商证书的域名列表即可。
PHP self 指向定义的 class。
PHP static 指向运行的 class,一般只有子类覆盖父类的 static 成员或者方法时,在父类中使用 static 会访问到子类。
class ParentClass { public static function hello() { echo "ParentClass: hello\n"; } public static function run() { self::hello(); static::hello(); } } class ChildClass extends ParentClass { public static function hello() { echo "ChildClass: hello\n"; } } ParentClass::run(); // 输出 "ParentClass: hello" "ParentClass: hello" ChildClass::run(); // 输出 "ParentClass: hello" "ChildClass: hello"
PHP 安装,从官网下载源码压缩包,进行 configure 遇到几个错误:
./configure –prefix=/usr/local/php –with-config-file-path=/usr/local/php/etc –with-bz2 –with-curl –enable-ftp –enable-sockets –disable-ipv6 –with-gd –with-jpeg-dir=/usr/local –with-png-dir=/usr/local –with-freetype-dir=/usr/local –enable-gd-native-ttf –with-iconv-dir=/usr/local –enable-mbstring –enable-calendar –with-gettext –with-libxml-dir=/usr/local –with-zlib –with-pdo-mysql=mysqlnd –with-mysqli=mysqlnd –with-mysql=mysqlnd –enable-dom –enable-xml –with-libdir=lib64 –enable-pdo –enable-fpm
configure: error: Please reinstall the BZip2 distribution
解决方案
yum install bzip2
yum install bzip2-devel
bzip2 可能已经安装过,bzip2-devel 没有安装
configure: error: Please reinstall the libcurl distribution –
easy.h should be in <curl-dir>/include/curl/
解决方案
yum install curl-devel
configure: error: jpeglib.h not found
解决方案
yum install libjpeg
yum -y install libjpeg-devel
libjpeg 可能已经安装过,libjpeg-devel 没有安装
基于七牛SDK实现目录上传和同步
使用官方提供的PHP SDK实现,重新使用PHP实现目录同步,而不是使用官方提供的Windows 程序,主要是因为 qrsbox.exe 会同步目录下的所有文件,包括 .svn 文件和一些项目配置文件,如 .project
七牛的PHP SDK下载地址:http://developer.qiniu.com/code/v7/sdk/php.html
本次实现的源码有几个功能:
PHP脚本每次执行都会比较目录下的所有文件,以此判断是否需要同步。
同名不同内容文件上传时会提示文件已存在,先删除旧文件,再提交新文件。
没有实现的功能:
所有魔术变量表示的值均区分大小写
表示代码所在类的名字
// 文件 define.php namespace Base { abstract class Fruit { public function __construct() { // 输出 Base\Fruit echo __CLASS__; echo "\n"; // 输出 Sub\Apple echo get_class($this); echo "\n"; } public function hello() { // 输出 Base\Fruit echo __CLASS__; echo "\n"; } } } namespace Sub { class Apple extends \Base\Fruit { public function say() { // 输出 Sub\Apple echo __CLASS__; echo "\n"; } } } // 文件 test.php require __DIR__ . '/define.php'; $Apple = new Sub\Apple(); $Apple->hello(); $Apple->say();
表示代码所在文件所在的完整目录名,等价于 dirname(__FILE__),不包括末尾的斜杠 /
// 文件 test.php // 输出 test.php 所在目录 /opt/wwwroot/test echo __DIR__; echo "\n";
表示代码所在函数的名称
function foo() { // 输出函数名 foo echo __FUNCTION__; echo "\n"; } foo();
表示代码所在文件的完整路径名
// 文件 test.php // 输出 test.php 所在目录 /opt/wwwroot/test/test.php echo __FILE__;
表示代码所在文件中的行号
// 文件 test.php // 输出代码行号 echo __LINE__;
表示代码所在函数的名称
// 文件 define.php namespace Base; function foo() { // 输出函数名 Base\foo echo __FUNCTION__; echo "\n"; // 输出函数名 Base\foo echo __METHOD__; echo "\n"; } class Foo { public function bar() { // 输出函数名 bar echo __FUNCTION__; echo "\n"; // 输出方法名 Base\Foo::bar echo __METHOD__; echo "\n"; } public static function staticBar() { // 输出函数名 staticBar echo __FUNCTION__; echo "\n"; // 输出方法名 Base\Foo::staticBar echo __METHOD__; echo "\n"; } } // 文件 test.php require __DIR__ . '/define.php'; Base\foo(); $foo = new Base\Foo(); $foo->bar(); $foo->staticBar();
表示代码所在 trait 的名称,包含完整限定的 namespace
// 文件 define.php namespace Base { trait Wheel { public function roll() { // 输出 Base\Wheel echo __TRAIT__; echo "\n"; // 输出 Sub\Car echo __CLASS__; echo "\n"; } } } namespace Sub { class Car { use \Base\Wheel; public function start() { $this->roll(); // 输出空字符串 echo __TRAIT__; echo "\n"; // 输出 Sub\Car echo __CLASS__; echo "\n"; } } } // 文件 test.php require __DIR__ . '/define.php'; $car = new Sub\Car(); $car->start();
表示当前代码所在命名空间
namespace Vendor\Util; // 输出命名空间 Vendor\Util echo __NAMESPACE__; echo "\n"; function foo() { // 输出命名空间 Vendor\Util echo __NAMESPACE__; echo "\n"; } foo();
PHP 超全局变量(Superglobal)是自动化的全局变量,在任何作用域都可以使用,不需要使用 global 关键字声明 $variable。
变量名 | 说明 |
$GLOBALS | 引用了全部全局变量的数组,如 $GLOBALS[‘_GET’]、$GLOBALS[‘_POST’] 等等,以及一个对自身 GLOBALS 的引用。 |
$_COOKIE | PHP 解析浏览器请求携带的 cookie 生成的数组 |
$_ENV | $_ENV 包含当前的环境变量,与 phpinfo() 输出的 Environment 表格一致,但默认为空,通过 getenv() 可以获取到指定的环境变量 |
$_FILES | $_FILES 包含当前 HTTP POST 上传的文件列表。 |
$_GET | $_GET 包含 URL 参数列表 |
$_POST | $_POST 包含 POST 参数列表 |
$_REQUEST | 可能包含 $_GET、$_POST、$_COOKIE,不包含 $_FILES。 request_order 控制包含的参数列表。 |
$_SERVER | 包含服务器和脚本执行环境信息,比如 HTTP 请求头信息、脚本信息,不同服务器信息会有不同。 |
$_SESSION | 包含当前 SESSION 变量列表。需要先调用 session_start() 开启会话。 |
$argc | 以命令行(CLI)形式运行 PHP 脚本,传递给脚本的参数总数。需要开启 register_argc_argv |
$argv | 以命令行(CLI)形式运行 PHP 脚本,传递给脚本的参数数组。需要开启 register_argc_argv |
$http_response_header | 保存当前作用域下最新 HTTP 请求的的响应头信息 |
$php_errormsg | 开启 track_errors 时,PHP 执行的最新错误信息 |
$name = 'Seven'; function hello() { $name = 'Eight'; echo "local name is $name"; // Eight echo "\n", echo "global name is " . $GLOBALS['name']; // Seven }
php.ini 中的 register_globals(在 PHP 5.4.0 中被移除)表示将 globals 变量中的成员自动注册为全局变量。
// 设置 register_globals = on; $_GET['name'] = 'Seven'; $name === $_GET['name']; // true // 设置 register_globals = off; // 不会将 global 变量中的成员自动注册为全局变量
$_COOKIE 包含当前请求的 cookie 列表,在请求中使用 setcookie 再调用 $_COOKIE 不会立刻获得相同的值。
// 设置 cookie setcookie('name', 'Seven'); // 获取 cookie $_COOKIE['name'] // 不能立刻获得设置的 cookie // 因为 $_COOKIE 存放的是当前浏览器发送到服务器的 cookie // 其次, setcookie 不一定设置 cookie 成功
默认的 $_ENV 数组为空,是因为 variables_order可能没有包含 E —— _ENV。
CEGPS 分别对应 $_COOKIE、$_ENV、$_GET、$_POST、$_SERVER,variables_order 的默认值没有包含 E,因此不会设置超全局变量 $_ENV。variables_order 为空则不会设置任何超全局变量。
$_FILES 包含的是文件列表,是一个二维数组,一般结合 move_uploaded_file() 将上传的文件存放到需要的位置,类似结构如下:
array( // myfile 是 input 表单的 name 属性 'myfile' => array( 'name' => '', // 文件的原名称 'type' => '', // 文件的 MIME 类型 'size' => '', // 文件的字节大小 'tmp_name' => '', // 存放的包含路径的临时文件名 'error' => '', // 上传的错误代码 ); ); // 如果 php 对 upload_tmp_dir 没有写入权限, // 则上传的文件无法存放在文件系统上
$_POST 只解析类似 URL 参数列表结构的 POST BODY 中的数据,比如将 name=Seven 解析为 $_POST[‘name’] = Seven,调用 AJAX XMLHttpRequest 的 send() 方法传递的字符串一般类似 name=Seven&age=20。
$_POST 不能解析文件流,也不能解析 send() 的 JSON 字符串,需要使用 @file_get_contents(‘php://input’) 获取 POST 的内容,再进行解析——比如 json_decode 获得 JSON,base64_decode 获得二进制流。
// request_order = 'GP' // 表示 $_REQUEST 包含 $_GET、$_POST 内容 // 同名 key 后者覆盖前者 // request_order = 'GPC' // 表示 $_REQUEST 包含 $_GET、$_POST、$_COOKIE 内容
$_SERVER['HTTP_HOST'] // HTTP 请求的主机 $_SERVER['HTTP_USER_AGENT'] // HTTP 请求 User-Agent $_SERVER['HTTP_COOKIE'] // HTTP 请求 COOKIE $_SERVER['HTTP_ACCEPT_LANGUAGE'] // 可以接受的语言列表 // 与 HTTP 请求相关的信息大部分以 HTTP_ 开始
返回一个二维数组,包含定义的函数名列表:区分 internal、user。
// get_defined_functions 返回值 array ( 'internal' => array ( 'zend_version', 'func_num_args', 'func_get_arg', // ... ), 'user' => array ( 'get_client_ip', 'get_client_device', 'get_client_browser', ), );
返回定义的常量数组,常量名作为 key,常量值作为 value。
返回定义的变量数组。
将数组的成员导入到当前作用域。
$a = array( 'b' => 'b', ); extract($a); echo $b; // 输出字符串 b // 如果数组的成员与当前变量名重名会发生什么?
开发 PHP 不像开发 Java 有较强的文档注释规范,通过注释可以便捷地生成接口文档。
我在寻找 PHP 接口文档工具时使用过 phpDoc,页面效果不怎么好。辗转使用了 phpDocumentor,页面效果可以接受。
使用 phpDocumentor 步骤(仅在 Windows 系统验证通过):
访问百度盘 http://pan.baidu.com/s/1bnErGXh 下载 phar 文件。
在 PHP5 目录(php.ini 所在的目录)下创建 phar 目录,并把 phpDocumentor.phar 移动到该目录。
进入 PHP5 目录,打开 cmd 命令窗口,执行 phpDocumentor 命令
php phar\phpDocumentor.phar -d D:\ran\framework -t D:\zhengxianjun_cdn\www\ran-api
-d 参数表示源代码目录
-t 参数表示生成的接口文档存放目录
命令执行完成之后会在 D:\zhengxianjun_cdn\www\ran-api\reports\errors.html 记录下源代码中接口描述不规范的点,逐条修改即可。
phpDocument 生成的接口文档的样式中模式会使用 Google 的字体,在国内自然打不开,导致网页打开很慢。找到 D:\zhengxianjun_cdn\www\ran-api\css\template.css,删除第一行的 @import url(https://fonts.googleapis.com/css?family=Source+Sans+Pro);。因为每次生成接口文档都会覆盖 template.css,另一个一劳永逸的办法是在 hosts 文件中加上 127.0.0.1 fonts.googleapis.com
phpDocument 生成的接口文档适配手机网页,但查看不是很便捷。
使用 phpDocument,就需要按照它的规范来抒写代码注释。
phpDocument 代码注释规范:http://www.phpdoc.org/docs/latest/index.html。
支持 28 个标签,有些标签没有完全实现,但实际常用的标签就 10 个左右。
在注释文档中支持部分 html 标签,例如
对应的展示效果如下
想要编写复杂的代码示例仍然不是很容易,但通过注释生成接口文档基本已满足我的需求。
想要细致了解 phpDocument.phar 文件的用法,可以通过 php phar\phpDocumentor.phar –help 命令获得帮助。
PHP Ran Framework 的接口文档便是通过 phpDocumentor 创建的 http://ran-api.qiniudn.com
使用 PHP 进行图片压缩,首先通过 imagecreatefrom* 系列函数创建源图像 resource 对象,再通过 imagecreate 或者 imagecreatetruecolor 函数创建确定宽度和高度的目标图像对象,接着进行一系列处理,最后通过 imagecopy* 系列函数把源图像对象拷贝到目标图像对象。完成上述处理之后,调用 imagegif、imagejpeg、imagepng 把目标图像对象转换为图像字符串输出到文件或者浏览器。似乎没有直接把图像对象转换为字符串的函数?可以通过 ob_start 捕获字符串输出,并调用 ob_get_clean 取得字符串。
imagecreatefromgd | 从 GD 文件或 URL 创建图像对象 |
imagecreatefromgd2 | 从 GD2 文件或 URL 创建图像对象 |
imagecreatefromgif | 从 gif 文件创建图像对象 |
imagecreatefromjpeg | 从 jpeg/jpg 文件创建图像对象 |
imagecreatefrompng | 从 png 文件创建图像对象 |
imagecreatefromwbmp | 从 wbmp 文件创建图像对象 |
imagecreatefromwebp | 从 webp 文件创建图像对象 |
imagecreatefromxbm | 从 xbm 文件创建图像对象 |
imagecreatefromxpm | 从 xpm 文件创建图像对象 |
上述所有函数全部接收一个参数 — $filename, 失败时均会返回 false | |
imagecreatefromstring | 从字符串创建图像对象 |
接收一个参数 — 图像二进制字符串 |
imagecopy | 拷贝图像的一部分 |
imagecopymerge | 拷贝并合并图像的一部分 |
imagecopymergegray | 用灰度拷贝并合并图像的一部分 |
imagecopyresampled | 重采样拷贝部分图像并调整大小 |
imagecopyresized | 拷贝部分图像并调整大小 |
$image = 'D:\TEMP\woxinfeishi-bukezhuanye.jpg'; $source = imagecreatefromjpeg($image); $s_width = imagesx($source); $s_height = imagesy($source); $max_width = 200; $max_height = 200; if ($s_width > $max_width || $s_height > $max_height) { $s_rate = $s_width / $s_height; $t_rate = $max_width / $max_height; if ($s_rate > $t_rate) { $t_width = $max_width; $t_height = floor($t_width / $s_rate); } else { $t_height = $max_height; $t_width = floor($t_height * $s_rate); } $target = imagecreatetruecolor($t_width, $t_height); imagecopyresampled($target, $source, 0, 0, 0, 0, $t_width, $t_height, $s_width, $s_height); } else { $target = $source; } ob_start(); imagejpeg($target); $image_data = ob_get_clean(); // 图像数据 imagedestroy($source); imagedestroy($target);
上述代码实现图片等比压缩,如果是缩放到固定高宽则只需要直接设置为最终高宽。