У меня есть два интерфейса (ClienInterface, ClientFactoryInterface) и два класса, их реализующие (ConcreteClient, ConcreteApiClientFactory). ConcreteClient имеет метод, не определенный в ClienInterface.
Когда я пытаюсь использовать этот метод в коде, я получаю ошибки PHPStan:
Call to an undefined method ClienInterface::mySpecificFunction().
Я пытался реализовать это, но безуспешно: https://phpstan.org/blog/generics-by-examples#couple-relevant-classes-together
Мой пример на игровой площадке PHPStan:
<?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();
}
}
Ошибки
Метод Test::__construct() имеет параметр $factory с универсальным интерфейсом ClientFactoryInterface, но не указывает его типы: TClienInterface
Вызов неопределенного метода ClienInterface::mySpecificFunction().
Пожалуйста, добавьте свой код и в сам вопрос stackoverflow. Код во внешних ссылках может истечь.
Мне кажется, это законное предупреждение от PHPStan. Если вы полагаетесь на конкретный метод, который не определен в интерфейсе, ваша переменная должна относиться к конкретному классу, который имеет этот метод, а не интерфейс.






PHPStan нужна информация о конкретном типе. Эту информацию можно предоставить несколькими способами:
/** @var ConcreteClient $client */, перед переменной. Но я бы вообще не рекомендовал этого делать, потому что о типе бетона знает только завод.if, которые могут соответствовать или не соответствовать вашим потребностям. Пример:if ($client instanceof ConcreteClient) {
$client->mySpecificFunction();
}
run(). Реализации будут вызывать определенные методы напрямую. Тогда позвоните run() Например:interface ClienInterface
{
public function run(): void;
}
class ConcreteClient implements ClienInterface {
public function run(): void {
$this->mySpecificFunction();
}
public function mySpecificFunction(): void {}
}
class Test {
// Skipped the constructor ...
public function getClient(string $type): void {
$client = $this->factory->getClientByType($type);
$client->run();
}
}
Test в вашем примере) или контроллер, независимо от контекста использования клиента. Это можно сделать с помощью шаблона Посетитель, например:interface ClientUserInterface
{
public function runForClientA(): void;
public function runForClientB(): void;
}
interface ClienInterface
{
public function run(ClientUserInterface $user): void;
}
class ConcreteClient implements ClienInterface {
public function run(ClientUserInterface $user): void {
$user->runForClientA();
}
}
class Test implements ClientUserInterface {
// Skipped the constructor...
public function runForClientA(): void {
echo 'runForClientA';
}
public function runForClientB(): void {
echo 'runForClientB';
}
public function getClient(string $type): void {
$client = $this->factory->getClientByType($type);
$client->run($this);
}
}
Другие ответы могут подойти, но я вижу, что вы начали с дженериков, и это может быть отличным решением. Требуется несколько изменений:
/** @template TClienInterface of ClienInterface */
interface ClientFactoryInterface
{
/**
* @return TClienInterface
*/
public function getClientByType(string $type): ClienInterface;
}
Определение возвращаемого типа getClientByType для универсального типа, определенного вами в классе, гарантирует возврат определенного типа.
class Test {
/**
* @param ClientFactoryInterface<ConcreteClient> $factory
*/
public function __construct(
private readonly ClientFactoryInterface $factory
) {}
}
Определение обобщенного значения в конструкторе гарантирует, что класс принимает только фабрики, возвращающие этот конкретный тип. Это проходит тесты PHPstan.
Утверждение, что $factory является ClientFactoryInterface<ConcreteClient>, может вызвать проблемы, если на самом деле фабрика возвращает разные типы конкретных классов (чего я и ожидал от фабрики, потому что именно это она и делает по определению).
Здесь вообще нет никаких утверждений. При создании экземпляра Test PHPstan проверит, что предоставляемая вами фабрика является экземпляром ClientFactoryInterface<ConcreteClient>. Предоставление ClientFactoryInterface с другим универсальным интерфейсом приведет к возникновению ошибок.
Ты прав; нет утверждения типа, и ваш ответ устраняет предупреждения/ошибки PHPStan. Что я хотел отметить, так это то, что тип ClientFactoryInterface<ConcreteClient> ограничивает фабрику созданием только ConcreteClient объектов, что в моем мире плохо сочетается с фабричным методом getClientByType(string $type) и целью создания фабрики как таковой.
Согласен, я думаю, что здесь мы можем иметь дело с проблемой XY.
Спасибо @vixducis, я думал, что это можно сделать, не указывая реализацию интерфейса в тестовом классе.
Реализация интерфейса PHP является «слабой» в том смысле, что она не мешает вам вызывать функции, которые, как вы знаете, существуют в конкретном классе, который у вас есть, - но то, что вы можете, не означает, что вы должны это делать.
Если вы правильно используете контракт, определенный интерфейсом, то у вас нет доступа к mySpecificFunction(). Что еще более важно, что происходит, когда кто-то меняет фабрику и возвращается другой конкретный класс, которого нет mySpecificFunction() — ваше приложение аварийно завершает работу.
Не пытайтесь обойти предупреждение PHPStan. Как прокомментировал Артур Баучер, это законное предупреждение, и вам следует изменить свой код.
Вы можете просто добавить что-то вроде /**@var ConcreteClient**/ над переменной $client.