Home > Software engineering >  When overloading a parent method, why did PHP8.1 change towards deprecating incompatible return type
When overloading a parent method, why did PHP8.1 change towards deprecating incompatible return type

Time:02-15

I overloaded query method of mysqli class like so:

class MySql extends \mysqli
{
    function query(string $sql): ?MySqlResult  // line #30
    {
        $result = parent::query($sql);
        return new MySqlResult($result);
    }
}

in PHP8.0 that was not an issue. However, as of PHP8.1 I am now getting this error:

Deprecated: Return type of Repository\MySql\MySql::query($sql, $resultmode = null) should either be compatible with mysqli::query(string $query, int $result_mode = MYSQLI_STORE_RESULT): mysqli_result|bool, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in repository\src\MySql\MySql.php on line 30

I know how to fix the error - I will probably end up changing the name of the method, since I want to return a my own custom object.

Question

I am looking for an answer that captures the need for this change from a theoretical and object-oriented perspective, maybe using language theory, or comparing it to other languages.

Why was this change necessary? What was the need or what was the reason to make this change? What there a way to allow overloaded return types in PHP when extending a class?

CodePudding user response:

The change was necessary to bring consistency to the language. It makes for a very messy code if the overridden method can return a different type. It basically means that the overridden function does something entirely different. This behaviour was never allowed in PHP. Code like this would always throw:

class A {
    public function foo():string {
        return '';
    }
}

class B extends A {
    public function foo():int {
        return 1;
    }
}

The only problem was that the standard classes did not specify the return types internally. Many methods could not specify a type due to returning resources, mixed, union types, etc. This means that effectively they did not have a return type. PHP rules say that if the overridden method has no return type, the child method can specify (narrow) the type:

class A {
    public function foo() { // this could also be :mixed but that was only introduced in PHP 8
        return '';
    }
}

class B extends A {
    public function foo():int {
        return 1;
    }
}

So, you are asking the wrong question. The question isn't why the return type cannot be overridden since PHP 8.1, because that was always the case, but rather why PHP internal classes didn't specify the return type.

Since PHP 8.1 it became possible to declare most return types. However, due to the breaking change that this would cause, the internal methods only throw Deprecation message for the moment as compared to Fatal Error that would normally be produced. In PHP 9.0 all of this will be fixed.


For your particular case, you should be using composition rather than inheritance. Inheritance should be avoided most of the time, especially with internal classes. Composition offers more flexibility and is easier to test.

CodePudding user response:

The way to understand this is to think of function signatures as contracts. In the built-in mysqli class, we have the following signature:

public function query(string $query, int $result_mode = MYSQLI_STORE_RESULT): mysqli_result|bool

We can translate this into English something like this:

  • If I have an instance of mysqli ...
  • ... I am allowed to call the method query on it (public) ...
  • ... with a string as the first parameter ...
  • ... and optionally an int as the second parameter ...
  • ... and I will be returned either a mysqli_result object, or a boolean

So, the following code is guaranteed by the contract to run successfully:

assert($foo instanceof \mysqli);
$result = $foo->query('Select 1', MYSQLI_USE_RESULT);
assert($result instanceof mysqli_result || is_bool($result));

Now let's run that code with an instance of your proposed class:

assert($foo instanceof \mysqli);
// Success: `MySql` is a sub-type of `\mysqli`
$result = $foo->query('Select 1', MYSQLI_USE_RESULT);
// Success, but second argument ignored
assert($result instanceof mysqli_result || is_bool($result));
// Failure! Function may return null, which doesn't meet this assertion
// If the custom MysqlResult doesn't extend mysqli_result, that will also fail

So, as you can see, your class fails to meet the contract of the built-in class.

This was always logically an error, in that you were violating the contract "in spirit", but it's only recently become possible for PHP to enforce this. That's why it's not currently a hard error, so that you have a chance to fix older code which was "getting away with it".

  • Related