0

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?

Dharman
  • 30,962
  • 25
  • 85
  • 135
Dennis
  • 7,907
  • 11
  • 65
  • 115
  • 5
    Use composition over inheritance, especially with standard classes – Dharman Feb 14 '22 at 16:37
  • 1
    I also recommend looking over the [RFC](https://wiki.php.net/rfc/internal_method_return_types) for the change. – Will B. Feb 14 '22 at 16:59
  • 2
    Sidenote: What you are trying to do violates the [Liskov substitution principle](https://en.wikipedia.org/wiki/Liskov_substitution_principle). It has only become apparent now that PHP actually specified the return type in the mysqli class but this code was always in violation of OOP – apokryfos Feb 14 '22 at 17:04
  • I have made a reference Q&A for this message, to avoid having lots of duplicates with slightly different examples: https://stackoverflow.com/q/71133749/157957 – IMSoP Feb 15 '22 at 21:45

2 Answers2

5

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 built-in 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'S built-in 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 methods of built-in classes 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 built-in classes. Composition offers more flexibility and is easier to test.

Dharman
  • 30,962
  • 25
  • 85
  • 135
  • 1
    I'm not sure the details about which types could be declared are all that relevant; even straight-forward cases like `Countable::count` returning `int` weren't declared, even though they could have been in 7.0. The important thing is that adding them in one go would have immediately caused a breaking change, which is why this deprecation period was added. – IMSoP Feb 15 '22 at 21:04
  • Thank you. I got a question on composition vs inheritance. My `MySql` class is providing an extension to `mysqli` interface. Why should it not be inherited? I thought that in O of SOLID (“classes should be open for extension, but closed for modification.”) this was a perfect candidate for en extension, aka inheritance. Why is it not the case here? I am not building a whole different object, I am extending native `mysqli` with a custom `MySql` class that has more features than `mysqli` offers. – Dennis Feb 16 '22 at 07:17
  • When you say easier to test.... what makes it harder to test here? (I'm trying to get a general feel for inheritance vs composition theory ...) – Dennis Feb 16 '22 at 07:47
  • 1
    @Dennis I encourage you to read https://r.je/you-do-not-need-inheritance-oop and watch https://www.youtube.com/watch?v=djd9zdlzyuA – Dharman Feb 16 '22 at 16:05
  • thank you. It looks like a case can be made to remove inheritance from languages altogether, akin to removing `goto`. – Dennis Feb 18 '22 at 04:30
3

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".

IMSoP
  • 89,526
  • 13
  • 117
  • 169
  • thank you, that makes sense. To use inheritance here I'd essentially need to do more work and to also be bound by the contract of the parent class. I do think now that I'm better off using the composition because I want to develop my own custom class, that does not need to be bound by the contracts of `mysqli` and `mysqli_result` classes. Using composition will also rid my class of all the extra variables used by the `mysqli` and `mysqli_result` classes that my class does not need to use – Dennis Feb 17 '22 at 08:02