Search code examples
phpsymfonydoctrine-ormphpstan

Doctrine PHPStan entity relation Collection never written, only read


I am attempting to use the correct typings according to Doctrine and PHPStan, however with an entity relation, I can't seem to make it right.

I am using PHP 8.1.6, Doctrine 2.6.3, PHPStan 1.7.3 and PHPStan/Doctrine 1.3.6.

My entity looks like this:

#[ORM\Entity]
class Thing
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column(options: ['unsigned' => true])]
    private ?int $id;

    #[ORM\Column]
    private string $name;

    /** @var Collection&iterable<Item> $items */
    #[ORM\OneToMany(mappedBy: 'thing', targetEntity: Item::class)]
    private Collection $items;

    public function getId(): ?int
    {
        return $this->id;
    }

    /**
     * @return iterable<Items>
     */
    public function getItems(): iterable
    {
        return $this->items;
    }
}

For the ID, it's not complaining (doctrine rules are loaded into PHPStan, which works just fine). However, for the $items Collection, it's saying "is never written, only read". This makes no sense as it is a Collection, and will not be written (but rather added to through it's methods).

I am not quite understanding why it is giving me this error, and I can't seem to find much about it, other than "It should work".


Solution

  • You have no "setters" nor "adders/removers" for that private property. So, PhpStan is indeed right.

    Either remove that property entirely from the inversed side of that relation (aka in the Thing class) or add some "adders/removers".

    This usually looks something like this.

    public function addItem(Item $item): self
    {
        if (!$this->items->contains($items)) {
            $this->items[] = $item;
            // optional but keeps both sides in sync
            $item->setThing($this);
        }
    
        return $this;
    }
    
    public function removeItem(Item $item): self
    {
        $this->items->removeElement($item);
        // should your Item::thing be not nullable, skip this
        $item->setThing(null);
    
        return $this;
    }