记PHP比较运算符的一个用例

最近被问到如下的一道题:

$x = false;
$i = 9;
for ($i = 9; $i > $x; $i -= 2) {
    echo $i . "\n";
}
//请说出代码执行后的输出结果。

一般想到int类型$i和bool类型的$x比较,bool类型的false会被转换成int类型的0,所以代码块会有5次输出后停止结束。

那么实际执行结果是怎样的呢?

实际执行一次,就会发现,结果是无限循环输出:9,7,5,3,1,-1,-3,-5,……。

为什么呢?

仔细查看PHP文档:https://www.php.net/manual/en/language.operators.comparison.php

会发现有这样的描述:

Type of Operand 1 Type of Operand 2 Result
null or string string Convert null to “”, numerical or lexical comparison
bool or null anything Convert both sides to boolfalse < true
object object Built-in classes can define its own comparison, different classes are uncomparable, same class see Object Comparison
stringresourceint or float stringresourceint or float Translate strings and resources to numbers, usual math
array array Array with fewer members is smaller, if key from operand 1 is not found in operand 2 then arrays are uncomparable, otherwise – compare value by value (see following example)
object anything object is always greater
array anything array is always greater

其中第二行就有关于bool类型和其他类型的比较,有明确说明,会把其他类型转换成bool类型。那么上文中的这道题,$i总是不等于0,即意味着$i总是会被转换成bool类型的true,true永远大于false,所以会有无限循环的输出。

 

Unix Timestamp(Unix时间戳)

UNIX时间,或称POSIX时间是UNIX或类UNIX系统使用的时间表示方式:从UTC 1970年1月1日0时0分0秒起至现在的总秒数,不考虑闰秒。在多数Unix系统上Unix时间可以透过date +%s指令来检查。(Unix time (also known as Epoch timePOSIX timeseconds since the Epoch, or UNIX Epoch time) is a system for describing a point in time. It is the number of seconds that have elapsed since the Unix epoch.) (英 [ˈiːpɒk]   美 [ˈepək])

从这个解释可以看出来,同一时刻,在全世界任一时区,获取的Unix时间戳是相同的。

所以,针对PHP而言,time()函数获取的到时间戳与时区无关。

time ( ) : int

Returns the current time measured in the number of seconds since the Unix Epoch (January 1 1970 00:00:00 GMT).

那么,进一步延伸,对于一个给定的日期时间字符串,例如:2020-01-01 00:00:00,那么获取这个日期时间对应的时间戳就是与时区有关的。因为不同时区下的2020-01-01 00:00:00距离UTC的1970-01-01 00:00:00的时间间隔是不一样的。

strtotime ( string $datetime [, int $now = time() ] ) : int

对于PHP而言,在使用strtotime函数时,如果日期时间字符串中没有包含时区信息,那么会使用默认的时区date_default_timezone_get()。(Each parameter of this function uses the default time zone unless a time zone is specified in that parameter. Be careful not to use different time zones in each parameter unless that is intended. )

$dateStr = '2020-01-01 00:00:00';

$timezone = 'Asia/Shanghai';
date_default_timezone_set($timezone);
echo sprintf("%13s)%25s ==> %s\n", $timezone, $dateStr, strtotime($dateStr));

$dateStrWithTimezone = '2020-01-01T00:00:00+08:00';
date_default_timezone_set($timezone);//优先读取日期时间字符串里的时区信息,此处单独设置的时区对下一行的strtotime无效
echo sprintf("%13s)%25s ==> %s\n", $timezone, $dateStrWithTimezone, strtotime($dateStrWithTimezone));

$timezone = 'UTC';
date_default_timezone_set($timezone);
echo sprintf("%13s)%25s ==> %s\n", $timezone, $dateStr, strtotime($dateStr));
echo sprintf("%13s)%25s ==> %s\n", $timezone, $dateStrWithTimezone, strtotime($dateStrWithTimezone));

output:
Asia/Shanghai)2020-01-01 00:00:00 ==> 1577808000
Asia/Shanghai)2020-01-01T00:00:00+08:00 ==> 1577808000
UTC)2020-01-01 00:00:00 ==> 1577836800
UTC)2020-01-01T00:00:00+08:00 ==> 1577808000 //优先读取日期时间字符串里的时区信息,runtime设置的时区对strtotime无效

 

date ( string $format [, int $timestamp = time() ] ) : string

Format a local time/date

同样的道理,同一个时间戳在不同的时区下,对应的日期时间字符串是不一样的。

date_default_timezone_set('UTC');
$timestamp = 1577836800;//UTC 2020-01-01 00:00:00
$sourceDatetime = new \DateTime(date('Y-m-d H:i:s', $timestamp));
echo sprintf("source datetime:%s(%s)\n", $sourceDatetime->format('Y-m-d H:i:s'), $sourceDatetime->getTimezone()->getName());

$timezone = 'Asia/Shanghai';
$targetDatetime = (new \DateTime(date('Y-m-d H:i:s', $timestamp)))
    ->setTimezone(new \DateTimeZone($timezone));
echo sprintf("target datetime:%s(%s)\n", $targetDatetime->format('Y-m-d H:i:s'), $targetDatetime->getTimezone()->getName());

output:
source datetime:2020-01-01 00:00:00(UTC)
target datetime:2020-01-01 08:00:00(Asia/Shanghai)

 

References:

https://zh.wikipedia.org/wiki/UNIX时间

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 不要设置别名。如果本文件名和Alias同名,则会导致systemctl enable php71-fpm报错:Failed to execute operation: Invalid argument

[Service]

#User=nobody

#Group=nobody

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

# 2020-04-24更新:参考PHP默认提供的配置文件,使用Type=notify
Type=notify
#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目录没有写权限等

 

PS:其实在编译PHP后,会自动生成配置文件,位置:php-${PHP_VERSION}/sapi/fpm/php-fpm.service ,然后直接把这个文件拷贝到/etc/systemd/system 目录即可