Search code examples
phptypescovariancephp-7.4

Type covariance with abstract class and traits in PHP


I'm developing a PHP (7.4) library and need to use traits for a new feature but I encountered an issue with parameter type covariance.

I have an abstract parent class like this :

<?php

abstract class ParentClass {
    abstract public function parentMethod($param): bool;
}

?>

I also have a trait :

<?php

trait MyTrait {
    abstract public function traitMethod($param): bool;
}

?>

I'm using both of the class and trait in a child class :

<?php

class ChildClass extends ParentClass {

    use MyTrait;

    // implementation of the abstract methods

    public function parentMethod(int $param): bool { // change parent method parameter type
        // implementation
    }

    public function traitMethod(int $param): bool { // change trait method parameter type
        // implementation
    }
}

?>

The problem here is that I get this error :

Fatal error: Declaration of ChildClass::parentMethod(int $param): bool must be compatible with ParentClass::parentMethod($param): bool

It seems like I can't change the parentMethod() parameter type. If I remove the int type on the parentMethod() definition I do not get the error ! Even with a specific type parameter on the trait method.

Why can I use covariant parameter type with trait abstract method but not with abstract class method ?


Solution

  • Covariance and Contravariance are concepts related to inheritance, using trait is not inheritance.

    Note: The above statement is not completely correct, I'll explain why at the end of this answer.

    From the PHP documentation

    A Trait is similar to a class, but only intended to group functionality in a fine-grained and consistent way. It is not possible to instantiate a Trait on its own. It is an addition to traditional inheritance and enables horizontal composition of behavior; that is, the application of class members without requiring inheritance.

    Why you see this error?

    Because int is not the supertype of everything and is not a pseudo type that represents any type (replace int with mixed in PHP 8 and see what happens). Also Type widening doesn't allow to use an arbitrary supertype (you can only omit the type)

    For example, let's say you define your parent method like this:

    abstract public function parentMethod(int $param): bool;
    

    Type widening allow you to only omit $param data type in your ChildClass.

    Contravariance, allows a parameter type to be less specific in a child method, than that of its parent

    So let's say we have another class called C that extends stdClass and we define parentMethod to accept only objects of type C

    class C extends stdClass {}
    
    abstract class ParentClass
    {
        abstract public function parentMethod(C $param): bool;
    }
    

    Now in ChildClass if we implement the parentMethod to accept objects of type stdClass

    public function parentMethod(stdClass $param): bool
    { 
        
    }
    

    This will work and no errors will be issued.

    That is contravariance.

    #Edit As for your question in the comments

    Why is it possible to type the parameter of the implemented trait method in the child class?

    Because traits are copy-pasted into a class, you can't impose OOP rules on them. That's why you can override a final method in a trait.

    trait Foo
    {
        final public function method($var)
        {
            return $var;
        }
    }
    class Bar
    {
        use Foo;
        // "Override" with no error
        final public function method($var)
        {
            return $var;
        }
    }
    

    The point of abstract methods in a trait is to force the exhibiting class to implement them (types and also access modifiers may be different)

    The PHP documentation states

    Caution A concrete class fulfills this requirement by defining a concrete method with the same name; its signature may be different.

    Update October, 2020

    Starting with PHP 8 the behavior of abstract methods in traits has changed and now abtract methods with mismatching signatures will fail with a fatal error and LSP rules will apply on them.

    Ok why is that ?

    This whole change started with a bug report, and there has been some discussion here

    It seems that prior to PHP 8 there was some sort of a conflict in behavior:

    And also because abstract itself indicates a contract, therefore what you were questioning about is legit and now with PHP 8 you will be happy https://3v4l.org/7sid7 :).