skip to Main Content

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


  1. PHPStan needs information about the concrete type. That information can be provided in a few ways:

    1. Type assertion: put PHPDoc like /** @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.
    2. Check if the object is an instance of the concrete class. This is a simple and safe solution. But as the number of concrete classes grows, you will likely need to add more ifs, which may or may not suit your needs. Example:
    if ($client instanceof ConcreteClient) {
        $client->mySpecificFunction();
    }
    
    1. Create a separate factory or a factory method that returns the concrete type instead of the interface.
    2. Encapsulate the logic of calling specific method(s) into the client interface by introducing a new method with a more generic meaning, e.g. run(). Implementations will call the specific methods directly. Then call the run() E.g.:
    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. There is also an option to separate the ‘specific’ methods from the concrete clients and move them to the calling class (Test in your example), or controller, whatever is the context of the client usage. That can be done using the Visitor pattern, e.g.:
    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);
        }
    }
    
    Login or Signup to reply.
  2. 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.

    Login or Signup to reply.
  3. 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 have mySpecificFunction() – 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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search