By Zamrony P. Juhara


2018-08-10 11:08:33 8 Comments

Let us imagine, we have following declaration of interface.

<?php 
namespace App\Sample;

interface A
{
    public function doSomething();
}

and class B that implements interface A.

<?php
namespace App\Sample;

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

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

Class C will depends on interface 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();

If instance class B is passed to C, PHP does allow call to any public methods of B even though we typehint constructor to accept A interface only.

How can I prevent this with the help of PHP, instead of relying on any team members to adhere interface specification?

Update : Let us assume I can not make doBOnlyThing() method private as it is required in other place or it is part of third-party library that I can not change.

2 comments

@AymDev 2018-08-10 14:04:49

This proxy class will throw an exception when using other methods than specified interface:

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

You should then change your C constructor:

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

Testing:

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

Output:
Method doBOnlyThing from B class is not part of A interface



Previous answer: I misunderstood OP's question. This class will throw an exception if a class has methods that none of the interface it implements has.
Use it as $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%20*/
    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);
        }
    }
}

I took help on the following answers:

Of course this solution is custom but as PHP won't solve this issue by itself I thought it would worth giving a try to build this myself.

@Zamrony P. Juhara 2018-08-11 00:02:55

I am not against class that declare public method that is not part of interface. I am trying to prevent any public method not part of interface to be called in other class which explicitly declared interface that it depends.

@AymDev 2018-08-11 05:27:26

Oh I'm off-topic. Sorry OP. I'll edit my answer to mark it off-topic but will leave it as it may help someone else maybe

@AymDev 2018-08-12 09:57:34

Hi @ZamronyP.Juhara, I updated my answer with a more accurate proxy, does it fullfill the requirements ?

@Zamrony P. Juhara 2018-08-12 10:49:19

Thank you for your effort to help my problem. Really appreciate it. Your proposed solution should work I guess. But 1) It introduces tight coupling with proxy. The reason we typehint interface is to avoid this kind of coupling, thus from my understanding, this defeat the purpose of interface, doesn't it? 2) You proxy class will only trigger exception if dBOnlyThing method is called. which is actually similar to duck-typing (as what we currently have). But again thank you very much. I guess I will try what @Danack proposed.

@AymDev 2018-08-12 10:52:16

You are completely right, this is just an attempt. It has been fun to do indeed :-)

@Danack 2018-08-10 11:11:21

You can't do it in PHP, as it doesn't prevent this type of method calling.

You can prevent it by using tools like PHPStan to detect method calls on parameters that aren't guaranteed to be there.

In almost any language there are features in the language that theoretically could be used, but the people in charge of a team of programmers choose to not allow those features to be how the team should be writing code.

Using static analysis tools, and other code quality tools are usually the best way to enforce these rules. Preferably on a pre-commit hook if you can set these up, otherwise in your automated build tools after a commit has been made.

Related Questions

Sponsored Content

29 Answered Questions

[SOLVED] How do you parse and process HTML/XML in PHP?

28 Answered Questions

[SOLVED] How to make a redirect in PHP?

  • 2009-04-20 14:13:22
  • Sam
  • 2173319 View
  • 988 Score
  • 28 Answer
  • Tags:   php redirect

7 Answered Questions

[SOLVED] How does PHP 'foreach' actually work?

24 Answered Questions

[SOLVED] How do I get PHP errors to display?

11 Answered Questions

[SOLVED] How to determine if a type implements an interface with C# reflection

28 Answered Questions

[SOLVED] How can I prevent SQL injection in PHP?

24 Answered Questions

[SOLVED] Why can't I define a static method in a Java interface?

15 Answered Questions

[SOLVED] How do you declare an interface in C++?

3 Answered Questions

[SOLVED] Why PHP Trait can't implement interfaces?

1 Answered Questions

[SOLVED] PHP: Type hinting in namespaces not working?

  • 2015-09-17 14:15:41
  • Xenonite
  • 549 View
  • 1 Score
  • 1 Answer
  • Tags:   php type-hinting

Sponsored Content