Skip to content

Commit 4a1ece8

Browse files
authored
Introduce DoctrineTypeDriverAwareDescriptor & DriverDetector
1 parent b09104d commit 4a1ece8

File tree

9 files changed

+346
-4
lines changed

9 files changed

+346
-4
lines changed

extension.neon

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ services:
8989
class: PHPStan\Type\Doctrine\DefaultDescriptorRegistry
9090
factory: @PHPStan\Type\Doctrine\DescriptorRegistryFactory::createRegistry
9191

92+
-
93+
class: PHPStan\Doctrine\Driver\DriverDetector
94+
arguments:
95+
failOnInvalidConnection: %featureToggles.bleedingEdge%
9296
-
9397
class: PHPStan\Reflection\Doctrine\DoctrineSelectableClassReflectionExtension
9498
-

phpstan.neon

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,14 @@ parameters:
4949
- '#^Cannot call method getWrappedResourceHandle\(\) on class\-string\|object\.$#'
5050
path: tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php
5151
reportUnmatched: false
52+
53+
-
54+
message: '#^Call to function method_exists\(\) with Doctrine\\DBAL\\Connection and ''getNativeConnection'' will always evaluate to true\.$#' # needed for older DBAL versions
55+
paths:
56+
- src/Doctrine/Driver/DriverDetector.php
57+
58+
-
59+
messages: # needed for older DBAL versions
60+
- '#^Class PgSql\\Connection not found\.$#'
61+
- '#^Class Doctrine\\DBAL\\Driver\\PgSQL\\Driver not found\.$#'
62+
- '#^Class Doctrine\\DBAL\\Driver\\SQLite3\\Driver not found\.$#'
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Doctrine\Driver;
4+
5+
use Doctrine\DBAL\Connection;
6+
use Doctrine\DBAL\Driver\IBMDB2\Driver as IbmDb2Driver;
7+
use Doctrine\DBAL\Driver\Mysqli\Driver as MysqliDriver;
8+
use Doctrine\DBAL\Driver\OCI8\Driver as Oci8Driver;
9+
use Doctrine\DBAL\Driver\PDO\MySQL\Driver as PdoMysqlDriver;
10+
use Doctrine\DBAL\Driver\PDO\OCI\Driver as PdoOciDriver;
11+
use Doctrine\DBAL\Driver\PDO\PgSQL\Driver as PdoPgSQLDriver;
12+
use Doctrine\DBAL\Driver\PDO\SQLite\Driver as PdoSQLiteDriver;
13+
use Doctrine\DBAL\Driver\PDO\SQLSrv\Driver as PdoSqlSrvDriver;
14+
use Doctrine\DBAL\Driver\PgSQL\Driver as PgSQLDriver;
15+
use Doctrine\DBAL\Driver\SQLite3\Driver as SQLite3Driver;
16+
use Doctrine\DBAL\Driver\SQLSrv\Driver as SqlSrvDriver;
17+
use mysqli;
18+
use PDO;
19+
use SQLite3;
20+
use Throwable;
21+
use function get_resource_type;
22+
use function is_resource;
23+
use function method_exists;
24+
use function strpos;
25+
26+
class DriverDetector
27+
{
28+
29+
public const IBM_DB2 = 'ibm_db2';
30+
public const MYSQLI = 'mysqli';
31+
public const OCI8 = 'oci8';
32+
public const PDO_MYSQL = 'pdo_mysql';
33+
public const PDO_OCI = 'pdo_oci';
34+
public const PDO_PGSQL = 'pdo_pgsql';
35+
public const PDO_SQLITE = 'pdo_sqlite';
36+
public const PDO_SQLSRV = 'pdo_sqlsrv';
37+
public const PGSQL = 'pgsql';
38+
public const SQLITE3 = 'sqlite3';
39+
public const SQLSRV = 'sqlsrv';
40+
41+
/** @var bool */
42+
private $failOnInvalidConnection;
43+
44+
public function __construct(bool $failOnInvalidConnection)
45+
{
46+
$this->failOnInvalidConnection = $failOnInvalidConnection;
47+
}
48+
49+
/**
50+
* @return self::*|null
51+
*/
52+
public function detect(Connection $connection): ?string
53+
{
54+
$driver = $connection->getDriver();
55+
56+
if ($driver instanceof MysqliDriver) {
57+
return self::MYSQLI;
58+
}
59+
60+
if ($driver instanceof PdoMysqlDriver) {
61+
return self::PDO_MYSQL;
62+
}
63+
64+
if ($driver instanceof PdoSQLiteDriver) {
65+
return self::PDO_SQLITE;
66+
}
67+
68+
if ($driver instanceof PdoSqlSrvDriver) {
69+
return self::PDO_SQLSRV;
70+
}
71+
72+
if ($driver instanceof PdoOciDriver) {
73+
return self::PDO_OCI;
74+
}
75+
76+
if ($driver instanceof PdoPgSQLDriver) {
77+
return self::PDO_PGSQL;
78+
}
79+
80+
if ($driver instanceof SQLite3Driver) {
81+
return self::SQLITE3;
82+
}
83+
84+
if ($driver instanceof PgSQLDriver) {
85+
return self::PGSQL;
86+
}
87+
88+
if ($driver instanceof SqlSrvDriver) {
89+
return self::SQLSRV;
90+
}
91+
92+
if ($driver instanceof Oci8Driver) {
93+
return self::OCI8;
94+
}
95+
96+
if ($driver instanceof IbmDb2Driver) {
97+
return self::IBM_DB2;
98+
}
99+
100+
// fallback to connection-based detection when driver is wrapped by middleware
101+
102+
if (!method_exists($connection, 'getNativeConnection')) {
103+
return null; // dbal < 3.3 (released in 2022-01)
104+
}
105+
106+
try {
107+
$nativeConnection = $connection->getNativeConnection();
108+
} catch (Throwable $e) {
109+
if ($this->failOnInvalidConnection) {
110+
throw $e;
111+
}
112+
return null; // connection cannot be established
113+
}
114+
115+
if ($nativeConnection instanceof mysqli) {
116+
return self::MYSQLI;
117+
}
118+
119+
if ($nativeConnection instanceof SQLite3) {
120+
return self::SQLITE3;
121+
}
122+
123+
if ($nativeConnection instanceof \PgSql\Connection) {
124+
return self::PGSQL;
125+
}
126+
127+
if ($nativeConnection instanceof PDO) {
128+
$driverName = $nativeConnection->getAttribute(PDO::ATTR_DRIVER_NAME);
129+
130+
if ($driverName === 'mysql') {
131+
return self::PDO_MYSQL;
132+
}
133+
134+
if ($driverName === 'sqlite') {
135+
return self::PDO_SQLITE;
136+
}
137+
138+
if ($driverName === 'pgsql') {
139+
return self::PDO_PGSQL;
140+
}
141+
142+
if ($driverName === 'oci') { // semi-verified (https://stackoverflow.com/questions/10090709/get-current-pdo-driver-from-existing-connection/10090754#comment12923198_10090754)
143+
return self::PDO_OCI;
144+
}
145+
146+
if ($driverName === 'sqlsrv') {
147+
return self::PDO_SQLSRV;
148+
}
149+
}
150+
151+
if (is_resource($nativeConnection)) {
152+
$resourceType = get_resource_type($nativeConnection);
153+
154+
if (strpos($resourceType, 'oci') !== false) { // not verified
155+
return self::OCI8;
156+
}
157+
158+
if (strpos($resourceType, 'db2') !== false) { // not verified
159+
return self::IBM_DB2;
160+
}
161+
162+
if (strpos($resourceType, 'SQL Server Connection') !== false) {
163+
return self::SQLSRV;
164+
}
165+
166+
if (strpos($resourceType, 'pgsql link') !== false) {
167+
return self::PGSQL;
168+
}
169+
}
170+
171+
return null;
172+
}
173+
174+
}

src/Type/Doctrine/Descriptors/BooleanType.php

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,24 @@
22

33
namespace PHPStan\Type\Doctrine\Descriptors;
44

5+
use Doctrine\DBAL\Connection;
6+
use PHPStan\Doctrine\Driver\DriverDetector;
57
use PHPStan\Type\Constant\ConstantIntegerType;
68
use PHPStan\Type\Type;
79
use PHPStan\Type\TypeCombinator;
10+
use function in_array;
811

9-
class BooleanType implements DoctrineTypeDescriptor
12+
class BooleanType implements DoctrineTypeDescriptor, DoctrineTypeDriverAwareDescriptor
1013
{
1114

15+
/** @var DriverDetector */
16+
private $driverDetector;
17+
18+
public function __construct(DriverDetector $driverDetector)
19+
{
20+
$this->driverDetector = $driverDetector;
21+
}
22+
1223
public function getType(): string
1324
{
1425
return \Doctrine\DBAL\Types\BooleanType::class;
@@ -33,4 +44,28 @@ public function getDatabaseInternalType(): Type
3344
);
3445
}
3546

47+
public function getDatabaseInternalTypeForDriver(Connection $connection): Type
48+
{
49+
$driverType = $this->driverDetector->detect($connection);
50+
51+
if ($driverType === DriverDetector::PGSQL || $driverType === DriverDetector::PDO_PGSQL) {
52+
return new \PHPStan\Type\BooleanType();
53+
}
54+
55+
if (in_array($driverType, [
56+
DriverDetector::SQLITE3,
57+
DriverDetector::PDO_SQLITE,
58+
DriverDetector::MYSQLI,
59+
DriverDetector::PDO_MYSQL,
60+
], true)) {
61+
return TypeCombinator::union(
62+
new ConstantIntegerType(0),
63+
new ConstantIntegerType(1)
64+
);
65+
}
66+
67+
// not yet supported driver, return the old implementation guess
68+
return $this->getDatabaseInternalType();
69+
}
70+
3671
}

src/Type/Doctrine/Descriptors/DecimalType.php

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,28 @@
22

33
namespace PHPStan\Type\Doctrine\Descriptors;
44

5+
use Doctrine\DBAL\Connection;
6+
use PHPStan\Doctrine\Driver\DriverDetector;
57
use PHPStan\Type\Accessory\AccessoryNumericStringType;
68
use PHPStan\Type\FloatType;
79
use PHPStan\Type\IntegerType;
10+
use PHPStan\Type\IntersectionType;
811
use PHPStan\Type\StringType;
912
use PHPStan\Type\Type;
1013
use PHPStan\Type\TypeCombinator;
14+
use function in_array;
1115

12-
class DecimalType implements DoctrineTypeDescriptor
16+
class DecimalType implements DoctrineTypeDescriptor, DoctrineTypeDriverAwareDescriptor
1317
{
1418

19+
/** @var DriverDetector */
20+
private $driverDetector;
21+
22+
public function __construct(DriverDetector $driverDetector)
23+
{
24+
$this->driverDetector = $driverDetector;
25+
}
26+
1527
public function getType(): string
1628
{
1729
return \Doctrine\DBAL\Types\DecimalType::class;
@@ -32,4 +44,28 @@ public function getDatabaseInternalType(): Type
3244
return TypeCombinator::union(new FloatType(), new IntegerType());
3345
}
3446

47+
public function getDatabaseInternalTypeForDriver(Connection $connection): Type
48+
{
49+
$driverType = $this->driverDetector->detect($connection);
50+
51+
if ($driverType === DriverDetector::SQLITE3 || $driverType === DriverDetector::PDO_SQLITE) {
52+
return TypeCombinator::union(new FloatType(), new IntegerType());
53+
}
54+
55+
if (in_array($driverType, [
56+
DriverDetector::MYSQLI,
57+
DriverDetector::PDO_MYSQL,
58+
DriverDetector::PGSQL,
59+
DriverDetector::PDO_PGSQL,
60+
], true)) {
61+
return new IntersectionType([
62+
new StringType(),
63+
new AccessoryNumericStringType(),
64+
]);
65+
}
66+
67+
// not yet supported driver, return the old implementation guess
68+
return $this->getDatabaseInternalType();
69+
}
70+
3571
}

src/Type/Doctrine/Descriptors/DoctrineTypeDescriptor.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,23 @@ interface DoctrineTypeDescriptor
1313
*/
1414
public function getType(): string;
1515

16+
/**
17+
* This is used for inferring direct column results, e.g. SELECT e.field
18+
* It should comply with convertToPHPValue return value
19+
*/
1620
public function getWritableToPropertyType(): Type;
1721

1822
public function getWritableToDatabaseType(): Type;
1923

24+
/**
25+
* This is used for inferring how database fetches column of such type
26+
*
27+
* This is not used for direct column type inferring,
28+
* but when such column appears in expression like SELECT MAX(e.field)
29+
*
30+
* Sometimes, the type cannot be reliably decided without driver context,
31+
* use DoctrineTypeDriverAwareDescriptor in such cases
32+
*/
2033
public function getDatabaseInternalType(): Type;
2134

2235
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Doctrine\Descriptors;
4+
5+
use Doctrine\DBAL\Connection;
6+
use PHPStan\Type\Type;
7+
8+
/** @api */
9+
interface DoctrineTypeDriverAwareDescriptor
10+
{
11+
12+
/**
13+
* This is used for inferring how database fetches column of such type
14+
* It should return the native type without stringification that may occur on certain PHP versions or driver configuration
15+
*
16+
* This is not used for direct column type inferring,
17+
* but when such column appears in expression like SELECT MAX(e.field)
18+
*
19+
* See: https://github.com/janedbal/php-database-drivers-fetch-test
20+
*
21+
* mysql sqlite pdo_pgsql pgsql
22+
* - decimal: string int|float string string
23+
* - float: float float string float
24+
* - bigint: int int int int
25+
* - bool: int int bool bool
26+
*/
27+
public function getDatabaseInternalTypeForDriver(Connection $connection): Type;
28+
29+
}

0 commit comments

Comments
 (0)