2021/11/25/PHP documentation gripe

From Woozle Writes Code
Jump to navigation Jump to search
Codeblog

Executive Summary

I got confused on a couple of points:

1. PHP7.4 doesn't recognize "mixed" as a type; it assumes it is a class name.
2. I find it confusing that return types are allowed to get more specific, but parameter types are only allowed to become less specific.

Item 2 especially just doesn't make sense to me; it seems to me that you would only ever want to get more specific in descendant classes.

I'd say that this ended up being primarily a PHP Gripe rather than a documentation gripe, but the documentation did use unnecessarily complex examples to illustrate the point... and they really should have had a table or something to clarify what is meant by "specific", especially with regard to non-objects.

The Investigation

The official PHP documentation says:

Covariance allows a child's method to return a more specific type than the return type of its parent's method. Whereas, contravariance allows a parameter type to be less specific in a child method, than that of its parent.

...and then goes on to note that both are supported in #PHP7.4 (which is what I am using).

However:

abstract class cClass1 {
    abstract function GetIt() : mixed;
}
abstract class cClass2 extends cClass1 {
    abstract function GetIt() : object;
}

PHP Fatal error: Declaration of cClass2::GetIt(): object must be compatible with cClass1::GetIt(): mixed in /home/htnet/site/git/ferreteria/base/tests/php2.php on line 6

...and also...

abstract class cClass1 {
    abstract function SetIt($v);
}
abstract class cClass2 extends cClass1 {
    abstract function SetIt(object $v);
}

PHP Fatal error: Declaration of cClass2::SetIt(object $v) must be compatible with cClass1::SetIt($v) in /home/htnet/site/git/ferreteria/base/tests/php2.php on line 6

So, kinda no.

The examples given in the documentation seem to be only about specificity of object types -- so I should be able to do this:

class cClassA {}
class cClassB extends cClassA {}
abstract class cClass1 {
    abstract function SetIt(cClassA $v);
}
abstract class cClass2 extends cClass1 {
    abstract function SetIt(cClassB $v);
}

...but, umm:

PHP Fatal error: Declaration of cClass2::SetIt(cClassB $v) must be compatible with cClass1::SetIt(cClassA $v) in /home/htnet/site/git/ferreteria/base/tests/php2.php on line 8

Looking at the actual example in the docs (condensed down a bit for readability):

abstract class Animal {
    protected string $name;
    public function __construct(string $name) { $this->name = $name; }
    abstract public function speak();
}
class Dog extends Animal {
    public function speak() { echo $this->name . " barks"; }
}
class Cat extends Animal {
    public function speak() { echo $this->name . " meows"; }
}
//---
interface AnimalShelter { public function adopt(string $name): Animal; }
class CatShelter implements AnimalShelter {
    public function adopt(string $name): Cat { return new Cat($name); } // returns Cat instead of Animal
}
class DogShelter implements AnimalShelter {
    public function adopt(string $name): Dog { return new Dog($name); } // returns Dog instead of Animal
}

$kitty = (new CatShelter)->adopt("Ricky");
$kitty->speak();
echo "\n";

$doggy = (new DogShelter)->adopt("Mavrick");
$doggy->speak();
echo "\n";

This works -- so what's the difference? Tentatively, it's the fact that the parent function is defined in an interface, not a class or trait -- but a test shows that this isn't the problem. Modifying AnimalShelter to be an abstract class instead of an interface gives us this:

abstract class AnimalShelter { abstract public function adopt(string $name): Animal; }
class CatShelter extends AnimalShelter {
    public function adopt(string $name): Cat { return new Cat($name); } // returns Cat instead of Animal
}

class DogShelter extends AnimalShelter {
    public function adopt(string $name): Dog { return new Dog($name); } // returns Dog instead of Animal
}

...which also works just fine.

Let's rewrite the doc example into the form in my examples at the top:

class ClassA {}
class ClassB extends ClassA {}

abstract class Class1 { abstract public function GetIt(): ClassA; }
abstract class Class2 extends Class1 { abstract public function GetIt(): ClassB; }

This works... and apparently the difference is that I'm dealing with return types rather than argument types. Changing the code to work with argument types produces the expected error:

class ClassA {}
class ClassB extends ClassA {}

abstract class Class1 { abstract public function SetIt(ClassA $o); }
abstract class Class2 extends Class1 { abstract public function SetIt(ClassB $o); }

PHP Fatal error: Declaration of Class2::SetIt(ClassB $o) must be compatible with Class1::SetIt(ClassA $o) in /home/htnet/site/git/ferreteria/base/tests/php2.php on line 7

Reversing the parameters does produce results that agree with the documentation, i.e. this works:

class ClassA {}
class ClassB extends ClassA {}

abstract class Class1 { abstract public function SetIt(ClassB $o); }
abstract class Class2 extends Class1 { abstract public function SetIt(ClassA $o); }

Also, at some point I was clued in to the fact that PHP was parsing "mixed" as a specific type (class?), not a pseudoclass. Removing "mixed" from the return-type works:

abstract class cClass1 {
    abstract function GetIt();
}
abstract class cClass2 extends cClass1 {
    abstract function GetIt() : object;
}

The same is true for function parameters:

abstract class cClass1 {
    abstract function SetIt(object $o);
}
abstract class cClass2 extends cClass1 {
    abstract function GetIt($o);
}