PHP Type Hinting Not Getting Along with Interfaces and Abstract Classes

Php type hinting not getting along with interfaces and abstract classes?

php doesn't seem to be recognizing the signatures of AnAbstractClass::method and ConcreteClass::method as compatible.

PHP is correct, they're not compatible. By allowing only instances of AClass (or its children) to be passed to ConcreteClass::method, you're breaking the contract that AnAbstractClass provides: Any of its subclasses must accept AnInterface as an argument to its method().

If your example worked, and I had another class BClass implementing AnInterface, we'd have a situation where according to AnAbstractClass, method() should accept instances of BClass, while according to ConcreteClass, it shouldn't.

Change your signature for ConcreteClass::method to match that of AnAbstractClass::method.

PHP type hint interface in abstract methods and type hint interface's child class in method implementation

One problem is classes D and E requiring concrete classes, B and C respectively. Now since you should be discouraging class inheritance I would avoid putting concrete classes in method signatures as much as I can. If your class needs to use a concrete class don’t leak that information, keep it private.

So it is better to have:

public function foo(SomethingAbstract $noIdeaWhatIAmButIDoStuff) {
$concrete = SomethingConcreteThatMakesUseOfAbstract($noIdeaWhatIAmButIDoStuff);
}

than:

public function foo(SomethingConcrete $tmi) {

}

I would recommend to think along these lines:

abstract class DE {

public abstract function foo(A $a);

}

class D extends DE {

public function foo(A $a) {
$b = new B($a);
//do with $b what you please
}

}

class E extends DE {

public function foo(A $a) {
$c = new C($a);
//now you can use $c which will leverage $a
}

}

If B and C objects need also to implement A (like in your example) then you have a decorator pattern.

Abstract class or interface where declaration method signature allows type hinting of child classes

I would argue for something along these lines. Note how I'm extending core concepts of objectively collecting, while behaving as a self-validating subject. Then I compose these various constructs into a knowable, concrete composite.

interface Transportable {};
interface User {};
interface Collected
{
public function isValid($item): bool;

// Contract, add is deterministic, require no return
public function add(Transportable $item): void;
}

trait SanityChecked
{
public function isValid($item): bool
{
return true;
}
}

abstract class Collector implements Collected
{
use SanityChecked;

private $items = [];

public function add(Transportable $item): void
{
if ($this->isValid($item) && $this->items[] = $item) {
return;
}

throw new Exception('This is the not the droid we were looking for.');
}
}

class Users extends Collector
{
public function isValid($test): bool
{
return $test instanceof User;
}
}

Which can be mocked as:

$users = new Users();

$users->add(new class() implements Transportable, User {});

echo 'Added first!'.PHP_EOL;

$users->add(new class() implements User {}); // Sorry, error.

echo 'Added second!'.PHP_EOL;

https://3v4l.org/O2qfJ

Another way of looking at it is to further extend behavior of trait:

trait ValidatesAsUser
{
public function isValid(Transportable $user): bool
{
return $user instanceof User;
}
}

class PortalUsers extends Collector
{
use ValidatesAsUser;
}

class ContentEditors extends PortalUsers {}

class Authors extends ContentEditors {}

class AuthorsWithPublishedStoriesByRating extends Authors {}

I think the last part about projections is particularly interesting.

PHP class implements interface where methods type hint interface getting compatibility errors

Well, according to this, Php type hinting not getting along with interfaces and abstract classes?

PHP intentionally does this. I don't really see why, since you can normally type hint interfaces and it seems like as long as you as a programmer were using the interfaces properly (i.e. requiring what you're using) then logically it'd be fine.

But, I guess it's intentional to work this way so I'll just change my code :/ Bummer, seems less descriptive to me, but that's alright.

Should concrete class follow the type hint of its interface?

Short answer:

This is expected behaviour since 7.2, and interfaces type hints are enforced to an extent; but implementing classes may omit the interface's type declaration (but can't declare a parameter type different from the one declared in the interface).

Long answer:

This was a change introduced on PHP 7.2.

If you try this in PHP where PHP_VERSION_ID >= 7 && PHP_VERSION_ID < 7.2 you get:

Fatal error: Declaration of Bar::foo($foo) must be compatible with MyInterface::foo(array $foo)

But on PHP_VERSION_ID >= 7.2 it "works". The explanation for the changes is documented here, and says:

Parameter type widening


Parameter types from overridden methods and from interface
implementations may now be omitted. This is still in compliance with
LSP, since parameters types are contravariant.

interface A {
public function Test(array $input); }

class B implements A {
public function Test($input){} // type omitted for $input }

You can omit the parameter type, but you can't declare an incompatible type.

E.g. if in your example you tried:

public function foo(string $foo)
{
return $foo;
}

It would fail all around.

A couple of links for further reading regarding this change:

  • The PR
  • A post where the PR is explained and defended
  • An example of developers taking advantage of the new functionality.

Typesafe / Type hinting in child classes of an abstract class in PHP

Having done a bit more digging around, and thanks to a helpful post from deceze, I think this idea hasn't been implemented. He has pointed out a similar answer it to which breaks the rules of SOLID.

http://en.wikipedia.org/wiki/SOLID

Basically - This says that I cannot change the parent class: this is the point of "extend".
I can only extend, not modify the parent class.
Changing the typesafeing is modifying the class.

HOWEVER - I would argue that this example doesn't break the rules of SOLID.
Typesafing with class that is extended from the base class should be allowed: You can never have a "User" that is not a "BaseModel"... so it is not modifying the parent class.

It looks like the only way to truly do this is

if ($item instanceof User) 
{
// logic
}

In my case the parent class is probably enough to catch most issues, but it's a shame I can't typesafe it the way I intended.

[Thanks to deceze. Upvoted.]

PHP - Type Hint A More Specific Type When Implementing An Interface

What you are asking about is called "covariant method parameter type".


The problem is that the MyEvent class can have by definition some extra behaviour that is not known to/by the Event interface. And since the handler() method expects a MyEvent type (and not an Event one), it can be concluded that the handler() method actually uses that extra behaviour; otherwise why change the type of the argument?

If PHP would allow this, it might happen that the handle() method of an instance of the MyEventHandler class will receive a non-MyEvent event object. And then it will cause a runtime error.

In fact, Wikipedia has a pretty good description of this issue here. Including a short snippet below (code is not PHP, but you'll get it):

class AnimalShelter {
void putAnimal(Animal animal) {
//...
}
}
class CatShelter extends AnimalShelter {

void putAnimal(covariant Cat animal) {
// ...
}
}

This is not type safe. By up-casting a CatShelter to an AnimalShelter, one can try to place a dog in a cat shelter. That does not meet CatShelter parameter restrictions, and will result in a runtime error. The lack of type safety (known as the "catcall problem" in the Eiffel community, where "cat" or "CAT" is a Changed Availability or Type) has been a long-standing issue. Over the years, various combinations of global static analysis, local static analysis, and new language features have been proposed to remedy it,[7] [8] and these have been implemented in some Eiffel compilers.

Interestingly enough, PHP does allow covariance in case of constructor arguments.

interface AnimalInterface {}

interface DogInterface extends AnimalInterface {}

class Dog implements DogInterface {}

class Pet
{
public function __construct(AnimalInterface $animal) {}
}

class PetDog extends Pet
{
public function __construct(DogInterface $dog)
{
parent::__construct($dog);
}
}

This can help in your specific case, if you'd move the arguments from the handle() method to the handler classes' constructors.



Related Topics



Leave a reply



Submit