在PHP中键入安全的泛型decorator


Type safe generic decorator in PHP

我想创建一个类型安全的通用日志装饰器。

我有很多存储库(接口),每个存储库都需要一个decorator来捕获它们可能引发的异常,将这些异常传递给LoggerInterface的实例,然后重新抛出它们。可以创建一个专用的装饰器并为每个装饰器进行测试,尽管这很麻烦(尤其是测试做得很好),而且我宁愿避免这样做。

首先想到的方法是使用__call创建一个更通用的装饰器。然而,这会导致对象实例无法实现它所装饰的存储库接口。在我的项目中,这是不可行的。有没有什么方法可以告诉PHP它确实实现了这个接口,比如使用一些神奇的反射?

我在stackoverflow上读过"如何在PHP中实现装饰器?"answers"在PHP中为方法结果缓存实现装饰器模式的最佳方式",这两篇文章都概述了专用方法和通用方法,但都没有提供以类型安全的方式执行通用方法的指示。这些问题发布后已经过去了一段时间,所以也许情况已经改变了。我使用的是PHP 7,如果需要,可以使用PHP 7.1。

PHPUnit_MockObject允许通过PHPUnit中熟悉的getMock方法调用的相同代码来构造实现接口的对象。这可以是通用装饰器的基础。然而,这需要在生产代码中使用一个模拟库。此外,这个库内部使用eval来完成它的工作。这使它不适合我的项目。

另一种方法是在运行时动态创建装饰类。不幸的是,PHP不允许开箱即用。如果runkit扩展不是一个选项,你可以模仿Doctrine ORM的做法:拥有一个执行以下步骤的DecoratorFactory

  • 对原始类使用Reflection来获取所有接口及其方法
  • 使用PHP代码生成一个文件,该文件包含一个实现所有接口的装饰代理类
  • 包括生成的代理类
  • 用原始类实例化生成的代理类,并返回代理类

请参阅上的示例http://www.doctrine-project.org/api/orm/2.4/source-class-Doctrine.ORM.Tools.EntityGenerator.html

获得所需内容的唯一真正方法是声明一个基本的Decorator类,该类完全实现必要的接口,然后在此基础上进行扩展。例如:

interface FooInterface {
    function doFoo();
    function doMoreFoo();
}
class Foo implements FooInterface {
    public function __construct() {}
    public function doFoo() {}
    public function moreFoo() {}
}
class FooDecoratorBase implements FooInterface {
    protected $foo;
    public function __construct(FooInterface $foo) { $this->foo = $foo; }
    public function doFoo() { $this->foo->doFoo(); }
    public function moreFoo() { $this->foo->moreFoo(); }
}
class ExtraFooDecorator extends FooDecoratorBase {
    public function extraFoo() {}
}

虽然我自己无法列举具体细节,但我尊敬的其他PHP开发人员告诉我,在生产代码中使用反射是个坏主意,因为它们的设计考虑的是测试/调试,而不是性能。

"Generic TypeParameters"在PHP中不可用(除了Facebook的HHVM变体)。所以这是不可能的。如果你真的想走这条路,@Sammitch的答案就是路。

然而,您正试图解决另一个问题。

装饰器模式充当包装器。如果您想为现有类添加附加功能,请使用它,而不更改类本身(或其对象状态)

请想想,你的装饰师会是什么样的:

function DoSomeOperation() {
    try {
        return $this->decoratedObject->DoSomeOperation(); //Object is injected in Decorators Constructor
    catch ('Exception $ex) {
        Logger::Log($ex);
        throw;
    }
}

为什么不简单地在你的存储库中没有装饰器:

function DoSomeOperation() {
    try {
        //Do The Logic
    catch ('Exception $ex) {
        Logger::Log($ex);
        throw;
    }
}

但也许PDO对象的装饰器/包装器是,你真正想要的是什么?(ᵔᴥᵔ)