Как сообщить PHPStan о методе, который существует в реализации класса, но не в его интерфейсе?

У меня есть два интерфейса (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().

Вы можете просто добавить что-то вроде /**@var ConcreteClient**/ над переменной $client.

Stem Florin 05.04.2024 13:55

Пожалуйста, добавьте свой код и в сам вопрос stackoverflow. Код во внешних ссылках может истечь.

vixducis 05.04.2024 14:17

Мне кажется, это законное предупреждение от PHPStan. Если вы полагаетесь на конкретный метод, который не определен в интерфейсе, ваша переменная должна относиться к конкретному классу, который имеет этот метод, а не интерфейс.

Arthur Boucher 05.04.2024 14:18
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Symfony Station Communiqué - 7 июля 2023 г
Symfony Station Communiqué - 7 июля 2023 г
Это коммюнике первоначально появилось на Symfony Station .
Оживление вашего приложения Laravel: Понимание режима обслуживания
Оживление вашего приложения Laravel: Понимание режима обслуживания
Здравствуйте, разработчики! В сегодняшней статье мы рассмотрим важный аспект управления приложениями, который часто упускается из виду в суете...
Установка и настройка Nginx и PHP на Ubuntu-сервере
Установка и настройка Nginx и PHP на Ubuntu-сервере
В этот раз я сделаю руководство по установке и настройке nginx и php на Ubuntu OS.
Коллекции в Laravel более простым способом
Коллекции в Laravel более простым способом
Привет, читатели, сегодня мы узнаем о коллекциях. В Laravel коллекции - это способ манипулировать массивами и играть с массивами данных. Благодаря...
Как установить PHP на Mac
Как установить PHP на Mac
PHP - это популярный язык программирования, который используется для разработки веб-приложений. Если вы используете Mac и хотите разрабатывать...
1
3
422
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

PHPStan нужна информация о конкретном типе. Эту информацию можно предоставить несколькими способами:

  1. Тип утверждения: поместите PHPDoc, например /** @var ConcreteClient $client */, перед переменной. Но я бы вообще не рекомендовал этого делать, потому что о типе бетона знает только завод.
  2. Проверьте, является ли объект экземпляром конкретного класса. Это простое и безопасное решение. Но по мере роста количества конкретных классов вам, вероятно, потребуется добавить больше if, которые могут соответствовать или не соответствовать вашим потребностям. Пример:
if ($client instanceof ConcreteClient) {
    $client->mySpecificFunction();
}
  1. Создайте отдельную фабрику или фабричный метод, который возвращает конкретный тип вместо интерфейса.
  2. Инкапсулируйте логику вызова определенных методов в клиентский интерфейс, вводя новый метод с более общим значением, например. 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();
    }
}
  1. Существует также возможность отделить «конкретные» методы от конкретных клиентов и переместить их в вызывающий класс (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>, может вызвать проблемы, если на самом деле фабрика возвращает разные типы конкретных классов (чего я и ожидал от фабрики, потому что именно это она и делает по определению).

Ruslan Osmanov 05.04.2024 14:49

Здесь вообще нет никаких утверждений. При создании экземпляра Test PHPstan проверит, что предоставляемая вами фабрика является экземпляром ClientFactoryInterface<ConcreteClient>. Предоставление ClientFactoryInterface с другим универсальным интерфейсом приведет к возникновению ошибок.

vixducis 05.04.2024 14:56

Ты прав; нет утверждения типа, и ваш ответ устраняет предупреждения/ошибки PHPStan. Что я хотел отметить, так это то, что тип ClientFactoryInterface<ConcreteClient> ограничивает фабрику созданием только ConcreteClient объектов, что в моем мире плохо сочетается с фабричным методом getClientByType(string $type) и целью создания фабрики как таковой.

Ruslan Osmanov 05.04.2024 15:07

Согласен, я думаю, что здесь мы можем иметь дело с проблемой XY.

vixducis 05.04.2024 15:13

Спасибо @vixducis, я думал, что это можно сделать, не указывая реализацию интерфейса в тестовом классе.

piotr85 08.04.2024 12:21

Реализация интерфейса PHP является «слабой» в том смысле, что она не мешает вам вызывать функции, которые, как вы знаете, существуют в конкретном классе, который у вас есть, - но то, что вы можете, не означает, что вы должны это делать.

Если вы правильно используете контракт, определенный интерфейсом, то у вас нет доступа к mySpecificFunction(). Что еще более важно, что происходит, когда кто-то меняет фабрику и возвращается другой конкретный класс, которого нет mySpecificFunction() — ваше приложение аварийно завершает работу.

Не пытайтесь обойти предупреждение PHPStan. Как прокомментировал Артур Баучер, это законное предупреждение, и вам следует изменить свой код.

Другие вопросы по теме