有自己属性的特征


Traits with own properties

PHP特性的目的是管理一堆逻辑。然而,什么是使这一堆逻辑根据一些专用属性工作并避免命名冲突的最佳方法呢?

我正在考虑MVC,特别是模型类。事实上,模特似乎是性格特征的好候选人。模型可以实现树形结构、可起草、可修订、可延迟等。

我想这样写:

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 ? */
        }
    }
}

如果我打算这样设置Tree特性:

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...*/
}

我将深入研究$_defaults的命名冲突,因为每个trait都需要自己的默认值。使用trait的名字作为属性名意味着使用类似(new ReflectionClass(__CLASS__))->getTraits())的东西…这一点都不酷。

换句话说,是否有一种方法可以创建带有"可重写的默认值"的特征?并避免命名冲突?

就像你在每个oop概念中所做的那样:睁开你的眼睛!这是完全一样的,因为你扩展了一个类,滥用了已经存在的属性。仅此而已。

class A {
  protected $defaults = array ('foo' => 'bar');
}
class B extends A {
  protected $defaults = array('foo2' => 42); // Bum: 'foo' disappeared
}

有一个通用的$_defaults -属性听起来像一个代码气味:什么"默认"?这个默认值吗?系统默认值?应用程序默认值?[1]设置你的类与值,而不是"默认值",因为那(传播默认值)的东西为初始化过程,或初始化具体属性(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']);
  }
}

注意事项:重要的是,您别名construct(),因为它也会与其他方法(来自其他特征)冲突,并且construct()不是一个特殊的方法。这样命名(来自我)只是为了澄清,它是"一种"构造函数。其他名称,如init()或这样的当然也可以工作;您必须自己在"真正的"构造函数中调用它(参见Foo::__construct())。

[1] "default"作为标识符类似于"type", "status", "i",…它们太普通了,用不上。

有几种技术是multiple inheritance的灵丹妙药,但它们在PHP中是不可行的。

当你试图做这些事情时,保留复杂的设计模式以保持简单的整个哲学就是在背后捅你一刀。

是的,Traits被引入,它可能是一个强大的工具,但正如你所看到的,它在很多情况下都是无用的,因为properties没有conflict resolutionaliases

还有一个选择,看看Aspect-Oriented Programming(AOP)。很多人都有掌握它的问题,但我建议任何人深入研究它,这是一个范式,将成为非常重要的(即使是PHPers)在接下来的几年

你可以有一个initsetOptions方法在你的trait中,当他们需要从他们添加的类中获取信息时。然后你可以从你的__construct方法中调用这个方法。

真正的问题是你不能在trait属性和类属性之间有冲突,否则会导致致命的错误。没有冲突解决方案,就像在方法名中一样。

最简单的方法就是这样:

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'];
    };
}

我在实现中遇到了类似的问题。我必须扩展Controller类来创建我的Child,但它也需要具有Dynamic特性,因为并不是所有的Dynamics都是控制器,我不想要用相同的代码编写ControllerDynamic和nonControllerDynamic。
我遇到的问题是,在trait中定义的数组:trait->$allowedCallbackNames,由trait用作if(in_array(name, $this->allowedCallbackNames)),然后不能在子中重写。和你遇到的问题一样。我们想到了三个选择。

  1. 使用方法代替(它们可以被覆盖,并且使用use trait {... as ...};可以避免冲突,允许trait方法被重新定义为其他名称(use trait {oldmethod as newmethod;})
  2. 在trait中使用trait定义的set/get逻辑来(重新)定义子属性,而不是把它作为私有的
  3. 改变整个行为,使用一个可调用的钩子,空钩子意味着默认行为(这更有意义,考虑到我们已经决定默认应该允许任何callbackNames)

首先在trait中不使用上面的代码,而是使用if ($this->isInAllowedCallbackNames),然后在trait中定义属性private $fullTraitName_allowedCallbackName。在trait->isInAllowedCallbackNames function中添加in_array逻辑,然后重写方法,而不是重写子元素中的属性(假设使用你在子元素中定义的数组,如果需要调用重新定义的parent/trait方法来扩展行为)。

第二种选择是简单地将public function getAllowedCallbackNames()public function setAllowedCallbackNames()添加到trait中,然后在init函数中调用它们以设置默认值为array_merge(getAllowedCallbackNames(), array(<new names>))

第三个选项是我选择的,因为我想要第一个选项,但我认为php的避免冲突的方法使代码更混乱(在声明时更改函数名是不好的)。所以我创建了trait->$methodFilter=null和一个函数trait->setMethodFilter($callable),它将可调用对象分配给methodFilter(并抛出异常,如果它不可调用),然后在trait中我使用if ((is_callable($this->methodFilter)?$this->methodFilter($name):true))