Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to enforce contract of type-hinted interface in PHP

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.

like image 942
Zamrony P. Juhara Avatar asked Aug 10 '18 11:08

Zamrony P. Juhara


People also ask

What is type hinting in PHP?

The PHP Type Hinting works for only some type of data. For functions arg/argument, the user will specify some user-defined class instance and it will be hinted by the class name. It works after the PHP 5 version.

When should we use interface type hinting?

Whenever we need to do type hinting to more than one related classes, we should be using interface type hinting. The message to take home from this tutorial is that, whenever we need to do type hinting to more than one related classes, we should be using interface type hinting.

Can an interface have a constant in PHP?

The class implementing the interface must declare all methods in the interface with a compatible signature . It's possible for interfaces to have constants. Interface constants work exactly like class constants . Prior to PHP 8.1.0, they cannot be overridden by a class/interface that inherits them. // allowed to override constants.

What is a union type in PHP?

A union type declaration accepts values of multiple different simple types, rather than a single one. Union types are specified using the syntax T1|T2|... . Union types are available as of PHP 8.0.0. The null type is supported as part of unions, such that T1|T2|null can be used to create a nullable union.


2 Answers

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.

like image 65
Danack Avatar answered Oct 09 '22 17:10

Danack


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 */
    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:

  • SO - php get interface methods
  • How to auto call function in php for every other function call

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.

like image 36
AymDev Avatar answered Oct 09 '22 16:10

AymDev