Skip to content

Commit afb40db

Browse files
authored
Avoid real connection for type inference
1 parent 5745ea6 commit afb40db

File tree

7 files changed

+118
-294
lines changed

7 files changed

+118
-294
lines changed

README.md

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,9 +132,6 @@ Most DQL features are supported, including `GROUP BY`, `DISTINCT`, all flavors o
132132

133133
Whether e.g. `SUM(e.column)` is fetched as `float`, `numeric-string` or `int` highly [depends on drivers, their setup and PHP version](https://github.com/janedbal/php-database-drivers-fetch-test).
134134
This extension autodetects your setup and provides quite accurate results for `pdo_mysql`, `mysqli`, `pdo_sqlite`, `sqlite3`, `pdo_pgsql` and `pgsql`.
135-
Sadly, this autodetection often needs real database connection, so in order to utilize precise types, your `objectManagerLoader` need to be able to connect to real database.
136-
137-
If you are using `bleedingEdge`, the connection failure is propagated. If not, it will be silently ignored and the type will be `mixed` or an union of possible types.
138135

139136
### Supported methods
140137

extension.neon

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,6 @@ services:
9191

9292
-
9393
class: PHPStan\Doctrine\Driver\DriverDetector
94-
arguments:
95-
failOnInvalidConnection: %featureToggles.bleedingEdge%
9694
-
9795
class: PHPStan\Reflection\Doctrine\DoctrineSelectableClassReflectionExtension
9896
-

src/Doctrine/Driver/DriverDetector.php

Lines changed: 63 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,8 @@
1414
use Doctrine\DBAL\Driver\PgSQL\Driver as PgSQLDriver;
1515
use Doctrine\DBAL\Driver\SQLite3\Driver as SQLite3Driver;
1616
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;
17+
use function get_class;
18+
use function is_a;
2519

2620
class DriverDetector
2721
{
@@ -38,139 +32,114 @@ class DriverDetector
3832
public const SQLITE3 = 'sqlite3';
3933
public const SQLSRV = 'sqlsrv';
4034

41-
/** @var bool */
42-
private $failOnInvalidConnection;
43-
44-
public function __construct(bool $failOnInvalidConnection)
35+
/**
36+
* @return self::*|null
37+
*/
38+
public function detect(Connection $connection): ?string
4539
{
46-
$this->failOnInvalidConnection = $failOnInvalidConnection;
40+
$driver = $connection->getDriver();
41+
42+
return $this->deduceFromDriverClass(get_class($driver)) ?? $this->deduceFromParams($connection);
4743
}
4844

49-
public function failsOnInvalidConnection(): bool
45+
/**
46+
* @return array<mixed>
47+
*/
48+
public function detectDriverOptions(Connection $connection): array
5049
{
51-
return $this->failOnInvalidConnection;
50+
return $connection->getParams()['driverOptions'] ?? [];
5251
}
5352

5453
/**
5554
* @return self::*|null
5655
*/
57-
public function detect(Connection $connection): ?string
56+
private function deduceFromDriverClass(string $driverClass): ?string
5857
{
59-
$driver = $connection->getDriver();
60-
61-
if ($driver instanceof MysqliDriver) {
58+
if (is_a($driverClass, MysqliDriver::class, true)) {
6259
return self::MYSQLI;
6360
}
6461

65-
if ($driver instanceof PdoMysqlDriver) {
62+
if (is_a($driverClass, PdoMysqlDriver::class, true)) {
6663
return self::PDO_MYSQL;
6764
}
6865

69-
if ($driver instanceof PdoSQLiteDriver) {
66+
if (is_a($driverClass, PdoSQLiteDriver::class, true)) {
7067
return self::PDO_SQLITE;
7168
}
7269

73-
if ($driver instanceof PdoSqlSrvDriver) {
70+
if (is_a($driverClass, PdoSqlSrvDriver::class, true)) {
7471
return self::PDO_SQLSRV;
7572
}
7673

77-
if ($driver instanceof PdoOciDriver) {
74+
if (is_a($driverClass, PdoOciDriver::class, true)) {
7875
return self::PDO_OCI;
7976
}
8077

81-
if ($driver instanceof PdoPgSQLDriver) {
78+
if (is_a($driverClass, PdoPgSQLDriver::class, true)) {
8279
return self::PDO_PGSQL;
8380
}
8481

85-
if ($driver instanceof SQLite3Driver) {
82+
if (is_a($driverClass, SQLite3Driver::class, true)) {
8683
return self::SQLITE3;
8784
}
8885

89-
if ($driver instanceof PgSQLDriver) {
86+
if (is_a($driverClass, PgSQLDriver::class, true)) {
9087
return self::PGSQL;
9188
}
9289

93-
if ($driver instanceof SqlSrvDriver) {
90+
if (is_a($driverClass, SqlSrvDriver::class, true)) {
9491
return self::SQLSRV;
9592
}
9693

97-
if ($driver instanceof Oci8Driver) {
94+
if (is_a($driverClass, Oci8Driver::class, true)) {
9895
return self::OCI8;
9996
}
10097

101-
if ($driver instanceof IbmDb2Driver) {
98+
if (is_a($driverClass, IbmDb2Driver::class, true)) {
10299
return self::IBM_DB2;
103100
}
104101

105-
// fallback to connection-based detection when driver is wrapped by middleware
106-
107-
if (!method_exists($connection, 'getNativeConnection')) {
108-
return null; // dbal < 3.3 (released in 2022-01)
109-
}
110-
111-
try {
112-
$nativeConnection = $connection->getNativeConnection();
113-
} catch (Throwable $e) {
114-
if ($this->failOnInvalidConnection) {
115-
throw $e;
116-
}
117-
return null; // connection cannot be established
118-
}
119-
120-
if ($nativeConnection instanceof mysqli) {
121-
return self::MYSQLI;
122-
}
123-
124-
if ($nativeConnection instanceof SQLite3) {
125-
return self::SQLITE3;
126-
}
127-
128-
if ($nativeConnection instanceof \PgSql\Connection) {
129-
return self::PGSQL;
130-
}
131-
132-
if ($nativeConnection instanceof PDO) {
133-
$driverName = $nativeConnection->getAttribute(PDO::ATTR_DRIVER_NAME);
134-
135-
if ($driverName === 'mysql') {
136-
return self::PDO_MYSQL;
137-
}
138-
139-
if ($driverName === 'sqlite') {
140-
return self::PDO_SQLITE;
141-
}
142-
143-
if ($driverName === 'pgsql') {
144-
return self::PDO_PGSQL;
145-
}
146-
147-
if ($driverName === 'oci') { // semi-verified (https://stackoverflow.com/questions/10090709/get-current-pdo-driver-from-existing-connection/10090754#comment12923198_10090754)
148-
return self::PDO_OCI;
149-
}
102+
return null;
103+
}
150104

151-
if ($driverName === 'sqlsrv') {
152-
return self::PDO_SQLSRV;
105+
/**
106+
* @return self::*|null
107+
*/
108+
private function deduceFromParams(Connection $connection): ?string
109+
{
110+
$params = $connection->getParams();
111+
112+
if (isset($params['driver'])) {
113+
switch ($params['driver']) {
114+
case 'pdo_mysql':
115+
return self::PDO_MYSQL;
116+
case 'pdo_sqlite':
117+
return self::PDO_SQLITE;
118+
case 'pdo_pgsql':
119+
return self::PDO_PGSQL;
120+
case 'pdo_oci':
121+
return self::PDO_OCI;
122+
case 'oci8':
123+
return self::OCI8;
124+
case 'ibm_db2':
125+
return self::IBM_DB2;
126+
case 'pdo_sqlsrv':
127+
return self::PDO_SQLSRV;
128+
case 'mysqli':
129+
return self::MYSQLI;
130+
case 'pgsql': // @phpstan-ignore-line never matches on PHP 7.3- with old dbal
131+
return self::PGSQL;
132+
case 'sqlsrv':
133+
return self::SQLSRV;
134+
case 'sqlite3': // @phpstan-ignore-line never matches on PHP 7.3- with old dbal
135+
return self::SQLITE3;
136+
default:
137+
return null;
153138
}
154139
}
155140

156-
if (is_resource($nativeConnection)) {
157-
$resourceType = get_resource_type($nativeConnection);
158-
159-
if (strpos($resourceType, 'oci') !== false) { // not verified
160-
return self::OCI8;
161-
}
162-
163-
if (strpos($resourceType, 'db2') !== false) { // not verified
164-
return self::IBM_DB2;
165-
}
166-
167-
if (strpos($resourceType, 'SQL Server Connection') !== false) {
168-
return self::SQLSRV;
169-
}
170-
171-
if (strpos($resourceType, 'pgsql link') !== false) {
172-
return self::PGSQL;
173-
}
141+
if (isset($params['driverClass'])) {
142+
return $this->deduceFromDriverClass($params['driverClass']);
174143
}
175144

176145
return null;

src/Type/Doctrine/Query/QueryResultTypeWalker.php

Lines changed: 9 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
use Doctrine\ORM\Query\ParserResult;
1515
use Doctrine\ORM\Query\SqlWalker;
1616
use PDO;
17-
use PDOException;
1817
use PHPStan\Doctrine\Driver\DriverDetector;
1918
use PHPStan\Php\PhpVersion;
2019
use PHPStan\ShouldNotHappenException;
@@ -41,7 +40,6 @@
4140
use PHPStan\Type\TypeCombinator;
4241
use PHPStan\Type\TypeTraverser;
4342
use PHPStan\Type\UnionType;
44-
use Throwable;
4543
use function array_key_exists;
4644
use function array_map;
4745
use function array_values;
@@ -55,7 +53,6 @@
5553
use function is_numeric;
5654
use function is_object;
5755
use function is_string;
58-
use function method_exists;
5956
use function serialize;
6057
use function sprintf;
6158
use function stripos;
@@ -108,6 +105,9 @@ class QueryResultTypeWalker extends SqlWalker
108105
/** @var DriverDetector::*|null */
109106
private $driverType;
110107

108+
/** @var array<mixed> */
109+
private $driverOptions;
110+
111111
/**
112112
* Map of all components/classes that appear in the DQL query.
113113
*
@@ -130,8 +130,6 @@ class QueryResultTypeWalker extends SqlWalker
130130
/** @var bool */
131131
private $hasGroupByClause;
132132

133-
/** @var bool */
134-
private $failOnInvalidConnection;
135133

136134
/**
137135
* @param Query<mixed> $query
@@ -224,8 +222,10 @@ public function __construct($query, $parserResult, array $queryComponents)
224222
is_object($driverDetector) ? get_class($driverDetector) : gettype($driverDetector)
225223
));
226224
}
227-
$this->driverType = $driverDetector->detect($this->em->getConnection());
228-
$this->failOnInvalidConnection = $driverDetector->failsOnInvalidConnection();
225+
$connection = $this->em->getConnection();
226+
227+
$this->driverType = $driverDetector->detect($connection);
228+
$this->driverOptions = $driverDetector->detectDriverOptions($connection);
229229

230230
parent::__construct($query, $parserResult, $queryComponents);
231231
}
@@ -2042,20 +2042,10 @@ private function hasAggregateWithoutGroupBy(): bool
20422042
private function shouldStringifyExpressions(Type $type): TrinaryLogic
20432043
{
20442044
if (in_array($this->driverType, [DriverDetector::PDO_MYSQL, DriverDetector::PDO_PGSQL, DriverDetector::PDO_SQLITE], true)) {
2045-
try {
2046-
$nativeConnection = $this->getNativeConnection();
2047-
assert($nativeConnection instanceof PDO);
2048-
} catch (Throwable $e) { // connection cannot be established
2049-
if ($this->failOnInvalidConnection) {
2050-
throw $e;
2051-
}
2052-
return TrinaryLogic::createMaybe();
2053-
}
2054-
2055-
$stringifyFetches = $this->isPdoStringifyEnabled($nativeConnection);
2045+
$stringifyFetches = isset($this->driverOptions[PDO::ATTR_STRINGIFY_FETCHES]) ? (bool) $this->driverOptions[PDO::ATTR_STRINGIFY_FETCHES] : false;
20562046

20572047
if ($this->driverType === DriverDetector::PDO_MYSQL) {
2058-
$emulatedPrepares = $this->isPdoEmulatePreparesEnabled($nativeConnection);
2048+
$emulatedPrepares = isset($this->driverOptions[PDO::ATTR_EMULATE_PREPARES]) ? (bool) $this->driverOptions[PDO::ATTR_EMULATE_PREPARES] : true;
20592049

20602050
if ($stringifyFetches) {
20612051
return TrinaryLogic::createYes();
@@ -2105,49 +2095,6 @@ private function shouldStringifyExpressions(Type $type): TrinaryLogic
21052095
return TrinaryLogic::createMaybe();
21062096
}
21072097

2108-
private function isPdoStringifyEnabled(PDO $pdo): bool
2109-
{
2110-
// this fails for most PHP versions, see https://github.com/php/php-src/issues/12969
2111-
// working since 8.2.15 and 8.3.2
2112-
try {
2113-
return (bool) $pdo->getAttribute(PDO::ATTR_STRINGIFY_FETCHES);
2114-
} catch (PDOException $e) {
2115-
$selectOne = $pdo->query('SELECT 1');
2116-
if ($selectOne === false) {
2117-
return false; // this should not happen, just return attribute default value
2118-
}
2119-
$one = $selectOne->fetchColumn();
2120-
2121-
// string can be returned due to old PHP used or because ATTR_STRINGIFY_FETCHES is enabled,
2122-
// but it should not matter as it behaves the same way
2123-
// (the attribute is there to maintain BC)
2124-
return is_string($one);
2125-
}
2126-
}
2127-
2128-
private function isPdoEmulatePreparesEnabled(PDO $pdo): bool
2129-
{
2130-
return (bool) $pdo->getAttribute(PDO::ATTR_EMULATE_PREPARES);
2131-
}
2132-
2133-
/**
2134-
* @return object|resource|null
2135-
*/
2136-
private function getNativeConnection()
2137-
{
2138-
$connection = $this->em->getConnection();
2139-
2140-
if (method_exists($connection, 'getNativeConnection')) {
2141-
return $connection->getNativeConnection();
2142-
}
2143-
2144-
if ($connection->getWrappedConnection() instanceof PDO) {
2145-
return $connection->getWrappedConnection();
2146-
}
2147-
2148-
return null;
2149-
}
2150-
21512098
private function isSupportedDriver(): bool
21522099
{
21532100
return in_array($this->driverType, [

0 commit comments

Comments
 (0)