1

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
    `A::foo()` works the same inside or outside the class, the context to `C` is lost. Both `parent` and `self` preserve the context though, which is `C`, so `C::who` is called in the end. – deceze Feb 28 '23 at 15:58
  • `self` and `parent` are relative to the actual class that the method is called from, not the class they're defined in. That's why it's called *late* binding. So if you do `B::test()` then `self` is `B`; if you do `C::test()` then `self` is `C`. – Barmar Feb 28 '23 at 15:59
  • hi @Barmar, but in the doc says that "*Static references to the current class like self:: or __CLASS__ are resolved using the class in which the function belongs, as in where it was defined*" –  Feb 28 '23 at 16:14
  • 1
    @Barmar Coder23 is correct, `self` and `parent` do _not_ perform Late Static Binding, they refer to the current class definition, and its parent; the effect being observed is that they _forward_ the calling context, so that when you _later_ use Late Static Binding, with the `static::` prefix, PHP "remembers" that the context was originally `C`, even though a chain of `self` and `parent` calls have happened in between. – IMSoP Feb 28 '23 at 16:19
  • hi @deceze +1 But, `parent` and `self` preserve the context because later I use `static`, right? i.e., by default `self` refers to the defining class and `parent` to the parent class, right? –  Feb 28 '23 at 17:45
  • 2
    `parent::foo()` explicitly calls `A::foo`, while `self::foo` calls `B::foo` which in the end is also `A::foo`. Both preserve the *context* of `C` though, so `static::who` resolves to `C::who` in both cases. – deceze Feb 28 '23 at 18:20
  • thank you very much @deceze♦ +1, good of you to clarify this, I was doubting my knowledge of `parent` and `self` haha –  Mar 01 '23 at 11:20
  • hi @deceze♦ +1 `self::foo()` ends up calling `A::foo()` because `B` inherits from `A`, right? –  Mar 01 '23 at 11:24
  • 2
    @George Yes. Because `B` doesn't define any `foo` method and thus inherits `A`'s. – deceze Mar 01 '23 at 11:36

1 Answers1

0

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
IMSoP
  • 89,526
  • 13
  • 117
  • 169
  • hi @IMSoP thank you. "*So A::test() is called, but with a calling context of B, resulting in our second line of debug*" - that happens because `self::test()` resolves to `A::test()` inside `forward_test()`, right?, i.e. the functionality of `self::` and `parent::` remains the same, it refers to the defining class and `parent::` to the parent class (obviously), right? –  Feb 28 '23 at 17:41
  • 1
    @Coder23 Yes, because the code of `forward_test()` is physically inside the definition of class A, `self::test()` always means `A::test()`. It's just that *when it's called*, it's called with a magic parameter of the "calling context". Something like `call_method(className: 'A', methodName: 'test', callingContext: $currentCallingContext)`. Whereas writing `A::test()` explicitly would be `call_method(className: 'A', methodName: 'test', callingContext: 'A')` – IMSoP Feb 28 '23 at 17:55
  • great @IMSoP+1, we can say then, that always "by default" the calling context is passed, but it is only "visible", it only makes sense when you use `static::`, right? –  Feb 28 '23 at 18:06
  • 1
    @Coder23 Yes, that sounds like a reasonable way of looking at it. – IMSoP Feb 28 '23 at 18:07
  • excellent @IMSoP +1, this answer is worth gold, I didn't know these "hidden" uses of `parent` and `self` combined with `static` (I thought I already knew everything about LSB). One last question. `self::class`, `static::class`, `parent::class::` is a **reliable way** to access the class they resolve to, right? –  Feb 28 '23 at 18:17
  • 1
    @Coder23 I believe so; it's not actually very well documented (or I'm looking in the wrong place) but I believe `something::class` always uses the same resolution rules that `something::some_method()` would. – IMSoP Feb 28 '23 at 18:21
  • great @IMSoP +1, thanks a lot, definitely marked as best answer. –  Feb 28 '23 at 18:22
  • hi @IMSoP +1. In the example question, when executing `self::foo();`, does PHP realize that we use `static::` in `foo()`, or how does it get to class A if it initially resolves to `B::foo()`? –  Feb 28 '23 at 18:38
  • i.e., how does it "jump" from class B to class A if `self` is "fixed" to the current class it defines? @IMSoP –  Feb 28 '23 at 18:41
  • 2
    @GeorgeMeijer When you call `self::foo()` in class `B`, the compiler issues something like `call_method(targetClass: 'B', methodName: 'foo', callingContext: $currentCallingContext)` (the real implementation will be rather more complicated, I'm sure). When that runs, it needs to actually find some PHP code to run; it finds that `B` doesn't have an implementation for `foo`, but *inherits* the implementation from `A`; so that's the code that ends up running. The existence of `static::` makes no difference to how that plays out, `$currentCallingContext` is just passed along in case it's needed. – IMSoP Feb 28 '23 at 18:49
  • ok +1 @IMSoP, now I get it, I forgot that `B` inherits from `A`, that's why it ends up invoking `A`'s implementation, that has nothing to do with `self`, that's merely how inheritance works, right? –  Feb 28 '23 at 18:57