2

I'm currently working on a PHP project where I've implemented dependency injection in my code. The code is functioning correctly, but I'm encountering an issue with PhpStorm (version 2023.1) misinterpreting the code, which is causing typehinting to break. I'm reaching out to the community for assistance in resolving this problem and helping PhpStorm correctly interpret my code to restore seamless typehinting functionality.

To provide some context, here's a simplified example of the code:

class B {

    public static function SayHello(){
        echo 'Hello World';
    }

}


class A {

    /**
     * @param B::class $B
     */
    public function __construct(
        private readonly string $B
    ){}

    public function start(){
        $this->B::SayHello(); // <---- I Would like that SayHallo() was available via typehinting
    }

}

class AFactory extends A {

    public function __construct(){
        parent::__construct(B::class);
    }
}


$class = new AFactory();
$class->start();

This behavior is puzzling because I have clearly indicated in the PHPDoc that $B should be of type B::class, referring to the class rather than just a generic string.

When attempting to address the issue, I made several attempts to inform PhpStorm that $this->B should be recognized as an instance of the B class, rather than just a string. One approach I tried was specifying in the PHPDoc that $B should be considered an instance of the B class. However, this approach introduced other problems.

PhpStorm interpreted the PHPDoc declaration as indicating that $B had already been initialized, which was not the case. As a result, it introduced further confusion and did not resolve the issue as expected.

I would greatly appreciate any help or suggestions from the community to resolve this issue or find alternative ways to implement Dependency Injection effectively.

LazyOne
  • 158,824
  • 45
  • 388
  • 391

3 Answers3

2

If your goal is to implement DI, then you don't want to pass a string containing the name of the class, you want to pass an object containing an instance of the class.

This behavior is puzzling because I have clearly indicated in the PHPDoc that $B should be of type B::class, referring to the class rather than just a generic string.

No. B::class is a string, which contains the name of the class, which is "B". You want an instance of B, so just use B:

// this means that $obj must be an instance of B
public function __construct(private readonly B $obj)
{
}

public function start()
{
    $this->obj::<start typing> // now your IDE will autocomplete B's methods
}

So, then you'd do:

$b = new B();
$a = new A($b); // This is where the DI happens.

In your example, you're saying that the constructor's argument can only ever be the exact string "B" -- which makes no sense, as if it can never change, why would you even bother taking it as a parameter.

Also note, if you're explicitly typehinting, then you generally don't need docblocks, as your IDE will be able to deduce type just from the typehints, and providing both gives you the possibility of having them conflict. Unless you're adding descriptions to the parameters, I'd leave them out. Otherwise, make sure they match:

/**
 * @param B $obj An instance of B.
 */
public function __construct(private readonly B $obj)
{
}
Alex Howansky
  • 50,515
  • 8
  • 78
  • 98
  • Regarding your last paragraph, there is a situation where deliberately having non-matching doc-blocks can be useful: many static analysis tools, including PHPStorm, handle a richer type system than PHP itself. So you can for instance write `/** @param string[] $listOfStrings */ function foo(array $listOfStrings) { ... }` – IMSoP May 19 '23 at 17:14
  • Doing it this way aims to make the code more testable. In this code example, I can easily replace the class of B with another mocked class with the same functionality. This helps me with unit testing because I've full control over the behavior of Class B. – Michel Bitter May 19 '23 at 17:15
  • 1
    _" I can easily replace the class of B with another mocked class with the same functionality"_ No, not if you typehint this class to `B`. You'd need to implement a common interface, typehint to the interface instead of a concrete class (see the `D` in `SOLID`), and then have both your live class and your mock implement the interface. – Alex Howansky May 19 '23 at 17:19
1

Unfortunately you cannot do this w/o polluting your code as you are unable to typehint $this->B. The workaround would be to reassign it to another variable and typehint that variable instead, like this:

    public function start(){
        /** @var B $cls */
        $cls = $this->B;
        $cls::SayHello();
    }
Marcin Orlowski
  • 72,056
  • 11
  • 123
  • 141
  • Thanks for your answer. Your solution does not solve the problem. but you helped me with finding the right solution. I will update the Post with the solution. – Michel Bitter May 19 '23 at 17:06
  • 1
    It's worth noting that this code works by *lying to the IDE*: `$cls` does *not* contain a value of type `B`, it contains a value of type `string`. Some analysis tools understand a pseudo-type `class-string`, which can be further qualified like `class-string`, but PHPStorm as of this writing doesn't seem to suggest statics based on that. – IMSoP May 19 '23 at 17:10
  • The problem is Intelij (and derivatives) does not still understand `class-string` so you have no much choice. – Marcin Orlowski May 19 '23 at 17:19
  • @MarcinOrlowski Actually, it seems I was testing wrong, `class-string` *does* work in the the current version of PhpStorm, at least for local variables. I'm not sure why it doesn't work when annotated on the property itself. – IMSoP May 19 '23 at 17:21
  • Unfortunately Intelij Idea with PHP plugin does not support that. – Marcin Orlowski May 20 '23 at 14:57
1

Thanks to the post of Marcin Olowsky, I've found a workaround to the problem. As he suggested, I had to pollute my code a bit. By writing the start() method as follow:

public function start(){
    /**
     * @var class-string<B> $c
     */
    $c = $this->B;
    $c::SayHello();

}

By doing it this way, PHPStorm knows the possibilities of $c and gives me the correct type hints.

  • 1
    This is not DI. You're still just passing a generic string, there's no run-time guarantee that it represents an existing class, that it has a method named SayHello, or that it can be called statically. – Alex Howansky May 19 '23 at 17:32
  • You're right, and I see your point. I'm currently looking into a better way to implement DI. – Michel Bitter May 19 '23 at 17:41