phpstan / phpstan-doctrine

Doctrine extensions for PHPStan
MIT License
595 stars 97 forks source link

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

Closed arderyp closed 2 months ago

arderyp commented 9 months ago

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 :)

mergeable[bot] commented 9 months ago

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

It will most likely be closed after manual review.

ondrejmirtes commented 9 months ago

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

arderyp commented 9 months ago

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

arderyp commented 9 months ago

@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 commented 8 months ago

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 commented 4 months ago

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)

ondrejmirtes commented 2 months ago

Hey, I'm sorry, but I don't see anything actionable I could use to fix these errors. If you find the time, please submit a failing test here.

github-actions[bot] commented 1 month ago

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.