Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avoid real connection for type inference #586

Merged
merged 5 commits into from
Jun 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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