Suppose I have a class C1 that implements two interfaces (I1, I2) in PHP 8.
I inject that class C1 in the constructor of another class C2 typehinting it with one of the interfaces (I1).
Now, when I call a method of I2 interface Sonarlint complains with message:
Potentially polymorphic call.
Can I get rid of this messages somehow?
Let's make this a bit more concrete:
interface I1 {
public function one();
}
class C2 {
public function __construct( I1 $injected ) {
$injected->one();
$injected->two();
}
}
Note that I haven't shown interface I2
or class C1
here, because there's nothing in class C2
that says they're relevant.
What the tool is telling you is that looking just at the definition of class C2
, there is no way to know that $injected->two()
is a valid method call - the only method we know for sure $injected
has is one()
, because we know it implements interface I1
.
The fact that in practice you always pass an object that has more methods, and implements an additional interface, is not documented in class C2
at all.
The way to fix that is to add more information to C2
, to say that the passed object must implement both I1
and I2
:
interface I1 {
public function one();
}
interface I2 {
public function two();
}
class C2 {
public function __construct( I1&I2 $injected ) {
// Both calls are guaranteed to be valid
$injected->one();
$injected->two();
}
}
class C1 implements I1, I2 {
public function one() { ... }
public function two() { ... }
}
new C2( new C1 ); // passes the check, because it implements both interfaces
This syntax with an &
in the type (I1&I2
) is known as an "intersection type", as opposed to the more common "union type" syntax, e.g. I1|I2
would mean "must implement at least one of I2
and I2
".
Intersection types were added in PHP 8.1. If you need your code to work on older versions, it's a little bit trickier, but can be achieved by introducing a new interface that inherits both existing interfaces. The difference is that class C2
must implement this new interface explicitly for the check to pass:
interface I1 {
public function one();
}
interface I2 {
public function two();
}
interface I1and2 extends I1, I2 {}
class C2 {
public function __construct( I1and2 $injected ) {
// Both calls are guaranteed to be valid
$injected->one();
$injected->two();
}
}
class C1 implements I1and2 {
public function one() { ... }
public function two() { ... }
}
new C2( new C1 ); // passes the check
Alternatively, if your two interfaces actually represent two separate abilities that class C2
happens to cover, but could be covered by separate objects, you can just have two parameters, each defining one required interface:
interface I1 {
public function one();
}
interface I2 {
public function two();
}
class C2 {
public function __construct( I1 $injected1, I2 $injected2 ) {
// Valid call on any I1 instance
$injected1->one();
// Valid call on any I2 instance
$injected2->two();
}
}
class C1 implements I1, I2 {
public function one() { ... }
public function two() { ... }
}
$something = new C1;
new C2( $something, $something ); // passes both checks
Remember in each case that the point of using interfaces in the first place is to document the invariant properties of the code, rather than the implementation you happen to have at the moment.