I often find myself wanting to be able to have a trait implement an interface, so that when I add the trait to a class, I can know the trait's requirements are met. For example, in Laravel I will create traits for many of my relationships like below:
trait HasOwner
{
public function owner()
{
return $this->belongsTo(User::class);
}
}
In the case of Laravel, I can be pretty well assured that every model I add this to will have a belongsTo method, however it still feels like I should be able to enforce this.
The only way I know to enforce this is to support it with a HasOwnerInterface or HasRelationshipsInterface, but the fact that by failing to add that when I'm adding the trait prevents it squawking feels like having airbags in a car, but which you need to turn on every time you start the engine.
This is what I think would be perfect:
trait HasOwner expects RelationshipInterface
{
public function owner()
{
return $this->belongsTo(User::class);
}
}
interface RelationshipInterface
{
public function belongsTo(Model $model): Relationship;
}
class Property implements RelationshipInterface
{
use HasOwner;
}
Is there another design pattern I should be using for this, or should I role up my sleeves and start fighting for this with the PHP core team to add this?
Remember that traits are essentially just syntax sugar over "automated copy-paste", so you cannot apply typical OOP techniques to them directly. As it stands (June 2021), the closer you might get is by imposing at least some requirements over trait-using classes with abstract methods:
Traits support the use of abstract methods in order to impose requirements upon the exhibiting class. Public, protected, and private methods are supported. Prior to PHP 8.0.0, only public and protected abstract methods were supported.
<?php
trait Hello {
public function sayHelloWorld() {
echo 'Hello'.$this->getWorld();
}
abstract public function getWorld();
}
class MyHelloWorld {
private $world;
use Hello;
public function getWorld() {
return $this->world;
}
public function setWorld($val) {
$this->world = $val;
}
}
?>
As a sidenote, there's an existing RFC for traits implementing interfaces, submitted in 2016 (!). Here's the key quote:
This first proposal is that a trait be permitted to declare that it implements an interface. Having the trait declare that it implements an interface makes the relationship between the interface (specification) and trait (implementation) explicit.
The trait must implement each of the methods from all the interfaces it implements. Failure to do so will be a fatal error. The method declarations must be compatible with the interface. Some or all of the trait’s implementing methods may be abstract, with the class including the trait providing the method implementation (similar to an abstract class that implements an interface).
Still, there's little progress on that RFC so far. Not clear why; while there's an open issue over proposal 2 (which is about propagating interfaces from traits to classes), there seems to be zero issues with proposal 1. No breaking changes are expected on that one either.
Traits do not support interfaces, but they do support abstract methods, which does most of what you want. As the PHP manual explains:
Traits support the use of abstract methods in order to impose requirements upon the exhibiting class.
So in your example, you can define the trait like this:
trait HasOwner
{
public function owner()
{
return $this->belongsTo(User::class);
}
abstract public function belongsTo(Model $model): Relationship;
}
Note that until PHP 8.0, the signature of the method was not enforced, so having any method named "belongsTo" would have compiled successfully; as of 8.0, I believe the signature is enforced completely as it would be for an interface.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With