diff --git a/src/Extensions/DoctrineConnectionExecuteQueryDynamicReturnTypeExtension.php b/src/Extensions/DoctrineConnectionExecuteQueryDynamicReturnTypeExtension.php index 27d15ff45..f541df332 100644 --- a/src/Extensions/DoctrineConnectionExecuteQueryDynamicReturnTypeExtension.php +++ b/src/Extensions/DoctrineConnectionExecuteQueryDynamicReturnTypeExtension.php @@ -54,11 +54,15 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method } $params = null; + $paramTypes = null; if (\count($args) > 1) { $params = $args[1]->value; } + if (\count($args) > 2) { + $paramTypes = $args[2]->value; + } - $resultType = $this->inferType($args[0]->value, $params, $scope); + $resultType = $this->inferType($args[0]->value, $params, $paramTypes, $scope); if (null !== $resultType) { return $resultType; } @@ -66,16 +70,17 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method return $defaultReturn; } - private function inferType(Expr $queryExpr, ?Expr $paramsExpr, Scope $scope): ?Type + private function inferType(Expr $queryExpr, ?Expr $paramsExpr, ?Expr $paramTypesExpr, Scope $scope): ?Type { if (null === $paramsExpr) { $queryReflection = new QueryReflection(); $queryStrings = $queryReflection->resolveQueryStrings($queryExpr, $scope); } else { $parameterTypes = $scope->getType($paramsExpr); + $boundParameterTypes = $scope->getType($paramsExpr); $queryReflection = new QueryReflection(); - $queryStrings = $queryReflection->resolvePreparedQueryStrings($queryExpr, $parameterTypes, $scope); + $queryStrings = $queryReflection->resolvePreparedQueryStrings($queryExpr, $parameterTypes, $boundParameterTypes, $scope); } $doctrineReflection = new DoctrineReflection(); diff --git a/src/Extensions/DoctrineConnectionFetchDynamicReturnTypeExtension.php b/src/Extensions/DoctrineConnectionFetchDynamicReturnTypeExtension.php index 85fcd2600..0475018fc 100644 --- a/src/Extensions/DoctrineConnectionFetchDynamicReturnTypeExtension.php +++ b/src/Extensions/DoctrineConnectionFetchDynamicReturnTypeExtension.php @@ -88,7 +88,7 @@ private function inferType(MethodReflection $methodReflection, Expr $queryExpr, $queryStrings = $queryReflection->resolveQueryStrings($queryExpr, $scope); } else { $parameterTypes = $scope->getType($paramsExpr); - $queryStrings = $queryReflection->resolvePreparedQueryStrings($queryExpr, $parameterTypes, $scope); + $queryStrings = $queryReflection->resolvePreparedQueryStrings($queryExpr, $parameterTypes, null, $scope); } return $doctrineReflection->createFetchType($queryStrings, $methodReflection); diff --git a/src/Extensions/DoctrineStatementExecuteDynamicReturnTypeExtension.php b/src/Extensions/DoctrineStatementExecuteDynamicReturnTypeExtension.php index 614c3d21f..38efeaae6 100644 --- a/src/Extensions/DoctrineStatementExecuteDynamicReturnTypeExtension.php +++ b/src/Extensions/DoctrineStatementExecuteDynamicReturnTypeExtension.php @@ -70,7 +70,7 @@ private function inferType(MethodReflection $methodReflection, MethodCall $metho } $queryReflection = new QueryReflection(); - $queryStrings = $queryReflection->resolvePreparedQueryStrings($queryExpr, $parameterTypes, $scope); + $queryStrings = $queryReflection->resolvePreparedQueryStrings($queryExpr, $parameterTypes, null, $scope); return $doctrineReflection->createGenericResult($queryStrings, QueryReflector::FETCH_TYPE_BOTH); } diff --git a/src/Extensions/PdoStatementExecuteTypeSpecifyingExtension.php b/src/Extensions/PdoStatementExecuteTypeSpecifyingExtension.php index 2e3055c2c..c5dd17b91 100644 --- a/src/Extensions/PdoStatementExecuteTypeSpecifyingExtension.php +++ b/src/Extensions/PdoStatementExecuteTypeSpecifyingExtension.php @@ -66,7 +66,7 @@ private function inferStatementType(MethodReflection $methodReflection, MethodCa $parameterTypes = $scope->getType($args[0]->value); $queryReflection = new QueryReflection(); - $queryStrings = $queryReflection->resolvePreparedQueryStrings($queryExpr, $parameterTypes, $scope); + $queryStrings = $queryReflection->resolvePreparedQueryStrings($queryExpr, $parameterTypes, null, $scope); $reflectionFetchType = QueryReflection::getRuntimeConfiguration()->getDefaultFetchMode(); diff --git a/src/QueryReflection/QueryReflection.php b/src/QueryReflection/QueryReflection.php index a83eb1201..f16fdb322 100644 --- a/src/QueryReflection/QueryReflection.php +++ b/src/QueryReflection/QueryReflection.php @@ -73,12 +73,12 @@ public function getResultType(string $queryString, int $fetchType): ?Type * * @throws UnresolvableQueryException */ - public function resolvePreparedQueryStrings(Expr $queryExpr, Type $parameterTypes, Scope $scope): iterable + public function resolvePreparedQueryStrings(Expr $queryExpr, Type $parameterTypes, ?Type $boundParameterTypes, Scope $scope): iterable { $type = $scope->getType($queryExpr); if ($type instanceof UnionType) { - $parameters = $this->resolveParameters($parameterTypes); + $parameters = $this->resolveParameters($parameterTypes, $boundParameterTypes); if (null === $parameters) { return null; } @@ -103,7 +103,7 @@ public function resolvePreparedQueryStrings(Expr $queryExpr, Type $parameterType * * @throws UnresolvableQueryException */ - public function resolvePreparedQueryString(Expr $queryExpr, Type $parameterTypes, Scope $scope): ?string + public function resolvePreparedQueryString(Expr $queryExpr, Type $parameterTypes, ?Type, $boundParameterTypes, Scope $scope): ?string { $queryString = $this->resolveQueryExpr($queryExpr, $scope); @@ -111,7 +111,7 @@ public function resolvePreparedQueryString(Expr $queryExpr, Type $parameterTypes return null; } - $parameters = $this->resolveParameters($parameterTypes); + $parameters = $this->resolveParameters($parameterTypes, $boundParameterTypes); if (null === $parameters) { return null; } @@ -211,20 +211,20 @@ public static function getQueryType(string $query): ?string * * @throws UnresolvableQueryException */ - public function resolveParameters(Type $parameterTypes): ?array + public function resolveParameters(Type $parameterTypes, ?Type $boundParameterTypes): ?array { $parameters = []; if ($parameterTypes instanceof UnionType) { foreach (TypeUtils::getConstantArrays($parameterTypes) as $constantArray) { - $parameters = $parameters + $this->resolveConstantArray($constantArray, true); + $parameters = $parameters + $this->resolveConstantArray($constantArray, $boundParameterTypes, true); } return $parameters; } if ($parameterTypes instanceof ConstantArrayType) { - return $this->resolveConstantArray($parameterTypes, false); + return $this->resolveConstantArray($parameterTypes, $boundParameterTypes, false); } return null; @@ -235,7 +235,7 @@ public function resolveParameters(Type $parameterTypes): ?array * * @throws UnresolvableQueryException */ - private function resolveConstantArray(ConstantArrayType $parameterTypes, bool $forceOptional): array + private function resolveConstantArray(ConstantArrayType $parameterTypes, ?Type $boundParameterTypes, bool $forceOptional): array { $parameters = []; @@ -259,7 +259,7 @@ private function resolveConstantArray(ConstantArrayType $parameterTypes, bool $f $param = new Parameter( $placeholderName, $valueTypes[$i], - QuerySimulation::simulateParamValueType($valueTypes[$i], true), + QuerySimulation::simulateParamValueType($valueTypes[$i], true), // TODO pass $i of $boundParameterTypes here if a resolved constant type is available? $isOptional ); @@ -268,7 +268,7 @@ private function resolveConstantArray(ConstantArrayType $parameterTypes, bool $f $param = new Parameter( null, $valueTypes[$i], - QuerySimulation::simulateParamValueType($valueTypes[$i], true), + QuerySimulation::simulateParamValueType($valueTypes[$i], true), // TODO pass $i of $boundParameterTypes here if a resolved constant type is available? $isOptional ); diff --git a/src/QueryReflection/QuerySimulation.php b/src/QueryReflection/QuerySimulation.php index db50c6748..0b40cea67 100644 --- a/src/QueryReflection/QuerySimulation.php +++ b/src/QueryReflection/QuerySimulation.php @@ -73,6 +73,10 @@ public static function simulateParamValueType(Type $paramType, bool $preparedPar return null; } + if ($paramType instanceof ObjectType && $paramType->isInstanceOf(\DateTimeInterface::class)) { + return date(self::DATE_FORMAT, 0); + } + $stringType = new StringType(); $isStringableObjectType = $paramType instanceof ObjectType && $paramType->isInstanceOf(Stringable::class)->yes(); diff --git a/src/Rules/PdoStatementExecuteMethodRule.php b/src/Rules/PdoStatementExecuteMethodRule.php index ddfd5b91e..cb63c789b 100644 --- a/src/Rules/PdoStatementExecuteMethodRule.php +++ b/src/Rules/PdoStatementExecuteMethodRule.php @@ -95,7 +95,7 @@ private function checkErrors(MethodReflection $methodReflection, MethodCall $met } try { - $parameters = $queryReflection->resolveParameters($parameterTypes) ?? []; + $parameters = $queryReflection->resolveParameters($parameterTypes, null) ?? []; } catch (UnresolvableQueryException $exception) { return [ RuleErrorBuilder::message($exception->asRuleMessage())->tip(UnresolvableQueryException::RULE_TIP)->line($methodCall->getLine())->build(), diff --git a/tests/default/data/doctrine-dbal.php b/tests/default/data/doctrine-dbal.php index 37e045e8c..337f7441c 100644 --- a/tests/default/data/doctrine-dbal.php +++ b/tests/default/data/doctrine-dbal.php @@ -4,6 +4,7 @@ use Doctrine\DBAL\Cache\QueryCacheProfile; use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Types\Types; use function PHPStan\Testing\assertType; use staabm\PHPStanDba\Tests\Fixture\StringableObject; @@ -218,4 +219,49 @@ public function dateParameter(Connection $conn) $fetchResult = $conn->fetchOne($query, [date('Y-m-d', strtotime('-3hour'))]); assertType('int|false', $fetchResult); } + + public function customTypeParameters(Connection $conn, int $adaid) + { + $stmt = $conn->prepare('SELECT count(*) AS c FROM typemix WHERE c_datetime=:last_dt'); + $result = $stmt->execute(['dt' => new \DateTime()], ['dt' => Types::DATETIME_MUTABLE]); + assertType('Doctrine\DBAL\Result', $result); + + $stmt = $conn->prepare('SELECT count(*) AS c FROM typemix WHERE c_datetime=:last_dt'); + $result = $stmt->execute(['dt' => new \DateTime()], ['dt' => Types::DATETIME_IMMUTABLE]); + // TODO should probably fail as DateTime is passed to a DATETIME_IMMUTABLE type + assertType('Doctrine\DBAL\Result', $result); + } + + /** + * @param array $ids + * @param array $vals + */ + public function boundArrays(Connection $conn, array $ids, array $vals) + { + $result = $conn->executeQuery( + 'SELECT count(*) AS c FROM ada WHERE adaid IN (?)', + [$ids], + [\Doctrine\DBAL\Connection::PARAM_INT_ARRAY] + ); + assertType('Doctrine\DBAL\Result', $result); + + $result = $conn->executeQuery( + 'SELECT count(*) AS c FROM ada WHERE email IN (?)', + [$vals], + [\Doctrine\DBAL\Connection::PARAM_STR_ARRAY] + ); + assertType('Doctrine\DBAL\Result', $result); + + $result = $conn->executeQuery( + 'SELECT count(*) AS c FROM ada WHERE adaid IN (?)', + [$vals], // TODO should error as $vals is not int array + [\Doctrine\DBAL\Connection::PARAM_INT_ARRAY] + ); + + $result = $conn->executeQuery( + 'SELECT count(*) AS c FROM ada WHERE email IN (?)', + [$ids], // TODO should error as $ids is not str array + [\Doctrine\DBAL\Connection::PARAM_STR_ARRAY] + ); + } }