PHP中foreach循环时使用引用的一个坑

记录一个广为流传的关于使用PHP引用的坑。

$arr = ['abc', 'ben', 'cam', 'day'];
foreach($arr as $key=>&$val){
    $val = strtoupper($val);
}
foreach($arr as $val){
    echo "$val\n";
    print_r($arr);

}
echo "===END===\n";
print_r($arr);

解释:

//第二次使用$val做foreach循环时,此时$val还是保持着对$arr数组里的最后一个元素($arr[3])的引用。
//所以对于这个foreach,
//1st ==> $arr[3]=’abc’
//2nd ==> $arr[3] = ‘ben’
//3rd ==> $arr[3] = ‘cam’
//4th ==> $arr[3] = ‘day’

所以正确的做法是在使用引用的foreach结束后马上unset($val)
或者干脆不用使用引用,foreach时使用$key=>$val格式,然后使用$arr[$key]方式修改变量并保存。

Wall Clock and Monotonic Clock

Wall clock(time) VS Monotonic clock(time)

Wall clock(time)就是我们一般意义上的时间,就像墙上钟所指示的时间。

Monotonic clock(time)字面意思是单调时间,实际上它指的是从某个点开始后(比如系统启动以后)流逝的时间,jiffies一定是单调递增的!

而特别要强调的是计算两个时间点的差值一定要用Monotonic clock(time),因为Wall clock(time)是可以被修改的,比如计算机时间被回拨(比如校准或者人工回拨等情况),或者闰秒( leap second),会导致两个wall clock(time)可能出现负数。(因为操作系统或者上层应用不一定完全支持闰秒,出现闰秒后系统时间会在后续某个点会调整为正确值,就有可能出现时钟回拨(当然也不是一定,比如ntpdate就有可能出现时钟回拨,但是ntpd就不会))

 

PHP 7.3新增了hrtime函数

hrtime ([ bool $get_as_number = FALSE ] ) : mixed

Returns the system’s high resolution time, counted from an arbitrary point in time. The delivered timestamp is monotonic and can not be adjusted.

<?php


while(true){
    var_dump(time());
//    $micro = microtime(true);
//    var_dump($micro);
//    echo PHP_EOL;

    //var_dump(hrtime());
    $nanosecond = hrtime(true);
    var_dump($nanosecond/1000/1000/1000);//nanosecond => second
    echo PHP_EOL;
    echo PHP_EOL;
    echo PHP_EOL;
    echo PHP_EOL;

    sleep(5);
}

执行此脚本,然后在过程中手动更改系统时间。执行结果如下

 

……

int(1554975423)

float(481987.58385405)

 

int(1554975428)

float(481992.58812)

 

int(1554975433)    ==>    北京时间:2019/4/11 17:37:13

float(481997.59318082)

 

int(1554889031)   ==>      北京时间:2019/4/10 17:37:11

float(482002.59569814)

 

int(1554889036)

float(482007.59825222)

……

 

hrtime()的返回值就是一直单调递增的,而time()的返回值就可能出现跳跃。所以在计算时间差时要用hrtime(),注意PHP版本至少要7.3 。

 

 

php.ini中的cgi.fix_pathinfo选项

PHP里经常要获取当前请求的URL路径信息。一般可以通过环境变量$_SERVER[‘PATH_INFO’]获取,而配置文件中的cgi.fix_pathinifo选项则与这个值的获取相关。而$_SERVER[‘PATH_INFO’]中的key PATH_INFO是一个CGI 1.1的标准,经常用来做为传递参数给后端的CGI服务器。

被很多系统用来优化url路径格式,比如对于很多框架,下面这个网址:
http://www.test.com/index.php/test/my.html?c=index&m=search
我们可以得到
$_SERVER[‘PATH_INFO’] = ‘/test/my.html’
$_SERVER[‘QUERY_STRING’] = ‘c=index&m=search’;

我们再说下php.ini中的配置参数cgi.fix_pathinfo,它是用来对设置cgi模式下为php是否提供绝对路径信息或PATH_INFO信息。没有这个参数之前PHP设置绝对路径PATH_TRANSLATED的值为SCRIPT_FILENAME,没有PATH_INFO值。设置cgi.fix_pathinfo=1后,cgi设置完整的路径信息PATH_TRANSLATED的值为SCRIPT_FILENAME,并且设置PATH_INFO信息;如果设为cgi.fix_pathinfo=0则只设置绝对路径PATH_TRANSLATED的值为SCRIPT_FILENAME。cgi.fix_pathinfo的默认值是1。
nginx默认是不会设置PATH_INFO环境变量的的值,需要通过正则匹配设置SCRIPT_FILENAME,但这样会带来安全隐患,需要把cgi.fix_pathinfo=0设置为0。但是一旦关闭这个这场,PHP就获取不到PATH_INFO信息,那些依赖PATH_INFO进行URL美化的程序就失效了。
关于安全隐患的问题,请看 http://www.laruence.com/2010/05/20/1495.html

网上给出了一些方案,在关闭cgi.fix_pathinfo时使依赖PATH_INFO美化url的程序能够正常工作。
1.可以通过rewrite方式代替php中的PATH_INFO
实例:thinkphp的pathinfo解决方案
设置URL_MODEL=2
location / {
if (!-e $request_filename){
rewrite ^/(.*)$ /index.php?s=/$1 last;
}
}
2.nginx配置文件中设置PATH_INFO值
请求的网址是/abc/index.php/abc
PATH_INFO的值是/abc
SCRIPT_FILENAME的值是$doucment_root/abc/index.php
SCRIPT_NAME /abc/index.php
旧版本的nginx使用如下方式配置
location ~ .php($|/) {
set $script $uri;
set $path_info “”;

if ($uri ~ “^(.+.php)(/.+)”) {
set $script $1;
set $path_info $2;
}

fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$script;
fastcgi_param SCRIPT_NAME $script;
fastcgi_param PATH_INFO $path_info;
}
新版本的nginx也可以使用fastcgi_split_path_info指令来设置PATH_INFO,旧的方式不再推荐使用,在location段添加如下配置。
location ~ ^.+.php {

fastcgi_split_path_info ^((?U).+.php)(/?.+)$;
fastcgi_param SCRIPT_FILENAME /path/to/php$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info;

}

通过上面的描述,我们似乎得出了一个结论:为了安全要关闭掉cgi.fix_pathinfo设置。
但是我们来看看php.ini的配置及说明

; cgi.fix_pathinfo provides *real* PATH_INFO/PATH_TRANSLATED support for CGI. PHP’s
; previous behaviour was to set PATH_TRANSLATED to SCRIPT_FILENAME, and to not grok
; what PATH_INFO is. For more information on PATH_INFO, see the cgi specs. Setting
; this to 1 will cause PHP CGI to fix its paths to conform to the spec. A setting
; of zero causes PHP to behave as before. Default is 1. You should fix your scripts
; to use SCRIPT_FILENAME rather than PATH_TRANSLATED.
; http://php.net/cgi.fix-pathinfo
cgi.fix_pathinfo=1

设置cgi.fix_pathinfo=1才符合cgi标准。那么有什么办法可以在保持cgi.fx_pathinfo默认设置的情况下,保证系统安全吗?

好消息,新版本PHP(我验证至少PHP5已经有了这个参数)的fpm配置里新增了一个额外参数(php-fpm.d/www.conf),
security.limit_extensions = .php .php3 .php4 .php5 .php7

专门用来限制PHP脚本引擎只支持解析哪些扩展名的文件

; Limits the extensions of the main script FPM will allow to parse. This can
; prevent configuration mistakes on the web server side. You should only limit
; FPM to .php extensions to prevent malicious users to use other extensions to
; execute php code.
; Note: set an empty value to allow all extensions.
; Default Value: .php
;security.limit_extensions = .php .php3 .php4 .php5 .php7

所以在使用nginx+php-fpm时,可以不用修改系统默认的cgi. fix_pathinfo=1设置了。

References:
http://www.laruence.com/2010/05/20/1495.html

nginx下支持PATH_INFO详解

https://serverfault.com/questions/627903/is-the-php-option-cgi-fix-pathinfo-really-dangerous-with-nginx-php-fpm

PHP的print

有这样的一题:

echo ‘2’.print(2)+3;

问输出是多少?

运行结果是……(下面有分析过程及答案,提前预告:正确答案不是224,此题并不涉及运算符优先级https://secure.php.net/manual/en/language.operators.precedence.php

print 实际上不是函数(而是语言结构),所以可以不用圆括号包围参数列表。

print(2) <=> print 2

print (2)+3 <=> print 2+3

所以 echo ‘2’.print(2)+3;  ==> print 2+3先输出5,echo ‘2’.1 输出“21”,所以整体输出521 。

 

类似的情况

echo ‘2’.print(2)+3; //521

echo ‘4’.print(2)+3+1*7; //1241

echo ‘1’.print(2)+3+1*7,33; //121133 注意:逗号会分隔print的参数作用范围

聊聊APCU

APCU的前身是APC(Alternative PHP Cache),APC的主要用途有两项:

  1. 将PHP代码编译之后所产生的bytecode暂存在共享内存内供重复使用,以提升应用的运行效率。(Opcode Cache)
  2. 提供用户数据缓存功能。(User Data Cache)

其中第一点是其主要功能,因为PHP的运行机制——每次接受一个请求时都要初始化所有的资源(将源代码转换成Opcode,……),执行代码,然后释放资源;所以启用Opcache Cache后,可以在初始化资源阶段减少CPU和内存的消耗。

但是PHP从PHP 5.5开始,使用ZendOptimizerPlus作为内置的Opcode Cache实现。所以现在APCU的主要功能便不再有意义了,而且其官方也随后表示不再维护APC了。

因此APCU出现了!

APCu is APC stripped of opcode caching.

The first APCu codebase was versioned 4.0.0, it was forked from the head of the APC master branch at the time.

PHP 7 support is available as of APCu 5.0.0.

以上就是关于APCU的前世今生。目前还处于活跃开发中的与APCU类似的工具,还有laruence(鸟哥)开发的Yac。

那么很明确了,APCU就一个功能:用户数据缓存(User Data Cache or Object Caching)。

如何使用APCU嘛,大家看看文档就知道了

https://secure.php.net/manual/en/book.apcu.php

今天要说的是这个APCU缓存和memcahce/redis不一样的地方。你通过memcache/redis存储一个数据,在缓存有效期内,同一机器上的不同的PHP进程(FPM+CLI)都是能够取到这份数据的。

对的,注意关键字 “同一机器上的不同的PHP进程”,对于APCU而言,PHP-FPM模式下所有的php-fpm进程(即使是不同的pool)属于同一个父进程,所以是可以共享缓存数据的;但是cli模式每次都是单独一个全新进程,因而和php-fpm模式的进程是不能共享缓存数据的。所以如果你的业务场景需要在cli和php-fpm两种模式下共享数据一定要小心了,可能memcache或者redis才是你更好的选择。

 

References:

https://github.com/krakjoe/apcu/issues/121

https://github.com/krakjoe/apcu/issues/255

https://github.com/laruence/yac/issues/61

json_encode() in PHP

之前看Yii2的源码的发现有这样一段代码

public static function encode($value, $options = 320)
{
	$expressions = [];
	$value = static::processData($value, $expressions, uniqid('', true));
	set_error_handler(function() {
		static::handleJsonError(JSON_ERROR_SYNTAX);
	}, E_WARNING);
	$json = json_encode($value, $options);
	restore_error_handler();
	static::handleJsonError(json_last_error());
	return $expressions === [] ? $json : strtr($json, $expressions);
}

为什么要设置set_error_handler呢?根据文档(http://php.net/manual/en/function.json-encode.php),encode失败时会返回FALSE,根本不会抛出异常。

查看git提交记录,https://github.com/yiisoft/yii2/commit/7bea7b65fdf21ae3e58339cc04376123416fcf81,发现这次提交的comment为“Improved JSON error handling to support PHP 5.5 error codes”,里面有说明

PHP 5.4 is throwing E_WARNING json_encode(): type is unsupported, encoded as null when you are trying to encode Resource
PHP 5.5 is result false with json_last_error() JSON_ERROR_UNSUPPORTED_TYPE
重现代码:

    $fp = fopen('php://stdin', 'r');
    var_dump(json_encode(['a' => $fp]));

JSON_ERROR_UNSUPPORTED_TYPE常量正好是在PHP 5.5.0加入的,而Yii2支持的PHP版本为 >= PHP 5.4 。

nginx+php-fpm架构下wordpress升级失败分析

本站使用nginx+php-fpm架构,之前遇到wordpress内置升级功能无效的问题。

当然使用FTP方式升级没有问题,但是太麻烦了,所以通过网页升级是最值得推荐的。为了网页完成升级功能,服务器要的相关设置需要改动

1.必须保证整个wordpress文件目录的属主和php-fpm进程执行用户一致。

2.必须保证nginx的fastcgi_read_timeout参数(默认60s)足够大。

在systemd中配置php-fpm.service

vim /etc/systemd/system/php-fpm.service

[Unit]

Description=PHP-FPM

After=network.target syslog.target

[Install]

WantedBy=default.target

Alias=php-fpm.service

[Service]

#User=nobody

#Group=nobody

#User and Group can be set in the php-fpm configure file

Type=forking

#PIDFile=/usr/local/php7/var/run/php-fpm.pid

#不需要在这里指定pid文件位置,需要到php-fpm.conf文件中指定

ExecStart=/usr/local/php7/sbin/php-fpm

ExecStop=/bin/kill -INT $MAINPID

ExecReload=/bin/kill -USR2 $MAINPID

保存设置
systemctl daemon-reload

启动php-fpm.service
systemctl start php-fpm

systemctl enable php-fpm设置为开机启动

如果启动失败,请查看日志,常见错误都是权限问题,比如pid目录和php-fpm设置的log目录没有写权限等

PHP 5.3 ~ PHP 5.6 功能变动

PHP 5.3 ~ PHP 5.6 功能变动

本文将会介绍自 PHP 5.3 起,直至 PHP 5.6 中增加/删除/更改的特征。

总纲

PHP 5.3以前:autoload,PDO 和 MySQLi,类型约束,JSON 支持,Phar支持,……

PHP5.3:弃用的功能,匿名函数,新增魔术方法,命名空间,后期静态绑定,Heredoc 和 Nowdoc, const, 三元运算符,……

PHP5.4:Short Open Tag, 数组简写形式,Traits, 内置 Web 服务器,……

PHP5.5:yield, list() 用于 foreach, ……

PHP5.6: 常量增强,可变函数参数,命名空间增强,……

PHP 5.3以前

类型约束

通过类型约束可以限制参数的类型,不过这一机制并不完善,目前仅适用于类、接口、 array(数组,从PHP 5.1开始)、callable(可执行类型,从PHP 5.4开始) 以及string 和 int(从PHP 7.0开始).(http://php.net/manual/zh/language.oop5.typehinting.php)

JSON 支持

包括 json_encode(), json_decode() 等函数,JSON 系列函数,可以将 PHP 中的数组结构与 JSON 字符串进行转换。值得注意的是 json_decode() 默认会返回一个对象而非数组,如果需要返回数组需要将第二个参数设置为 true.

Phar支持

http://php.net/manual/zh/phar.requirements.php

继续阅读PHP 5.3 ~ PHP 5.6 功能变动

迁移WordPress到新服务器

 

  1. 备份:通过FTP备份全部的文件。导出数据库sql文件。
  2. 迁移:把备份的文件解压到新的web服务器根目录下;导入sql文件到新的mysql数据库中。修改数据库配置文件wp-config.php,将用户名、密码等更改为新MySQL服务器的信息。

    更改博客的安装地址和博客地址,在数据库中找到wp-options表,找到第一条记录siteurl以及home,这两条记录的值改成你的新域名。只有完成这一步后,才可以顺利进入后台,否则输入密码,会自动跳转到原来的老域名。

    UPDATE wp_options SET option_value = replace( option_value, ‘http://老域名’, ‘https://taobig.org’) WHERE option_name = ‘home’ OR option_name =’siteurl’ ;

    修改文章内部所有的用到老链接的域名字符串。此时得要将文章内链的旧域名修改为新域名。

    UPDATE wp_posts SET post_content = replace(post_content, ‘http://老域名’, ‘https://taobig.org’);

    UPDATE wp_posts SET guid = replace( guid, ‘http://老域名’, ‘https://taobig.org’ );