【问题标题】:How to enforce contract of type-hinted interface in PHP如何在 PHP 中执行类型提示接口的合同
【发布时间】:2019-01-17 23:47:06
【问题描述】:

让我们想象一下,我们有以下接口声明。

<?php 
namespace App\Sample;

interface A
{
    public function doSomething();
}

以及实现接口A的类B

<?php
namespace App\Sample;

class B implements A
{
    public function doSomething()
    {
        //do something
    }

    public function doBOnlyThing()
    {
        //do thing that specific to B
    }  
}

C 将依赖于接口A

<?php
namespace App\Sample;

class C
{
    private $a;

    public function __construct(A $a)
    {
        $this->a = $a;
    }

    public function doManyThing()
    {
        //this call is OK
        $this->a->doSomething();

        //if $this->a is instance of B, 
        //PHP does allow following call
        //how to prevent this?
        $this->a->doBOnlyThing();            
    }  
}

...
(new C(new B()))->doManyThing();

如果将实例类 B 传递给 C,PHP 确实允许调用 B 的任何公共方法,即使我们键入提示构造函数只接受 A 接口。

如何在 PHP 的帮助下避免这种情况,而不是依赖任何团队成员来遵守接口规范?

更新:假设我不能将 doBOnlyThing() 方法设为私有,因为它在其他地方是必需的,或者它是我无法更改的第三方库的一部分。

【问题讨论】:

  • 你不能把B::doBOnlyThing设置成private function吗?
  • @NiettheDarkAbsol 让我们说,我不能将其设为私有,因为它在其他地方使用。
  • 我听不懂。无法实例化接口,您在构造函数中输入了A 接口并发送了一个对象B 来实现它(您不能发送A 实例)。这有什么问题?看到投票我错过了一些东西。
  • @AymDev 如果您在 Java 中使用接口,我正在寻找的是相似的。如果您有声明为接口的属性并尝试调用不属于接口规范的方法,编译器会触发编译错误。
  • 如果获得 "only B things" 方法有帮助:$A = class_implements(($b = new B)); $A_methods = get_class_methods(array_shift($A)); echo '&lt;pre&gt;' . print_r(array_diff(get_class_methods($b), $A_methods), true) . '&lt;/pre&gt;';

标签: php interface type-hinting


【解决方案1】:

该代理类在使用指定接口以外的其他方法时会抛出异常:

class RestrictInterfaceProxy
{
    private $subject;
    private $interface;
    private $interface_methods;

    function __construct($subject, $interface)
    {
        $this->subject           = $subject;
        $this->interface         = $interface;
        $this->interface_methods = get_class_methods($interface);
    }

    public function __call($method, $args)
    {
        if (in_array($method, $this->interface_methods)) {
            return call_user_func([$this->subject, $method], $args);
        } else {
            $class = get_class($this->subject);
            $interface = $this->interface;
            throw new \BadMethodCallException("Method <b>$method</b> from <b>$class</b> class is not part of <b>$interface</b> interface");
        }
    }
}

然后您应该更改您的 C 构造函数:

class C
{
    private $a;

    public function __construct(A $a)
    {
        // Just send the interface name as 2nd parameter
        $this->a = new RestrictInterfaceProxy($a, 'A');
    }

    public function doManyThing()
    {
        $this->a->doSomething();
        $this->a->doBOnlyThing();
    }
}

测试:

try {
    (new C(new B()))->doManyThing();
} catch (\Exception $e) {
    die($e->getMessage());
}

输出:
B 类中的方法 doBOnlyThing 不属于 A 接口



上一个答案:我误解了 OP 的问题。如果一个类具有它所实现的任何接口都没有的方法,则该类将引发异常。
将其用作$proxified = new InterfaceProxy(new Foo);

class InterfaceProxy
{
    private $subject;

    /* In PHP 7.2+ you should typehint object
    see http://php.net/manual/en/migration72.new-features.php */
    function __construct($subject)
    {
        $this->subject = $subject;

        // Here, check if $subject is complying
        $this->respectInterfaces();
    }

    // Calls your object methods
    public function __call($method, $args)
    {
        if (is_callable([$this->subject, $method])) {
            return call_user_func([$this->subject, $method], $args);
        } else {
            $class = get_class($this->subject);
            throw new \BadMethodCallException("No callable method $method at $class class");
        }
    }

    private function respectInterfaces() : void
    {
        // List all the implemented interfaces methods
        $interface_methods = [];
        foreach(class_implements($this->subject) as $interface) {
            $interface_methods = array_merge($interface_methods, get_class_methods($interface));
        }

        // Throw an Exception if the object has extra methods
        $class_methods = get_class_methods($this->subject);
        if (!empty(array_diff($class_methods, $interface_methods))) {
            throw new \Exception('Class <b>' . get_class($this->subject) . '</b> is not respecting its interfaces', 1);
        }
    }
}

我得到了以下答案的帮助:

当然,这个解决方案是自定义的,但由于 PHP 不能自己解决这个问题,我认为值得尝试自己构建它。

【讨论】:

  • 我不反对声明不属于接口的公共方法的类。我试图阻止任何不属于接口的公共方法在其他明确声明它所依赖的接口的类中被调用。
  • 哦,我跑题了。对不起OP。我将编辑我的答案以将其标记为离题,但会留下它,因为它可能会帮助其他人
  • 嗨@ZamronyP.Juhara,我用更准确的代理更新了我的答案,它是否满足要求?
  • 感谢您为解决我的问题所做的努力。真的很感激。我猜您提出的解决方案应该可行。但是 1)它引入了与代理的紧密耦合。我们键入接口的原因是为了避免这种耦合,因此根据我的理解,这违背了接口的目的,不是吗? 2) 只有当dBOnlyThing 方法被调用时,你的代理类才会触发异常。这实际上类似于鸭式打字(就像我们目前所拥有的那样)。但再次非常感谢你。我想我会尝试@Danack 提出的建议。
  • 你完全正确,这只是一个尝试。确实很有趣:-)
【解决方案2】:

你不能在 PHP 中这样做,因为它不会阻止这种类型的方法调用。

您可以使用PHPStan 之类的工具来检测对不保证存在的参数的方法调用,从而防止这种情况发生。

几乎在任何语言中,理论上都可以使用该语言的特性,但负责程序员团队的人员选择不允许这些特性成为团队编写代码的方式。

使用静态分析工具和其他代码质量工具通常是执行这些规则的最佳方式。如果您可以设置这些,最好在预提交挂钩上进行设置,否则在提交后在您的自动构建工具中。

【讨论】:

    猜你喜欢
    • 2023-01-25
    • 2021-11-17
    • 2016-01-19
    • 2021-05-16
    • 1970-01-01
    • 2016-12-28
    • 2021-05-11
    • 1970-01-01
    • 2011-06-01
    相关资源
    最近更新 更多