8

I know the following could potentially create problems elsewhere and is probably bad design, but I still would like to know why this fails (for my own edification):

class Test {
    // singleton
    private function __construct(){}
    private static $i;
    public static function instance(){
        if(!self::$i){
            self::$i = new Test();
        }
        return self::$i;
    }
    // pass static requests to the instance
    public static function __callStatic($method, $parameters){
        return call_user_func_array(array(self::instance(), $method), $parameters);
    }
    private $someVar = 1;
    public function getSomeVar(){
        return $this->someVar;
    }
}

print Test::getSomeVar();

The error is Using $this when not in object context

Obviously $this is unavailable from a static method, but the static method is handing it off to an instance method invokation via call_user_func_array, which should make $this the instance...

/EDIT

I'm aware that $this is not available in static context. However, this works:

print call_user_func_array(array(Test::instance(), 'getSomeVar'), array());

Which is exactly what's happening in the __callStatic overload, so something's amiss...

/EDIT 2

scope is definitely getting handled strangely. if you pull the singleton instance to any other class, it works as expected:

class Test {
    // singleton
    private function __construct(){}
    private static $i;
    public static function instance(){
        if(!self::$i){
            self::$i = new Blah();
        }
        return self::$i;
    }
    // pass static requests to the instance
    public static function __callStatic($method, $parameters){
        return call_user_func_array(array(static::instance(), $method), $parameters);
    }
}

class Blah {
    private $someVar = 1;
    public function getSomeVar(){
        return $this->someVar;
    }
}

print Test::getSomeVar();
hakre
  • 193,403
  • 52
  • 435
  • 836
momo
  • 3,885
  • 4
  • 33
  • 54
  • To quote PHP manual: `__callStatic() is triggered when invoking inaccessible methods in a static context.` - your method is public, make it private/protected and it will work as you expect ;) ref: https://www.php.net/manual/en/language.oop5.overloading.php#object.callstatic – jave.web Feb 25 '21 at 03:07
  • (Also the target ***static*** method should be defined as ***static*** too of course) – jave.web Feb 26 '21 at 07:42

2 Answers2

11

You can not staticfy your object methods through __callStatic calls in PHP. It will only be invoked when the method so far does not exist (incl. not being visible from the calling context). In your case Test::getSomeVar() is already defined.

Because of backwards compatibility, PHP does not check if only a static method exists, but actually if generally a method exists.

In your case you are calling the non-static method statically, so $this is not defined because __callStatic has not been invoked. If you would have enabled warnings and notices to the highest level (recommended for development), PHP would have warned you about that.

The correct usage therefore is:

echo Test::instance()->getSomeVar();

As with any other implementation of a Singleton.

So you are just using __callStatic wrong, it only works for methods not yet defined. Choose another tool/design for the job, like the aggregation example you use with your Blah class.

A further note:

You should normally prevent to use any static context in PHP anyway, especially as you exploit a lot of magic functionality here, too, which is another smell. It looks like you have a pile of design issues before you even finished your code. You will only increase the likelihood to run into hard to debug and maintain code with all this. But that just so you do not say in some time you have not been warned.

See Who needs singletons? in case you want to learn more.

hakre
  • 193,403
  • 52
  • 435
  • 836
  • @hakre thanks for the detailed response - i'll accept for clarity. this is not something i'm working on personally, but rather the result of investigation into some of the new popular frameworks and micro-frameworks. turns out the laravel, for example, a highly regarded (but fairly recent) entrant to the world of PHP frameworks, uses the pattern described in my last edit (the Blah class - which they refer to as a "payload"). you post explains why they separate the static instance. thanks – momo Nov 21 '12 at 20:54
  • 2
    If they use static calls for decoration, Lavarel is a framework to create global variables. Something normally superfluous, PHP offers global variables out of the box. Probably they don't really want this for part of the code, so they add static calls through *decoration*, a more common pattern: http://en.wikipedia.org/wiki/Decorator_pattern - However a static decorator will destroy flexbility which does not sound right for a framework, too - Also see http://i.imgur.com/RJEsz.png – hakre Nov 22 '12 at 08:47
  • @hakre interesting. they're calling it the "facade" pattern, but i did dig into the internals and they're using the same approach as is described in my "Blah" example. according to http://www.thenerdary.net/post/30859565484/laravel-4, looks like the next release (4) will be implementing the decorator pattern you described, still using static members to access the single entry point via a common application variable. – momo Nov 22 '12 at 23:12
  • 1
    http://en.wikipedia.org/wiki/Facade_pattern - the facade pattern has - at best a single - method. The static method accessors offered in your example reduces the flexibility to benefit from objects, thus is a one-way and I would not use common pattern names so much with it because in class based programming and with global (static) variables, all this goes into the wrong direction. Take care, you might get into confusion with these terms when discussing with other developers. – hakre Nov 23 '12 at 00:19
  • @hakre again, good info. i'm reading up on IoC and DI and think I get the point. – momo Nov 23 '12 at 00:57
  • **This is false** the true definition is `__callStatic() is triggered when invoking inaccessible methods in a static context.` - that applies to **ALL** even private methods, therefore it get's triggered for all methods that are UNACCESSIBLE or don't exist. – jave.web Feb 25 '21 at 03:05
  • @jave.web: yes, that is correct, _unaccessible_ is a good description, in some context perhaps _invisible_ as `private` etc. is commonly coined as _visibility_. However _existence_ and defining something out of existence does not inherently look wrong to me, especially given the context of the answer albeit as you correctly state there is very likely imprecision in the wording depending on context and reader (divers and inclusive of all identities, genders and sexual orientations). – hakre Feb 26 '21 at 07:01
  • @hakre especially in the context of the OP, the target method being public is what is **really** causing the issue, calling non-static statically is deprecated and should be used in general, but that's not the **real** issue here :) – jave.web Feb 26 '21 at 07:39
  • @jave.web: Yes, again that is absolutely correct and this only happens prior PHP 8, I warmheartedly welcome your ongoing very helpful additions. In my previous answers I missed to add introductory notes that at a later date or with a different light the answer might appear not as appealing as it likely was to be meant to the reader (divers and inclusive of all identities, genders and sexual orientations and _not_ limited to the sole person asking the question). Much appreciated! – hakre Feb 26 '21 at 11:04
  • @hakre I'm glad we're on the same note ;) – jave.web Feb 26 '21 at 11:11
1

It doesn't work like that. In a static call you have no instance, no object and no $this. Call static only works with static methods in which $this is unavailable.

The documentation says:

__callStatic() is triggered when invoking inaccessible methods in a static context.

I know this is not the answer you were hoping for. As you have anticipated: you don't need of overloading in PHP, just create your app and leave this thing out.

Shoe
  • 74,840
  • 36
  • 166
  • 272
  • The problem is a misunderstanding of what "in a static context" means. "Context" is "where" it's called. if you're calling `Widget::foo()` from within a Widget object. It's being called in an object context. The "::" does not mean "call statically" – Brad Kent Jan 31 '17 at 16:17