php.net |  support |  documentation |  report a bug |  advanced search |  search howto |  statistics |  random bug |  login
Doc Bug #81436 Unclear behaviors with __call/__callStatic/forward_static_call and LSB
Submitted: 2021-09-14 11:20 UTC Modified: 2021-09-16 08:23 UTC
Votes:1
Avg. Score:5.0 ± 0.0
Reproduced:1 of 1 (100.0%)
Same Version:1 (100.0%)
Same OS:1 (100.0%)
From: dev98 at outlook dot de Assigned:
Status: Open Package: Class/Object related
PHP Version: Irrelevant OS: Windows x64, Manjaro Linux x64
Private report: No CVE-ID: None
Have you experienced this issue?
Rate the importance of this bug to you:

 [2021-09-14 11:20 UTC] dev98 at outlook dot de
Description:
------------
When we utilize the __callStatic function with an appropriate static call on the same class the magic function is defined while being in an object scope, for some reason, the __call method will get invoked. This seems very unintuitive as the php doc states:

"__call() is triggered when invoking inaccessible methods in an object context."

We also tested it in 7.3.10-nts-Win32-VC15-x64, 5.6.40-nts-Win32-VC11-x86 and 8.0.10-nts-Linux-x64. Results are consistent.

Testscript is crunched down in lines. For a nice formatting and code comments please visit https://stackoverflow.com/questions/69161169/unexpected-behavior-when-utilizing-call-and-callstatic-from-different-scopes

I have read the threads with bug id 51176 & 77344
and you might argue that "that's not a static call operator, its a scope resolution operator." But since 2008 calling non static members statically causes error messages. Further more when you replace the calls on the non existing members with an forward_static_call, which literally is documented with "Call a static method", it results in the same behavior. Reproducible with replacing the calls
A::BLA(), B::BLA() with forward_static_call(array("A", "BLA")),        forward_static_call(array("B", "BLA"));

I understand the behavior daniel described in bug.php?id=51176 [2013-08-17 13:59 UTC] daniel dot ruthardt at zoesolutions dot eu, but either the __call magic function catches the wrong call or forward_static_call should be rather called forward_scope_resolution_operator_call. Ive got the feeling here that there is not a consent about how the :: operator should actually work.

Thanks in advance and lovely greetings from Germany

Test script:
---------------
class B {
    public function __call($name, $arguments) {print("  __CALL on B\r\n");}
    public static function __callStatic($name, $arguments) {print("  __STATICCALL on B\r\n");}
}
class A {
    public function __call($name, $arguments) {print("  __CALL on A\r\n");}
    public static function __callStatic($name, $arguments) {print("  __STATICCALL on A\r\n");}
    
	public static function doMagicCall_from_staticContext() {
        A::BLA(); // expect A::__callStatic, works
        B::BLA(); // expect B::__callStatic, works
    }
    public function doMagicCall_from_objectContext() {
        A::BLA(); // expect A::__callStatic, got $this->__call(), prints "__CALL on A"
        B::BLA(); // expect B::__callStatic, works
    }
}

$a = new A();
print("1. Call from static context:\r\n");
A::doMagicCall_from_staticContext();
print("\r\n2. Call from object context:\r\n");
$a->doMagicCall_from_objectContext(); // provokes the behavior

Expected result:
----------------
1. Call from static context:
  __STATICCALL on A
  __STATICCALL on B

2. Call from object context:
  __STATICCALL on A
  __STATICCALL on B

Actual result:
--------------
1. Call from static context:
  __STATICCALL on A
  __STATICCALL on B

2. Call from object context:
  __CALL on A
  __STATICCALL on B

Patches

Add a Patch

Pull Requests

Add a Pull Request

History

AllCommentsChangesGit/SVN commitsRelated reports
 [2021-09-14 13:13 UTC] requinix@php.net
-Status: Open +Status: Feedback
 [2021-09-14 13:13 UTC] requinix@php.net
> and you might argue that "that's not a static call operator, its a scope resolution operator."

But that's exactly the answer: the double colon is to resolve how a method should be called, not to do a static call. "A::BLA" will resolve to an instance call if you're in the scope of an instance of A or a static call if not.

A::doMagicCall_from_staticContext:
- has no instance of A so A::BLA() is a static method call
- has no instance of B so B::BLA() is a static method call

A->doMagicCall_from_objectContext:
- inside an instance of A so A::BLA() is an instanced method call
- has no instance of B so B::BLA() is a static method call

Look at the typical child constructor pattern:

  public function __construct() {
    parent::__construct();
    $this->foo = "bar";
  }

parent::__construct is not called statically, and neither "parent" nor "__construct" have any kind of special meaning that would secretly change ::'s scope resolution process to work differently than it normally would.

__call and __callStatic only enter the picture at the very last minute when PHP is actually trying to call the method and when it finds out that "BLA" does not exist. By that point it has already decided whether "BLA" should be an instanced call or not, and it will select __call or __callStatic appropriately.
 [2021-09-14 13:16 UTC] requinix@php.net
Of course it's only once I hit the Submit button do I see an edit to make:

> the double colon is to resolve how a method should be called

Makes more sense to say it resolves *where* a method should be called.
 [2021-09-14 13:31 UTC] nerdbeere2k at gmail dot com
I fail to understand why this works the way it does as well.
If you replace the A::BLA code with:
forward_static_call(array("A", "BLA"))

I feel it should definitely call the static method BLA on A and not anything else.

Or asked differently: How would one call A::BLA from an object context? Is this impossible?
 [2021-09-14 14:47 UTC] requinix@php.net
> If you replace the A::BLA code with:
> forward_static_call(array("A", "BLA"))
> I feel it should definitely call the static method BLA on A and not anything else.

Consider that "static" methods are not so much "this method only exists statically" but rather "this method can be called statically". Static-ness is more like an invocation capability than it is a requirement; you can call static methods with "$this->" just fine (though it's poor practice to do so).
https://3v4l.org/uO01h

So it's not quite correct to say "the static method BLA".

If you follow that train of thought further, a forward_static_call()ed A::BLA is very much like a regular A::BLA call. The problem that forward_static_call() solves is one that stems from the fact that PHP allows static things to be inherited down a class hierarchy [1], and that otherwise there is no way for a child class to call a parent class's static method while retaining the child's "static" information.

If that's confusing, think about what happens with object instances. $this always refers to the child-est members in the inheritance hierarchy <https://3v4l.org/H1T04>. Static members need something similar.

Before PHP 5.3 and late static bindings [2], there was no similar mechanism for static members. "self" always indicated the class where it was used. LSB changed how static members were handled internally and introduced/borrowed "static" to be like the static version of $this, ie. to refer to the child-est members.

...except there's a slight problem here because PHP needs some way to know who that so-called child is. It was already very capable of handling that with instance members because there's, you know, the instance. But there is no instance in memory when dealing with static method calls. Thus LSB's "runtime information" [3] to fill in the gap. Then forward_static_call() was created to provide users with a way of saying that a static method somewhere should be invoked, except rather than *reset* the "runtime information" like would normally happen with a static call [4], PHP should *retain and reuse* it.

I think I may have digressed a bit...

> Or asked differently: How would one call A::BLA from an object context? Is this impossible?

You mean is it possible here to trigger A's __callStatic() with $name="BLA"?
As far as I know: no, it is not currently possible to convince PHP to execute in that way.

I see bug #65466 but I feel like I've seen one or two bug reports/feature requests about the same phenomenon more recently than that. Also, a concrete use case would be helpful - especially one that can't be resolved in a more logical way than needing __callStatic.

On that note, keep in mind that __callStatic does not mean "you attempted to call a static method which did not exist". It means "you attempted to call a method which did not exist and at a time when you did not have an instance of the class on hand". Remember what I said at the beginning of this wall of text: static does not mean it's *only* static but that it *can be* invoked statically.

Personally, I don't use __callStatic at all. Too many nuances about how and when static things work.


[1] Don't care for it myself, but whether this makes sense or not is beside the point.
[2] https://www.php.net/manual/en/language.oop5.late-static-bindings.php
[3] In the LSB docs, 
  > This feature was named "late static bindings" with an internal perspective in
  > mind. "Late binding" comes from the fact that static:: will not be resolved
  > using the class where the method is defined but it will rather be computed
  > using runtime information.
[4] IIRC there used to be ways to trick PHP into leaking $this or a static method's scope into unrelated code, but I believe those have all been patched/fixed/removed.
 [2021-09-16 07:22 UTC] dev98 at outlook dot de
Requinix thanks for the answers. I really appreciate your effort and time put into it. I fully understand your points.

Maybe the php.net documentation just needs a few additional notes regarding the forward_static_call and __callStatic topic, as you stated that there have been a few reports about that in the past.
 [2021-09-16 08:23 UTC] requinix@php.net
-Summary: Unexpected behavior when utilizing __call and __callStatic from different scope +Summary: Unclear behaviors with __call/__callStatic/forward_static_call and LSB -Status: Feedback +Status: Open -Type: Bug +Type: Documentation Problem -Package: Scripting Engine problem +Package: Class/Object related
 [2021-09-16 08:23 UTC] requinix@php.net
> Maybe the php.net documentation just needs a few additional notes regarding the
> forward_static_call and __callStatic topic, as you stated that there have been a
> few reports about that in the past.

Sure.
 
PHP Copyright © 2001-2021 The PHP Group
All rights reserved.
Last updated: Thu Oct 28 21:03:36 2021 UTC