phpoopstaticlate-static-binding

Forwarding and non-forwarding calls - Late Static Binding


Note:

Late static bindings' resolution will stop at a fully resolved static call with no fallback. On the other hand, static calls using keywords like parent:: or self:: will forward the calling information.

Example #4 Forwarding and non-forwarding calls

https://www.php.net/manual/en/language.oop5.late-static-bindings.php

<?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(); // what? - Why is resolved to C if B's father is A?
        self::foo(); // what? - Why is it not resolved in B?
    }

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

C::test();
?>

Output:

A
C
C

I don't understand the use of parent and self in this example, could you please explain?


Solution

  • During each call to a static method, the PHP runtime knows two pieces of information:


    Let's look at a simpler example first:

    class A {
       public static function test() {
           echo 'Defined in ', self::class, '; called in ', static::class, "\n";
       }
    }
    class B extends A {
    }
    

    Calling A::test(); will output Defined in A; called in A - self and static refer to the same class.

    Calling B::test(); will output Defined in A; called in B - although there is no method called test() defined in class B, PHP still knows that you called it while referring to B.


    The "forwarding" comes in when you use self:: more than once - PHP keeps track of the original calling context:

    class A {
       public static function test() {
           echo __FUNCTION__,  ' defined in ', self::class, '; called in ', static::class, "\n";
       }
       public static function forward_test() {
           echo __FUNCTION__,  ' defined in ', self::class, '; called in ', static::class, "\n";
           self::test();
       }
    }
    class B extends A {
        public static function test() {
            echo 'this method is not called from forward_test()';
        }
    }
    B::forward_test();
    

    Outputs:

    forward_test defined in A; called in B
    test defined in A; called in B
    

    What happens here is this:


    Internally, you can imagine the compiler replacing each method call with a call_method function that needs the target class name, the method name, and the calling context.

    For self::test(), it can immediately replace self with the current class, which is A, and outputs something like:

    call_method(targetClass: 'A', methodName: 'test', callingContext: $currentCallingContext)
    

    Only when it runs, is $currentCallingContext defined, and forwarded.

    An explicit call to A::test() explicitly defines both the target class and the calling context:

    call_method(targetClass: 'A', methodName: 'test', callingContext: 'A')
    

    Conversely, a "Late Static Binding" call to static::test() defines the target class based on the calling context:

    call_method(targetClass: $currentCallingContext, methodName: 'test', callingContext: $currentCallingContext)
    

    The same thing is happening with the parent calls in the example in the question:


    An expanded example that shows some more information, and more variations:

    class A {
        public static function foo() {
            echo __FUNCTION__,  ' defined in ', self::class, '; called in ', static::class, "\n";
            static::who();
        }
    
        public static function who() {
            echo __FUNCTION__,  ' defined in ', self::class, '; called in ', static::class, "\n";
            echo __CLASS__."\n";
        }
    }
    
    class B extends A {
        public static function foo() {
            echo __FUNCTION__,  ' defined in ', self::class, '; called in ', static::class, "\n";
            static::who();
        }
        
        public static function test() {
            echo __FUNCTION__,  ' defined in ', self::class, '; called in ', static::class, "\n";
            echo "A::foo():\n";
            A::foo();
            echo "B::foo():\n";
            B::foo();
            echo "C::foo():\n";
            C::foo();
            echo "parent::foo():\n";
            parent::foo();
            echo "self::foo():\n";
            self::foo();
        }
    
        public static function who() {
            echo __FUNCTION__,  ' defined in ', self::class, '; called in ', static::class, "\n";
            echo __CLASS__."\n";
        }
    }
    class C extends B {
        public static function who() {
            echo __FUNCTION__,  ' defined in ', self::class, '; called in ', static::class, "\n";
            echo __CLASS__."\n";
        }
    }
    
    C::test();
    

    Outputs:

    test defined in B; called in C
    A::foo():
    foo defined in A; called in A
    who defined in A; called in A
    A
    B::foo():
    foo defined in B; called in B
    who defined in B; called in B
    B
    C::foo():
    foo defined in B; called in C
    who defined in C; called in C
    C
    parent::foo():
    foo defined in A; called in C
    who defined in C; called in C
    C
    self::foo():
    foo defined in B; called in C
    who defined in C; called in C
    C