我只是第一次尝试使用PHP Traits,以保持我的代码DRY:

>让我有些紧张的是,该特征正在使用它访问类的容器,这实际上意味着使用该特征的类需要扩展Controller(Symfony\Bundle\FrameworkBundle\Controller\Controller)才能使该特征起作用。 br />
使用特质时这是正常现象还是不好的做法?

评论

我期待看到答案,因为我对特质了解甚少。您可以通过重构为checkPermission($ object_id,$ judge,$ user)来解决问题,而不需要trait函数了解$ this-> container,但是我很好奇要在此处了解最佳实践。 />
是的,您可能希望将其发布为答案。另一个选择是我可以使一个抽象控制器扩展symfony框架控制器,然后对其进行扩展,在这种情况下可能会更好。

@MarcelBurkhard:感谢您接受我的咆哮,但是当您这样做时,我正忙于更新我的答案,添加了一些(我认为)更多的细节以及可能滥用/过度使用特质的原因,值得一看

我看到您已经在您对问题的回答中指出了这一点,但是我想在原始帖子中对此加以说明。在提供的代码中,DI容器被用作服务定位器,这是一种反模式,因为它可以有效地隐藏类的真正依赖关系。人们应该避免像$ container-> get('alias')这样的调用,而倾向于将依赖项注入到类中,例如通过构造函数。

#1 楼

尽管您现在已经看到了亮光,并且知道不要以这种方式使用特征,但是我还是继续在这里发表评论。我将在此过程中不断进行更新,因为这是我一段时间以来可以保证的事情。
需要特性的特性就像在类中使用global
,这不是特质的有效用例。根本。
它的命名空间是错误的

现在,这些只是我能想到的第一件事,每件事都足以像您正确地那样放弃特性。但是,请允许我解释一下这些单行代码的含义:
违反合同+实施不良合同:
类定义了合同。那是给定的。如果将类X的实例传递给函数,则需要处理该函数。您实际上是在说“此对象具有特定的工作,您可以在其上调用这些方法,和/或访问这些属性”。上课时,务必要遵守合同。您可以添加一些东西,但永远不要更改基类做出的承诺。
例如,如果基类的构造函数期望array类型的1个参数(通过类型提示强制实现),则其所有子元素应该使用相同的构造函数,或者如果他们选择覆盖它,则定义一个接受1 array参数的构造函数。允许他们放宽类型提示,但不能更改。它们可以随意添加可选参数,但是不能强制将其他参数传递给构造函数:
abstract class Base
{
    public function __construct(array $param)
    {}
}
class ValidChild extends Base
{}
class AnotherChild extends Base
{
    public function __construct(array $param, $opt = null)
    {}
}
class IffyButTheoreticallyFine extends Base
{
    public function __construct($param, array $opt = null)
    {}
}

这些都是有效的子类,但是:不是。它们是邪恶的,但可悲的是司空见惯:
class Bastard extends Base
{
    public function __construct()
    {
    }
}
class Ungrateful extends Base
{
    public function __construct(array $param = null)
    {}
}
class EvilTwin extends Base
{
    public function __construct(stdClass $param)
    {
        parent::__construct( (array) $param);
    }
}
class Psychopath extends Base
{
    public function __construct(PDO $db, array $parent = null)
    {
        if (!$parent) {
            $parent = [1, 2, 3];
        }
        parent::__construct($parent);
    }
}

现在,这如何适用于您的情况?简单:特征是减少代码重复的工具。类(合同)包含/使用以完成其工作时经常需要的功能。对于使用特质的类,特质无权定义该类的合同。特质与其用户的关系在某种程度上与子级与其父级的关系相同:继承合同的是子级,而不是父级。因此,必须遵循的原则是,任何类,无论其工作如何,都必须能够使用特征,而不必更改其自身的契约和依赖关系。
您的特征就不是有效的用例:全部定义上,需要为其所有用户添加$this->container属性,这当然会影响该类的合同及其子代合同。我认为可以肯定地说,包含global的类定义是错误代码的明显标志,这一说法已被普遍接受。一个类是作用域的,它的方法是作用域的,它们都不应该依赖全局作用域来起作用。 global $someVar也是如此:因为它可以用于多种类,所以特征必须与上下文(和范围)无关。如果它需要一个属性,则特征必须定义它。如果该属性必须具有特定的类型,则您唯一可以做的就是定义一个属性,并为其强制实施一个抽象的setter,这会施加类型限制。但是,这是不可靠的,因为可以使用多个特征,这可能会导致与用户发生冲突,并且类本身也可以直接访问该属性。最后:这种方法使我们回到正题:这样的特征不再是特征。
这意味着特征不能具有依赖关系。如果特征具有依赖项,则不再是特征:它是traitabstract class,但绝对不是特征。但是,这里有一个很小的灰色区域:
虽然这不是我的最佳实践想法,但有人可能会认为写一个特征与Interface一起使用是可以接受的,例如:确保数据模型可遍历的单个接口,例如Interface接口。在这种情况下,这样的特征也许是可以接受的。特质。此时提供一个很小的,100%有效的特征示例可能是有用的。假设您有一些模型可以表示数据库记录,用户输入(表单数据)或完全其他的模型。所有这些模型都包含用户信息,包括电子邮件地址。具有设置器的模型是理想的选择,以确保每当收集一些数据时,便对该数据进行验证(即IteratorAggregate)。由于这些类表示不同的事物(数据库记录或表单),因此它们很可能已经从抽象类继承。无需复制电子邮件设置程序,可以使用特征:命名空间,尽管很明显它只能在控制器中其他任何地方使用。

Update
更新时间。尽管我(坚定地)支持我先前所说的(特征无法签订合同),但肯定会有人争辩说他们可以做到。至少因为PHP的filter_var($email, FILTER_VALIDATE_EMAIL)构造使您可以声明Model方法:
trait IteratorAggregateTrait
{
    public function getIterator()
    {
        if (!$this instanceof IteratorAggregate) {
            throw new \LogicException(
                sprintf(
                    '%s must implement IteratorAggregate to use %s',
                     get_class($this),
                     __TRAIT__
                )
            );
        }
        return new ArrayIterator($this);
    }
}

然后,此特征对其用户强加了部分契约:使用该特征的类必须定义trait方法。那么,您可能会说出什么问题呢?问题很简单:引入了特征以解决可能需要多重继承的假定问题,因此,一个类可以使用多个特征,并且该类有可能自行解决有关方法名称的冲突。因此,让我们假设上面的特征正在被使用,并且您还有另外几个特征,其中一个看起来像这样:
trait UserDataTrait
{
    public $email = null;
    public function setEmail($email)
    {
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            throw new \InvalidArgumentException('bad email');
        }
        $this->email = $email;
        return $this;
    }
}
abstract在这里未声明为抽象):
trait Evil
{
    protected $dependency = null;
    public function someMethod()
    {
        return $this->dependency->getValue();
    }
    abstract public function setDependency(SomeType $dep);
}

恭喜,该特性所施加的契约及其功能被破坏了,仅仅是因为该类已选择遵守setDependency所施加的契约。无需火箭科学家就可以意识到,虽然这些特征在理论上可以施加最低限度的合同,但在确保遵守合同方面没有那么有效。
现在这是一个简单的例子,但是如果您需要一个更合理的方案:假设2个特征Evil::setDependencyPure都定义了一个方法AB写入默认日志文件,logError更通用,并且希望您将资源作为参数传递,或写入A::logError默认值。
因为您的项目很大,所以您要处理多个继承级别,并且在某些时候,某些类最终实现了这两种特性。当然,根据类的不同,Bstderr的优先级更高。后者更有可能用在worker / crons / backend代码中。
现在,如果此对象选择使用A::logError覆盖B:logError,您可能会浪费大量时间试图弄清楚为什么A::logError不会将错误消息写入默认日志。简而言之:
特质合同是较弱的建议,很容易被覆盖
回到特征中的B::logError方法的定义:我将不得不对其进行更多的测试,但是AFAIK,这很糟糕设计。 logError包含您的抽象方法(因为一个类强制执行严格的契约),或者您从一开始就在类中实现该方法。特质应该减少代码重复,而抽象方法则要求您每当使用特质时都重写方法的签名和实现。如果那是您想要的,为什么首先要打扰使用特征? 1个或多个特征,并且不包含其自身的其他定义。很好,但是如果我们看一下上面的两个特征(abstractabstract class),该怎么办。 Pure是抽象Evil方法的有效实现吗?如果是这样,该特征方法将被视为抽象方法的实现吗?如果不是这样,则包含摘要的特征不能在不包含其自身定义的类中使用,因此,此类特征是特殊情况(因为它们的使用受到限制)。
如果Pure::setDependencyEvil::setDependency中抽象方法的有效实现(Pure::setDependencyEvil或其父类的别名),并且特征可以用于实现抽象特征方法,那么就会出现另一个问题:维护地狱:更改以下任一方法这些特征可能会破坏您的代码,具体取决于使用这些特征的方式和位置。每当您仅更改一个特征时,您最终都必须使用这两个特征来测试所有类。当您这样看时,我会说一些代码重复是两种弊端中的较小者。

评论


\ $ \ begingroup \ $
我只想指出,虽然通常您的子类(必须在我的书中!)尊重您覆盖的任何方法中规定的规则,但构造函数是一个例外。这是因为子类即使具有相同的功能,也可能具有与超类完全不同的依赖关系。 PHP不会抱怨构造函数签名不匹配,并且程序员在实例化新的class();时应该知道要求是什么。真正需要强制执行构造函数的唯一地方是该类是否由工厂构建
\ $ \ endgroup \ $
–GordonM
18年2月28日在15:40

\ $ \ begingroup \ $
这将进入我的团队的设计指南。我们将自己陷于非常依赖的特征中,当我们转向新框架时,特征将受到严格限制。这种解释将帮助我强制执行。谢谢!
\ $ \ endgroup \ $
–注意错误
19年7月11日在12:37

#2 楼

经过一些“研究”后,我得出结论,您不应该像我一样使用Traits。我得出此结论的主要原因是可测试性和可读性差。 (请阅读下面的第一个源代码)
我上面的代码(存在问题)违反了一些设计原则,我将重构控制器以扩展abstract控制器,该控制器实现了我在扩展它的所有控制器中所需的checkPermission功能。 />来源:

http://blog.ircmaxell.com/2011/07/are-traits-new-eval.html
https://stackoverflow.com/a/7892869 / 982075

编辑:(2015-07-28)
已经过去了几个月,我回到了这个答案,我想提一下代码中的主要缺陷。
上面的controller依赖于注入的整个服务容器,这称为服务定位器(反)模式。
依赖关系是通过调用$this->container->get('name_of_some_service')而不是直接注入来获取的。这违反了demeter的定律,并导致不良的可测试性和可重用性。
依赖注入是实现此目标的方法,它也得到symfony框架的支持。
以下链接将帮助您入门:
如何将控制器定义为服务通过使用依赖注入,您可以降低与框架的耦合,在大多数情况下,通过查看构造函数,即可轻松看到给定类的依赖。

评论


\ $ \ begingroup \ $
只是为了完整性:虽然服务定位器确实会影响可测试性,但是诸如代码接收之类的测试框架确实具有允许您用可模拟对象替换容器的模块,从而有效地解决了测试过程中面临的问题
\ $ \ endgroup \ $
– Elias Van Ootegem
15年7月29日在8:56

#3 楼

可以在单个Closure的范围内重新绑定$ this,因此您可以访问任何传入对象的容器

function isJudgePermitted($object, int $object_id) : bool {
  return (function (int $object_id) : bool {
    $judge = $this->container->get('acme_judge');
    $user = $this->container->get('security.context')->getToken()->getUser();
    return $judge->isPermitted($user, $object_id);
  })->call($object, $object_id);
}

// Inside SomeController.

public function someAction(Request $request, $object_id) {
  $judge_permitted = isJudgePermitted($this, $object_id);
  $judge_permitted ?: throw $this->createAccessDeniedException("Brabbel");
  //do other stuff
}