phpstan-doctrine
phpstan-doctrine copied to clipboard
order of QueryBuilder::select arguments is not preserved in QueryBuilder::getResult
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,
];
}
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