Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
arderyp opened this issue Jan 26, 2024 · 5 comments

Comments

@arderyp
Copy link

arderyp commented Jan 26, 2024

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

Copy link

mergeable bot commented Jan 26, 2024

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

It will most likely be closed after manual review.

@ondrejmirtes ondrejmirtes transferred this issue from phpstan/phpstan Jan 27, 2024
@ondrejmirtes
Copy link
Member

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

@arderyp
Copy link
Author

arderyp commented Jan 27, 2024

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

@arderyp
Copy link
Author

arderyp commented Jan 27, 2024

@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
Copy link
Author

arderyp commented Mar 5, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants