Reputation: 12035
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?
Upvotes: 21
Views: 13494
Reputation: 985
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.
Upvotes: 0
Reputation: 39394
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.
Upvotes: 2
Reputation: 134
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();
}
Upvotes: 4
Reputation: 3224
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();
}
}
}
Upvotes: 0
Reputation: 29462
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.
Upvotes: 4