代码之家  ›  专栏  ›  技术社区  ›  Zamrony P. Juhara

如何在PHP中执行类型提示接口的契约

  •  7
  • Zamrony P. Juhara  · 技术社区  · 6 年前

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

    <?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 将取决于接口 一个 .

    <?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();
    

    If实例类 传递给 C类 ,PHP确实允许调用B的任何公共方法,即使我们typehint构造函数接受 一个 仅接口。

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

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

    2 回复  |  直到 6 年前
        1
  •  9
  •   Danack    6 年前

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

    你可以通过使用像 PHPStan 以检测对不能保证存在的参数的方法调用。

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

    使用静态分析工具和其他代码质量工具通常是实施这些规则的最佳方法。最好是在预提交钩子上(如果您可以设置这些钩子的话),否则在提交之后在您的自动生成工具中。

        2
  •  1
  •   AymDev    6 年前

    使用指定接口以外的其他方法时,此代理类将引发异常:

    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());
    }
    

    输出:
    方法 多联机 类不是 接口



    上一个答案: 我误解了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本身无法解决这个问题,我认为值得自己尝试构建这个解决方案。