diff --git a/lib/Doctrine/ORM/Query/AST/Functions/IdentityFunction.php b/lib/Doctrine/ORM/Query/AST/Functions/IdentityFunction.php index 09019312c6..be00521ee2 100644 --- a/lib/Doctrine/ORM/Query/AST/Functions/IdentityFunction.php +++ b/lib/Doctrine/ORM/Query/AST/Functions/IdentityFunction.php @@ -10,6 +10,7 @@ use Doctrine\ORM\Query\QueryException; use Doctrine\ORM\Query\SqlWalker; +use function assert; use function reset; use function sprintf; @@ -23,7 +24,7 @@ class IdentityFunction extends FunctionNode /** @var PathExpression */ public $pathExpression; - /** @var string */ + /** @var string|null */ public $fieldMapping; /** @@ -31,14 +32,14 @@ class IdentityFunction extends FunctionNode */ public function getSql(SqlWalker $sqlWalker) { - $platform = $sqlWalker->getEntityManager()->getConnection()->getDatabasePlatform(); - $quoteStrategy = $sqlWalker->getEntityManager()->getConfiguration()->getQuoteStrategy(); + assert($this->pathExpression->field !== null); + $entityManager = $sqlWalker->getEntityManager(); + $platform = $entityManager->getConnection()->getDatabasePlatform(); + $quoteStrategy = $entityManager->getConfiguration()->getQuoteStrategy(); $dqlAlias = $this->pathExpression->identificationVariable; $assocField = $this->pathExpression->field; - $qComp = $sqlWalker->getQueryComponent($dqlAlias); - $class = $qComp['metadata']; - $assoc = $class->associationMappings[$assocField]; - $targetEntity = $sqlWalker->getEntityManager()->getClassMetadata($assoc['targetEntity']); + $assoc = $sqlWalker->getMetadataForDqlAlias($dqlAlias)->associationMappings[$assocField]; + $targetEntity = $entityManager->getClassMetadata($assoc['targetEntity']); $joinColumn = reset($assoc['joinColumns']); if ($this->fieldMapping !== null) { @@ -63,7 +64,7 @@ public function getSql(SqlWalker $sqlWalker) } // The table with the relation may be a subclass, so get the table name from the association definition - $tableName = $sqlWalker->getEntityManager()->getClassMetadata($assoc['sourceEntity'])->getTableName(); + $tableName = $entityManager->getClassMetadata($assoc['sourceEntity'])->getTableName(); $tableAlias = $sqlWalker->getSQLTableAlias($tableName, $dqlAlias); $columnName = $quoteStrategy->getJoinColumnName($joinColumn, $targetEntity, $platform); diff --git a/lib/Doctrine/ORM/Query/AST/Functions/SizeFunction.php b/lib/Doctrine/ORM/Query/AST/Functions/SizeFunction.php index 2adeb420cb..f4932671e0 100644 --- a/lib/Doctrine/ORM/Query/AST/Functions/SizeFunction.php +++ b/lib/Doctrine/ORM/Query/AST/Functions/SizeFunction.php @@ -10,6 +10,8 @@ use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\SqlWalker; +use function assert; + /** * "SIZE" "(" CollectionValuedPathExpression ")" * @@ -27,18 +29,19 @@ class SizeFunction extends FunctionNode */ public function getSql(SqlWalker $sqlWalker) { - $platform = $sqlWalker->getEntityManager()->getConnection()->getDatabasePlatform(); - $quoteStrategy = $sqlWalker->getEntityManager()->getConfiguration()->getQuoteStrategy(); + assert($this->collectionPathExpression->field !== null); + $entityManager = $sqlWalker->getEntityManager(); + $platform = $entityManager->getConnection()->getDatabasePlatform(); + $quoteStrategy = $entityManager->getConfiguration()->getQuoteStrategy(); $dqlAlias = $this->collectionPathExpression->identificationVariable; $assocField = $this->collectionPathExpression->field; - $qComp = $sqlWalker->getQueryComponent($dqlAlias); - $class = $qComp['metadata']; + $class = $sqlWalker->getMetadataForDqlAlias($dqlAlias); $assoc = $class->associationMappings[$assocField]; $sql = 'SELECT COUNT(*) FROM '; if ($assoc['type'] === ClassMetadata::ONE_TO_MANY) { - $targetClass = $sqlWalker->getEntityManager()->getClassMetadata($assoc['targetEntity']); + $targetClass = $entityManager->getClassMetadata($assoc['targetEntity']); $targetTableAlias = $sqlWalker->getSQLTableAlias($targetClass->getTableName()); $sourceTableAlias = $sqlWalker->getSQLTableAlias($class->getTableName(), $dqlAlias); @@ -60,7 +63,7 @@ public function getSql(SqlWalker $sqlWalker) . $sourceTableAlias . '.' . $quoteStrategy->getColumnName($class->fieldNames[$targetColumn], $class, $platform); } } else { // many-to-many - $targetClass = $sqlWalker->getEntityManager()->getClassMetadata($assoc['targetEntity']); + $targetClass = $entityManager->getClassMetadata($assoc['targetEntity']); $owningAssoc = $assoc['isOwningSide'] ? $assoc : $targetClass->associationMappings[$assoc['mappedBy']]; $joinTable = $owningAssoc['joinTable']; diff --git a/lib/Doctrine/ORM/Query/Parser.php b/lib/Doctrine/ORM/Query/Parser.php index 9e8aab41d2..d54ac92485 100644 --- a/lib/Doctrine/ORM/Query/Parser.php +++ b/lib/Doctrine/ORM/Query/Parser.php @@ -60,6 +60,7 @@ use Doctrine\ORM\Query\AST\UpdateStatement; use Doctrine\ORM\Query\AST\WhenClause; use Doctrine\ORM\Query\AST\WhereClause; +use LogicException; use ReflectionClass; use function array_intersect; @@ -84,6 +85,16 @@ /** * An LL(*) recursive-descent parser for the context-free grammar of the Doctrine Query Language. * Parses a DQL query, reports any errors in it, and generates an AST. + * + * @psalm-type QueryComponent = array{ + * metadata?: ClassMetadata, + * parent?: string|null, + * relation?: mixed[]|null, + * map?: string|null, + * resultVariable?: AST\Node|string, + * nestingLevel: int, + * token: array + * } */ class Parser { @@ -143,16 +154,16 @@ class Parser /** @psalm-var list */ private $deferredIdentificationVariables = []; - /** @psalm-var list */ + /** @psalm-var list */ private $deferredPartialObjectExpressions = []; - /** @psalm-var list */ + /** @psalm-var list */ private $deferredPathExpressions = []; /** @psalm-var list */ private $deferredResultVariables = []; - /** @psalm-var list */ + /** @psalm-var list */ private $deferredNewObjectExpressions = []; /** @@ -186,7 +197,7 @@ class Parser /** * Map of declared query components in the parsed query. * - * @psalm-var array> + * @psalm-var array */ private $queryComponents = []; @@ -503,6 +514,7 @@ public function syntaxError($expected = '', $token = null) * @psalm-param array|null $token * * @return void + * @psalm-return no-return * * @throws QueryException */ @@ -709,7 +721,7 @@ private function processDeferredPartialObjectExpressions(): void { foreach ($this->deferredPartialObjectExpressions as $deferredItem) { $expr = $deferredItem['expression']; - $class = $this->queryComponents[$expr->identificationVariable]['metadata']; + $class = $this->getMetadataForDqlAlias($expr->identificationVariable); foreach ($expr->partialFieldSet as $field) { if (isset($class->fieldMappings[$field])) { @@ -791,8 +803,7 @@ private function processDeferredPathExpressions(): void foreach ($this->deferredPathExpressions as $deferredItem) { $pathExpression = $deferredItem['expression']; - $qComp = $this->queryComponents[$pathExpression->identificationVariable]; - $class = $qComp['metadata']; + $class = $this->getMetadataForDqlAlias($pathExpression->identificationVariable); $field = $pathExpression->field; if ($field === null) { @@ -860,7 +871,7 @@ private function processRootEntityAliasSelected(): void } foreach ($this->identVariableExpressions as $dqlAlias => $expr) { - if (isset($this->queryComponents[$dqlAlias]) && $this->queryComponents[$dqlAlias]['parent'] === null) { + if (isset($this->queryComponents[$dqlAlias]) && ! isset($this->queryComponents[$dqlAlias]['parent'])) { return; } } @@ -1096,11 +1107,11 @@ public function JoinAssociationPathExpression() $this->match(Lexer::T_DOT); $this->match(Lexer::T_IDENTIFIER); + assert($this->lexer->token !== null); $field = $this->lexer->token['value']; // Validate association field - $qComp = $this->queryComponents[$identVariable]; - $class = $qComp['metadata']; + $class = $this->getMetadataForDqlAlias($identVariable); if (! $class->hasAssociation($field)) { $this->semanticalError('Class ' . $class->name . ' has no association named ' . $field); @@ -1263,6 +1274,7 @@ public function SimpleSelectClause() public function UpdateClause() { $this->match(Lexer::T_UPDATE); + assert($this->lexer->lookahead !== null); $token = $this->lexer->lookahead; $abstractSchemaName = $this->AbstractSchemaName(); @@ -1319,6 +1331,7 @@ public function DeleteClause() $this->match(Lexer::T_FROM); } + assert($this->lexer->lookahead !== null); $token = $this->lexer->lookahead; $abstractSchemaName = $this->AbstractSchemaName(); @@ -1787,6 +1800,7 @@ public function RangeVariableDeclaration() $this->match(Lexer::T_AS); } + assert($this->lexer->lookahead !== null); $token = $this->lexer->lookahead; $aliasIdentificationVariable = $this->AliasIdentificationVariable(); $classMetadata = $this->em->getClassMetadata($abstractSchemaName); @@ -1819,13 +1833,15 @@ public function JoinAssociationDeclaration() $this->match(Lexer::T_AS); } + assert($this->lexer->lookahead !== null); + $aliasIdentificationVariable = $this->AliasIdentificationVariable(); $indexBy = $this->lexer->isNextToken(Lexer::T_INDEX) ? $this->IndexBy() : null; $identificationVariable = $joinAssociationPathExpression->identificationVariable; $field = $joinAssociationPathExpression->associationField; - $class = $this->queryComponents[$identificationVariable]['metadata']; + $class = $this->getMetadataForDqlAlias($identificationVariable); $targetClass = $this->em->getClassMetadata($class->associationMappings[$field]['targetEntity']); // Building queryComponent @@ -1897,6 +1913,7 @@ public function PartialObjectExpression() $partialObjectExpression = new AST\PartialObjectExpression($identificationVariable, $partialFieldSet); + assert($this->lexer->token !== null); // Defer PartialObjectExpression validation $this->deferredPartialObjectExpressions[] = [ 'expression' => $partialObjectExpression, @@ -2330,6 +2347,8 @@ public function SelectExpression() $aliasResultVariable = null; if ($mustHaveAliasResultVariable || $this->lexer->isNextToken(Lexer::T_IDENTIFIER)) { + assert($this->lexer->lookahead !== null); + assert($expression instanceof AST\Node || is_string($expression)); $token = $this->lexer->lookahead; $aliasResultVariable = $this->AliasResultVariable(); @@ -2426,6 +2445,7 @@ public function SimpleSelectExpression() } if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER)) { + assert($this->lexer->lookahead !== null); $token = $this->lexer->lookahead; $resultVariable = $this->AliasResultVariable(); $expr->fieldIdentificationVariable = $resultVariable; @@ -3621,4 +3641,13 @@ public function CustomFunctionsReturningStrings() return $function; } + + private function getMetadataForDqlAlias(string $dqlAlias): ClassMetadata + { + if (! isset($this->queryComponents[$dqlAlias]['metadata'])) { + throw new LogicException(sprintf('No metadata for DQL alias: %s', $dqlAlias)); + } + + return $this->queryComponents[$dqlAlias]['metadata']; + } } diff --git a/lib/Doctrine/ORM/Query/SqlWalker.php b/lib/Doctrine/ORM/Query/SqlWalker.php index d896f85728..f552641751 100644 --- a/lib/Doctrine/ORM/Query/SqlWalker.php +++ b/lib/Doctrine/ORM/Query/SqlWalker.php @@ -17,12 +17,14 @@ use Doctrine\ORM\Query; use Doctrine\ORM\Utility\HierarchyDiscriminatorResolver; use Doctrine\ORM\Utility\PersisterHelper; +use LogicException; use function array_diff; use function array_filter; use function array_keys; use function array_map; use function array_merge; +use function assert; use function count; use function implode; use function in_array; @@ -40,6 +42,8 @@ /** * The SqlWalker is a TreeWalker that walks over a DQL AST and constructs * the corresponding SQL. + * + * @psalm-import-type QueryComponent from Parser */ class SqlWalker implements TreeWalker { @@ -127,21 +131,14 @@ class SqlWalker implements TreeWalker /** * Map of all components/classes that appear in the DQL query. * - * @psalm-var array + * @psalm-var array */ private $queryComponents; /** * A list of classes that appear in non-scalar SelectExpressions. * - * @psalm-var list + * @psalm-var array */ private $selectedClasses = []; @@ -225,20 +222,22 @@ public function getEntityManager() * @param string $dqlAlias The DQL alias. * * @return mixed[] - * @psalm-return array{ - * metadata: ClassMetadata, - * parent: string, - * relation: mixed[], - * map: mixed, - * nestingLevel: int, - * token: array - * } + * @psalm-return QueryComponent */ public function getQueryComponent($dqlAlias) { return $this->queryComponents[$dqlAlias]; } + public function getMetadataForDqlAlias(string $dqlAlias): ClassMetadata + { + if (! isset($this->queryComponents[$dqlAlias]['metadata'])) { + throw new LogicException(sprintf('No metadata for DQL alias: %s', $dqlAlias)); + } + + return $this->queryComponents[$dqlAlias]['metadata']; + } + /** * {@inheritdoc} */ @@ -411,6 +410,7 @@ private function generateOrderedCollectionOrderByItems(): string continue; } + assert(isset($qComp['metadata'])); $persister = $this->em->getUnitOfWork()->getEntityPersister($qComp['metadata']->name); foreach ($qComp['relation']['orderBy'] as $fieldName => $orientation) { @@ -444,7 +444,7 @@ private function generateDiscriminatorColumnConditionSQL(array $dqlAliases): str $sqlParts = []; foreach ($dqlAliases as $dqlAlias) { - $class = $this->queryComponents[$dqlAlias]['metadata']; + $class = $this->getMetadataForDqlAlias($dqlAlias); if (! $class->isInheritanceTypeSingleTable()) { continue; @@ -613,7 +613,7 @@ public function walkDeleteStatement(AST\DeleteStatement $AST) */ public function walkEntityIdentificationVariable($identVariable) { - $class = $this->queryComponents[$identVariable]['metadata']; + $class = $this->getMetadataForDqlAlias($identVariable); $tableAlias = $this->getSQLTableAlias($class->getTableName(), $identVariable); $sqlParts = []; @@ -634,7 +634,7 @@ public function walkEntityIdentificationVariable($identVariable) */ public function walkIdentificationVariable($identificationVariable, $fieldName = null) { - $class = $this->queryComponents[$identificationVariable]['metadata']; + $class = $this->getMetadataForDqlAlias($identificationVariable); if ( $fieldName !== null && $class->isInheritanceTypeJoined() && @@ -657,7 +657,7 @@ public function walkPathExpression($pathExpr) case AST\PathExpression::TYPE_STATE_FIELD: $fieldName = $pathExpr->field; $dqlAlias = $pathExpr->identificationVariable; - $class = $this->queryComponents[$dqlAlias]['metadata']; + $class = $this->getMetadataForDqlAlias($dqlAlias); if ($this->useSqlTableAliases) { $sql .= $this->walkIdentificationVariable($dqlAlias, $fieldName) . '.'; @@ -671,7 +671,7 @@ public function walkPathExpression($pathExpr) // Just use the foreign key, i.e. u.group_id $fieldName = $pathExpr->field; $dqlAlias = $pathExpr->identificationVariable; - $class = $this->queryComponents[$dqlAlias]['metadata']; + $class = $this->getMetadataForDqlAlias($dqlAlias); if (isset($class->associationMappings[$fieldName]['inherited'])) { $class = $this->em->getClassMetadata($class->associationMappings[$fieldName]['inherited']); @@ -724,9 +724,11 @@ public function walkSelectClause($selectClause) $resultAlias = $selectedClass['resultAlias']; // Register as entity or joined entity result - if ($this->queryComponents[$dqlAlias]['relation'] === null) { + if (! isset($this->queryComponents[$dqlAlias]['relation'])) { $this->rsm->addEntityResult($class->name, $dqlAlias, $resultAlias); } else { + assert(isset($this->queryComponents[$dqlAlias]['parent'])); + $this->rsm->addJoinedEntityResult( $class->name, $dqlAlias, @@ -873,7 +875,7 @@ public function walkIndexBy($indexBy) case AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION: // Just use the foreign key, i.e. u.group_id $fieldName = $pathExpression->field; - $class = $this->queryComponents[$alias]['metadata']; + $class = $this->getMetadataForDqlAlias($alias); if (isset($class->associationMappings[$fieldName]['inherited'])) { $class = $this->em->getClassMetadata($class->associationMappings[$fieldName]['inherited']); @@ -969,7 +971,8 @@ public function walkJoinAssociationDeclaration($joinAssociationDeclaration, $joi $joinedDqlAlias = $joinAssociationDeclaration->aliasIdentificationVariable; $indexBy = $joinAssociationDeclaration->indexBy; - $relation = $this->queryComponents[$joinedDqlAlias]['relation']; + $relation = $this->queryComponents[$joinedDqlAlias]['relation'] ?? null; + assert($relation !== null); $targetClass = $this->em->getClassMetadata($relation['targetEntity']); $sourceClass = $this->em->getClassMetadata($relation['sourceEntity']); $targetTableName = $this->quoteStrategy->getTableName($targetClass, $this->platform); @@ -1323,8 +1326,7 @@ public function walkSelectExpression($selectExpression) $fieldName = $expr->field; $dqlAlias = $expr->identificationVariable; - $qComp = $this->queryComponents[$dqlAlias]; - $class = $qComp['metadata']; + $class = $this->getMetadataForDqlAlias($dqlAlias); $resultAlias = $selectExpression->fieldIdentificationVariable ?: $fieldName; $tableName = $class->isInheritanceTypeJoined() @@ -1418,8 +1420,7 @@ public function walkSelectExpression($selectExpression) $partialFieldSet = []; } - $queryComp = $this->queryComponents[$dqlAlias]; - $class = $queryComp['metadata']; + $class = $this->getMetadataForDqlAlias($dqlAlias); $resultAlias = $selectExpression->fieldIdentificationVariable ?: null; if (! isset($this->selectedClasses[$dqlAlias])) { @@ -1591,8 +1592,7 @@ public function walkNewObject($newObjectExpression, $newObjectResultAlias = null case $e instanceof AST\PathExpression: $dqlAlias = $e->identificationVariable; - $qComp = $this->queryComponents[$dqlAlias]; - $class = $qComp['metadata']; + $class = $this->getMetadataForDqlAlias($dqlAlias); $fieldType = $class->fieldMappings[$e->field]['type']; $fieldName = $e->field; $fieldMapping = $class->fieldMappings[$fieldName]; @@ -1730,7 +1730,7 @@ public function walkGroupByItem($groupByItem) return $this->walkPathExpression($resultVariable); } - if (isset($resultVariable->pathExpression)) { + if ($resultVariable instanceof AST\Node && isset($resultVariable->pathExpression)) { return $this->walkPathExpression($resultVariable->pathExpression); } @@ -1740,14 +1740,14 @@ public function walkGroupByItem($groupByItem) // IdentificationVariable $sqlParts = []; - foreach ($this->queryComponents[$groupByItem]['metadata']->fieldNames as $field) { + foreach ($this->getMetadataForDqlAlias($groupByItem)->fieldNames as $field) { $item = new AST\PathExpression(AST\PathExpression::TYPE_STATE_FIELD, $groupByItem, $field); $item->type = AST\PathExpression::TYPE_STATE_FIELD; $sqlParts[] = $this->walkPathExpression($item); } - foreach ($this->queryComponents[$groupByItem]['metadata']->associationMappings as $mapping) { + foreach ($this->getMetadataForDqlAlias($groupByItem)->associationMappings as $mapping) { if ($mapping['isOwningSide'] && $mapping['type'] & ClassMetadataInfo::TO_ONE) { $item = new AST\PathExpression(AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION, $groupByItem, $mapping['fieldName']); $item->type = AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION; @@ -1830,7 +1830,7 @@ public function walkWhereClause($whereClause) if ($this->em->hasFilters()) { $filterClauses = []; foreach ($this->rootAliases as $dqlAlias) { - $class = $this->queryComponents[$dqlAlias]['metadata']; + $class = $this->getMetadataForDqlAlias($dqlAlias); $tableAlias = $this->getSQLTableAlias($class->table['name'], $dqlAlias); $filterExpr = $this->generateFilterConditionSQL($class, $tableAlias); @@ -1941,7 +1941,7 @@ public function walkCollectionMemberExpression($collMemberExpr) $fieldName = $collPathExpr->field; $dqlAlias = $collPathExpr->identificationVariable; - $class = $this->queryComponents[$dqlAlias]['metadata']; + $class = $this->getMetadataForDqlAlias($dqlAlias); switch (true) { // InputParameter @@ -2081,7 +2081,7 @@ public function walkInstanceOfExpression($instanceOfExpr) $sql = ''; $dqlAlias = $instanceOfExpr->identificationVariable; - $discrClass = $class = $this->queryComponents[$dqlAlias]['metadata']; + $discrClass = $class = $this->getMetadataForDqlAlias($dqlAlias); if ($class->discriminatorColumn) { $discrClass = $this->em->getClassMetadata($class->rootEntityName); diff --git a/lib/Doctrine/ORM/Query/TreeWalker.php b/lib/Doctrine/ORM/Query/TreeWalker.php index 845c7bdc69..544bbef1d1 100644 --- a/lib/Doctrine/ORM/Query/TreeWalker.php +++ b/lib/Doctrine/ORM/Query/TreeWalker.php @@ -5,10 +5,11 @@ namespace Doctrine\ORM\Query; use Doctrine\ORM\AbstractQuery; -use Doctrine\ORM\Mapping\ClassMetadata; /** * Interface for walkers of DQL ASTs (abstract syntax trees). + * + * @psalm-import-type QueryComponent from Parser */ interface TreeWalker { @@ -18,6 +19,7 @@ interface TreeWalker * @param AbstractQuery $query The parsed Query. * @param ParserResult $parserResult The result of the parsing process. * @param mixed[] $queryComponents The query components (symbol table). + * @psalm-param array $queryComponents The query components (symbol table). */ public function __construct($query, $parserResult, array $queryComponents); @@ -25,14 +27,7 @@ public function __construct($query, $parserResult, array $queryComponents); * Returns internal queryComponents array. * * @return array> - * @psalm-return array + * @psalm-return array */ public function getQueryComponents(); @@ -41,6 +36,7 @@ public function getQueryComponents(); * * @param string $dqlAlias The DQL alias. * @param array $queryComponent + * @psalm-param QueryComponent $queryComponent * * @return void */ diff --git a/lib/Doctrine/ORM/Query/TreeWalkerAdapter.php b/lib/Doctrine/ORM/Query/TreeWalkerAdapter.php index 6b64cd17c4..bcec88cdaa 100644 --- a/lib/Doctrine/ORM/Query/TreeWalkerAdapter.php +++ b/lib/Doctrine/ORM/Query/TreeWalkerAdapter.php @@ -5,7 +5,6 @@ namespace Doctrine\ORM\Query; use Doctrine\ORM\AbstractQuery; -use Doctrine\ORM\Mapping\ClassMetadata; use function array_diff; use function array_keys; @@ -13,6 +12,8 @@ /** * An adapter implementation of the TreeWalker interface. The methods in this class * are empty. This class exists as convenience for creating tree walkers. + * + * @psalm-import-type QueryComponent from Parser */ abstract class TreeWalkerAdapter implements TreeWalker { @@ -33,14 +34,7 @@ abstract class TreeWalkerAdapter implements TreeWalker /** * The query components of the original query (the "symbol table") that was produced by the Parser. * - * @psalm-var array + * @psalm-var array */ private $_queryComponents; diff --git a/lib/Doctrine/ORM/Query/TreeWalkerChain.php b/lib/Doctrine/ORM/Query/TreeWalkerChain.php index 4dee17da1e..9a93b69615 100644 --- a/lib/Doctrine/ORM/Query/TreeWalkerChain.php +++ b/lib/Doctrine/ORM/Query/TreeWalkerChain.php @@ -5,7 +5,6 @@ namespace Doctrine\ORM\Query; use Doctrine\ORM\AbstractQuery; -use Doctrine\ORM\Mapping\ClassMetadata; use Generator; use function array_diff; @@ -15,6 +14,8 @@ * Represents a chain of tree walkers that modify an AST and finally emit output. * Only the last walker in the chain can emit output. Any previous walkers can modify * the AST to influence the final output produced by the last walker. + * + * @psalm-import-type QueryComponent from Parser */ class TreeWalkerChain implements TreeWalker { @@ -36,14 +37,7 @@ class TreeWalkerChain implements TreeWalker * The query components of the original query (the "symbol table") that was produced by the Parser. * * @var array> - * @psalm-var array + * @psalm-var array */ private $queryComponents; diff --git a/lib/Doctrine/ORM/Tools/Pagination/CountOutputWalker.php b/lib/Doctrine/ORM/Tools/Pagination/CountOutputWalker.php index bb6a632d2a..22bea60510 100644 --- a/lib/Doctrine/ORM/Tools/Pagination/CountOutputWalker.php +++ b/lib/Doctrine/ORM/Tools/Pagination/CountOutputWalker.php @@ -8,6 +8,7 @@ use Doctrine\DBAL\Platforms\SQLServerPlatform; use Doctrine\ORM\Query; use Doctrine\ORM\Query\AST\SelectStatement; +use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\ParserResult; use Doctrine\ORM\Query\ResultSetMapping; use Doctrine\ORM\Query\SqlWalker; @@ -28,6 +29,8 @@ * * Works with composite keys but cannot deal with queries that have multiple * root entities (e.g. `SELECT f, b from Foo, Bar`) + * + * @psalm-import-type QueryComponent from Parser */ class CountOutputWalker extends SqlWalker { @@ -37,9 +40,6 @@ class CountOutputWalker extends SqlWalker /** @var ResultSetMapping */ private $rsm; - /** @var mixed[] */ - private $queryComponents; - /** * Stores various parameters that are otherwise unavailable * because Doctrine\ORM\Query\SqlWalker keeps everything private without @@ -48,12 +48,12 @@ class CountOutputWalker extends SqlWalker * @param Query $query * @param ParserResult $parserResult * @param mixed[] $queryComponents + * @psalm-param array $queryComponents */ public function __construct($query, $parserResult, array $queryComponents) { - $this->platform = $query->getEntityManager()->getConnection()->getDatabasePlatform(); - $this->rsm = $parserResult->getResultSetMapping(); - $this->queryComponents = $queryComponents; + $this->platform = $query->getEntityManager()->getConnection()->getDatabasePlatform(); + $this->rsm = $parserResult->getResultSetMapping(); parent::__construct($query, $parserResult, $queryComponents); } @@ -97,7 +97,7 @@ public function walkSelectStatement(SelectStatement $AST) $fromRoot = reset($from); $rootAlias = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable; - $rootClass = $this->queryComponents[$rootAlias]['metadata']; + $rootClass = $this->getMetadataForDqlAlias($rootAlias); $rootIdentifier = $rootClass->identifier; // For every identifier, find out the SQL alias by combing through the ResultSetMapping diff --git a/lib/Doctrine/ORM/Tools/Pagination/LimitSubqueryOutputWalker.php b/lib/Doctrine/ORM/Tools/Pagination/LimitSubqueryOutputWalker.php index 6a15160e70..b25b3f8fdb 100644 --- a/lib/Doctrine/ORM/Tools/Pagination/LimitSubqueryOutputWalker.php +++ b/lib/Doctrine/ORM/Tools/Pagination/LimitSubqueryOutputWalker.php @@ -18,6 +18,7 @@ use Doctrine\ORM\Query\AST\PartialObjectExpression; use Doctrine\ORM\Query\AST\SelectExpression; use Doctrine\ORM\Query\AST\SelectStatement; +use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\ParserResult; use Doctrine\ORM\Query\QueryException; use Doctrine\ORM\Query\ResultSetMapping; @@ -45,6 +46,8 @@ * * Works with composite keys but cannot deal with queries that have multiple * root entities (e.g. `SELECT f, b from Foo, Bar`) + * + * @psalm-import-type QueryComponent from Parser */ class LimitSubqueryOutputWalker extends SqlWalker { @@ -56,9 +59,6 @@ class LimitSubqueryOutputWalker extends SqlWalker /** @var ResultSetMapping */ private $rsm; - /** @var mixed[] */ - private $queryComponents; - /** @var int */ private $firstResult; @@ -92,12 +92,12 @@ class LimitSubqueryOutputWalker extends SqlWalker * @param Query $query * @param ParserResult $parserResult * @param mixed[] $queryComponents + * @psalm-param array $queryComponents */ public function __construct($query, $parserResult, array $queryComponents) { - $this->platform = $query->getEntityManager()->getConnection()->getDatabasePlatform(); - $this->rsm = $parserResult->getResultSetMapping(); - $this->queryComponents = $queryComponents; + $this->platform = $query->getEntityManager()->getConnection()->getDatabasePlatform(); + $this->rsm = $parserResult->getResultSetMapping(); // Reset limit and offset $this->firstResult = $query->getFirstResult(); @@ -413,7 +413,7 @@ private function generateSqlAliasReplacements(): array // Generate DQL alias -> SQL table alias mapping foreach (array_keys($this->rsm->aliasMap) as $dqlAlias) { - $metadataList[$dqlAlias] = $class = $this->queryComponents[$dqlAlias]['metadata']; + $metadataList[$dqlAlias] = $class = $this->getMetadataForDqlAlias($dqlAlias); $aliasMap[$dqlAlias] = $this->getSQLTableAlias($class->getTableName(), $dqlAlias); } @@ -511,7 +511,7 @@ private function getSQLIdentifier(SelectStatement $AST): array $fromRoot = reset($from); $rootAlias = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable; - $rootClass = $this->queryComponents[$rootAlias]['metadata']; + $rootClass = $this->getMetadataForDqlAlias($rootAlias); $rootIdentifier = $rootClass->identifier; // For every identifier, find out the SQL alias by combing through the ResultSetMapping diff --git a/lib/Doctrine/ORM/Tools/Pagination/LimitSubqueryWalker.php b/lib/Doctrine/ORM/Tools/Pagination/LimitSubqueryWalker.php index 7e7f975ab9..e7fb59bffb 100644 --- a/lib/Doctrine/ORM/Tools/Pagination/LimitSubqueryWalker.php +++ b/lib/Doctrine/ORM/Tools/Pagination/LimitSubqueryWalker.php @@ -134,6 +134,7 @@ private function validate(SelectStatement $AST): void $queryComponent = $queryComponents[$expression->identificationVariable]; if ( isset($queryComponent['parent']) + && isset($queryComponent['relation']) && $queryComponent['relation']['type'] & ClassMetadataInfo::TO_MANY ) { throw new RuntimeException('Cannot select distinct identifiers from query with LIMIT and ORDER BY on a column from a fetch joined to-many association. Use output walkers.'); diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 47f25e9798..75d4bed100 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -975,16 +975,6 @@ parameters: count: 1 path: lib/Doctrine/ORM/Query/SqlWalker.php - - - message: "#^Offset 'resultVariable' on array\\{metadata\\: Doctrine\\\\ORM\\\\Mapping\\\\ClassMetadata, parent\\: string, relation\\: array, map\\: mixed, nestingLevel\\: int, token\\: array\\} in isset\\(\\) does not exist\\.$#" - count: 3 - path: lib/Doctrine/ORM/Query/SqlWalker.php - - - - message: "#^Offset string on array\\ in isset\\(\\) does not exist\\.$#" - count: 1 - path: lib/Doctrine/ORM/Query/SqlWalker.php - - message: "#^Parameter \\#1 \\$entity of static method Doctrine\\\\ORM\\\\OptimisticLockException\\:\\:lockFailed\\(\\) expects object, class\\-string\\ given\\.$#" count: 1 @@ -1005,11 +995,6 @@ parameters: count: 2 path: lib/Doctrine/ORM/Query/SqlWalker.php - - - message: "#^Strict comparison using \\=\\=\\= between array and null will always evaluate to false\\.$#" - count: 1 - path: lib/Doctrine/ORM/Query/SqlWalker.php - - message: "#^Return type \\(void\\) of method Doctrine\\\\ORM\\\\Query\\\\TreeWalkerAdapter\\:\\:getExecutor\\(\\) should be compatible with return type \\(Doctrine\\\\ORM\\\\Query\\\\Exec\\\\AbstractSqlExecutor\\) of method Doctrine\\\\ORM\\\\Query\\\\TreeWalker\\:\\:getExecutor\\(\\)$#" count: 1 @@ -1715,16 +1700,6 @@ parameters: count: 1 path: lib/Doctrine/ORM/Tools/Pagination/CountWalker.php - - - message: "#^Access to an undefined property object\\:\\:\\$name\\.$#" - count: 1 - path: lib/Doctrine/ORM/Tools/Pagination/LimitSubqueryOutputWalker.php - - - - message: "#^Offset 'parent' on array\\{metadata\\: Doctrine\\\\ORM\\\\Mapping\\\\ClassMetadata, parent\\: string, relation\\: array, map\\: mixed, nestingLevel\\: int, token\\: array\\} in isset\\(\\) always exists and is not nullable\\.$#" - count: 1 - path: lib/Doctrine/ORM/Tools/Pagination/LimitSubqueryWalker.php - - message: "#^Return type \\(void\\) of method Doctrine\\\\ORM\\\\Tools\\\\Pagination\\\\LimitSubqueryWalker\\:\\:walkSelectStatement\\(\\) should be compatible with return type \\(string\\) of method Doctrine\\\\ORM\\\\Query\\\\TreeWalker\\:\\:walkSelectStatement\\(\\)$#" count: 1 diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 9ff0fccc8e..f476eb5250 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1696,19 +1696,9 @@ $parser->getLexer()->token['value'] - - $class->associationMappings - - - $parser->getLexer()->token['value'] - - - $fieldMapping + $pathExpression - - $this->fieldMapping !== null - @@ -1752,9 +1742,6 @@ - - $class->associationMappings - $collectionPathExpression @@ -2214,10 +2201,9 @@ $this->ConditionalExpression() $this->ConditionalExpression() - + $aliasIdentVariable $dql - $field $fromClassName $functionName $functionName @@ -2238,7 +2224,7 @@ $token['value'] $token['value'] - + $glimpse['type'] $glimpse['value'] $lookahead['type'] @@ -2303,7 +2289,6 @@ $this->lexer->token['value'] $this->lexer->token['value'] $this->lexer->token['value'] - $this->lexer->token['value'] $token['type'] $token['type'] $token['type'] @@ -2378,10 +2363,9 @@ - + $likeExpr->stringPattern instanceof AST\Functions\FunctionNode $likeExpr->stringPattern instanceof AST\PathExpression - $this->queryComponents[$dqlAlias]['relation'] === null '' is_string($expression) is_string($stringExpr) @@ -2395,9 +2379,6 @@ $factor $selectedClass['class']->name - - $this->selectedClasses[$joinedDqlAlias] - walkConditionalPrimary @@ -2450,10 +2431,8 @@ dispatch - + $query - $this->queryComponents - $this->selectedClasses $likeExpr->stringPattern instanceof AST\InputParameter @@ -2559,9 +2538,6 @@ _getQueryComponents - - $this->_queryComponents -