늦은 정적 바인딩

늦은 정적 바인딩 (Late Static Bindings)

PHP는 늦은 정적 바인딩(late static bindings) 이라는 기능을 제공하며, 이를 사용하면 정적 상속 문맥에서 호출된 클래스(called class)를 참조할 수 있다. static:: 키워드를 사용하는 이 기능이 self::와 어떻게 다르게 동작하는지를 예제와 함께 살펴본다.

출처: Late Static Bindings

본문

PHP는 late static bindings 이라는 기능을 구현하는데, 이 기능을 사용하면 정적 상속의 맥락에서 called class(호출된 클래스)를 참조할 수 있습니다.

좀 더 정확히 말하면, late static bindings은 마지막 "non-forwarding call"(전달되지 않은 호출)에서 이름이 지정된 클래스를 저장하는 방식으로 동작합니다. 정적 메서드 호출의 경우에는 명시적으로 이름이 지정된 클래스(보통 :: 연산자의 왼쪽에 있는 클래스)가 그 클래스이고, 비(非)정적 메서드 호출의 경우에는 객체의 클래스가 그 클래스입니다. "forwarding call"(전달 호출)이란 self::, parent::, static:: 에 의해 도입된 정적 호출이거나, 클래스 계층을 타고 올라가는 경우에는 forward_static_call() 입니다.

get_called_class() 함수를 사용하면 called class의 이름을 담은 문자열을 얻을 수 있고, static:: 도 그 스코프를 도입합니다.

이 기능은 내부적 관점에서 "late static bindings" 이라고 이름 붙여졌습니다. "Late binding"이라는 표현은 static:: 이 메서드가 정의된 클래스를 사용해 해석되지 않고, 런타임 정보를 이용해 계산된다는 사실에서 비롯된 것입니다.

또한 정적 메서드 호출에 사용할 수 있기 때문에(그것만을 위한 것은 아니지만) "static binding" 이라고도 불렸습니다.

self:: 의 한계

self::__CLASS__ 와 같은 현재 클래스에 대한 정적 참조는 함수가 속한 클래스, 즉 함수가 정의된 클래스를 사용해 해석됩니다.

예제 #1 self:: 사용

<?php

class A
{
    public static function who()
    {
        echo __CLASS__;
    }

    public static function test()
    {
        self::who();
    }
}

class B extends A
{
    public static function who()
    {
        echo __CLASS__;
    }
}

B::test();

?>

위 예제의 출력은 다음과 같습니다.

A

Late Static Bindings의 사용

Late static bindings은 런타임에 처음 호출된 클래스를 참조하는 키워드를 도입함으로써 그런 한계를 해결하려고 합니다. 기본적으로, 앞선 예제의 test() 안에서 B 를 참조할 수 있게 해 주는 키워드인 셈입니다. 새로운 키워드를 도입하는 대신 이미 예약되어 있던 static 을 사용하기로 했습니다.

예제 #2 static:: 단순 사용

<?php

class A
{
    public static function who()
    {
        echo __CLASS__;
    }

    public static function test()
    {
        static::who(); // Here comes Late Static Bindings
    }
}

class B extends A
{
    public static function who()
    {
        echo __CLASS__;
    }
}

B::test();

?>

위 예제의 출력은 다음과 같습니다.

B

Note: 비정적(non-static) 문맥에서는 called class가 객체 인스턴스의 클래스가 됩니다. $this-> 는 같은 스코프에서 private 메서드를 호출하려고 하므로, static:: 을 사용하면 다른 결과가 나올 수 있습니다. 또 다른 차이점은 static:: 은 정적 프로퍼티만 참조할 수 있다는 점입니다.

예제 #3 비정적 문맥에서의 static:: 사용

<?php

class A
{
    private function foo()
    {
        echo "Success!\n";
    }

    public function test()
    {
        $this->foo();
        static::foo();
    }
}

class B extends A
{
   /* foo() will be copied to B, hence its scope will still be A and
   * the call be successful */
}

class C extends A
{
    private function foo()
    {
        /* Original method is replaced; the scope of the new one is C */
    }
}

$b = new B();
$b->test();

$c = new C();
try {
    $c->test();
} catch (Error $e) {
    echo $e->getMessage();
}

?>

위 예제의 출력은 다음과 같습니다.

Success!
Success!
Success!
Call to private method C::foo() from scope A

Note: Late static bindings의 해석은 완전히 해석된 정적 호출(fallback 없음)에서 멈춥니다. 반면 parent::self:: 같은 키워드를 사용한 정적 호출은 호출 정보를 계속 전달(forward)합니다.

예제 #4 Forwarding과 non-forwarding 호출

<?php

class A
{
    public static function foo()
    {
        static::who();
    }

    public static function who()
    {
        echo __CLASS__ . "\n";
    }
}

class B extends A
{
    public static function test()
    {
        A::foo();
        parent::foo();
        self::foo();
    }

    public static function who()
    {
        echo __CLASS__ . "\n";
    }
}

class C extends B
{
    public static function who()
    {
        echo __CLASS__ . "\n";
    }
}

C::test();

?>

위 예제의 출력은 다음과 같습니다.

A
C
C

더 알아보기

  • get_called_class() — 호출된 클래스의 이름을 반환
  • static:: 키워드와 정적 멤버의 실제 사용 예시는 위 예제들을 통해 확인할 수 있다.
  • PHP의 상속(extends)과 메서드 오버라이딩 동작 방식 관련 문서.