3

I guess that the "... must be compatible with ..." is in place to enforce Liskov Substitution Principle. But I am not sure this is what LSP says?

I have a code like this:

class General
{
    public static function create(): General
    {
        return new static;
    }

    public function doSomething()
    {
        echo get_class($this) . ' speaking!' . PHP_EOL;
    }
}


class Specific extends General
{
    public static function create(): Specific
    {
        return parent::create();
    }
}


function doSomething(General $object)
{
    $object->doSomething();
}

doSomething(General::create());
doSomething(Specific::create());

Which produces:

PHP Fatal error: Declaration of Specific::create(): Specific must be compatible with General::create(): General in ...

The LSP is often cited as:

Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.

And this is not violated here as far as I understand. So what is wrong here? Is it some special restriction that doesn't have anything to do with LSP? Is it a bug in PHP? Am I doing something wrong without knowing?

UPDATE: I found this thread (Parameter type covariance in specializations). I understand and fully agree that the example there violates LSP. But my situation is different (reverse in fact).

Josef Sábl
  • 7,538
  • 9
  • 54
  • 66
  • 2
    I believe it's your return type declarations. Code that relies on `Specific::create` returning a `Specific` would break if it got a `General` class instead when calling `General::create`. – ceejayoz Apr 16 '18 at 14:39
  • Okay, but code written for Specific class cannot get the General class because typehint would prevent it from passing. The Specific extends General, not the other way around. E. g. `doSomethingDifferent(Specific $object)` can never get General class as $object. – Josef Sábl Apr 16 '18 at 14:48
  • 2
    I think you've run into the same issue I did when working with interfaces. You're expecting covariant behaviour but PHP only supports invariant behaviour. – GordonM Apr 16 '18 at 14:50
  • https://stackoverflow.com/questions/39068983/php-7-interfaces-return-type-hinting-and-self – GordonM Apr 16 '18 at 14:50

2 Answers2

1

http://php.net/manual/en/functions.returning-values.php

When overriding a parent method, the child's method must match any return type declaration on the parent. If the parent doesn't define a return type, then the child method may do so.

Your Specific::create function should indicate a General return type.

Otherwise, code written for Specific::create would potentially break when running General::create, as it would receive a different class.

ceejayoz
  • 176,543
  • 40
  • 303
  • 368
  • Thank you! Could you please elaborate on your last sentence? I cannot quite imagine the situation. – Josef Sábl Apr 16 '18 at 14:45
  • So the LSP says "Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it." If your derived class returns something fundamentally different (even if, in your case, it's not *functionally* different - **yet**), the base class can't necessarily rely on that derived class's return values. – ceejayoz Apr 16 '18 at 14:47
  • I understand. But my point is that this is not true in this case. Because the doSomething function (the one that references the base class, General) is always able to use object of derived class (Specific) which is derived from General. That means that doSomething gets the class it expects (General). Specific IS General as well as it is Specific. You know what I mean? I got lost in explaining a bit :-) – Josef Sábl Apr 16 '18 at 14:54
0

The LSP states that method parameters must be contravariant while return values must be covariant.

In your case you have covariant return types which satisfies the LSP.

The problem is in PHP itself. This restriction has nothing to do with LSP, it's just that earlier versions of PHP haven't implemented that yet.

Since PHP 7.2 (7.4) it now supports fully the LSP, i.e. parameter contravariance and return value covariance: https://www.php.net/manual/en/language.oop5.variance.php

UPD. But your code contains another issue: your \Specific::create() method must return an instance of Specific as you have stated it in its signature, but it tries to return the value returned by \General::create() which is stated to be General (and we know that instances of General are not instanceof Specific). This is quite misleading. E.g. PhpStorm will warn you about it: Return value is expected to be 'Specific', 'General' returned. But PHP doesn't throw an error about that.

This could be fixed by adding a PhpDoc to the \General::create():

class General
{
    /**
     * @return static
     */
    public static function create(): General
    {
        return new static;
    }
whyer
  • 783
  • 5
  • 16