The objective of PHP traits is to manage a bunch of logic. However what is the best way to make this bunch of logic works according some dedicated attributes and avoiding naming conflicts ?
I'm thinking about MVC and particularly the model class. Indeed models seems to be good candidates for traits. Models can implement tree structure, be draftable, revisionnable, sluggable, and so on.
I would like to write something like this:
class MyModel extends Model {
use Tree, Revision, Slug;
protected $_behaviors = array(
'Tree' => array('parentFieldname' => 'p_id'),
'Revision' => array(/* some options */),
'Slug' => array(/* some options */)
);
public function __construct() {
foreach($this->_behaviors as &$options) {
$options += /* trait defaults ? */
}
}
}
If I intend to set the Tree trait like this :
trait Tree {
protected $_defaults = array(
'parentFieldname' => 'parent_id',
/*...other options...*/
);
public function moveUp();
public function moveDown();
public function setParent(); //<- need the `'parentFieldname' => 'p_id'`attribute
/*...and so on...*/
}
I'll reach into naming conflicts because of $_defaults
since each trait needs its own defaults. Using the name of the trait as attribute name imply using something like (new ReflectionClass(__CLASS__))->getTraits())
... which is not really awesome.
In other words is there a way to create traits with "overridable defaults" and avoid naming conflicts ?
Like you will do in every OOP-concept: Open your eyes! Thats exactly the same, as you extends a class and misuse already existing property. Nothing more.
class A {
protected $defaults = array ('foo' => 'bar');
}
class B extends A {
protected $defaults = array('foo2' => 42); // Bum: 'foo' disappeared
}
Having a generic $_defaults
-property sounds to me like a code smell anyway: What "defaults"? This defaults? System defaults? Application defaults? [1] Setup your class with values, not "defaults", because thats (spreading default value) something for an initialization process, or the initialization of the concrete properties (public $parentFieldName = 'parent_id';
)
trait A {
public $parentFieldName = 'parent_id';
public function construct ($options) { /
if (isset($options['parentFieldName'])) {
$this->parentFieldName = $options['parentFieldName'];
}
}
}
class Foo {
use A {
A::construct as protected constructA;
}
public function __construct ($options) {
$this->constructA($options['A']);
}
}
Some notes: It's important, that you alias construct()
because it would conflict with other methods (from other traits) as well, and construct()
is not a special method. It's only named this way (from me) to clarify, that it is "a kind of" constructor. Other names like init()
or such would work as well of course ;) You must call it within the "real" constructor (see Foo::__construct()
) yourself.
[1] "default" as an identifier is like "type", "status", "i", ...: They are to generic to be useful.
You can have an init
or setOptions
method in your trait when they need to get informations from the class they are added on. You can then call this method from your __construct
method.
The real problem is that you can't have conflict between trait attributes and class attributes, that would result in a fatal error. There's no conflict resolution, like there is in the method name.
The easiest thing would be to have something like this:
trait Tree {
protected $Tree_options; // Prefixing with the trait name should protect you from naming collision (less than optimal)
public function init($options) {
// This should be called int he __construct of MyModel
$this->Tree_options = array_merge($this->Tree_options, $options);
}
public function moveUp();
public function moveDown();
public function setParent() {
$parent = $this->Tree_options['p_id'];
};
}
There are several techniques of which multiple inheritance
is the silver bullet, but they aren't doable in PHP.
The whole philosophy thing about sparing complex design patterns to keep it simple stabs you in the back when trying to do things such as these.
Yes, Traits
were introduced, which could be a powerful tool, but as you can see it's useless in alot of situations simply because there is no conflict resolution
or aliases
for properties
.
There is one more option though, take a look at Aspect-Oriented Programming(AOP)
. Alot of people are having problems getting a hold on it, but I would recommend anyone to dive into it, this is a paradigm thats going to become very important(Even for PHPers) the next upcoming years
I'm having a similar problem in my implementation. I have to extend the Controller class, to create my Child, but it also needs to be of the trait Dynamic, as not all Dynamics will be controllers and I don't want to have to code a ControllerDynamic and a nonControllerDynamic with the same code in it.
I hit the problem that an array: trait->$allowedCallbackNames
defined in the trait, used by the trait as if(in_array(name, $this->allowedCallbackNames))
, then can't be overridden in the Child. Same problem you're having. We hit upon 3 options.
use trait {... as ...};
which allows a trait method to be redefined as some other name (use trait {oldmethod as newmethod;}
)Firstly instead of using the above code in the trait, instead use if ($this->isInAllowedCallbackNames)
, then define the property private $fullTraitName_allowedCallbackName
in the trait as originally intended. Add the in_array logic in the trait->isInAllowedCallbackNames function
, then instead of overriding the property in the Child, override the method (presumably using the array you define in the child, and if needs be calling the redefined parent/trait method to extend the behaviour).
Second option simply add public function getAllowedCallbackNames()
and public function setAllowedCallbackNames()
to the trait, then call them in init
function to set defaults as array_merge(getAllowedCallbackNames(), array(<new names>))
Third opion is the one I've gone with, because I want the first option, but I think PHPs collision avoidance approach makes for messier code (changing function names at declation time is bad). So I've created trait->$methodFilter=null
and a function trait->setMethodFilter($callable)
which assigns the callable to the methodFilter (and throws an exception if it's not callable), then in the trait I use if ((is_callable($this->methodFilter)?$this->methodFilter($name):true))