变量表达式语义变更
1. 案例
预发环境PHP7.1.3,个人开发环境PHP5.6;
pf业务某代码片段在预发机器出现问题:
$marketGoodsCondition->$value['r']
2. 重现
作者意图:
<?php
$obj = new \stdClass;
$obj->bar = 42;
$propArr = ["foo" => "bar"];
$prop = $propArr["foo"]; ❗️❗️❗️注意这里
var_dump($obj->$prop); ❗️❗️❗️注意这里
Output for 5.6.0 - 5.6.30, 7.0.0 - 7.1.4
int(42)
于是简化成如下写法:
<?php
$obj = new \stdClass;
$obj->bar = 42;
$propArr = ["foo" => "bar"];
var_dump($obj->$propArr["foo"]); ❗️❗️❗️注意这里
Output for 7.0.0 - 7.1.4
Notice: Array to string conversion in ...
Notice: Undefined property: stdClass::$Array in ...
NULL
Output for 5.6.0 - 5.6.30
int(42)
3. opcode
先不解释,直接看opcode
$obj->$propArr["foo"]
PHP7.1.3
compiled vars: !0 = $obj, !1 = $propArr
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
2 0 E > FETCH_OBJ_R ❗️❗️❗️注意这里 $2 !0, !1
1 FETCH_DIM_R ❗️❗️❗️注意这里 $3 $2, 'foo'
2 FREE $3
3 3 > RETURN 1
人肉翻译:
0 $2 = $obj->$propArr
1 $3 = $2['foo']
PHP5.6.16
compiled vars: !0 = $obj, !1 = $propArr
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
2 0 E > FETCH_DIM_R ❗️❗️❗️注意这里 $0 !1, 'foo'
1 FETCH_OBJ_R ❗️❗️❗️注意这里 $1 !0, $0
2 FREE $1
3 3 > RETURN 1
人肉翻译:
0 $0 = $propArr['foo']
1 $1 = $obj->$0
答案一目了然。
升级PHP7阶段,需要保证代码在PHP5与PHP7下语义一致,所以不能使用任何PHP7新语法,同时也要避免5与7语义不兼容的情况出现;
$obj->$val[$key]
在PHP7中恢复PHP5语义的方法是加括号$obj->{$val[$key]}
,显式控制优先级;
<?php
$obj = new \stdClass;
$obj->bar = 42;
$propArr = ["foo" => "bar"];
var_dump($obj->{$propArr["foo"]});
Output for 5.6.0 - 5.6.30, 7.0.0 - 7.1.4
int(42)
PHP7.1.3
compiled vars: !0 = $obj, !1 = $propArr
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
2 0 E > FETCH_DIM_R $0 !1, 'foo'
1 FETCH_OBJ_R $1 !0, $0
2 FREE $1
3 3 > RETURN 1
4. 分析:AST与统一变量语法
表达式优先级发生变化的根本问题根源是PHP执行流程发生变化,从原来一个pass,Parser直接生成opcodes,变更为生成中间产物AST;
php5: init -> parser -> opcodes -> execute
php5: init -> parser -> ast -> opcodes -> execute
AST的引入的两个原因:
- 编译器与解释器分离提升代码质量与可维护性;
- 语法特性引入不再被one-pass流程限制;
在AST基础上实现的统一变量语法
(uniform_variable_syntax)是导致上文兼容性的罪魁祸首
;应该算是AST引入牺牲的兼容性。
这里简要说一下 引入AST带来的变化:
1. yield不再需要括号
解决了PHP5.x时,yield表达式在表达式上下文中必须加括号的限。
$result = yield fn(); // PHP7
$result = (yield fn()); // PHP5
$result = yield;
$result = yield $v;
$result = yield $k => $v;
2. 括号不再影响行为
<?php
function retVal() { $var = 1; return $var; }
function &retRef() { $var = 1; return $var; }
function func(&$var) {}
正确
func(retRef());
提示正确
func(retVal());
Output for 7.0.0 - 7.1.4
Notice: Only variables should be passed by reference in
Output for 5.6.0 - 5.6.30
Strict Standards: Only variables should be passed by reference in
括号影响行为,PHP5无报错
func( ( retVal() ) ); ❗️️️️️️❗️❗️注意这里
Output for 7.0.0 - 7.1.4
Notice: Only variables should be passed by reference in
Output for 5.6.0 - 5.6.30
3. list 变更
3.1 PHP5.x list 从右至左赋值,AST实现修正为从左至右赋值
list($array[], $array[], $array[]) = [1, 2, 3];
var_dump($array);
// OLD: $array = [3, 2, 1]
// NEW: $array = [1, 2, 3]
3.2 兼容性问题,list赋值等号两边使用同一个变量:
$a = [1, 2];
list($a, $b) = $a;
// OLD: $a = 1, $b = 2
// NEW: $a = 1, $b = null + "Undefined index 1"
$b = [1, 2];
list($a, $b) = $b;
// OLD: $a = null + "Undefined index 0", $b = 2
// NEW: $a = 1, $b = 2
3.3 现在嵌套list,每个offst只访问一次:
list(list($a, $b)) = $array;
// 注意,从右至左赋值过程中,$array[0]被访问了两次
// OLD:
$b = $array[0][1];
$a = $array[0][0];
// 现在有临时变量产生
// NEW:
$_tmp = $array[0];
$a = $_tmp[0];
$b = $_tmp[1];
这里看opcodes list(list($a, $b)) = $array;
PHP5.6
compiled vars: !0 = $a, !1 = $b, !2 = $array
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
2 0 E > FETCH_DIM_R $0 !2, 0
1 FETCH_DIM_R $1 $0, 1
2 ASSIGN !1, $1
3 FETCH_DIM_R $3 !2, 0
4 FETCH_DIM_R $4 $3, 0
5 ASSIGN !0, $4
4 6 > RETURN 1
人肉翻译:
!0 = $a, !1 = $b, !2 = $array
0 $0 = $array[0]
1 $1 = $0[1]
2 $b = $1
3 $3 = $array[0]
4 $4 = $3[0]
5 $a = $4
6 return
PHP7.1
compiled vars: !0 = $array, !1 = $a, !2 = $b
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
2 0 E > FETCH_LIST $3 !0, 0
1 FETCH_LIST $4 $3, 0
2 ASSIGN !1, $4
3 FETCH_LIST $6 $3, 1
4 ASSIGN !2, $6
5 FREE $3
4 6 > RETURN 1
人肉翻译:
!0 = $array, !1 = $a, !2 = $b
0 $3 = $array[0]
1 $4 = $3[0] ❗️️❗️❗️ 这里复用 临时变量 $3
2 $a = $3
3 $6 = $3[1] ❗️❗️❗️ 这里复用 临时变量 $3
4 $b = $6
5 unset($3)
6 return
3.4 完全禁用空的list()解构赋值:
list() = $a; // INVALID
list($b, list()) = $a; // INVALID
foreach ($a as list()) // INVALID (was also invalid previously)
4. 允许直接调用__clone方法
$obj->__clone()
然后简要说一下PHP7.x引入统一变量语法。
缘由是为了达成一致与完整的变量语法,坏处是引入了兼容性问题,改变了可变变量
的语义;
之前不合法的表达式,在统一变量语法下合法:
// support missing combinations of operations
$foo()['bar']()
[$obj1, $obj2][0]->prop
getStr(){0}
// support nested ::
$foo['bar']::$baz
$foo::$bar::$baz
$foo->bar()::baz()
// support nested ()
foo()()
$foo->bar()()
Foo::bar()()
$foo()()
// support operations on arbitrary (...) expressions
(...)['foo']
(...)->foo
(...)->foo()
(...)::$foo
(...)::foo()
(...)()
// two more practical examples for the last point
(function() { ... })() # 这种写法在js中常见,使用闭包来封装与解决作用域污染,但因为PHP非链式的作用,没什么用途
($obj->closure)()
// support all operations on dereferencable scalars (not very useful)
"string"->toLower()
[$obj, 'method']()
'Foo'::$bar
语义变更的表达式案例:
// old meaning // new meaning
$$foo['bar']['baz'] ${$foo['bar']['baz']} ($$foo)['bar']['baz']
$foo->$bar['baz'] $foo->{$bar['baz']} ($foo->$bar)['baz']
$foo->$bar['baz']() $foo->{$bar['baz']}() ($foo->$bar)['baz']()
Foo::$bar['baz']() Foo::{$bar['baz']}() (Foo::$bar)['baz']()
PHP7.x统一修改为从左至右解析;
不再支持的表达式
global $$foo->bar;
// instead use:
global ${$foo->bar};