3

I'm havinh a problem on PHP 5.3. I need to call a method by using __callStatic, but if I use it in a instancied object, PHP call __call instead.

Above a real life example:

<?php

    class A {
        function __call($method, $args){
            // Note: $this is defined!
            echo "Ops! Don't works. {$this->c}";
        }

        static function __callStatic($method, $args){
            echo 'Fine!';
        }
    }

    class B extends A {
        public $c = 'Real Ops!';

        function useCallStatic(){
            static::foo();
            // === A::foo();
            // === B::foo();
        }
    }

    $foo = new B();
    $foo->useCallStatic();

    // This works:
    // B::foo();

?>

Prints: Ops! Don't works. Real Ops!

Note that I call useCallStatic from a new B(). If I call directly works fine like B::foo().

What I can do to it works fine?

Joachim Sauer
  • 302,674
  • 57
  • 556
  • 614
David Rodrigues
  • 12,041
  • 16
  • 62
  • 90
  • What if you made `useCallStatic()` static? – BoltClock May 30 '11 at 23:33
  • At first glance, it seems like a bug. If you remove the `__call`, it works fine. – Matthew May 30 '11 at 23:35
  • @Bolt It works, but the problem is that I really need to call *objective*, one time that I will use `$this` on `useCallStatic`, but not on `foo`. – David Rodrigues May 30 '11 at 23:36
  • @Kon yeah! If I remove works fine. I'm really thinking that it is a bug. But I not found on PHP Bugs. – David Rodrigues May 30 '11 at 23:37
  • & myself. It's not a bug, even though it sort of looks like one. One must remember that `::` in the context of a class method does not mean "call this static function" but only "call this function after resolving which class to use." The `__call` method happens to have precedence over the `__callStatic` method when either would be appropriate. – Matthew May 31 '11 at 00:14

5 Answers5

7

After thinking about it, it's not really a bug. But it's definitely unintuitive.

<?php
class A
{
  public function foo()
  {
    static::bar();
  }

  public function bar()
  {
    echo "bar()\n";
  }
}

$a = new A();
$a->foo();

This is valid, and calls bar(). It does not mean call bar statically. It means to look up the current class name and call function bar if it exists, whether or not it's a static function.

To clarify a bit more: do you think parent::bar() means to call a static function called bar()? No, it means call whatever function is named bar().

Consider:

<?php
class A
{
  function __call($name, $args)
  {
    echo "__call()\n";
  }

  static function __callStatic($name, $ags)
  {
    echo "__callStatic()\n";
  }

  function regularMethod()
  {
    echo "regularMethod()\n";
  }

  static function staticMethod()
  {
    echo "staticMethod()\n";
  }

}

class B extends A
{
  function foo()
  {
    parent::nonExistant();   
    static::nonExistant();
    parent::regularMethod();
    parent::staticMethod(); 
  }
}

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

The parent::nonExistant() method invokes A::__call(), as does static::nonExistant(). Invoking A::__callStatic() for either call would be equally as valid! There is no way for PHP to know which one you want to be called. However, by design, PHP gives __call the priority when invoked from this sort of context.

parent::regularMethod() invokes the non static function regularMethod(). (Again, the :: operator does not mean "call this static function.") Likewise parent::staticMethod() invokes A::staticMethod() as one might expect.

To solve your problem:

You could manually call self::__callStatic('foo') in the inherited class.

Or within the __call method of class A filter against a known list and call self::__callStatic.

function __call($f, $a)
{
  if ($f == 'foo')
    return self::__callStatic($f, $a); 
  else
  {
  }
}

It's ugly, but at least people who extend the class won't need to do anything special.

Matthew
  • 47,584
  • 11
  • 86
  • 98
  • it is a good answer, but the problem is that the final method can be called by two modes: with context or static. Have a small difference between this methods, but it can't be ignored. It's just the problem. – David Rodrigues May 31 '11 at 12:05
  • It's a bad design on your end if a method with the same name can be called in two different ways (static or not). There is no way that I am aware of that you can force a static call while within a method function in the way you desire. – Matthew May 31 '11 at 12:24
  • Yeah. I need rethink my design. Maybe it is possible yet. Thanks for everybody. – David Rodrigues May 31 '11 at 13:01
2

This is probably due to the standard notation of calling parent methods, regardless of state. You could check if the $this keyword is set in the __call method and if not, make a call to __callStatic ?

EDIT: This isn't a bug per se, the static call is being made from an object context. Take a look at this bug thread on php.

JamesHalsall
  • 13,224
  • 4
  • 41
  • 66
  • @Jaitsu I can't. `__call` is important too, I need use it on some moments. Is possible to check if is a static call? – David Rodrigues May 30 '11 at 23:41
  • If you check that `$this` is set (i.e. `isset($this)`) then you can determine whether it's a static call or not (`$this` won't be set in a static call) – JamesHalsall May 30 '11 at 23:43
  • @Jaitsu it is, but `$this` is defined too :( I have tried this before. – David Rodrigues May 30 '11 at 23:44
  • What about... `$is_static = !(isset($this) && get_class($this) == __CLASS__)`. Scrap that, I'm forgetting this is in a non-static method... hmm – JamesHalsall May 30 '11 at 23:47
  • @Jaitsu don't works. `static::foo` and `$this->foo` returns same (`true`). – David Rodrigues May 30 '11 at 23:50
  • @David Rodrigues, I updated my answer. Not sure that there is a way around this without rethinking your design – JamesHalsall May 30 '11 at 23:55
  • @Jaitsu Yeah! I noted this. But don't make sense, because if I call `static::` I won't take context. If I want context, I need use `$this->` not? – David Rodrigues May 31 '11 at 00:03
  • @David, see my answer... Jaitsu is right. It's not a bug, it's how it's supposed to work. `static::` or `self::` (etc) does not mean to call a static method. It just affects how it looks up the class. (If you are in a static context, it will call a static method... but only because calling an instance method makes no sense in that case.) – Matthew May 31 '11 at 00:09
1

First, make sure you're using PHP 5.3.0 or newer, as that's when __callStatic was implemented. It all seems to work as expected, see this example:

<?php
class A {
    public function __call($method, $args) {
        echo 'A::__call, ' .
           var_export(array($method, $args), true), PHP_EOL;
    }

    /** As of PHP 5.3.0 */
    public static function __callStatic($method, $args) {
        echo 'A::__callStatic, ' .
           var_export(array($method, $args), true), PHP_EOL;
    }
}

$a = new A;
$a->foo('abc');
A::bar('123');

class B extends A {
    function invokeStatic($args) {
        echo 'B::invokeStatic', PHP_EOL;
        self::someStatic($args);
    }
}

$b = new B;
$b->invokeStatic('456');
?>

The output should be (or at least, it is for me):

A::__call, array (
  0 => 'foo',
  1 => 
  array (
    0 => 'abc',
  ),
)
A::__callStatic, array (
  0 => 'bar',
  1 => 
  array (
    0 => '123',
  ),
)
B::invokeStatic
A::__callStatic, array (
  0 => 'someStatic',
  1 => 
  array (
    0 => '456',
  ),
)

When I run your example above, I just get "Fine!" as the only output, which is exactly what I would have expected.

Exactly what version of PHP are you running? You can check with this:

$ php -r 'echo phpversion(), PHP_EOL;'
5.3.3-7+squeeze1
dossy
  • 1,617
  • 16
  • 26
  • 2
    Upgrade to 5.3.4 and you won't get that behavior. None of these while called inside a class method mean "call a static function": `parent::foo()`, `self::foo()`, `static::foo()`. They simply mean call function `foo()` after resolving the object on the left of the `::` operator. If both `__call` and `__callStatic` are defined and you are calling from within a non-static context, the former takes precedence. Neither way is "correct" or a "bug" because it's an ambiguous call. At least by giving `__call` priority, you can forward the call to `__callStatic` as my answer suggests. – Matthew May 31 '11 at 04:21
  • Interesting - I just tested on PHP 5.3.6, and indeed static resolution has changed (and IMHO, feels broken) as both of you have pointed out. The whole *point* of `__callStatic()` was to have PHP handle the routing of method invocations vs. static function calls, rather than overloading `__call()` ... – dossy May 31 '11 at 13:04
  • I would go so far as to say this IS a bug, because even `forward_static_call()` isn't invoking `__callStatic()` in the parent class, which is the whole point of the newly introduced function. Perhaps this will get fixed in PHP 5.3.7? Still, good to know that this was changed and between which release versions. – dossy May 31 '11 at 13:21
0

Inheritance of static methods is a bit weird in PHP. I suspect you need to redefine __callStatic in B

Halcyon
  • 57,230
  • 10
  • 89
  • 128
  • I can't redefine, it is a Framework that I'm developing. Is a bit annoying to ask the user to redefine a method – David Rodrigues May 30 '11 at 23:48
  • But if you do redefine, does it work as expected? I guess another solution would be to use reflection and figure out which functions are static? Is it really absolutely necessary to use `__call` at all? I'm not a big fan because of crazy issues like these and implied magic. Maybe just wait till we get _traits_? – Halcyon May 30 '11 at 23:54
  • if I redefine, it *don't* works too. :/ The second suggestion is good (*maybe* I use that). But if the static method don't exists on this case, I can't handle `__callStatic` to show an error, for instance. You get? – David Rodrigues May 30 '11 at 23:57
0

Good news

I make a BIG work arround, but it works pretty fine. This is the code:

class NoContext {
    public static function call($callback, $args = null){
        return call_user_func_array($callback, $args ?: array());
    }
}

You need to call NoContext::call (statically) like you call call_user_func or similar. It will to remove the $this context. I still thinks that it is a PHP bug, but...

To solve my problem, I do:

class B extends A {
    function useCallStatic(){
        NoContext::call('A::foo');
    }
}

And it will prints: Fine!.

Thanks everybody that help me. I really learn too much with your replies and I wait that my tips be userful sometime to you.

David Rodrigues
  • 12,041
  • 16
  • 62
  • 90