diff --git a/UPGRADE.md b/UPGRADE.md index 31966567bd..04a1e33edd 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -243,6 +243,23 @@ Use `toIterable()` instead. # Upgrade to 2.12 +## Un-deprecate `Doctrine\ORM\Proxy\Proxy` + +Because no forward-compatible new proxy solution had been implemented yet, the +current proxy mechanism is not considered deprecated anymore for the time +being. This applies to the following interfaces/classes: + +* `Doctrine\ORM\Proxy\Proxy` +* `Doctrine\ORM\Proxy\ProxyFactory` + +These methods have been un-deprecated: + +* `Doctrine\ORM\Configuration::getAutoGenerateProxyClasses()` +* `Doctrine\ORM\Configuration::getProxyDir()` +* `Doctrine\ORM\Configuration::getProxyNamespace()` + +Note that the `Doctrine\ORM\Proxy\Autoloader` remains deprecated and will be removed in 3.0. + ## Deprecate helper methods from `AbstractCollectionPersister` The following protected methods of diff --git a/lib/Doctrine/ORM/Configuration.php b/lib/Doctrine/ORM/Configuration.php index 5d0a680b39..d61d30b5d0 100644 --- a/lib/Doctrine/ORM/Configuration.php +++ b/lib/Doctrine/ORM/Configuration.php @@ -17,6 +17,7 @@ use Doctrine\ORM\Mapping\EntityListenerResolver; use Doctrine\ORM\Mapping\NamingStrategy; use Doctrine\ORM\Mapping\QuoteStrategy; +use Doctrine\ORM\Proxy\ProxyFactory; use Doctrine\ORM\Query\Filter\SQLFilter; use Doctrine\ORM\Repository\DefaultRepositoryFactory; use Doctrine\ORM\Repository\RepositoryFactory; @@ -33,6 +34,8 @@ * It combines all configuration options from DBAL & ORM. * * Internal note: When adding a new configuration option just write a getter/setter pair. + * + * @psalm-import-type AutogenerateMode from ProxyFactory */ class Configuration extends \Doctrine\DBAL\Configuration { @@ -54,10 +57,6 @@ public function setProxyDir($dir) /** * Gets the directory where Doctrine generates any necessary proxy class files. * - * @deprecated 2.7 We're switch to `ocramius/proxy-manager` and this method isn't applicable any longer - * - * @see https://github.com/Ocramius/ProxyManager - * * @return string|null */ public function getProxyDir() @@ -68,11 +67,8 @@ public function getProxyDir() /** * Gets the strategy for automatically generating proxy classes. * - * @deprecated 2.7 We're switch to `ocramius/proxy-manager` and this method isn't applicable any longer - * - * @see https://github.com/Ocramius/ProxyManager - * * @return int Possible values are constants of Doctrine\Common\Proxy\AbstractProxyFactory. + * @psalm-return AutogenerateMode */ public function getAutoGenerateProxyClasses() { @@ -95,10 +91,6 @@ public function setAutoGenerateProxyClasses($autoGenerate) /** * Gets the namespace where proxy classes reside. * - * @deprecated 2.7 We're switch to `ocramius/proxy-manager` and this method isn't applicable any longer - * - * @see https://github.com/Ocramius/ProxyManager - * * @return string|null */ public function getProxyNamespace() diff --git a/lib/Doctrine/ORM/Mapping/Driver/AttributeDriver.php b/lib/Doctrine/ORM/Mapping/Driver/AttributeDriver.php index 3155fa753c..e8c6e80ae5 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/AttributeDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/AttributeDriver.php @@ -30,12 +30,20 @@ class AttributeDriver extends AnnotationDriver Mapping\MappedSuperclass::class => 2, ]; + /** + * The annotation reader. + * + * @var AttributeReader + */ + protected $reader; + /** * @param array $paths */ public function __construct(array $paths) { - parent::__construct(new AttributeReader(), $paths); + $this->reader = new AttributeReader(); + $this->addPaths($paths); } /** @@ -260,7 +268,7 @@ public function loadMetadataForClass($className, ClassMetadata $metadata): void // Check for JoinColumn/JoinColumns annotations $joinColumns = []; - $joinColumnAttributes = $this->reader->getPropertyAnnotation($property, Mapping\JoinColumn::class); + $joinColumnAttributes = $this->reader->getPropertyAnnotationCollection($property, Mapping\JoinColumn::class); foreach ($joinColumnAttributes as $joinColumnAttribute) { $joinColumns[] = $this->joinColumnToArray($joinColumnAttribute); @@ -365,11 +373,11 @@ public function loadMetadataForClass($className, ClassMetadata $metadata): void ]; } - foreach ($this->reader->getPropertyAnnotation($property, Mapping\JoinColumn::class) as $joinColumn) { + foreach ($this->reader->getPropertyAnnotationCollection($property, Mapping\JoinColumn::class) as $joinColumn) { $joinTable['joinColumns'][] = $this->joinColumnToArray($joinColumn); } - foreach ($this->reader->getPropertyAnnotation($property, Mapping\InverseJoinColumn::class) as $joinColumn) { + foreach ($this->reader->getPropertyAnnotationCollection($property, Mapping\InverseJoinColumn::class) as $joinColumn) { $joinTable['inverseJoinColumns'][] = $this->joinColumnToArray($joinColumn); } diff --git a/lib/Doctrine/ORM/Mapping/Driver/AttributeReader.php b/lib/Doctrine/ORM/Mapping/Driver/AttributeReader.php index 341580d4fb..a5b36420e4 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/AttributeReader.php +++ b/lib/Doctrine/ORM/Mapping/Driver/AttributeReader.php @@ -6,6 +6,7 @@ use Attribute; use Doctrine\ORM\Mapping\Annotation; +use LogicException; use ReflectionAttribute; use ReflectionClass; use ReflectionMethod; @@ -14,58 +15,93 @@ use function assert; use function is_string; use function is_subclass_of; +use function sprintf; /** * @internal */ final class AttributeReader { - /** @var array */ + /** @var array,bool> */ private array $isRepeatableAttribute = []; - /** @return array */ + /** + * @psalm-return class-string-map> + * + * @template T of Annotation + */ public function getClassAnnotations(ReflectionClass $class): array { return $this->convertToAttributeInstances($class->getAttributes()); } - /** @return Annotation|RepeatableAttributeCollection|null */ - public function getClassAnnotation(ReflectionClass $class, $annotationName) - { - return $this->getClassAnnotations($class)[$annotationName] - ?? ($this->isRepeatable($annotationName) ? new RepeatableAttributeCollection() : null); - } - - /** @return array */ + /** + * @return class-string-map> + * + * @template T of Annotation + */ public function getMethodAnnotations(ReflectionMethod $method): array { return $this->convertToAttributeInstances($method->getAttributes()); } - /** @return Annotation|RepeatableAttributeCollection|null */ - public function getMethodAnnotation(ReflectionMethod $method, $annotationName) - { - return $this->getMethodAnnotations($method)[$annotationName] - ?? ($this->isRepeatable($annotationName) ? new RepeatableAttributeCollection() : null); - } - - /** @return array */ + /** + * @return class-string-map> + * + * @template T of Annotation + */ public function getPropertyAnnotations(ReflectionProperty $property): array { return $this->convertToAttributeInstances($property->getAttributes()); } - /** @return Annotation|RepeatableAttributeCollection|null */ + /** + * @param class-string $annotationName The name of the annotation. + * + * @return T|null + * + * @template T of Annotation + */ public function getPropertyAnnotation(ReflectionProperty $property, $annotationName) { + if ($this->isRepeatable($annotationName)) { + throw new LogicException(sprintf( + 'The attribute "%s" is repeatable. Call getPropertyAnnotationCollection() instead.', + $annotationName + )); + } + return $this->getPropertyAnnotations($property)[$annotationName] ?? ($this->isRepeatable($annotationName) ? new RepeatableAttributeCollection() : null); } + /** + * @param class-string $annotationName The name of the annotation. + * + * @return RepeatableAttributeCollection + * + * @template T of Annotation + */ + public function getPropertyAnnotationCollection( + ReflectionProperty $property, + string $annotationName + ): RepeatableAttributeCollection { + if (! $this->isRepeatable($annotationName)) { + throw new LogicException(sprintf( + 'The attribute "%s" is not repeatable. Call getPropertyAnnotation() instead.', + $annotationName + )); + } + + return $this->getPropertyAnnotations($property)[$annotationName] ?? new RepeatableAttributeCollection(); + } + /** * @param array $attributes * - * @return array + * @return class-string-map> + * + * @template T of Annotation */ private function convertToAttributeInstances(array $attributes): array { @@ -98,6 +134,9 @@ private function convertToAttributeInstances(array $attributes): array return $instances; } + /** + * @param class-string $attributeClassName + */ private function isRepeatable(string $attributeClassName): bool { if (isset($this->isRepeatableAttribute[$attributeClassName])) { diff --git a/lib/Doctrine/ORM/Mapping/Driver/RepeatableAttributeCollection.php b/lib/Doctrine/ORM/Mapping/Driver/RepeatableAttributeCollection.php index 93aacee1ad..995d293f82 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/RepeatableAttributeCollection.php +++ b/lib/Doctrine/ORM/Mapping/Driver/RepeatableAttributeCollection.php @@ -8,7 +8,8 @@ use Doctrine\ORM\Mapping\Annotation; /** - * @template-extends ArrayObject + * @template-extends ArrayObject + * @template T of Annotation */ final class RepeatableAttributeCollection extends ArrayObject { diff --git a/lib/Doctrine/ORM/Proxy/Proxy.php b/lib/Doctrine/ORM/Proxy/Proxy.php index b5fe76212a..4d2a77ec0d 100644 --- a/lib/Doctrine/ORM/Proxy/Proxy.php +++ b/lib/Doctrine/ORM/Proxy/Proxy.php @@ -8,8 +8,6 @@ /** * Interface for proxy classes. - * - * @deprecated 2.7 This interface is being removed from the ORM and won't have any replacement, proxies will no longer implement it. */ interface Proxy extends BaseProxy { diff --git a/lib/Doctrine/ORM/Proxy/ProxyFactory.php b/lib/Doctrine/ORM/Proxy/ProxyFactory.php index 7bbe196953..c3f9b5fd42 100644 --- a/lib/Doctrine/ORM/Proxy/ProxyFactory.php +++ b/lib/Doctrine/ORM/Proxy/ProxyFactory.php @@ -20,7 +20,7 @@ /** * This factory is used to create proxy objects for entities at runtime. * - * @deprecated 2.7 This class is being removed from the ORM and won't have any replacement + * @psalm-type AutogenerateMode = AbstractProxyFactory::AUTOGENERATE_NEVER|AbstractProxyFactory::AUTOGENERATE_ALWAYS|AbstractProxyFactory::AUTOGENERATE_FILE_NOT_EXISTS|AbstractProxyFactory::AUTOGENERATE_EVAL|AbstractProxyFactory::AUTOGENERATE_FILE_NOT_EXISTS_OR_CHANGED */ class ProxyFactory extends AbstractProxyFactory { @@ -48,7 +48,8 @@ class ProxyFactory extends AbstractProxyFactory * @param string $proxyDir The directory to use for the proxy classes. It must exist. * @param string $proxyNs The namespace to use for the proxy classes. * @param bool|int $autoGenerate The strategy for automatically generating proxy classes. Possible - * values are constants of Doctrine\Common\Proxy\AbstractProxyFactory. + * values are constants of {@see AbstractProxyFactory}. + * @psalm-param bool|AutogenerateMode $autoGenerate */ public function __construct(EntityManagerInterface $em, $proxyDir, $proxyNs, $autoGenerate = AbstractProxyFactory::AUTOGENERATE_NEVER) { 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/Repository/RepositoryFactory.php b/lib/Doctrine/ORM/Repository/RepositoryFactory.php index 7528578104..e792e3897b 100644 --- a/lib/Doctrine/ORM/Repository/RepositoryFactory.php +++ b/lib/Doctrine/ORM/Repository/RepositoryFactory.php @@ -16,9 +16,11 @@ interface RepositoryFactory * Gets the repository for an entity class. * * @param EntityManagerInterface $entityManager The EntityManager instance. - * @param string $entityName The name of the entity. + * @param class-string $entityName The name of the entity. * - * @return ObjectRepository + * @return ObjectRepository + * + * @template T of object */ public function getRepository(EntityManagerInterface $entityManager, $entityName); } 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 4068ca5990..ff6c3d7327 100644 --- a/lib/Doctrine/ORM/Tools/Pagination/LimitSubqueryOutputWalker.php +++ b/lib/Doctrine/ORM/Tools/Pagination/LimitSubqueryOutputWalker.php @@ -17,6 +17,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; @@ -44,6 +45,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 { @@ -55,9 +58,6 @@ class LimitSubqueryOutputWalker extends SqlWalker /** @var ResultSetMapping */ private $rsm; - /** @var mixed[] */ - private $queryComponents; - /** @var int */ private $firstResult; @@ -91,12 +91,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(); @@ -411,7 +411,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); } @@ -509,7 +509,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/lib/Doctrine/ORM/Tools/SchemaValidator.php b/lib/Doctrine/ORM/Tools/SchemaValidator.php index db27180cdc..93ff4ad9ec 100644 --- a/lib/Doctrine/ORM/Tools/SchemaValidator.php +++ b/lib/Doctrine/ORM/Tools/SchemaValidator.php @@ -134,9 +134,9 @@ public function validateClass(ClassMetadataInfo $class) 'field ' . $assoc['targetEntity'] . '#' . $assoc['inversedBy'] . ' which does not exist.'; } elseif ($targetMetadata->associationMappings[$assoc['inversedBy']]['mappedBy'] === null) { $ce[] = 'The field ' . $class->name . '#' . $fieldName . ' is on the owning side of a ' . - 'bi-directional relationship, but the specified mappedBy association on the target-entity ' . - $assoc['targetEntity'] . '#' . $assoc['mappedBy'] . ' does not contain the required ' . - "'inversedBy' attribute."; + 'bi-directional relationship, but the specified inversedBy association on the target-entity ' . + $assoc['targetEntity'] . '#' . $assoc['inversedBy'] . ' does not contain the required ' . + "'mappedBy=\"" . $fieldName . "\"' attribute."; } elseif ($targetMetadata->associationMappings[$assoc['inversedBy']]['mappedBy'] !== $fieldName) { $ce[] = 'The mappings ' . $class->name . '#' . $fieldName . ' and ' . $assoc['targetEntity'] . '#' . $assoc['inversedBy'] . ' are ' . diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 5a07c939e6..f56a4f1d8e 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -221,15 +221,10 @@ parameters: path: lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php - - message: "#^Argument of an invalid type Doctrine\\\\ORM\\\\Mapping\\\\InverseJoinColumn supplied for foreach, only iterables are supported\\.$#" + message: "#^PHPDoc type Doctrine\\\\ORM\\\\Mapping\\\\Driver\\\\AttributeReader of property Doctrine\\\\ORM\\\\Mapping\\\\Driver\\\\AttributeDriver\\:\\:\\$reader is not covariant with PHPDoc type Doctrine\\\\Common\\\\Annotations\\\\Reader of overridden property Doctrine\\\\Persistence\\\\Mapping\\\\Driver\\\\AnnotationDriver\\:\\:\\$reader\\.$#" count: 1 path: lib/Doctrine/ORM/Mapping/Driver/AttributeDriver.php - - - message: "#^Argument of an invalid type Doctrine\\\\ORM\\\\Mapping\\\\JoinColumn supplied for foreach, only iterables are supported\\.$#" - count: 2 - path: lib/Doctrine/ORM/Mapping/Driver/AttributeDriver.php - - message: "#^PHPDoc type array\\ of property Doctrine\\\\ORM\\\\Mapping\\\\Driver\\\\AttributeDriver\\:\\:\\$entityAnnotationClasses is not covariant with PHPDoc type array\\ of overridden property Doctrine\\\\Persistence\\\\Mapping\\\\Driver\\\\AnnotationDriver\\:\\:\\$entityAnnotationClasses\\.$#" count: 1 @@ -240,11 +235,6 @@ parameters: count: 1 path: lib/Doctrine/ORM/Mapping/Driver/AttributeDriver.php - - - message: "#^Parameter \\#1 \\$reader of method Doctrine\\\\Persistence\\\\Mapping\\\\Driver\\\\AnnotationDriver\\:\\:__construct\\(\\) expects Doctrine\\\\Common\\\\Annotations\\\\Reader, Doctrine\\\\ORM\\\\Mapping\\\\Driver\\\\AttributeReader given\\.$#" - count: 1 - path: lib/Doctrine/ORM/Mapping/Driver/AttributeDriver.php - - message: "#^Access to an undefined property Doctrine\\\\Persistence\\\\Mapping\\\\ClassMetadata\\\\:\\:\\$name\\.$#" count: 1 @@ -745,16 +735,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 @@ -775,11 +755,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 @@ -1270,16 +1245,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 8b5fa0adc4..c77c689531 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -177,24 +177,10 @@ (bool) $flag - - - ProxyFactory - - $connection - - ProxyFactory - ProxyFactory - - - getAutoGenerateProxyClasses - getProxyDir - getProxyNamespace - is_object($entity) is_object($entity) @@ -252,11 +238,6 @@ new $class($this) - - - ProxyFactory - - $persister->load($criteria, null, null, [], null, 1, $orderBy) @@ -630,9 +611,6 @@ $metadata - - new AttributeReader() - $value[0] $value[0] @@ -643,17 +621,13 @@ $mapping - + $entityAnnotationClasses + $reader $listenerClassName - - $joinColumnAttributes - $this->reader->getPropertyAnnotation($property, Mapping\InverseJoinColumn::class) - $this->reader->getPropertyAnnotation($property, Mapping\JoinColumn::class) - assert($method instanceof ReflectionMethod) assert($method instanceof ReflectionMethod) @@ -1408,19 +1382,9 @@ $parser->getLexer()->token['value'] - - $class->associationMappings - - - $parser->getLexer()->token['value'] - - - $fieldMapping + $pathExpression - - $this->fieldMapping !== null - @@ -1464,9 +1428,6 @@ - - $class->associationMappings - $collectionPathExpression @@ -1916,10 +1877,9 @@ $this->ConditionalExpression() $this->ConditionalExpression() - + $aliasIdentVariable $dql - $field $fromClassName $functionName $functionName @@ -1940,7 +1900,7 @@ $token['value'] $token['value'] - + $glimpse['type'] $glimpse['value'] $lookahead['type'] @@ -2005,7 +1965,6 @@ $this->lexer->token['value'] $this->lexer->token['value'] $this->lexer->token['value'] - $this->lexer->token['value'] $token['type'] $token['type'] $token['type'] @@ -2071,10 +2030,9 @@ - + $likeExpr->stringPattern instanceof AST\Functions\FunctionNode $likeExpr->stringPattern instanceof AST\PathExpression - $this->queryComponents[$dqlAlias]['relation'] === null '' is_string($expression) is_string($stringExpr) @@ -2088,9 +2046,6 @@ $factor $selectedClass['class']->name - - $this->selectedClasses[$joinedDqlAlias] - walkConditionalPrimary @@ -2143,10 +2098,8 @@ dispatch - + $query - $this->queryComponents - $this->selectedClasses $likeExpr->stringPattern instanceof AST\InputParameter @@ -2252,9 +2205,6 @@ _getQueryComponents - - $this->_queryComponents - @@ -2406,10 +2356,6 @@ - - getProxyDir - getProxyDir - $metadata->name diff --git a/tests/Doctrine/Tests/ORM/Mapping/AnnotationDriverTest.php b/tests/Doctrine/Tests/ORM/Mapping/AnnotationDriverTest.php index 2bdce36ac9..9357208a4a 100644 --- a/tests/Doctrine/Tests/ORM/Mapping/AnnotationDriverTest.php +++ b/tests/Doctrine/Tests/ORM/Mapping/AnnotationDriverTest.php @@ -137,6 +137,9 @@ protected function loadDriverForCMSModels(): AnnotationDriver return $annotationDriver; } + /** + * @return AnnotationDriver + */ protected function loadDriver(): MappingDriver { return $this->createAnnotationDriver(); diff --git a/tests/Doctrine/Tests/ORM/Mapping/AttributeReaderTest.php b/tests/Doctrine/Tests/ORM/Mapping/AttributeReaderTest.php new file mode 100644 index 0000000000..823fe6b6f0 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Mapping/AttributeReaderTest.php @@ -0,0 +1,45 @@ +expectException(LogicException::class); + $this->expectExceptionMessage( + 'The attribute "Doctrine\ORM\Mapping\Index" is repeatable. Call getPropertyAnnotationCollection() instead.' + ); + $reader->getPropertyAnnotation($property, ORM\Index::class); + } + + public function testItThrowsWhenGettingNonRepeatableAnnotationWithTheWrongMethod(): void + { + $reader = new AttributeReader(); + $property = new ReflectionProperty(TestEntity::class, 'id'); + $this->expectException(LogicException::class); + $this->expectExceptionMessage( + 'The attribute "Doctrine\ORM\Mapping\Id" is not repeatable. Call getPropertyAnnotation() instead.' + ); + $reader->getPropertyAnnotationCollection($property, ORM\Id::class); + } +} + +#[ORM\Entity] +#[ORM\Index(name: 'bar', columns: ['id'])] +class TestEntity +{ + #[ORM\Id, ORM\Column(type: 'integer'), ORM\GeneratedValue] + /** @var int */ + public $id; +} diff --git a/tests/Doctrine/Tests/ORM/Tools/SchemaValidatorTest.php b/tests/Doctrine/Tests/ORM/Tools/SchemaValidatorTest.php index 501fae65ec..653cd4f357 100644 --- a/tests/Doctrine/Tests/ORM/Tools/SchemaValidatorTest.php +++ b/tests/Doctrine/Tests/ORM/Tools/SchemaValidatorTest.php @@ -151,6 +151,25 @@ public function testInvalidBiDirectionalRelationMappingMissingInversedByAttribut ); } + /** + * @group 9536 + */ + public function testInvalidBiDirectionalRelationMappingMissingMappedByAttribute(): void + { + $class = $this->em->getClassMetadata(Issue9536Owner::class); + $ce = $this->validator->validateClass($class); + + self::assertEquals( + [ + 'The field Doctrine\Tests\ORM\Tools\Issue9536Owner#one is on the owning side of a bi-directional ' . + 'relationship, but the specified inversedBy association on the target-entity ' . + "Doctrine\Tests\ORM\Tools\Issue9536Target#two does not contain the required 'mappedBy=\"one\"' " . + 'attribute.', + ], + $ce + ); + } + /** * @group DDC-3322 */ @@ -435,6 +454,46 @@ class DDC3274Two private $one; } +/** + * @Entity + */ +class Issue9536Target +{ + /** + * @var mixed + * @Id + * @Column + * @GeneratedValue + */ + private $id; + + /** + * @var Issue9536Owner + * @OneToOne(targetEntity="Issue9536Owner") + */ + private $two; +} + +/** + * @Entity + */ +class Issue9536Owner +{ + /** + * @var mixed + * @Id + * @Column + * @GeneratedValue + */ + private $id; + + /** + * @var Issue9536Target + * @OneToOne(targetEntity="Issue9536Target", inversedBy="two") + */ + private $one; +} + /** * @Entity */