Skip to content

Introduce DoctrineTypeDriverAwareDescriptor & DriverDetector #578

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

Merged
merged 3 commits into from
Jun 25, 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
4 changes: 4 additions & 0 deletions extension.neon
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ services:
class: PHPStan\Type\Doctrine\DefaultDescriptorRegistry
factory: @PHPStan\Type\Doctrine\DescriptorRegistryFactory::createRegistry

-
class: PHPStan\Doctrine\Driver\DriverDetector
arguments:
failOnInvalidConnection: %featureToggles.bleedingEdge%
-
class: PHPStan\Reflection\Doctrine\DoctrineSelectableClassReflectionExtension
-
Expand Down
11 changes: 11 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,14 @@ parameters:
- '#^Cannot call method getWrappedResourceHandle\(\) on class\-string\|object\.$#'
path: tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php
reportUnmatched: false

-
message: '#^Call to function method_exists\(\) with Doctrine\\DBAL\\Connection and ''getNativeConnection'' will always evaluate to true\.$#' # needed for older DBAL versions
paths:
- src/Doctrine/Driver/DriverDetector.php

-
messages: # needed for older DBAL versions
- '#^Class PgSql\\Connection not found\.$#'
- '#^Class Doctrine\\DBAL\\Driver\\PgSQL\\Driver not found\.$#'
- '#^Class Doctrine\\DBAL\\Driver\\SQLite3\\Driver not found\.$#'
174 changes: 174 additions & 0 deletions src/Doctrine/Driver/DriverDetector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
<?php declare(strict_types = 1);

namespace PHPStan\Doctrine\Driver;

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Driver\IBMDB2\Driver as IbmDb2Driver;
use Doctrine\DBAL\Driver\Mysqli\Driver as MysqliDriver;
use Doctrine\DBAL\Driver\OCI8\Driver as Oci8Driver;
use Doctrine\DBAL\Driver\PDO\MySQL\Driver as PdoMysqlDriver;
use Doctrine\DBAL\Driver\PDO\OCI\Driver as PdoOciDriver;
use Doctrine\DBAL\Driver\PDO\PgSQL\Driver as PdoPgSQLDriver;
use Doctrine\DBAL\Driver\PDO\SQLite\Driver as PdoSQLiteDriver;
use Doctrine\DBAL\Driver\PDO\SQLSrv\Driver as PdoSqlSrvDriver;
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;

class DriverDetector
{

public const IBM_DB2 = 'ibm_db2';
public const MYSQLI = 'mysqli';
public const OCI8 = 'oci8';
public const PDO_MYSQL = 'pdo_mysql';
public const PDO_OCI = 'pdo_oci';
public const PDO_PGSQL = 'pdo_pgsql';
public const PDO_SQLITE = 'pdo_sqlite';
public const PDO_SQLSRV = 'pdo_sqlsrv';
public const PGSQL = 'pgsql';
public const SQLITE3 = 'sqlite3';
public const SQLSRV = 'sqlsrv';

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

public function __construct(bool $failOnInvalidConnection)
{
$this->failOnInvalidConnection = $failOnInvalidConnection;
}

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

if ($driver instanceof MysqliDriver) {
return self::MYSQLI;
}

if ($driver instanceof PdoMysqlDriver) {
return self::PDO_MYSQL;
}

if ($driver instanceof PdoSQLiteDriver) {
return self::PDO_SQLITE;
}

if ($driver instanceof PdoSqlSrvDriver) {
return self::PDO_SQLSRV;
}

if ($driver instanceof PdoOciDriver) {
return self::PDO_OCI;
}

if ($driver instanceof PdoPgSQLDriver) {
return self::PDO_PGSQL;
}

if ($driver instanceof SQLite3Driver) {
return self::SQLITE3;
}

if ($driver instanceof PgSQLDriver) {
return self::PGSQL;
}

if ($driver instanceof SqlSrvDriver) {
return self::SQLSRV;
}

if ($driver instanceof Oci8Driver) {
return self::OCI8;
}

if ($driver instanceof IbmDb2Driver) {
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;
}

if ($driverName === 'sqlsrv') {
return self::PDO_SQLSRV;
}
}

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;
}
}

return null;
}

}
37 changes: 36 additions & 1 deletion src/Type/Doctrine/Descriptors/BooleanType.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,24 @@

namespace PHPStan\Type\Doctrine\Descriptors;

use Doctrine\DBAL\Connection;
use PHPStan\Doctrine\Driver\DriverDetector;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function in_array;

class BooleanType implements DoctrineTypeDescriptor
class BooleanType implements DoctrineTypeDescriptor, DoctrineTypeDriverAwareDescriptor
{

/** @var DriverDetector */
private $driverDetector;

public function __construct(DriverDetector $driverDetector)
{
$this->driverDetector = $driverDetector;
}

public function getType(): string
{
return \Doctrine\DBAL\Types\BooleanType::class;
Expand All @@ -33,4 +44,28 @@ public function getDatabaseInternalType(): Type
);
}

public function getDatabaseInternalTypeForDriver(Connection $connection): Type
{
$driverType = $this->driverDetector->detect($connection);

if ($driverType === DriverDetector::PGSQL || $driverType === DriverDetector::PDO_PGSQL) {
return new \PHPStan\Type\BooleanType();
}

if (in_array($driverType, [
DriverDetector::SQLITE3,
DriverDetector::PDO_SQLITE,
DriverDetector::MYSQLI,
DriverDetector::PDO_MYSQL,
], true)) {
return TypeCombinator::union(
new ConstantIntegerType(0),
new ConstantIntegerType(1)
);
}

// not yet supported driver, return the old implementation guess
return $this->getDatabaseInternalType();
}

}
38 changes: 37 additions & 1 deletion src/Type/Doctrine/Descriptors/DecimalType.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,28 @@

namespace PHPStan\Type\Doctrine\Descriptors;

use Doctrine\DBAL\Connection;
use PHPStan\Doctrine\Driver\DriverDetector;
use PHPStan\Type\Accessory\AccessoryNumericStringType;
use PHPStan\Type\FloatType;
use PHPStan\Type\IntegerType;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function in_array;

class DecimalType implements DoctrineTypeDescriptor
class DecimalType implements DoctrineTypeDescriptor, DoctrineTypeDriverAwareDescriptor
{

/** @var DriverDetector */
private $driverDetector;

public function __construct(DriverDetector $driverDetector)
{
$this->driverDetector = $driverDetector;
}

public function getType(): string
{
return \Doctrine\DBAL\Types\DecimalType::class;
Expand All @@ -32,4 +44,28 @@ public function getDatabaseInternalType(): Type
return TypeCombinator::union(new FloatType(), new IntegerType());
}

public function getDatabaseInternalTypeForDriver(Connection $connection): Type
{
$driverType = $this->driverDetector->detect($connection);

if ($driverType === DriverDetector::SQLITE3 || $driverType === DriverDetector::PDO_SQLITE) {
return TypeCombinator::union(new FloatType(), new IntegerType());
}

if (in_array($driverType, [
DriverDetector::MYSQLI,
DriverDetector::PDO_MYSQL,
DriverDetector::PGSQL,
DriverDetector::PDO_PGSQL,
], true)) {
return new IntersectionType([
new StringType(),
new AccessoryNumericStringType(),
]);
}

// not yet supported driver, return the old implementation guess
return $this->getDatabaseInternalType();
}

}
13 changes: 13 additions & 0 deletions src/Type/Doctrine/Descriptors/DoctrineTypeDescriptor.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,23 @@ interface DoctrineTypeDescriptor
*/
public function getType(): string;

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

public function getWritableToDatabaseType(): Type;

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

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Doctrine\Descriptors;

use Doctrine\DBAL\Connection;
use PHPStan\Type\Type;

/** @api */
interface DoctrineTypeDriverAwareDescriptor
{

/**
* This is used for inferring how database fetches column of such type
* It should return the native type without stringification that may occur on certain PHP versions or driver configuration
*
* This is not used for direct column type inferring,
* but when such column appears in expression like SELECT MAX(e.field)
*
* See: https://github.com/janedbal/php-database-drivers-fetch-test
*
* mysql sqlite pdo_pgsql pgsql
* - decimal: string int|float string string
* - float: float float string float
* - bigint: int int int int
* - bool: int int bool bool
*/
public function getDatabaseInternalTypeForDriver(Connection $connection): Type;

}
Loading