PHP trait: is there a proper way to ensure that class using a trait extends a super class which contains certain method?

11,959

Solution 1

No, there is not. In fact, this example is very bad, since the purpose of introducing traits was to introduce same functionality to many classes without relying on inheritance, and using parent not only requires class to have parent, but also it should have specific method.

On a side note, parent calls are not checked at the compile time, you can define simple class that does not extend anything with parent calls in it's methods, ant it will work until one of these method is called.

Solution 2

You can check if $this extends a specific class or implements a specific interface :

interface SayHelloInterface {
    public function sayHello();
}

trait SayWorldTrait {
    public function sayHello() {
        if (!in_array('SayHello', class_parents($this))) {
            throw new \LogicException('SayWorldTrait may be used only in classes that extends SayHello.');
        }
        if (!$this instanceof SayHelloInterface) {
            throw new \LogicException('SayWorldTrait may be used only in classes that implements SayHelloInterface.');
        }
        parent::sayHello();
        echo 'World!';
    }
}

class SayHello {
    public function sayHello() {
        echo 'Hello ';
    }
}

class First extends SayHello {
    use SayWorldTrait;
}

class Second implements SayHelloInterface {
    use SayWorldTrait;
}

try {
    $test = new First();
    $test->sayHello(); // throws logic exception because the First class does not implements SayHelloInterface
} catch(\Exception $e) {
    echo $e->getMessage();
}

try {
    $test = new Second();
    $test->sayHello(); // throws logic exception because the Second class does not extends SayHello
} catch(\Exception $e) {
    echo $e->getMessage();
}

Solution 3

The PHP compilation stage merely creates bytecode. Everything else is done at run-time, including polymorphic decision making. Thus, code like below compiles:

class A {}
class B extends A {
    public function __construct() {
        parent::__construct();
    }
}

but blows up when run:

$b = new B;

Thus you strictly cannot have parent checking at compile-time. The best you can do is defer this to run-time as early as possible. As other answers have shown, it's possible to do this inside your trait method with instanceof. I personally prefer using type-hinting when I know a trait method needs a particular contract.

All of this boils down to the fundamental purpose of traits: compile time copy and paste. That's it. Traits know nothing of contracts. Know nothing of polymorphism. They simply provide a mechanism for re-use. When coupled with interfaces, they're quite powerful, though somewhat verbose.

In fact, the first academic discussion of traits lays bare the idea that traits are meant to be "pure" in that they have no knowledge of the object around them:

A trait is essentially a group of pure methods that serves as a building block for classes and is a primitive unit of code reuse. In this model, classes are composed from a set of traits by specifying glue code that connects the traits together and accesses the necessary state.

"If Traits Weren't Evil, They'd Be Funny" nicely summarizes the points, the pitfalls, and the options. I don't share the author's vitriol for traits: I use them when it makes sense and always in pair with an interface. That the example in the PHP documentation encourages bad behavior is unfortunate.

Share:
11,959

Related videos on Youtube

Maciej Sz
Author by

Maciej Sz

Some of my own thoughts on programming: Backward compatibility holds back humankind. Dynamic typing is like arrangement without consequences; static typing gives court to enforce contracts In OOP you must be very self-disciplined to restrain yourself from screwing the code; in FP that discipline is given to you for free. And some borrowed quotes: A good programmer is someone who looks both ways before crossing a one-way street

Updated on August 22, 2022

Comments

  • Maciej Sz
    Maciej Sz over 1 year

    Example #2 from PHP manual http://php.net/manual/en/language.oop5.traits.php states

    <?php
    class Base {
        public function sayHello() {
            echo 'Hello ';
        }
    }
    
    trait SayWorld {
        public function sayHello() {
            parent::sayHello();
            echo 'World!';
        }
    }
    
    class MyHelloWorld extends Base {
        use SayWorld;
    }
    
    $o = new MyHelloWorld();
    $o->sayHello();
    ?>
    

    This is correct code, but it's not safe to use parent:: in that context. Let's say I wrote my own 'hello world' class which does not inherit any other classes:

    <?php
    class MyOwnHelloWorld
    {
        use SayWorld;
    }
    ?>
    

    This code will not produce any errors until I call the sayHello() method. This is bad.

    On the other hand if the trait needs to use a certain method I can write this method as abstract, and this is good as it ensures that the trait is correctly used at compile time. But this does not apply to parent classes:

    <?php
    trait SayWorld
    {
        public function sayHelloWorld()
        {
            $this->sayHello();
            echo 'World!';
        }
    
        public abstract function sayHello(); // compile-time safety
    
    }
    

    So my question is: Is there a way to ensure (at compile time, not at runtime) that class which uses a certain trait will have parent::sayHello() method?

  • Maciej Sz
    Maciej Sz over 11 years
    Thanks for this. I am aware that simple classes do not check the parent calls at compile time, but it is still a correct code. My ever-so-wise IDE can figure out the parent call, which means that when I switch from PHP to a strong-typed language it still would be correct code. This allows me to sleep at night. But when it comes to multiple inheritance I am not aware of any other language that can't do just that. A concept of traits in Scala allows inheritance in both ways. C++ and Python classes can inherit multiple parents.
  • Maciej Sz
    Maciej Sz over 11 years
    So I guess I am looking a way around PHP limitations which would make my code a little bit more type-safe.
  • Maciej Sz
    Maciej Sz about 7 years
    During the time since I asked this question I actually stopped using traits at all. And I try to rewrite my old code whenever possible to drop traits. You might say I grew up. They are sometimes useful but only in very few situations.