phpcovariancereturn-typephpdoc

PHP - Spoofing return-type covariance for code prediction


Consider:

abstract class BaseModel {
    protected static string $dataObjectClass;

    public function toDataObject(): ?IDataObject
    {
        return static::$dataObjectClass::from($this);
    }
}

class FooModel extends BaseModel {
    protected static string $dataObjectClass = FooDataObject::class;
}

$do = (new FooModel)->toDataObject();

Code prediction in IDEs like PHPStorm on $do would indicate a return type of null|IDataObject and... not tell me anything of value about its nature as an instance of FooDataObject except what might be defined in IDataObject.

Yet I can know with certainty as the programmer that the returned object will either be null or an instance of FooDataObject.

Are there any tricks to inform IDEs that the returned type can be a FooDataObject?

Two half-baked, hacky solutions I've tried are:

  1. Override toDataObject() in the subclass and expand the return type to be null|IDataObject|FooDataObject. This works to unlock the properties and methods of FooDataObject for prediction but it means overloading a method for one sole, silly purpose -- ugly overhead.
  2. Include @method FooDataObject toDataObject() in the subclass phpdoc. This is far leaner and cleaner but, of course, the @method notation seems to have zero support for multiple return types. Try to do @method null|FooDataObject toDataObject() and PHPStorm (at least) only recognizes the first type.

So any hints or tricks would be appreciated. I'm a stickler for code prediction and I want to establish a practice in this application that makes coding easier tomorrow.


Solution

  • When I test @method using the following I do indeed get auto completion from PhpStorm:

    interface I{}
    class Data implements I{}
    
    abstract class P
    {
        public function getThing(): ?I
        {
            return null;
        }
    }
    
    
    /**
     * @method Data getThing()
     */
    class C extends P {}
    
    (new C)->getThing();
    

    enter image description here

    As you mentioned, overriding the method only to change the return type does add redundancies, but you also get runtime type checking which may or may not be advantageous to you, especially because you can explicitly remove the null possibility from the return.

    interface I{}
    class Data implements I{}
    
    abstract class P
    {
        public function getThing(): ?I
        {
            return null;
        }
    }
    
    class C extends P {
        public function getThing(): Data
        {
            return new Data;
        }
    }
    
    var_dump((new C)->getThing());
    

    Demo: https://3v4l.org/hdscV

    EDIT Sorry, I forgot about the union type portion, but that is also working for me:

    interface I{}
    class Data implements I{}
    
    abstract class P
    {
        public function getThing(): null|I
        {
            return null;
        }
    }
    
    /**
     * @method null|P getThing()
     */
    class C extends P {
    }
    

    enter image description here