diff --git a/extension.neon b/extension.neon index 0c798f8f..2f8daea1 100644 --- a/extension.neon +++ b/extension.neon @@ -89,6 +89,10 @@ services: class: PHPStan\Type\Doctrine\DefaultDescriptorRegistry factory: @PHPStan\Type\Doctrine\DescriptorRegistryFactory::createRegistry + - + class: PHPStan\Doctrine\Driver\DriverDetector + arguments: + failOnInvalidConnection: %featureToggles.bleedingEdge% - class: PHPStan\Reflection\Doctrine\DoctrineSelectableClassReflectionExtension - diff --git a/phpstan.neon b/phpstan.neon index 8dfe69fa..c467b761 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -49,3 +49,14 @@ parameters: - '#^Cannot call method getWrappedResourceHandle\(\) on class\-string\|object\.$#' path: tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php reportUnmatched: false + + - + message: '#^Call to function method_exists\(\) with Doctrine\\DBAL\\Connection and ''getNativeConnection'' will always evaluate to true\.$#' # needed for older DBAL versions + paths: + - src/Doctrine/Driver/DriverDetector.php + + - + messages: # needed for older DBAL versions + - '#^Class PgSql\\Connection not found\.$#' + - '#^Class Doctrine\\DBAL\\Driver\\PgSQL\\Driver not found\.$#' + - '#^Class Doctrine\\DBAL\\Driver\\SQLite3\\Driver not found\.$#' diff --git a/src/Doctrine/Driver/DriverDetector.php b/src/Doctrine/Driver/DriverDetector.php new file mode 100644 index 00000000..0a4371be --- /dev/null +++ b/src/Doctrine/Driver/DriverDetector.php @@ -0,0 +1,174 @@ +failOnInvalidConnection = $failOnInvalidConnection; + } + + /** + * @return self::*|null + */ + public function detect(Connection $connection): ?string + { + $driver = $connection->getDriver(); + + if ($driver instanceof MysqliDriver) { + return self::MYSQLI; + } + + if ($driver instanceof PdoMysqlDriver) { + return self::PDO_MYSQL; + } + + if ($driver instanceof PdoSQLiteDriver) { + return self::PDO_SQLITE; + } + + if ($driver instanceof PdoSqlSrvDriver) { + return self::PDO_SQLSRV; + } + + if ($driver instanceof PdoOciDriver) { + return self::PDO_OCI; + } + + if ($driver instanceof PdoPgSQLDriver) { + return self::PDO_PGSQL; + } + + if ($driver instanceof SQLite3Driver) { + return self::SQLITE3; + } + + if ($driver instanceof PgSQLDriver) { + return self::PGSQL; + } + + if ($driver instanceof SqlSrvDriver) { + return self::SQLSRV; + } + + if ($driver instanceof Oci8Driver) { + return self::OCI8; + } + + if ($driver instanceof IbmDb2Driver) { + return self::IBM_DB2; + } + + // fallback to connection-based detection when driver is wrapped by middleware + + if (!method_exists($connection, 'getNativeConnection')) { + return null; // dbal < 3.3 (released in 2022-01) + } + + try { + $nativeConnection = $connection->getNativeConnection(); + } catch (Throwable $e) { + if ($this->failOnInvalidConnection) { + throw $e; + } + return null; // connection cannot be established + } + + if ($nativeConnection instanceof mysqli) { + return self::MYSQLI; + } + + if ($nativeConnection instanceof SQLite3) { + return self::SQLITE3; + } + + if ($nativeConnection instanceof \PgSql\Connection) { + return self::PGSQL; + } + + if ($nativeConnection instanceof PDO) { + $driverName = $nativeConnection->getAttribute(PDO::ATTR_DRIVER_NAME); + + if ($driverName === 'mysql') { + return self::PDO_MYSQL; + } + + if ($driverName === 'sqlite') { + return self::PDO_SQLITE; + } + + if ($driverName === 'pgsql') { + return self::PDO_PGSQL; + } + + if ($driverName === 'oci') { // semi-verified (https://stackoverflow.com/questions/10090709/get-current-pdo-driver-from-existing-connection/10090754#comment12923198_10090754) + return self::PDO_OCI; + } + + if ($driverName === 'sqlsrv') { + return self::PDO_SQLSRV; + } + } + + if (is_resource($nativeConnection)) { + $resourceType = get_resource_type($nativeConnection); + + if (strpos($resourceType, 'oci') !== false) { // not verified + return self::OCI8; + } + + if (strpos($resourceType, 'db2') !== false) { // not verified + return self::IBM_DB2; + } + + if (strpos($resourceType, 'SQL Server Connection') !== false) { + return self::SQLSRV; + } + + if (strpos($resourceType, 'pgsql link') !== false) { + return self::PGSQL; + } + } + + return null; + } + +} diff --git a/src/Type/Doctrine/Descriptors/BooleanType.php b/src/Type/Doctrine/Descriptors/BooleanType.php index 955883a8..b9e59574 100644 --- a/src/Type/Doctrine/Descriptors/BooleanType.php +++ b/src/Type/Doctrine/Descriptors/BooleanType.php @@ -2,13 +2,24 @@ namespace PHPStan\Type\Doctrine\Descriptors; +use Doctrine\DBAL\Connection; +use PHPStan\Doctrine\Driver\DriverDetector; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use function in_array; -class BooleanType implements DoctrineTypeDescriptor +class BooleanType implements DoctrineTypeDescriptor, DoctrineTypeDriverAwareDescriptor { + /** @var DriverDetector */ + private $driverDetector; + + public function __construct(DriverDetector $driverDetector) + { + $this->driverDetector = $driverDetector; + } + public function getType(): string { return \Doctrine\DBAL\Types\BooleanType::class; @@ -33,4 +44,28 @@ public function getDatabaseInternalType(): Type ); } + public function getDatabaseInternalTypeForDriver(Connection $connection): Type + { + $driverType = $this->driverDetector->detect($connection); + + if ($driverType === DriverDetector::PGSQL || $driverType === DriverDetector::PDO_PGSQL) { + return new \PHPStan\Type\BooleanType(); + } + + if (in_array($driverType, [ + DriverDetector::SQLITE3, + DriverDetector::PDO_SQLITE, + DriverDetector::MYSQLI, + DriverDetector::PDO_MYSQL, + ], true)) { + return TypeCombinator::union( + new ConstantIntegerType(0), + new ConstantIntegerType(1) + ); + } + + // not yet supported driver, return the old implementation guess + return $this->getDatabaseInternalType(); + } + } diff --git a/src/Type/Doctrine/Descriptors/DecimalType.php b/src/Type/Doctrine/Descriptors/DecimalType.php index b008ffe5..64184c45 100644 --- a/src/Type/Doctrine/Descriptors/DecimalType.php +++ b/src/Type/Doctrine/Descriptors/DecimalType.php @@ -2,16 +2,28 @@ namespace PHPStan\Type\Doctrine\Descriptors; +use Doctrine\DBAL\Connection; +use PHPStan\Doctrine\Driver\DriverDetector; use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\FloatType; use PHPStan\Type\IntegerType; +use PHPStan\Type\IntersectionType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use function in_array; -class DecimalType implements DoctrineTypeDescriptor +class DecimalType implements DoctrineTypeDescriptor, DoctrineTypeDriverAwareDescriptor { + /** @var DriverDetector */ + private $driverDetector; + + public function __construct(DriverDetector $driverDetector) + { + $this->driverDetector = $driverDetector; + } + public function getType(): string { return \Doctrine\DBAL\Types\DecimalType::class; @@ -32,4 +44,28 @@ public function getDatabaseInternalType(): Type return TypeCombinator::union(new FloatType(), new IntegerType()); } + public function getDatabaseInternalTypeForDriver(Connection $connection): Type + { + $driverType = $this->driverDetector->detect($connection); + + if ($driverType === DriverDetector::SQLITE3 || $driverType === DriverDetector::PDO_SQLITE) { + return TypeCombinator::union(new FloatType(), new IntegerType()); + } + + if (in_array($driverType, [ + DriverDetector::MYSQLI, + DriverDetector::PDO_MYSQL, + DriverDetector::PGSQL, + DriverDetector::PDO_PGSQL, + ], true)) { + return new IntersectionType([ + new StringType(), + new AccessoryNumericStringType(), + ]); + } + + // not yet supported driver, return the old implementation guess + return $this->getDatabaseInternalType(); + } + } diff --git a/src/Type/Doctrine/Descriptors/DoctrineTypeDescriptor.php b/src/Type/Doctrine/Descriptors/DoctrineTypeDescriptor.php index 75f56c9b..c0bffd92 100644 --- a/src/Type/Doctrine/Descriptors/DoctrineTypeDescriptor.php +++ b/src/Type/Doctrine/Descriptors/DoctrineTypeDescriptor.php @@ -13,10 +13,23 @@ interface DoctrineTypeDescriptor */ public function getType(): string; + /** + * This is used for inferring direct column results, e.g. SELECT e.field + * It should comply with convertToPHPValue return value + */ public function getWritableToPropertyType(): Type; public function getWritableToDatabaseType(): Type; + /** + * This is used for inferring how database fetches column of such type + * + * This is not used for direct column type inferring, + * but when such column appears in expression like SELECT MAX(e.field) + * + * Sometimes, the type cannot be reliably decided without driver context, + * use DoctrineTypeDriverAwareDescriptor in such cases + */ public function getDatabaseInternalType(): Type; } diff --git a/src/Type/Doctrine/Descriptors/DoctrineTypeDriverAwareDescriptor.php b/src/Type/Doctrine/Descriptors/DoctrineTypeDriverAwareDescriptor.php new file mode 100644 index 00000000..765c8f2e --- /dev/null +++ b/src/Type/Doctrine/Descriptors/DoctrineTypeDriverAwareDescriptor.php @@ -0,0 +1,29 @@ +driverDetector = $driverDetector; + } + public function getType(): string { return \Doctrine\DBAL\Types\FloatType::class; @@ -29,4 +43,29 @@ public function getDatabaseInternalType(): Type return TypeCombinator::union(new \PHPStan\Type\FloatType(), new IntegerType()); } + public function getDatabaseInternalTypeForDriver(Connection $connection): Type + { + $driverType = $this->driverDetector->detect($connection); + + if ($driverType === DriverDetector::PDO_PGSQL) { + return new IntersectionType([ + new StringType(), + new AccessoryNumericStringType(), + ]); + } + + if (in_array($driverType, [ + DriverDetector::SQLITE3, + DriverDetector::PDO_SQLITE, + DriverDetector::MYSQLI, + DriverDetector::PDO_MYSQL, + DriverDetector::PGSQL, + ], true)) { + return new \PHPStan\Type\FloatType(); + } + + // not yet supported driver, return the old implementation guess + return $this->getDatabaseInternalType(); + } + } diff --git a/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php b/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php index 57561ef4..76183c49 100644 --- a/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php +++ b/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php @@ -7,6 +7,7 @@ use Composer\InstalledVersions; use Doctrine\DBAL\Types\Type; use Iterator; +use PHPStan\Doctrine\Driver\DriverDetector; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\Doctrine\DefaultDescriptorRegistry; @@ -70,7 +71,7 @@ protected function getRule(): Rule new DateTimeImmutableType(), new DateTimeType(), new DateType(), - new DecimalType(), + new DecimalType(new DriverDetector(true)), new JsonType(), new IntegerType(), new StringType(),