Skip to content

Commit

Permalink
Avoid real connection for type inference
Browse files Browse the repository at this point in the history
  • Loading branch information
janedbal authored Jun 26, 2024
1 parent 5745ea6 commit afb40db
Show file tree
Hide file tree
Showing 7 changed files with 118 additions and 294 deletions.
3 changes: 0 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,6 @@ Most DQL features are supported, including `GROUP BY`, `DISTINCT`, all flavors o

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).
This extension autodetects your setup and provides quite accurate results for `pdo_mysql`, `mysqli`, `pdo_sqlite`, `sqlite3`, `pdo_pgsql` and `pgsql`.
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.

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.

### Supported methods

Expand Down
2 changes: 0 additions & 2 deletions extension.neon
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,6 @@ services:

-
class: PHPStan\Doctrine\Driver\DriverDetector
arguments:
failOnInvalidConnection: %featureToggles.bleedingEdge%
-
class: PHPStan\Reflection\Doctrine\DoctrineSelectableClassReflectionExtension
-
Expand Down
157 changes: 63 additions & 94 deletions src/Doctrine/Driver/DriverDetector.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,8 @@
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;
use function get_class;
use function is_a;

class DriverDetector
{
Expand All @@ -38,139 +32,114 @@ class DriverDetector
public const SQLITE3 = 'sqlite3';
public const SQLSRV = 'sqlsrv';

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

public function __construct(bool $failOnInvalidConnection)
/**
* @return self::*|null
*/
public function detect(Connection $connection): ?string
{
$this->failOnInvalidConnection = $failOnInvalidConnection;
$driver = $connection->getDriver();

return $this->deduceFromDriverClass(get_class($driver)) ?? $this->deduceFromParams($connection);
}

public function failsOnInvalidConnection(): bool
/**
* @return array<mixed>
*/
public function detectDriverOptions(Connection $connection): array
{
return $this->failOnInvalidConnection;
return $connection->getParams()['driverOptions'] ?? [];
}

/**
* @return self::*|null
*/
public function detect(Connection $connection): ?string
private function deduceFromDriverClass(string $driverClass): ?string
{
$driver = $connection->getDriver();

if ($driver instanceof MysqliDriver) {
if (is_a($driverClass, MysqliDriver::class, true)) {
return self::MYSQLI;
}

if ($driver instanceof PdoMysqlDriver) {
if (is_a($driverClass, PdoMysqlDriver::class, true)) {
return self::PDO_MYSQL;
}

if ($driver instanceof PdoSQLiteDriver) {
if (is_a($driverClass, PdoSQLiteDriver::class, true)) {
return self::PDO_SQLITE;
}

if ($driver instanceof PdoSqlSrvDriver) {
if (is_a($driverClass, PdoSqlSrvDriver::class, true)) {
return self::PDO_SQLSRV;
}

if ($driver instanceof PdoOciDriver) {
if (is_a($driverClass, PdoOciDriver::class, true)) {
return self::PDO_OCI;
}

if ($driver instanceof PdoPgSQLDriver) {
if (is_a($driverClass, PdoPgSQLDriver::class, true)) {
return self::PDO_PGSQL;
}

if ($driver instanceof SQLite3Driver) {
if (is_a($driverClass, SQLite3Driver::class, true)) {
return self::SQLITE3;
}

if ($driver instanceof PgSQLDriver) {
if (is_a($driverClass, PgSQLDriver::class, true)) {
return self::PGSQL;
}

if ($driver instanceof SqlSrvDriver) {
if (is_a($driverClass, SqlSrvDriver::class, true)) {
return self::SQLSRV;
}

if ($driver instanceof Oci8Driver) {
if (is_a($driverClass, Oci8Driver::class, true)) {
return self::OCI8;
}

if ($driver instanceof IbmDb2Driver) {
if (is_a($driverClass, IbmDb2Driver::class, true)) {
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;
}
return null;
}

if ($driverName === 'sqlsrv') {
return self::PDO_SQLSRV;
/**
* @return self::*|null
*/
private function deduceFromParams(Connection $connection): ?string
{
$params = $connection->getParams();

if (isset($params['driver'])) {
switch ($params['driver']) {
case 'pdo_mysql':
return self::PDO_MYSQL;
case 'pdo_sqlite':
return self::PDO_SQLITE;
case 'pdo_pgsql':
return self::PDO_PGSQL;
case 'pdo_oci':
return self::PDO_OCI;
case 'oci8':
return self::OCI8;
case 'ibm_db2':
return self::IBM_DB2;
case 'pdo_sqlsrv':
return self::PDO_SQLSRV;
case 'mysqli':
return self::MYSQLI;
case 'pgsql': // @phpstan-ignore-line never matches on PHP 7.3- with old dbal
return self::PGSQL;
case 'sqlsrv':
return self::SQLSRV;
case 'sqlite3': // @phpstan-ignore-line never matches on PHP 7.3- with old dbal
return self::SQLITE3;
default:
return null;
}
}

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;
}
if (isset($params['driverClass'])) {
return $this->deduceFromDriverClass($params['driverClass']);
}

return null;
Expand Down
71 changes: 9 additions & 62 deletions src/Type/Doctrine/Query/QueryResultTypeWalker.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
use Doctrine\ORM\Query\ParserResult;
use Doctrine\ORM\Query\SqlWalker;
use PDO;
use PDOException;
use PHPStan\Doctrine\Driver\DriverDetector;
use PHPStan\Php\PhpVersion;
use PHPStan\ShouldNotHappenException;
Expand All @@ -41,7 +40,6 @@
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\TypeTraverser;
use PHPStan\Type\UnionType;
use Throwable;
use function array_key_exists;
use function array_map;
use function array_values;
Expand All @@ -55,7 +53,6 @@
use function is_numeric;
use function is_object;
use function is_string;
use function method_exists;
use function serialize;
use function sprintf;
use function stripos;
Expand Down Expand Up @@ -108,6 +105,9 @@ class QueryResultTypeWalker extends SqlWalker
/** @var DriverDetector::*|null */
private $driverType;

/** @var array<mixed> */
private $driverOptions;

/**
* Map of all components/classes that appear in the DQL query.
*
Expand All @@ -130,8 +130,6 @@ class QueryResultTypeWalker extends SqlWalker
/** @var bool */
private $hasGroupByClause;

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

/**
* @param Query<mixed> $query
Expand Down Expand Up @@ -224,8 +222,10 @@ public function __construct($query, $parserResult, array $queryComponents)
is_object($driverDetector) ? get_class($driverDetector) : gettype($driverDetector)
));
}
$this->driverType = $driverDetector->detect($this->em->getConnection());
$this->failOnInvalidConnection = $driverDetector->failsOnInvalidConnection();
$connection = $this->em->getConnection();

$this->driverType = $driverDetector->detect($connection);
$this->driverOptions = $driverDetector->detectDriverOptions($connection);

parent::__construct($query, $parserResult, $queryComponents);
}
Expand Down Expand Up @@ -2042,20 +2042,10 @@ private function hasAggregateWithoutGroupBy(): bool
private function shouldStringifyExpressions(Type $type): TrinaryLogic
{
if (in_array($this->driverType, [DriverDetector::PDO_MYSQL, DriverDetector::PDO_PGSQL, DriverDetector::PDO_SQLITE], true)) {
try {
$nativeConnection = $this->getNativeConnection();
assert($nativeConnection instanceof PDO);
} catch (Throwable $e) { // connection cannot be established
if ($this->failOnInvalidConnection) {
throw $e;
}
return TrinaryLogic::createMaybe();
}

$stringifyFetches = $this->isPdoStringifyEnabled($nativeConnection);
$stringifyFetches = isset($this->driverOptions[PDO::ATTR_STRINGIFY_FETCHES]) ? (bool) $this->driverOptions[PDO::ATTR_STRINGIFY_FETCHES] : false;

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

if ($stringifyFetches) {
return TrinaryLogic::createYes();
Expand Down Expand Up @@ -2105,49 +2095,6 @@ private function shouldStringifyExpressions(Type $type): TrinaryLogic
return TrinaryLogic::createMaybe();
}

private function isPdoStringifyEnabled(PDO $pdo): bool
{
// this fails for most PHP versions, see https://github.com/php/php-src/issues/12969
// working since 8.2.15 and 8.3.2
try {
return (bool) $pdo->getAttribute(PDO::ATTR_STRINGIFY_FETCHES);
} catch (PDOException $e) {
$selectOne = $pdo->query('SELECT 1');
if ($selectOne === false) {
return false; // this should not happen, just return attribute default value
}
$one = $selectOne->fetchColumn();

// string can be returned due to old PHP used or because ATTR_STRINGIFY_FETCHES is enabled,
// but it should not matter as it behaves the same way
// (the attribute is there to maintain BC)
return is_string($one);
}
}

private function isPdoEmulatePreparesEnabled(PDO $pdo): bool
{
return (bool) $pdo->getAttribute(PDO::ATTR_EMULATE_PREPARES);
}

/**
* @return object|resource|null
*/
private function getNativeConnection()
{
$connection = $this->em->getConnection();

if (method_exists($connection, 'getNativeConnection')) {
return $connection->getNativeConnection();
}

if ($connection->getWrappedConnection() instanceof PDO) {
return $connection->getWrappedConnection();
}

return null;
}

private function isSupportedDriver(): bool
{
return in_array($this->driverType, [
Expand Down
Loading

0 comments on commit afb40db

Please sign in to comment.