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.
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.
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.
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.
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.
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.
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();
}
}
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:
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With