Skip to content

Commit 7d9d2dc

Browse files
committed
Introduce DoctrineTypeDriverAwareDescriptor
1 parent b09104d commit 7d9d2dc

File tree

9 files changed

+331
-4
lines changed

9 files changed

+331
-4
lines changed

extension.neon

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,8 @@ services:
317317
-
318318
class: PHPStan\Type\Doctrine\Descriptors\BooleanType
319319
tags: [phpstan.doctrine.typeDescriptor]
320+
arguments:
321+
bleedingEdge: %featureToggles.bleedingEdge%
320322
-
321323
class: PHPStan\Type\Doctrine\Descriptors\DateImmutableType
322324
tags: [phpstan.doctrine.typeDescriptor]
@@ -341,9 +343,13 @@ services:
341343
-
342344
class: PHPStan\Type\Doctrine\Descriptors\DecimalType
343345
tags: [phpstan.doctrine.typeDescriptor]
346+
arguments:
347+
bleedingEdge: %featureToggles.bleedingEdge%
344348
-
345349
class: PHPStan\Type\Doctrine\Descriptors\FloatType
346350
tags: [phpstan.doctrine.typeDescriptor]
351+
arguments:
352+
bleedingEdge: %featureToggles.bleedingEdge%
347353
-
348354
class: PHPStan\Type\Doctrine\Descriptors\GuidType
349355
tags: [phpstan.doctrine.typeDescriptor]

phpstan.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,8 @@ 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/DriverType.php

src/Doctrine/Driver/DriverType.php

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
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 DriverType
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+
42+
/**
43+
* @return self::*|null
44+
*/
45+
public static function detect(Connection $connection, bool $failOnInvalidConnection): ?string
46+
{
47+
$driver = $connection->getDriver();
48+
49+
if ($driver instanceof MysqliDriver) {
50+
return self::MYSQLI;
51+
}
52+
53+
if ($driver instanceof PdoMysqlDriver) {
54+
return self::PDO_MYSQL;
55+
}
56+
57+
if ($driver instanceof PdoSQLiteDriver) {
58+
return self::PDO_SQLITE;
59+
}
60+
61+
if ($driver instanceof PdoSqlSrvDriver) {
62+
return self::PDO_SQLSRV;
63+
}
64+
65+
if ($driver instanceof PdoOciDriver) {
66+
return self::PDO_OCI;
67+
}
68+
69+
if ($driver instanceof PdoPgSQLDriver) {
70+
return self::PDO_PGSQL;
71+
}
72+
73+
if ($driver instanceof SQLite3Driver) {
74+
return self::SQLITE3;
75+
}
76+
77+
if ($driver instanceof PgSQLDriver) {
78+
return self::PGSQL;
79+
}
80+
81+
if ($driver instanceof SqlSrvDriver) {
82+
return self::SQLSRV;
83+
}
84+
85+
if ($driver instanceof Oci8Driver) {
86+
return self::OCI8;
87+
}
88+
89+
if ($driver instanceof IbmDb2Driver) {
90+
return self::IBM_DB2;
91+
}
92+
93+
// fallback to connection-based detection when driver is wrapped by middleware
94+
95+
if (!method_exists($connection, 'getNativeConnection')) {
96+
return null; // dbal < 3.3 (released in 2022-01)
97+
}
98+
99+
try {
100+
$nativeConnection = $connection->getNativeConnection();
101+
} catch (Throwable $e) {
102+
if ($failOnInvalidConnection) {
103+
throw $e;
104+
}
105+
return null; // connection cannot be established
106+
}
107+
108+
if ($nativeConnection instanceof mysqli) {
109+
return self::MYSQLI;
110+
}
111+
112+
if ($nativeConnection instanceof SQLite3) {
113+
return self::SQLITE3;
114+
}
115+
116+
if ($nativeConnection instanceof \PgSql\Connection) {
117+
return self::PGSQL;
118+
}
119+
120+
if ($nativeConnection instanceof PDO) {
121+
$driverName = $nativeConnection->getAttribute(PDO::ATTR_DRIVER_NAME);
122+
123+
if ($driverName === 'mysql') {
124+
return self::PDO_MYSQL;
125+
}
126+
127+
if ($driverName === 'sqlite') {
128+
return self::PDO_SQLITE;
129+
}
130+
131+
if ($driverName === 'pgsql') {
132+
return self::PDO_PGSQL;
133+
}
134+
135+
if ($driverName === 'oci') { // semi-verified (https://stackoverflow.com/questions/10090709/get-current-pdo-driver-from-existing-connection/10090754#comment12923198_10090754)
136+
return self::PDO_OCI;
137+
}
138+
139+
if ($driverName === 'sqlsrv') {
140+
return self::PDO_SQLSRV;
141+
}
142+
}
143+
144+
if (is_resource($nativeConnection)) {
145+
$resourceType = get_resource_type($nativeConnection);
146+
147+
if (strpos($resourceType, 'oci') !== false) { // not verified
148+
return self::OCI8;
149+
}
150+
151+
if (strpos($resourceType, 'db2') !== false) { // not verified
152+
return self::IBM_DB2;
153+
}
154+
155+
if (strpos($resourceType, 'SQL Server Connection') !== false) {
156+
return self::SQLSRV;
157+
}
158+
159+
if (strpos($resourceType, 'pgsql link') !== false) {
160+
return self::PGSQL;
161+
}
162+
}
163+
164+
return null;
165+
}
166+
167+
}

src/Type/Doctrine/Descriptors/BooleanType.php

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

33
namespace PHPStan\Type\Doctrine\Descriptors;
44

5+
use Doctrine\DBAL\Connection;
6+
use PHPStan\Doctrine\Driver\DriverType;
57
use PHPStan\Type\Constant\ConstantIntegerType;
68
use PHPStan\Type\Type;
79
use PHPStan\Type\TypeCombinator;
810

9-
class BooleanType implements DoctrineTypeDescriptor
11+
class BooleanType implements DoctrineTypeDescriptor, DoctrineTypeDriverAwareDescriptor
1012
{
1113

14+
/** @var bool */
15+
private $bleedingEdge;
16+
17+
public function __construct(bool $bleedingEdge)
18+
{
19+
$this->bleedingEdge = $bleedingEdge;
20+
}
21+
1222
public function getType(): string
1323
{
1424
return \Doctrine\DBAL\Types\BooleanType::class;
@@ -33,4 +43,28 @@ public function getDatabaseInternalType(): Type
3343
);
3444
}
3545

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

src/Type/Doctrine/Descriptors/DecimalType.php

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

33
namespace PHPStan\Type\Doctrine\Descriptors;
44

5+
use Doctrine\DBAL\Connection;
6+
use PHPStan\Doctrine\Driver\DriverType;
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;
1114

12-
class DecimalType implements DoctrineTypeDescriptor
15+
class DecimalType implements DoctrineTypeDescriptor, DoctrineTypeDriverAwareDescriptor
1316
{
1417

18+
/** @var bool */
19+
private $bleedingEdge;
20+
21+
public function __construct(bool $bleedingEdge)
22+
{
23+
$this->bleedingEdge = $bleedingEdge;
24+
}
25+
1526
public function getType(): string
1627
{
1728
return \Doctrine\DBAL\Types\DecimalType::class;
@@ -32,4 +43,28 @@ public function getDatabaseInternalType(): Type
3243
return TypeCombinator::union(new FloatType(), new IntegerType());
3344
}
3445

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

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)