如何在PHP中强制执行类型提示接口协定

时间:2018-08-10 11:08:34

标签: php interface type-hinting

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

<?php 
namespace App\Sample;

interface A
{
    public function doSomething();
}

和实现接口B的类A

<?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,即使我们键入hint构造函数只接受A接口,PHP也会允许调用B的任何公共方法。

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

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

2 个答案:

答案 0 :(得分:9)

您无法在PHP中完成此操作,因为它不会阻止这种类型的方法调用。

您可以使用PHPStan之类的工具来检测未保证存在的参数上的方法调用。

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

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

答案 1 :(得分: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本身无法解决此问题,我认为值得尝试自己构建一个。