I have two interfaces (ClienInterface
, ClientFactoryInterface
) and two classes implementing them (ConcreteClient
, ConcreteApiClientFactory
). ConcreteClient
has method not definded in ClienInterface
.
When I try to use this method in code, I get PHPStan errors:
Call to an undefined method ClienInterface::mySpecificFunction().
I’ve tried to implement this but with no luck:
https://phpstan.org/blog/generics-by-examples#couple-relevant-classes-together
My example on PHPStan playground:
<?php declare(strict_types = 1);
interface ClienInterface
{
}
/** @template TClienInterface of ClienInterface */
interface ClientFactoryInterface
{
public function getClientByType(string $type): ClienInterface;
}
class ConcreteClient implements ClienInterface {
public function mySpecificFunction(): void {}
}
/** @implements ClientFactoryInterface<ConcreteClient> */
class ConcreteApiClientFactory implements ClientFactoryInterface {
public function getClientByType(string $type): ClienInterface {
return new ConcreteClient();
}
}
class Test {
public function __construct(
private readonly ClientFactoryInterface $factory
) {}
public function getClient(string $type): void {
$client = $this->factory->getClientByType($type);
$client->mySpecificFunction();
}
}
Errors
Method Test::__construct() has parameter $factory with generic interface ClientFactoryInterface but does not specify its types: TClienInterface
Call to an undefined method ClienInterface::mySpecificFunction().
3
Answers
PHPStan needs information about the concrete type. That information can be provided in a few ways:
/** @var ConcreteClient $client */
before the variable. But I wouldn’t recommend this at all, because only the factory has the knowledge about the concrete type.if
s, which may or may not suit your needs. Example:run()
. Implementations will call the specific methods directly. Then call therun()
E.g.:Test
in your example), or controller, whatever is the context of the client usage. That can be done using the Visitor pattern, e.g.:Other answers might be fine, but I see you’ve started with generics, which could be a great solution. There are a couple of changes required:
Defining the returntype of
getClientByType
to the generic you’ve defined on the class makes sure the specific type is returned.Defining the generic on the constructor makes sure the class only accepts factories that return that specific type. Thiss passes PHPstan’s tests.
PHP’s interface implementation is "weak" in that it does not prevent you calling functions which you know exist on the concrete class you have – but just because you can doesn’t mean you should.
If you correctly use the contract defined by the interface then you do not have access to
mySpecificFunction()
. More importantly, what happens when someone changes the factory and a different concrete class is returned which does not havemySpecificFunction()
– your application crashes.Don’t try to work around the PHPStan warning. As Arthur Boucher commented, this is a legitimate warning and you should change your code.