phpphpstorm

PhpStorm intellisense not working properly with method chaining + multiple classes


I'm having some trouble with PhpStorm intellisense when working with several classes. This is the structure I'm dealing with:

trait ContextTrait
{
    public Subscriptions $subscription;
    
    public function setSubscription(Subscriptions $value): self 
    {
        $this->subscription = $value;
        
        return $this;
    }
}

abstract class Transaction
{
    use ContextTrait;
    
    protected ?int $paymentMethodId = null;
    
    public function setPaymentMethodId(?int $value): self 
    {
        $this->paymentMethodId = $value;

        return $this;
    }
}

class Invoice extends Transaction
{
    protected ?string $dueDate = null;
    
    public function setDueDate(?string $value): self
    {
        $this->dueDate = $value;
        
        return $this;
    }
}

When initializing the class like this:

$invoice = (new Invoice)
    ->setSubscription(...)
    ->setPaymentMethodId(...)
    ->setDueDate(...);

I'm having the following issues:

  1. The intellisense (while typing) only works for the functions ->setSubscription and ->setPaymentMethodId, it doesn't work for ->setDueDate.
  2. Pressing F12 on ->setDueDate doesn't work, it returns the error "Cannot find declaration to go to".
  3. The function setDueDate is grayed out in the class, stating that the function is never used anywhere.

If I use a different instantiation approach (without chaining):

$invoice = new Invoice;
$invoice->setSubscription(...);
$invoice->setPaymentMethodId(...);
$invoice->setDueDate(...);

Everything works perfectly. Is there any way to fix this?


Edit:

An example of what is shows to me in PhpStorm:

  1. Using chaining method: It does not show the ->setDueDate function.

enter image description here

  1. Not using chaining method: It shows the ->setDueDate function.

enter image description here


Solution

  • Meanwhile, I tried with static [instead of : self] and it indeed works, why?

    This boils down in the PHP language (PhpStorm's IntelliSense, better Code completion tries to mimic it):

    self is the class itself, that is always concrete:

    abstract class Transaction
    {
        ...
        public function setPaymentMethodId(?int $value): self 
        {
            ...
            return $this;
        }
    }
    

    When chaining, the code completion resolves self to Transaction:

    $invoice = (new Invoice)
        ->setSubscription(...)
        ->setPaymentMethodId(...) # resolves to Transaction
        ->setDueDate(...); # Transaction has no such method
    

    Now with static this turns into what is called Late Static Bindings.

    Late means late (when it happens), and binding means to associate the concrete class. As an abstract class cannot be instantiated, the class actually never is purely self but one that extends from it and that is not abstract:

    class Invoice extends Transaction
    {
        ...
        public function setDueDate(?string $value): self
        {
            ...
        }
    }
    

    Again with the chaining and : static on setPaymentMethodId(), the concrete class Invoice is being resolved for code completion:

    $invoice = (new Invoice)
        ->setSubscription(...)
        ->setPaymentMethodId(...) # resolves to Invoice
        ->setDueDate(...); # Invoice has such a method
    

    This to this point is just about static vs. self for the late static binding in PHP.

    Now what the IDE needs to do during chaining (as compared to individual $object->method() calls), is to resolve it one after the other in the chain. This adds complexity and my educated guess is, that the analyzer has a certain depth for which it resolves and if it sees self it is reducing the overhead for a best quick result.

    There are also other guess-mechanisms on special classes as they work like decorators (e.g. the traversable IteratorIterator) and if you hint towards generics (or as pointed out in the comment for traits).

    In general you can also benefit from using interfaces as they are easy to look-up both for the IDE and for PHP. You also go away from diverse abstract or non-abstract implementations (by making them pure abstract classes, known as interfaces in PHP).