变量表达式语义变更

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的引入的两个原因:

  1. 编译器与解释器分离提升代码质量与可维护性;
  2. 语法特性引入不再被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};

参考文档

PHP RFC: Abstract syntax tree

PHP RFC: Uniform Variable Syntax

results matching ""

    No results matching ""