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

5

21

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?

Knee answered 21/10, 2012 at 21:58 Comment(1)
What you can do is add an abstract method to force the class using that trait to have the method you require. that way it becomes independent of the class hierarchy (the idea behind traits :P )Tent
B
4

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.

Bryannabryansk answered 21/10, 2012 at 22:14 Comment(2)
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.Knee
So I guess I am looking a way around PHP limitations which would make my code a little bit more type-safe.Knee
M
4

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();
}
Mezzosoprano answered 27/4, 2015 at 16:26 Comment(0)
C
2

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.

Christychristye answered 24/1, 2017 at 21:0 Comment(1)
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.Knee
M
0

I think there is a way completely without traits:

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

class SayWorld
{
    protected $parent = null;

    function __construct(BaseClass $base) {
        $this->parent = $base;
    }

    public function sayHelloWorld()
    {
        $this->parent->sayHello();
        echo 'World!';
    }
}

class MyHelloWorld extends Base {
    protected $SayWorld = null;

    function __construct() {
        $this->SayWorld = new SayWorld($this);
    }

    public function __call ( string $name , array $arguments ) {
        if(method_exists($this->SayWorld, $name)) {
            $this->SayWorld->$name();
        }
    }
}
Mercurialize answered 28/2, 2015 at 16:41 Comment(0)
A
0

Unfortunately there isn't a way to force to use a trait for a specific class in PHP. But at the development time you can use comment annotations for accessing parent class's methods or properties. For this you can use @extends, @implements, @method and @property annotations. For example:

<?php

namespace App\Models\Traits;

use App\Models\User;
use Illuminate\Database\Concerns\BuildsQueries;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;


/**
 * @extends Model
 * @method Model|Builder setRole(string $role)
 *
 * // for example
 * @implements BuildsQueries
 * @method someExampleMethod()
 * @property $exampleProperty
 */
trait WithPermission
{
    public function scopeSetRole(Builder $query, string $role): void
    {
        $query->join('model_has_roles', function ($join) {
            $join
                ->on($this->getTable() . '.user_id', '=', 'model_has_roles.model_id')
                ->where('model_has_roles.model_type', User::class);
        })
            ->join('roles', 'model_has_roles.role_id', '=', 'roles.id')
            ->where('roles.name', $role);
    }

}


After that you must be sure about that you used this trait in proper class. In this example I used getTable() method from Model class and editor can bring methods as auto complete with this way.

Alger answered 16/3, 2023 at 8:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.