Declaration not compatible with interface (PHP)

Viewed 106

Let's say I have one parent class with two child classes like this

abstract class Vehicle {
    public function setManufacturer(string $manufacturer) { ... }
}

class Bicycle extends Vehicle {
    public function addSaddle() { ... }
}

class Car extends Vehicle {
    public function setSpeedLimit(float $limit) { ... }
}

Now I want to use these objects with an interface like this

interface VehicleInterface {
    public function create(Vehicle $vehicle);
}

class BicycleService implements VehicleInterface {
    public function create(Bicycle $bicycle) {
        $bicycle->setManufacturer('Some company'); // Common for all Vehicle objects
        $bicycle->addSaddle(); // Common only for Bicycle objects
    }
}

It seems that this is not possible since BicycleService create(...) is not compatible with the interface. Is there any way around this other than removing type hints?

4 Answers

Consider this:

class NotBicycle implements Vehicle { ... }

interface VehicleInterface {
    public function create(Vehicle $vehicle);
}

class BicycleService implements VehicleInterface {
    public function create(Bicycle $bicycle) {
        $bicycle->setManufacturer('Some company'); // Common for all Vehicle objects
        $bicycle->addSaddle(); // Common only for Bicycle objects
    }
}

function createVehicle(VehicleInterface $service, Vehicle $vehicle) {
    $service->create($vehicle);
}
$service = new BicycleService();
$vehicle = new NotBicycle();
createVehicle($service, $vehicle);

Even if you were somehow able to only accept a Bicycle in the BicycleService, createVehicle($service, $vehicle) will still work because VehicleInterface has a method create(Vehicle $vehicle). Therefore in order to get this to work the way you want it, you need to basically break what an interface is.

Your only real option is to add a runtime type check. Like e.g.

class BicycleService implements VehicleInterface {
    public function create(Vehicle $bicycle) {
        if (!$bicycle instance of Bicycle) { 
           throw new TypeError('Expected Bicycle but got '.get_class($bicycle)); 
        }
        $bicycle->setManufacturer('Some company'); // Common for all Vehicle objects
        $bicycle->addSaddle(); // Common only for Bicycle objects
    }
}

What you are trying to do is called a covariant method parameter type and is not allowed in most object-oriented programming languages because it breaks the Liskov substitution principle

Read more on parameter covariance and contravariance

when you implemts to your class you have to follow the contract. So ever class the implemts the Vehicle class must have setManufacturer method as you decleard.

You need to pass your Vehicle class into it and then use instanceof to check the interface. In the below case, I changed the naming convention.

interface Vehicle {
    public function setSpeedLimit(Float $limit) : Car;
    public function setManufacturer(String $manufacturer) : MethodOfTransport;
}

interface NonVehicle {
    public function addSaddle() : Bicycle;
    public function setManufacturer(String $manufacturer) : MethodOfTransport;
}

abstract class MethodOfTransport
{
    public function setManufacturer(String $manufacturer) : MethodOfTransport { return $this; }
}

class Bicycle extends MethodOfTransport implements NonVehicle
{
    public function addSaddle() : Bicycle { return $this; }
}

class Car extends MethodOfTransport implements Vehicle
{
    public function setSpeedLimit(Float $limit) : Car { return $this; }
}

$b = new Bicycle();
$c = new Car();

As stated, I cleared up some confusion with naming conventions since a Bicycle is not a Vehicle but a MethodOfTransport.

interface VehicleServiceInterface
{
    # Change the return DataType to MethodOfTransport if Vehicle implements multiple classes
    public function create(MethodOfTransport $t) : Car;
}

class VehicleService implements VehicleServiceInterface
{
    public function create(MethodOfTransport $t) : Car
    {
        if (!$t instanceof Vehicle)
            throw new Exception( 'Cannot create a Vehicle on a NonVehicle instance.' );
            
        $t->setManufacturer( 'Foo' )->setSpeedLimit( 140 );
        return $t;
    }
}

$c = (new VehicleService())->create( $c ); // Works
$b = (new VehicleService())->create( $b ); // Throws Exception

Both Car and Bicycle are a MethodOfTransport however, one is a Vehicle and the other is a NonVehicle. Both can be passed into the method, but we can ensure that it is the correct type by using instanceof.

You can, however, set the return type to be of the create method to be of Car due to the fact we're filtering only the Vehicle instance in this case. If you have multiple Vehicle implemented classes, then change this to return MethodOfTransport

See it working on 3v4l.org.

Related