2022-04-12

[php-src読書録]その12: method呼び出し, private, protected

php-srcのコードリーディングした内容をコツコツ残すテスト。その12

classの続き。method呼び出し, private, protected

<?php

class Hoge
{
  private $fuga;

  public function call($foo)
  {
    echo $foo;
  }
}

$hoge = new Hoge();
$hoge->call(111);
$hoge->fuga;

opcode

$_main:
     ; (lines=9, args=0, vars=1, tmps=5)
     ; (before optimizer)
     ; /path/to/12.php:1-16
     ; return  [] RANGE[0..0]
0000 V1 = NEW 0 string("Hoge")
0001 DO_FCALL
0002 ASSIGN CV0($hoge) V1
0003 INIT_METHOD_CALL 1 CV0($hoge) string("call")
0004 SEND_VAL_EX int(111) 1
0005 DO_FCALL
0006 T5 = FETCH_OBJ_R CV0($hoge) string("fuga")
0007 FREE T5
0008 RETURN int(1)
LIVE RANGES:
     1: 0001 - 0002 (new)

Hoge::call:
     ; (lines=3, args=1, vars=1, tmps=0)
     ; (before optimizer)
     ; /path/to/12.php:7-10
     ; return  [] RANGE[0..0]
0000 CV0($foo) = RECV 1
0001 ECHO CV0($foo)
0002 RETURN null

INIT_METHOD_CALL FETCH_OBJ_R あたり

zend_compile_method_call() でメソッド呼び出しはコンパイルされる
ZEND_INIT_METHOD_CALL をemit
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L4529:L4529

zend_compile_call_common() で引数などコンパイル
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L4557:L4557

zend_compile_prop() => zend_compile_prop() => zend_delayed_compile_prop() と呼び出していって、ZEND_FETCH_OBJ_R をemit
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L2923:L2923

ZEND_INIT_METHOD のopcodeは ZEND_INIT_METHOD_CALL_SPEC_CV_CONST_HANDLER がハンドリングする

obj = Z_OBJ_P(object);
function_name = RT_CONSTANT(opline, opline->op2);
fbc = obj->handlers->get_method(&obj, Z_STR_P(function_name), ((IS_CONST == IS_CONST) ? (RT_CONSTANT(opline, opline->op2) + 1) : NULL));
// zend_hash_find(&zobj->ce->function_table, lc_method_name) を呼び出してmethodのzend_functionのポインタを取得

call = zend_vm_stack_push_call_frame(call_info,
	fbc, opline->extended_value, obj);
call->prev_execute_data = EX(call);
EX(call) = call;

FETCH_OBJ_R のopcodeは ZEND_FETCH_OBJ_R_SPEC_CV_CONST_HANDLER がハンドリングする。 ZEND_FETCH_OBJ_R_SPEC_CV_CONST_HANDLERZEND_FETCH_OBJ_R_SPEC_CV_CONST_INLINE_HANDLER を呼び出す

container = EX_VAR(opline->op1.var);
zend_object *zobj = Z_OBJ_P(container);
name = Z_STR_P(RT_CONSTANT(opline, opline->op2));
retval = zobj->handlers->read_property(zobj, name, BP_VAR_R, cache_slot, EX_VAR(opline->result.var));

read_property は関数ポインタで zend_std_read_property() を呼び出す。 zend_std_read_property()zend_get_property_offset() を呼び出す。

property_offset = zend_get_property_offset(zobj->ce, name, (type == BP_VAR_IS) || (zobj->ce->__get != NULL), cache_slot, &prop_info);

zend_get_property_offset() はプロパティ名からプロパティのzvalを取得し属性に応じた処理を行う。

zv = zend_hash_find(&ce->properties_info, member)
property_info = (zend_property_info*)Z_PTR_P(zv);
flags = property_info->flags;

private変数にアクセスしようとすると最終的にここに到達してエラーになる
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_object_handlers.c#L355:L355

if (flags & (ZEND_ACC_CHANGED|ZEND_ACC_PRIVATE|ZEND_ACC_PROTECTED)) {
  if (property_info->ce != scope) {
    if (flags & ZEND_ACC_PRIVATE) {
      if (property_info->ce != ce) {
        
      } else {
        if (!silent) {
          zend_bad_property_access(property_info, ce, member);

flagsにはZEND_ACC_PRIVATEが入っているので1,3行目の分岐を通る。

2行目は今のコールスコープがプロパティのクラスと異なる場合という分岐。 今はmainスクリプトのスコープからプロパティを呼び出しているので「異なる」という判定になる。

property_info->ce != EG(current_execute_data)->func->common.scope;

ちなみに、メソッドのスコープはここで設定されている↓
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L7089:L7089

4行目はオブジェクトのクラスとプロパティのクラスが異なる場合という分岐。スーパークラスのprivateプロパティを呼び出すとこの判定を通る。

今回はオブジェクトのクラスとプロパティのクラスが同じなのでelseを通り、slientが0なので zend_bad_property_access() が呼ばれエラーとなる。

protectedの場合、is_protected_compatible_scope() でアクセス可否を判定する
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_object_handlers.c#L361

具体的には、呼び出し元の継承チェーンに呼び出し先のクラスが含まれるか、あるいは呼び出し先のクラスの継承チェーンに呼び出し元が含まれるか、というのを判定している。
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_object_handlers.c#L236-L240

前者はこんな感じなパターン。protectedがスーパークラス側にある場合。

<?php

class Base
{
  protected $hoge = 123;
}
class Hoge extends Base
{
  public function hoge()
  {
    return $this->hoge . PHP_EOL;
  }
}

echo (new Hoge)->hoge();

後者はこんな感じ。これできるの初めて知った。

<?php

class Base
{
  public function hoge()
  {
    return $this->fuga;
  }
}

class Hoge extends Base
{
  protected $fuga = 123;
}

echo (new Hoge)->hoge();
このエントリーをはてなブックマークに追加