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

order of QueryBuilder::select arguments is not preserved in QueryBuilder::getResult

Open CircleCode opened this issue 2 years ago • 1 comments

According to https://github.com/phpstan/phpstan-doctrine/blob/90e60ba9dbea4b29c7b87026a29e91ac0a02674e/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php#L117-L127, QueryBuilder::getResult returns an array withh all returned types, and does not preserve order.

As an example, we had this code:

    /*
     * @return array{ 0: SearchCustomization | null, 1: LocalizationDetail | null }
     */
    public function findLandingInformation(string $url, string $locale): array
    {
        $queryBuilder = $this->getEntityManager()->createQueryBuilder()
            ->select('sc, ld')
            ->from(SearchCustomization::class, 'sc')
            ->innerJoin(SearchCustomizationText::class, 'sct', Join::WITH, 'sct.searchCustomization = sc')
            ->innerJoin(Language::class, 'l', Join::WITH, 'sct.language = l')
            ->leftJoin(LocalizationDetail::class, 'ld', Join::WITH, 'ld.localization = sc.localization')
                    ->where('sct.url = :url')
                    ->andWhere('l.code = :language')
                    ->andWhere('sc.isActive = 1')
                    ->andWhere('ld.language = l')
                    ->setParameter('url', $url)
                    ->setParameter('language', $locale);

        return $queryBuilder->getQuery()->getResult();
    }

and phpstan complained that

Method findLandingInformation()  
         should return array{Api\Entity\SearchCustomization|null,                        
         Api\Entity\LocalizationDetail|null} but returns array<int,                      
         Api\Entity\LocalizationDetail|Api\Entity\SearchCustomization|null>.

so we had to write this to help phpstan understand:

    /*
     * @return array{ 0: SearchCustomization | null, 1: LocalizationDetail | null }
     */
    public function findLandingInformation(string $url, string $locale): array
    {
        $queryBuilder = $this->getEntityManager()->createQueryBuilder()
            ->select('sc, ld')
            ->from(SearchCustomization::class, 'sc')
            ->innerJoin(SearchCustomizationText::class, 'sct', Join::WITH, 'sct.searchCustomization = sc')
            ->innerJoin(Language::class, 'l', Join::WITH, 'sct.language = l')
            ->leftJoin(LocalizationDetail::class, 'ld', Join::WITH, 'ld.localization = sc.localization')
                    ->where('sct.url = :url')
                    ->andWhere('l.code = :language')
                    ->andWhere('sc.isActive = 1')
                    ->andWhere('ld.language = l')
                    ->setParameter('url', $url)
                    ->setParameter('language', $locale);

        [$searchCustomization, $localizationDetail] = $queryBuilder->getQuery()->getResult();

        return [
            is_a($searchCustomization, SearchCustomization::class) ? $searchCustomization : null,
            is_a($localizationDetail, LocalizationDetail::class) ? $localizationDetail : null,
        ];
    }

CircleCode avatar Nov 03 '23 11:11 CircleCode

Hi @CircleCode, it better start with how the Query is templated https://github.com/phpstan/phpstan-doctrine/blob/1.4.x/src/Type/Doctrine/Query/QueryType.php similar to an array array<Tkey, Tresult> https://github.com/phpstan/phpstan-src/blob/1.11.x/src/Type/ArrayType.php

You can try

$query = $this->getEntityManager()->createQueryBuilder()
            ->select('sc, ld')
            ->from(SearchCustomization::class, 'sc')
            ->innerJoin(SearchCustomizationText::class, 'sct', Join::WITH, 'sct.searchCustomization = sc')
            ->innerJoin(Language::class, 'l', Join::WITH, 'sct.language = l')
            ->leftJoin(LocalizationDetail::class, 'ld', Join::WITH, 'ld.localization = sc.localization')
            ->getQuery();
\PHPstan\dumpType($query);

You'll get something like `Query<array-key, SearchCustomization | LocalizationDetail>

To support keeping shape, I think we need a Query shape like we have array shape https://github.com/phpstan/phpstan-src/blob/1.11.x/src/Type/Constant/ConstantArrayType.php and we need to change both the

new QueryType($queryString, $typeBuilder->getIndexType(), $typeBuilder->getResultType());

calls and the simplification made by the typeBuilder here https://github.com/phpstan/phpstan-doctrine/blob/1.4.x/src/Type/Doctrine/Query/QueryResultTypeBuilder.php#L214

VincentLanglet avatar Feb 09 '24 07:02 VincentLanglet