phpinheritancecovariant-return-types

Is there a good way to adhere to the covariance/contravariance rule while not writing repetitive code?


I have some code like follows. I tried to simplify the code to be easy to understand as much as possible.

class BaseFooClass {
    protected $keys = [];
    private $map = [];
    public function __construct($keyValuePairs) {
        foreach($this->keys as $key => $value) {
            $this->map[$key] = $keyValuePairs[$key] ?? null;
        }
    }
}

class ChildFooClass1 extends BaseFooClass {
    protected $keys = ['foo1_a', 'foo1_b'];
}

class ChildFooClass2 extends BaseFooClass {
    protected $keys = ['foo2_a', 'foo2_b', 'foo2_c'];
}

//... (there are like a hundred child foo classes)

abstract class BaseBarClass {
    protected $classIndex;
    protected function getFooBase(int $dataIndex) : ?BaseFooClass 
    {
        // GetRemoteData is assumed to be a global function, the important thing here is the retrieved data depends on classIndex and dataIndex
        // If $classIndex is 1, the $keyValuePairs will look like ['foo1_a' => value1, 'foo1_b' => value2] where value1 and value2 depend on $dataIndex
        $keyValuePairs = GetRemoteData($this->classIndex, $dataIndex);
        if (checkDataIntegrity($keyValuePairs)) {
            $class = "ChildFooClass" . $this->classIndex;
            return new $class($keyValuePairs);
        }
        return null;
    }
}

class ChildBarClass1 extends BaseBarClass {
    protected $classIndex=1;
    public function getFoo(int $dataIndex) : ?ChildFooClass1 
    {
        // this line violates covariance/contravariance rule
        return $this->getFooBase($dataIndex);
    }
}

class ChildBarClass2 extends BaseBarClass {
    protected $classIndex=2;
    // input to getFoo in each BarClass can be different
    public function getFoo($someInput) : ?ChildFooClass2
    {
        $dataIndex = $this->calculateDataIndex($someInput);
        // this line also violates covariance/contravariance rule
        return $this->getFooBase($dataIndex);
    }
}

The three critertia I want to meet are:

(1) I want to make sure ChildBarClass1::getFoo only returns ChildFooClass1, but also making sure BaseBarClass::getFooBase only returns a class that inherits BaseFooClass. Rule of thumb: make type declaration as strict as possible.

(2) I want to make sure there is no repeated code. I don't want to call GetRemoteData and checkDataIntegrity in every single getFoo function. In fact there are much more stuff than these two in the real code as well.

(3) I want to adhere to the covariance/contravariance rule. Currently the third last line shown in the code violates this rule as it uses a function that returns the parent foo class.

I cannot seem to come up with a good solution. Anything I came up with will either break either rules or make the code look like a "hack" and really ugly. If anyone can provide a good way to solve this problem, without breaking any of the three criteria or changing the general structure (i.e. every Foo class has a corresponding Bar class handling creating the Foo object), I will really appreciate.


Solution

  • Given your code, you could simply remove the return type from getFooBase() (or with php v8, make it : mixed). The method is not part of the public api. And as such there's no real loss here.

    As your getFoo()s return values are typed individually, you'd get a type error anyways if whatever getFooBase() returned wasn't suitable.