phpstan-doctrine icon indicating copy to clipboard operation
phpstan-doctrine copied to clipboard

False Possitive in Abstract Respoitory classes mapped to Abstract Entity classes: "<entity> has no field or association named <field>"

Open arderyp opened this issue 1 year ago • 6 comments

Bug report

Here is an example of what I am seeing. It is incorrect on all counts.

------ ------------------------------------------------------------------------------------------------------------- 
  Line   src/App/Repository/AbstractEntityRepository.php                                                       
 ------ ------------------------------------------------------------------------------------------------------------- 
  19     QueryBuilder: [Semantical Error] line 0, col 86 near 'active = 1': Error: Class                              
         App\Entity\AbstractEntity has no field or association named active                      
  40     QueryBuilder: [Semantical Error] line 0, col 86 near 'altId': Error: Class              
         App\Entity\AbstractEntity has no field or association named altId  
  49     QueryBuilder: [Semantical Error] line 0, col 86 near 'altId': Error: Class              
         App\Entity\AbstractEntity has no field or association named altId  
  82     QueryBuilder: [Semantical Error] line 0, col 86 near 'number = :number': Error: Class                        
         App\Entity\AbstractEntity has no field or association named number                      
  93     QueryBuilder: [Semantical Error] line 0, col 86 near 'active = 1 ORDER': Error: Class                        
         App\Entity\AbstractEntity has no field or association named active                      
 ------ ------------------------------------------------------------------------------------------------------------- 

 ------ -------------------------------------------------------------------------------------------------------------------- 
  Line   tests/Default/Common/Controller/AbstractControllerTestCase.php                                                      
 ------ -------------------------------------------------------------------------------------------------------------------- 
  484    QueryBuilder: [Semantical Error] line 0, col 20 near 'id) FROM App\Common\Entity\AbstractEntity': Error: Class  
         App\Common\Entity\AbstractEntity has no field or association named id                                           
 ------ --------------------------------------------------------------------------------------------------------------------

I don't know how to replicate this type of thing on phpstan.org given the Doctrine dependency. Let me know if you might have a suggestion.

Code snippet that reproduces the problem

No response

Expected output

no erros

Did PHPStan help you today? Did it make you happy in any way?

always :)

arderyp avatar Jan 26 '24 23:01 arderyp

This bug report is missing a link to reproduction at phpstan.org/try.

It will most likely be closed after manual review.

mergeable[bot] avatar Jan 26 '24 23:01 mergeable[bot]

You should at least post some code that leads to this behaviour.

ondrejmirtes avatar Jan 27 '24 06:01 ondrejmirtes

I will post later this weekend when I get a chance.

arderyp avatar Jan 27 '24 16:01 arderyp

@ondrejmirtes

I've updated the phpstan output obfuscation to be more clear and broken it into chunks with the corresponding code below each chunk. I've also marked the phpstan errors lines with corresponding // PHPSTAN comments.

These issues only surfaced when updating to latest phpstan from 1.10.32 to 1.10.57 and phpstan-doctrine from 1.3.42 to 1.3.59.

Chunk One

------ ------------------------------------------------------------------------------------------------------------- 
  Line   src/App/Repository/AbstractAppRepository.php                                                       
 ------ ------------------------------------------------------------------------------------------------------------- 
  19     QueryBuilder: [Semantical Error] line 0, col 86 near 'active = 1': Error: Class                              
         App\Entity\AbstractAppEntity has no field or association named active                      
  40     QueryBuilder: [Semantical Error] line 0, col 86 near 'altId': Error: Class              
         App\Entity\AbstractAppEntity has no field or association named altId  
  49     QueryBuilder: [Semantical Error] line 0, col 86 near 'altId': Error: Class              
         App\Entity\AbstractAppEntity has no field or association named altId  
  82     QueryBuilder: [Semantical Error] line 0, col 86 near 'number = :number': Error: Class                        
         App\Entity\AbstractAppEntity has no field or association named number                      
  93     QueryBuilder: [Semantical Error] line 0, col 86 near 'active = 1 ORDER': Error: Class                        
         App\Entity\AbstractAppEntity has no field or association named active                      
 ------ ------------------------------------------------------------------------------------------------------------- 
use App\Entity\AbstractAppEntity;
use Doctrine\ORM\EntityRepository;

/**
 * @template TypeEntity of AbstractAppEntity
 * @extends EntityRepository<TypeEntity>
 */
abstract class AbstractAppRepository extends EntityRepository
{
    /** @return class-string<TypeEntity> */
    abstract protected function getEntityClass(): string;

    /** @return TypeEntity[] */
    public function findAllActive(): array
    {
        return $this->getEntityManager()->createQueryBuilder()
            ->select('entity')
            ->from($this->getEntityClass(), 'entity')
            ->where('entity.active = 1')     // PHPSTAN ERROR: "AbstractAppEntity has no field or association named active"
            ->getQuery()
            ->getResult();
    }

    /** @return TypeEntity|null */
    public function findByAltId(string $altId, bool $active = null): ?AbstractAppEntity
    {
        $qb = $this->getEntityManager()->createQueryBuilder()
            ->select('entity')
            ->from($this->getEntityClass(), 'entity')
            ->where('entity.altId = :altId')    // PHPSTAN ERROR: "AbstractAppEntity has no field or association named altId"
            ->setParameter('altId', $altId);
        if (null !== $active) {
            $qb->andWhere('entity.active = :active')
                ->setParameter('active', $active);
        }
        return $qb->getQuery()->getOneOrNullResult();
    }

    /**
     * @param string[] $ids
     * @return TypeEntity[]
     */
    public function findAltIds(array $altIds): array
    {
        return $this->getEntityManager()->createQueryBuilder()
            ->select('entity')
            ->from($this->getEntityClass(), 'entity')
            ->where('entity.altId NOT IN (:altIds)')    // PHPSTAN ERROR: "AbstractAppEntity has no field or association named altId"
            ->andWhere('entity.active = 1')
            ->setParameter('altIds', $altIds)
            ->getQuery()
            ->getResult();
    }

    /**
     * @param array{
     *     number               : int,
     *     prefix               : string|null,
     *     suffix               : string|null,
     * } $options
     */
    public function findLatest(array $options): ?AbstractAppEntity
    {
        $qb = $this->getEntityManager()->createQueryBuilder()
            ->select('entity')
            ->from($this->getEntityClass(), 'entity')
            ->where('entity.number = :number')    // PHPSTAN ERROR: "AbstractAppEntity has no field or association named number"
            ->setParameter('number', $options['number']);
        foreach (['prefix', 'suffix'] as $option) {
            if (! empty($options[$option])) {
                $qb->andWhere("entity.$option = :$option")
                    ->setParameter($option, $options[$option]);
            }
        }
        $result = $qb->orderBy('entity.id', 'ASC')
            ->setMaxResults(1)
            ->getQuery()
            ->getResult();
        return $result ? $result[0] : null;
    }

    /** @return ?TypeEntity */
    public function findLatestActive(): ?AbstractAppEntity
    {
        $results = $this->getEntityManager()->createQueryBuilder()
            ->select('entity')
            ->from($this->getEntityClass(), 'entity')
            ->andWhere('entity.active = 1')  // PHPSTAN ERROR: "AbstractAppEntity has no field or association named active"
            ->orderBy('entity.id', 'DESC')
            ->setMaxResults(1)
            ->getQuery()
            ->getResult();
        return $results ? $results[0] : null;
    }

    ...
}
use App\Common\Entity\AbstractCommonEntity;
use App\Common\Entity\CommonEntityInterface;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;

/**
 * @ORM\MappedSuperclass(repositoryClass="App\Repository\AbstractAppRepository")
 * @ORM\HasLifecycleCallbacks()
 */
abstract class AbstractAppEntity extends AbstractCommonEntity implements CommonEntityInterface
{
    /**
     * @Assert\NotBlank()
     * @Assert\NotNull()
     * @ORM\Column(name="active", type="boolean", nullable=false)
     */
    protected bool $active;    // PHPSTAN is wrong, the field exists.

    /**
     * @Assert\NotBlank()
     * @Assert\NotNull()
     * @ORM\Column(name="alt_id", type="string", nullable=false)
     */
    protected string $altId;    // PHPSTAN is wrong, the field exists.

    /**
     * @Assert\Range(min=1, max=10000)
     * @ORM\Column(name="number", type="smallint", nullable=false)
     */
    protected int $number;    // PHPSTAN is wrong, the field exists.

    /**
     * @Assert\NotIdenticalTo("")
     * @ORM\Column(name="prefix", type="string", nullable=true)
     */
    protected ?string $prefix;

    /**
     * @Assert\NotIdenticalTo("")
     * @ORM\Column(name="suffix", type="string", nullable=true)
     */
    protected ?string $suffix;

    public function getActive(): bool
    {
        return $this->active;
    }

    public function getAltId(): string
    {
        return $this->altId;
    }

    public function getNumber(): int
    {
        return $this->number;
    }

    public function getPrefix(): ?string
    {
        return $this->prefix;
    }

    public function getSuffix(): ?string
    {
        return $this->suffix;
    }

    public function setActive(bool $active): static
    {
        $this->active = $active;
        return $this;
    }

Chunk Two

 ------ -------------------------------------------------------------------------------------------------------------------- 
  Line   tests/Default/Common/Controller/AbstractCommonControllerTestCase.php                                                      
 ------ -------------------------------------------------------------------------------------------------------------------- 
  484    QueryBuilder: [Semantical Error] line 0, col 20 near 'id) FROM App\Common\Entity\AbstractCommonEntity': Error: Class  
         App\Common\Entity\AbstractCommonEntity has no field or association named id                                           
 ------ --------------------------------------------------------------------------------------------------------------------
use App\Common\Entity\AbstractCommonEntity;
use App\Tests\Default\Common\AbstractWebTestCase;
use Doctrine\ORM\EntityRepository;
use Symfony\Component\Form\AbstractType;

/** @template TypeEntity of AbstractCommonEntity */
abstract class AbstractCommonControllerTestCase extends AbstractWebTestCase
{
    ...

    /** @param class-string<AbstractCommonEntity> $entityClass */
    protected function getEntityCount(string $entityClass): int
    {
        // PHPSTAN ERROR: "AbstractCommonEntity has no field or association named id"
        return (int) $this->em->createQueryBuilder()->select('count(entity.id)')->from($entityClass, 'entity')->getQuery()->getSingleScalarResult();
    }

    ...
}
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\MappedSuperclass
 * @ORM\HasLifecycleCallbacks()
 */
abstract class AbstractCommonEntity
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     * @ORM\Column(name="id", type="integer", nullable=false)
     */
    protected int $id;    // PHPSTAN is wrong, the field exists.

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

    /** Use this to check if an instance is new, in which case it won't have a database id */
    final public function isIdSet(): bool
    {
        return isset($this->id);
    }

    final public function unsetId(): static
    {
        unset($this->id);
        return $this;
    }
}

arderyp avatar Jan 27 '24 17:01 arderyp

for some reason, upgrading doctrine/orm from 2.19.0 to 3.1.0 caused this error to go away... despite causing errors elsewhere with doctrine.

arderyp avatar Mar 05 '24 02:03 arderyp

I had to roll back that orm change, but these errors mysteriously disappeared again. I was ignorning the errors yesterday. In an unrelated task, I deleted and reinstalled my vendor today, and not phpstan is reporting that there are no errors to ignore. So, I have no idea when/how they dissapeard (or what was wrong with my vendor directory, for that matter)

arderyp avatar Jun 28 '24 19:06 arderyp