diff --git a/.phpstan-dba.cache b/.phpstan-dba.cache index 7487986fe..b7db4e39d 100644 --- a/.phpstan-dba.cache +++ b/.phpstan-dba.cache @@ -3423,18 +3423,26 @@ Simulated query: SELECT email, adaid FROM ada . WHERE email=\'my_other_table\' L 'error' => NULL, 'result' => array ( - 1 => + 3 => PHPStan\Type\Constant\ConstantArrayType::__set_state(array( 'keyType' => PHPStan\Type\UnionType::__set_state(array( 'types' => array ( 0 => + PHPStan\Type\Constant\ConstantIntegerType::__set_state(array( + 'value' => 0, + )), + 1 => + PHPStan\Type\Constant\ConstantIntegerType::__set_state(array( + 'value' => 1, + )), + 2 => PHPStan\Type\Constant\ConstantStringType::__set_state(array( 'value' => 'adaid', 'isClassString' => false, )), - 1 => + 3 => PHPStan\Type\Constant\ConstantStringType::__set_state(array( 'value' => 'email', 'isClassString' => false, @@ -3464,10 +3472,18 @@ Simulated query: SELECT email, adaid FROM ada . WHERE email=\'my_other_table\' L 'isClassString' => false, )), 1 => + PHPStan\Type\Constant\ConstantIntegerType::__set_state(array( + 'value' => 0, + )), + 2 => PHPStan\Type\Constant\ConstantStringType::__set_state(array( 'value' => 'adaid', 'isClassString' => false, )), + 3 => + PHPStan\Type\Constant\ConstantIntegerType::__set_state(array( + 'value' => 1, + )), ), 'valueTypes' => array ( @@ -3475,36 +3491,36 @@ Simulated query: SELECT email, adaid FROM ada . WHERE email=\'my_other_table\' L PHPStan\Type\StringType::__set_state(array( )), 1 => + PHPStan\Type\StringType::__set_state(array( + )), + 2 => + PHPStan\Type\IntegerRangeType::__set_state(array( + 'min' => 0, + 'max' => 4294967295, + )), + 3 => PHPStan\Type\IntegerRangeType::__set_state(array( 'min' => 0, 'max' => 4294967295, )), ), - 'nextAutoIndex' => 0, + 'nextAutoIndex' => 2, 'optionalKeys' => array ( ), )), - 3 => + 1 => PHPStan\Type\Constant\ConstantArrayType::__set_state(array( 'keyType' => PHPStan\Type\UnionType::__set_state(array( 'types' => array ( 0 => - PHPStan\Type\Constant\ConstantIntegerType::__set_state(array( - 'value' => 0, - )), - 1 => - PHPStan\Type\Constant\ConstantIntegerType::__set_state(array( - 'value' => 1, - )), - 2 => PHPStan\Type\Constant\ConstantStringType::__set_state(array( 'value' => 'adaid', 'isClassString' => false, )), - 3 => + 1 => PHPStan\Type\Constant\ConstantStringType::__set_state(array( 'value' => 'email', 'isClassString' => false, @@ -3534,18 +3550,10 @@ Simulated query: SELECT email, adaid FROM ada . WHERE email=\'my_other_table\' L 'isClassString' => false, )), 1 => - PHPStan\Type\Constant\ConstantIntegerType::__set_state(array( - 'value' => 0, - )), - 2 => PHPStan\Type\Constant\ConstantStringType::__set_state(array( 'value' => 'adaid', 'isClassString' => false, )), - 3 => - PHPStan\Type\Constant\ConstantIntegerType::__set_state(array( - 'value' => 1, - )), ), 'valueTypes' => array ( @@ -3553,20 +3561,12 @@ Simulated query: SELECT email, adaid FROM ada . WHERE email=\'my_other_table\' L PHPStan\Type\StringType::__set_state(array( )), 1 => - PHPStan\Type\StringType::__set_state(array( - )), - 2 => - PHPStan\Type\IntegerRangeType::__set_state(array( - 'min' => 0, - 'max' => 4294967295, - )), - 3 => PHPStan\Type\IntegerRangeType::__set_state(array( 'min' => 0, 'max' => 4294967295, )), ), - 'nextAutoIndex' => 2, + 'nextAutoIndex' => 0, 'optionalKeys' => array ( ), @@ -4702,6 +4702,155 @@ Simulated query: SELECT email, adaid FROM ada . WHERE email=\'my_other_table\' L )), ), ), + 'SELECT email, adaid, gesperrt, freigabe1u1 FROM ada ' => + array ( + 'error' => NULL, + 'result' => + array ( + 3 => + PHPStan\Type\Constant\ConstantArrayType::__set_state(array( + 'keyType' => + PHPStan\Type\UnionType::__set_state(array( + 'types' => + array ( + 0 => + PHPStan\Type\Constant\ConstantIntegerType::__set_state(array( + 'value' => 0, + )), + 1 => + PHPStan\Type\Constant\ConstantIntegerType::__set_state(array( + 'value' => 1, + )), + 2 => + PHPStan\Type\Constant\ConstantIntegerType::__set_state(array( + 'value' => 2, + )), + 3 => + PHPStan\Type\Constant\ConstantIntegerType::__set_state(array( + 'value' => 3, + )), + 4 => + PHPStan\Type\Constant\ConstantStringType::__set_state(array( + 'value' => 'adaid', + 'isClassString' => false, + )), + 5 => + PHPStan\Type\Constant\ConstantStringType::__set_state(array( + 'value' => 'email', + 'isClassString' => false, + )), + 6 => + PHPStan\Type\Constant\ConstantStringType::__set_state(array( + 'value' => 'freigabe1u1', + 'isClassString' => false, + )), + 7 => + PHPStan\Type\Constant\ConstantStringType::__set_state(array( + 'value' => 'gesperrt', + 'isClassString' => false, + )), + ), + )), + 'itemType' => + PHPStan\Type\UnionType::__set_state(array( + 'types' => + array ( + 0 => + PHPStan\Type\IntegerRangeType::__set_state(array( + 'min' => -128, + 'max' => 4294967295, + )), + 1 => + PHPStan\Type\StringType::__set_state(array( + )), + ), + )), + 'allArrays' => NULL, + 'keyTypes' => + array ( + 0 => + PHPStan\Type\Constant\ConstantStringType::__set_state(array( + 'value' => 'email', + 'isClassString' => false, + )), + 1 => + PHPStan\Type\Constant\ConstantIntegerType::__set_state(array( + 'value' => 0, + )), + 2 => + PHPStan\Type\Constant\ConstantStringType::__set_state(array( + 'value' => 'adaid', + 'isClassString' => false, + )), + 3 => + PHPStan\Type\Constant\ConstantIntegerType::__set_state(array( + 'value' => 1, + )), + 4 => + PHPStan\Type\Constant\ConstantStringType::__set_state(array( + 'value' => 'gesperrt', + 'isClassString' => false, + )), + 5 => + PHPStan\Type\Constant\ConstantIntegerType::__set_state(array( + 'value' => 2, + )), + 6 => + PHPStan\Type\Constant\ConstantStringType::__set_state(array( + 'value' => 'freigabe1u1', + 'isClassString' => false, + )), + 7 => + PHPStan\Type\Constant\ConstantIntegerType::__set_state(array( + 'value' => 3, + )), + ), + 'valueTypes' => + array ( + 0 => + PHPStan\Type\StringType::__set_state(array( + )), + 1 => + PHPStan\Type\StringType::__set_state(array( + )), + 2 => + PHPStan\Type\IntegerRangeType::__set_state(array( + 'min' => 0, + 'max' => 4294967295, + )), + 3 => + PHPStan\Type\IntegerRangeType::__set_state(array( + 'min' => 0, + 'max' => 4294967295, + )), + 4 => + PHPStan\Type\IntegerRangeType::__set_state(array( + 'min' => -128, + 'max' => 127, + )), + 5 => + PHPStan\Type\IntegerRangeType::__set_state(array( + 'min' => -128, + 'max' => 127, + )), + 6 => + PHPStan\Type\IntegerRangeType::__set_state(array( + 'min' => -128, + 'max' => 127, + )), + 7 => + PHPStan\Type\IntegerRangeType::__set_state(array( + 'min' => -128, + 'max' => 127, + )), + ), + 'nextAutoIndex' => 4, + 'optionalKeys' => + array ( + ), + )), + ), + ), 'SELECT email, adaid, gesperrt, freigabe1u1 FROM ada LIMIT 1' => array ( 'error' => NULL, diff --git a/.phpunit-phpstan-dba.cache b/.phpunit-phpstan-dba.cache index ffbf2dc70..27bcfef9d 100644 --- a/.phpunit-phpstan-dba.cache +++ b/.phpunit-phpstan-dba.cache @@ -4036,6 +4036,18 @@ Simulated query: SELECT email, adaid, gesperrt, freigabe1u1 FROM ada . WHERE ema )), ), ), + 'SELECT email, adaid, gesperrt, freigabe1u1 FROM ada WHERE adaid = ?' => + array ( + 'error' => + staabm\PHPStanDba\Error::__set_state(array( + 'message' => 'You have an error in your SQL syntax; check the manual that corresponds to your MySQL/MariaDB server version for the right syntax to use near \'? LIMIT 0\' at line 1', + 'code' => 1064, + )), + 'result' => + array ( + 3 => NULL, + ), + ), 'SELECT email, adaid, gesperrt, freigabe1u1 FROM ada WHERE adaid=1' => array ( 'error' => NULL, diff --git a/config/extensions.neon b/config/extensions.neon index db316c489..6ad15990f 100644 --- a/config/extensions.neon +++ b/config/extensions.neon @@ -9,11 +9,21 @@ services: tags: - phpstan.broker.dynamicMethodReturnTypeExtension + - + class: staabm\PHPStanDba\Extensions\DoctrineConnectionPrepareDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + - class: staabm\PHPStanDba\Extensions\DoctrineResultDynamicReturnTypeExtension tags: - phpstan.broker.dynamicMethodReturnTypeExtension + - + class: staabm\PHPStanDba\Extensions\DoctrineStatementExecuteDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + - class: staabm\PHPStanDba\Extensions\PdoQueryDynamicReturnTypeExtension tags: diff --git a/src/Extensions/DoctrineConnectionPrepareDynamicReturnTypeExtension.php b/src/Extensions/DoctrineConnectionPrepareDynamicReturnTypeExtension.php new file mode 100644 index 000000000..4aea7302f --- /dev/null +++ b/src/Extensions/DoctrineConnectionPrepareDynamicReturnTypeExtension.php @@ -0,0 +1,80 @@ +getName()), ['prepare'], true); + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type + { + $args = $methodCall->getArgs(); + $defaultReturn = ParametersAcceptorSelector::selectFromArgs( + $scope, + $methodCall->getArgs(), + $methodReflection->getVariants(), + )->getReturnType(); + + if (\count($args) < 1) { + return $defaultReturn; + } + + if ($scope->getType($args[0]->value) instanceof MixedType) { + return $defaultReturn; + } + + // make sure we don't report wrong types in doctrine 2.x + if (!InstalledVersions::satisfies(new VersionParser(), 'doctrine/dbal', '3.*')) { + return $defaultReturn; + } + + $resultType = $this->inferType($args[0]->value, $scope); + if (null !== $resultType) { + return $resultType; + } + + return $defaultReturn; + } + + private function inferType(Expr $queryExpr, Scope $scope): ?Type + { + $queryReflection = new QueryReflection(); + $queryString = $queryReflection->resolveQueryString($queryExpr, $scope); + if (null === $queryString) { + return null; + } + + $resultType = $queryReflection->getResultType($queryString, QueryReflector::FETCH_TYPE_BOTH); + if ($resultType) { + return new GenericObjectType(Statement::class, [$resultType]); + } + + return null; + } +} diff --git a/src/Extensions/DoctrineStatementExecuteDynamicReturnTypeExtension.php b/src/Extensions/DoctrineStatementExecuteDynamicReturnTypeExtension.php new file mode 100644 index 000000000..63a661cab --- /dev/null +++ b/src/Extensions/DoctrineStatementExecuteDynamicReturnTypeExtension.php @@ -0,0 +1,84 @@ +getName()), ['executequery', 'execute'], true); + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type + { + $args = $methodCall->getArgs(); + $defaultReturn = ParametersAcceptorSelector::selectFromArgs( + $scope, + $methodCall->getArgs(), + $methodReflection->getVariants(), + )->getReturnType(); + + if (\count($args) < 1) { + return $defaultReturn; + } + + // make sure we don't report wrong types in doctrine 2.x + if (!InstalledVersions::satisfies(new VersionParser(), 'doctrine/dbal', '3.*')) { + return $defaultReturn; + } + + $resultType = $this->inferType($methodReflection, $methodCall, $args[0]->value, $scope); + if (null !== $resultType) { + return $resultType; + } + + return $defaultReturn; + } + + private function inferType(MethodReflection $methodReflection, MethodCall $methodCall, Expr $paramsExpr, Scope $scope): ?Type + { + $parameterTypes = $scope->getType($paramsExpr); + + $stmtReflection = new PdoStatementReflection(); + $queryExpr = $stmtReflection->findPrepareQueryStringExpression($methodReflection, $methodCall); + if (null === $queryExpr) { + return null; + } + + $queryReflection = new QueryReflection(); + $queryString = $queryReflection->resolvePreparedQueryString($queryExpr, $parameterTypes, $scope); + if (null === $queryString) { + return null; + } + + $resultType = $queryReflection->getResultType($queryString, QueryReflector::FETCH_TYPE_BOTH); + if ($resultType) { + return new GenericObjectType(Result::class, [$resultType]); + } + + return null; + } +} diff --git a/src/PdoReflection/PdoStatementReflection.php b/src/PdoReflection/PdoStatementReflection.php index 80dfa6f9f..34d1ef545 100644 --- a/src/PdoReflection/PdoStatementReflection.php +++ b/src/PdoReflection/PdoStatementReflection.php @@ -4,21 +4,15 @@ namespace staabm\PHPStanDba\PdoReflection; -use PDOStatement; use PhpParser\Node\Expr; use PhpParser\Node\Expr\MethodCall; use PHPStan\Reflection\MethodReflection; -use PHPStan\ShouldNotHappenException; use staabm\PHPStanDba\QueryReflection\ExpressionFinder; final class PdoStatementReflection { public function findPrepareQueryStringExpression(MethodReflection $methodReflection, MethodCall $methodCall): ?Expr { - if ('execute' !== $methodReflection->getName() || PdoStatement::class !== $methodReflection->getDeclaringClass()->getName()) { - throw new ShouldNotHappenException(); - } - $exprFinder = new ExpressionFinder(); $queryExpr = $exprFinder->findQueryStringExpression($methodCall); diff --git a/tests/data/doctrine-dbal.php b/tests/data/doctrine-dbal.php index eebd14dfb..9e9ca890b 100644 --- a/tests/data/doctrine-dbal.php +++ b/tests/data/doctrine-dbal.php @@ -37,4 +37,18 @@ public function executeQuery(Connection $conn, array $types, QueryCacheProfile $ $stmt = $conn->executeCacheQuery('SELECT email, adaid, gesperrt, freigabe1u1 FROM ada WHERE adaid = ?', [1], $types, $qcp); assertType('Doctrine\DBAL\Result, 1: int<0, 4294967295>, gesperrt: int<-128, 127>, 2: int<-128, 127>, freigabe1u1: int<-128, 127>, 3: int<-128, 127>}>', $stmt); } + + public function executeStatement(Connection $conn, int $adaid) + { + $stmt = $conn->prepare('SELECT email, adaid, gesperrt, freigabe1u1 FROM ada'); + assertType('Doctrine\DBAL\Statement, 1: int<0, 4294967295>, gesperrt: int<-128, 127>, 2: int<-128, 127>, freigabe1u1: int<-128, 127>, 3: int<-128, 127>}>', $stmt); + + $stmt = $conn->prepare('SELECT email, adaid, gesperrt, freigabe1u1 FROM ada WHERE adaid = ?'); + $result = $stmt->execute([$adaid]); + assertType('Doctrine\DBAL\Result, 1: int<0, 4294967295>, gesperrt: int<-128, 127>, 2: int<-128, 127>, freigabe1u1: int<-128, 127>, 3: int<-128, 127>}>', $result); + + $stmt = $conn->prepare('SELECT email, adaid, gesperrt, freigabe1u1 FROM ada WHERE adaid = ?'); + $result = $stmt->executeQuery([$adaid]); + assertType('Doctrine\DBAL\Result, 1: int<0, 4294967295>, gesperrt: int<-128, 127>, 2: int<-128, 127>, freigabe1u1: int<-128, 127>, 3: int<-128, 127>}>', $result); + } }