Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why can't PHP traits have static abstract methods?

With late static binding in PHP v5.3, one can usefully declare static methods in interfaces; with traits in PHP v5.4, methods can be either static or abstract but not both. This appears to be illogical and inconsistent.

In particular, suppose one has an interface for which a trait provides all implementation, except for a static method; unless that method is declared in the trait, static analysers balk at any references thereto from within the trait. But providing a concrete implementation within the trait no longer forces implementing/using classes to provide their own implementation—which is dangerous; abstract static would be ideal, but is not allowed.

What is the explanation for this contradiction? How would you recommend resolving this problem?

interface MyInterface
{
    public static function getSetting();
    public function doSomethingWithSetting();
}

trait MyTrait
{
    public abstract static function getSetting(); // I want this...

    public function doSomethingWithSetting() {
        $setting = static::getSetting(); // ...so that I can do this
        /* ... */
    }
}

class MyClass implements MyInterface
{
    use MyTrait;
    public static function getSetting() { return /* ... */ }
}
like image 973
eggyal Avatar asked Sep 24 '15 12:09

eggyal


1 Answers

TL;DR: As of PHP 7, you can. Before then, you could define abstract static on trait, but internals deemed it bad practice.


Strictly, abstract means sub-class must implement, and static means code for this specific class only. Taken together, abstract static means "sub-class must implement code for this specific class only". Totally orthogonal concepts.

But... PHP 5.3+ supports static inheritance thanks to LSB. So we actually open that definition up a bit: self takes the former definition of static, while static becomes "code for this specific class or any of its sub-classes". The new definition of abstract static is "sub-class must implement code for this specific class or any of its sub-classes". This can lead some folks, who think of static in the strict sense, to confusion. See for example bug #53081.

What makes trait so special to elicit this warning? Well, take a look at the engine code that implements the notice:

if (ptr->flags & ZEND_ACC_STATIC && (!scope || !(scope->ce_flags & ZEND_ACC_INTERFACE))) {
    zend_error(error_type, "Static function %s%s%s() cannot be abstract", scope ? ZSTR_VAL(scope->name) : "", scope ? "::" : "", ptr->fname);
}

That code says the only place an abstract static is allowed is within an interface. It's not unique to traits, it's unique to the definition of abstract static. Why? Well, notice there's a slight corner case in our definition:

sub-class must implement code for this specific class or any of its sub-classes

With this code:

abstract class Foo {
    abstract public static function get();
}

That definition means I should be able to call Foo::get. After all Foo is a class (see that keyword "class" there) and in the strict definition, get is meant to be implemented in that class Foo. But clearly that makes no sense, because well, we're back to the orthogonality of strict static.

If you try it in PHP, you get the only rationale response possible:

Cannot call abstract method Foo::get()

So because PHP added static inheritance, it has to deal with these corner cases. Such is the nature of features. Some other languages (C#, Java, etc.) don't have this problem, because they adopt the strict definition and simply don't allow abstract static. To get rid of this corner case, and simplify the engine, we may enforce this "abstract static only in interface" rule in the future. Hence, E_STRICT.


I would use a service delegate to solve the problem:

I have common method I want to use in several classes. This common method relies on a static method that must be defined externally to the common code.

trait MyTrait
{
    public function doSomethingWithSetting() {
        $service = new MyService($this);
        return $service->doSomethingWithSetting();
    }
}

class MyService
{
    public function __construct(MyInterface $object) {
        $this->object = $object;
    }
    public function doSomethingWithSetting() {
        $setting = $this->object->getSetting();
        return $setting;
    }
}

Feels a bit Rube Goldberg though. Probably would look at the motivation for statics and consider refactoring them out.

like image 94
bishop Avatar answered Nov 05 '22 00:11

bishop