Skip to content

Commit

Permalink
Introduce DoctrineTypeDriverAwareDescriptor & DriverDetector
Browse files Browse the repository at this point in the history
  • Loading branch information
janedbal committed Jun 25, 2024
1 parent b09104d commit 4a1ece8
Show file tree
Hide file tree
Showing 9 changed files with 346 additions and 4 deletions.
4 changes: 4 additions & 0 deletions extension.neon
Original file line number Diff line number Diff line change
Expand Up @@ -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
-
Expand Down
11 changes: 11 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -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\.$#'
174 changes: 174 additions & 0 deletions src/Doctrine/Driver/DriverDetector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
<?php declare(strict_types = 1);

namespace PHPStan\Doctrine\Driver;

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Driver\IBMDB2\Driver as IbmDb2Driver;
use Doctrine\DBAL\Driver\Mysqli\Driver as MysqliDriver;
use Doctrine\DBAL\Driver\OCI8\Driver as Oci8Driver;
use Doctrine\DBAL\Driver\PDO\MySQL\Driver as PdoMysqlDriver;
use Doctrine\DBAL\Driver\PDO\OCI\Driver as PdoOciDriver;
use Doctrine\DBAL\Driver\PDO\PgSQL\Driver as PdoPgSQLDriver;
use Doctrine\DBAL\Driver\PDO\SQLite\Driver as PdoSQLiteDriver;
use Doctrine\DBAL\Driver\PDO\SQLSrv\Driver as PdoSqlSrvDriver;
use Doctrine\DBAL\Driver\PgSQL\Driver as PgSQLDriver;
use Doctrine\DBAL\Driver\SQLite3\Driver as SQLite3Driver;
use Doctrine\DBAL\Driver\SQLSrv\Driver as SqlSrvDriver;
use mysqli;
use PDO;
use SQLite3;
use Throwable;
use function get_resource_type;
use function is_resource;
use function method_exists;
use function strpos;

class DriverDetector
{

public const IBM_DB2 = 'ibm_db2';
public const MYSQLI = 'mysqli';
public const OCI8 = 'oci8';
public const PDO_MYSQL = 'pdo_mysql';
public const PDO_OCI = 'pdo_oci';
public const PDO_PGSQL = 'pdo_pgsql';
public const PDO_SQLITE = 'pdo_sqlite';
public const PDO_SQLSRV = 'pdo_sqlsrv';
public const PGSQL = 'pgsql';
public const SQLITE3 = 'sqlite3';
public const SQLSRV = 'sqlsrv';

/** @var bool */
private $failOnInvalidConnection;

public function __construct(bool $failOnInvalidConnection)
{
$this->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;
}

}
37 changes: 36 additions & 1 deletion src/Type/Doctrine/Descriptors/BooleanType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
}

}
38 changes: 37 additions & 1 deletion src/Type/Doctrine/Descriptors/DecimalType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
}

}
13 changes: 13 additions & 0 deletions src/Type/Doctrine/Descriptors/DoctrineTypeDescriptor.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Doctrine\Descriptors;

use Doctrine\DBAL\Connection;
use PHPStan\Type\Type;

/** @api */
interface DoctrineTypeDriverAwareDescriptor
{

/**
* This is used for inferring how database fetches column of such type
* It should return the native type without stringification that may occur on certain PHP versions or driver configuration
*
* This is not used for direct column type inferring,
* but when such column appears in expression like SELECT MAX(e.field)
*
* See: https://github.com/janedbal/php-database-drivers-fetch-test
*
* mysql sqlite pdo_pgsql pgsql
* - decimal: string int|float string string
* - float: float float string float
* - bigint: int int int int
* - bool: int int bool bool
*/
public function getDatabaseInternalTypeForDriver(Connection $connection): Type;

}
Loading

0 comments on commit 4a1ece8

Please sign in to comment.