0

Given an object/interface hierarchy like the following

<?php

interface Event 
{
    public function getType(): string;
}

final class MyEvent implements Event
{
    public function getType(): string
    {
        return 'MyEvent';
    }
}

interface EventHandler
{
    public function handle(Event $event): void;
}

final class MyEventHandler implements EventHandler
{
    public function handle(MyEvent $event): void
    {
        // do something with the concrete MyEvent object...
    }
}

Is it not possible to type hint the more specific MyEvent in the method which is implementing the handle method from the EventHandler interface?

my ide is marking this as an error

Method 'MyEventHandler::handle()' is not compatible with method 'EventHandler::handle()'.

while i feel like in the name of extensibility, this should be possible...

The signature of the implementing method is different to that of the method in the interface, but at runtime wouldn't a MyEvent be treated the same as an Event?

Obviously, if the concrete implementation type hints the Event interface, there is no problem, and at runtime the method will accept the implementing MyEvent but then we lose the intent that the concrete MyEventHandler should handle events of type MyEvent. There could be very many implementations of the Event and EventHandler interfaces, so being able to be as explicit as possible is what we're looking for here, rather than just type hinting the interface in the concrete handle method.

If this isn't possible with interfaces and type hinting, is there another way of implementing this with an abstract class perhaps while retaining the strong(ish) typing and conveying the intent? Or would we need an explicit method in the MyEventHandler to allow the type hint of the concrete Event class?

Pedro del Sol
  • 2,840
  • 9
  • 39
  • 52

1 Answers1

1

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.

Zoli Szabó
  • 4,366
  • 1
  • 13
  • 19