phpannotationsphpstan

How to tell PHPStan about method that exists in class implementation but not in it's interface?


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().


Solution

  • 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:

    /** @template TClienInterface of ClienInterface */
    interface ClientFactoryInterface
    {
        /**
         * @return TClienInterface
         */
        public function getClientByType(string $type): ClienInterface;
    }
    

    Defining the returntype of getClientByType to the generic you've defined on the class makes sure the specific type is returned.

    class Test {
        /**
         * @param ClientFactoryInterface<ConcreteClient> $factory
         */
        public function __construct(
            private readonly ClientFactoryInterface $factory
        ) {}
    }
    

    Defining the generic on the constructor makes sure the class only accepts factories that return that specific type. Thiss passes PHPstan's tests.