Forwarding and non-forwarding calls - Late Static Binding

99 Views Asked by At

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?

1

There are 1 best solutions below

11
IMSoP On BEST ANSWER

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

  • The calling context - normally, the class which was referred to when you called the method; this is what static:: refers to
  • The class where the actual definition of the method is located; this is what self:: refers to, and also what the magic constant __CLASS__ will resolve to

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:

  • PHP sets the calling context to class B, and looks for a method forward_test()
  • The definition of forward_test is in class A, and outputs our first line of debug
  • Now forward_test calls self::test(), and two things happen
    • the definition of test is looked up in class A, because that's where our definition of forward_test is
    • but the calling context is kept as class B, because self calls "forward" this information
  • So A::test() is called, but with a calling context of B, resulting in our second line of debug

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:

  • We call C::test()
  • The definition is in B, but the calling context is C
  • The call to parent::foo() resolves to the definition in A, but the calling context is forwarded, so is still C
  • So we are running A::foo() with a calling context of C
  • We then call static::who() which looks at the calling context to decide which method to run ("late static binding"), so it runs C::who()
  • We are now in a method defined in class C, and the magic constant __CLASS__ is the string 'C'

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