Skip to content

Commit

Permalink
Autodetect driver setup for precise int/float/bool inference in expre…
Browse files Browse the repository at this point in the history
…ssions (stringified or not)
  • Loading branch information
janedbal committed Jun 26, 2024
1 parent ad91388 commit a565fdb
Show file tree
Hide file tree
Showing 22 changed files with 6,061 additions and 597 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,14 @@ Queries are analyzed statically and do not require a running database server. Th

Most DQL features are supported, including `GROUP BY`, `DISTINCT`, all flavors of `JOIN`, arithmetic expressions, functions, aggregations, `NEW`, etc. Sub queries and `INDEX BY` are not yet supported (infered type will be `mixed`).

### Query type inference of expressions

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

The `getResult` method is supported when called without argument, or with the hydrateMode argument set to `Query::HYDRATE_OBJECT`:
Expand Down
18 changes: 11 additions & 7 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -42,21 +42,25 @@ parameters:
-
message: '#^Call to function method_exists\(\) with ''Doctrine\\\\ORM\\\\EntityManager'' and ''create'' will always evaluate to true\.$#'
path: src/Doctrine/Mapping/ClassMetadataFactory.php
reportUnmatched: false
-
messages:
- '#^Call to function method_exists\(\) with Doctrine\\DBAL\\Connection and ''getNativeConnection'' will always evaluate to true\.$#'
- '#^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/Type/Doctrine/Query/QueryResultTypeWalker.php
- 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\.$#'

-
message: '#^Call to an undefined method Doctrine\\DBAL\\Connection\:\:getWrappedConnection\(\)\.$#' # dropped in DBAL 4
path: src/Type/Doctrine/Query/QueryResultTypeWalker.php

-
messages: # oldest dbal has only getSchemaManager, dbal4 has only createSchemaManager
- '#^Call to function method_exists\(\) with Doctrine\\DBAL\\Connection and ''createSchemaManager'' will always evaluate to true\.$#'
- '#^Call to an undefined method Doctrine\\DBAL\\Connection\:\:getSchemaManager\(\)\.$#'
path: tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php
5 changes: 5 additions & 0 deletions src/Doctrine/Driver/DriverDetector.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ public function __construct(bool $failOnInvalidConnection)
$this->failOnInvalidConnection = $failOnInvalidConnection;
}

public function failsOnInvalidConnection(): bool
{
return $this->failOnInvalidConnection;
}

/**
* @return self::*|null
*/
Expand Down
19 changes: 17 additions & 2 deletions src/Type/Doctrine/CreateQueryDynamicReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
use Doctrine\Persistence\Mapping\MappingException;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Doctrine\Driver\DriverDetector;
use PHPStan\Php\PhpVersion;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\Doctrine\Query\QueryResultTypeBuilder;
Expand All @@ -37,10 +39,23 @@ final class CreateQueryDynamicReturnTypeExtension implements DynamicMethodReturn
/** @var DescriptorRegistry */
private $descriptorRegistry;

public function __construct(ObjectMetadataResolver $objectMetadataResolver, DescriptorRegistry $descriptorRegistry)
/** @var PhpVersion */
private $phpVersion;

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

public function __construct(
ObjectMetadataResolver $objectMetadataResolver,
DescriptorRegistry $descriptorRegistry,
PhpVersion $phpVersion,
DriverDetector $driverDetector
)
{
$this->objectMetadataResolver = $objectMetadataResolver;
$this->descriptorRegistry = $descriptorRegistry;
$this->phpVersion = $phpVersion;
$this->driverDetector = $driverDetector;
}

public function getClass(): string
Expand Down Expand Up @@ -87,7 +102,7 @@ public function getTypeFromMethodCall(

try {
$query = $em->createQuery($queryString);
QueryResultTypeWalker::walk($query, $typeBuilder, $this->descriptorRegistry);
QueryResultTypeWalker::walk($query, $typeBuilder, $this->descriptorRegistry, $this->phpVersion, $this->driverDetector);
} catch (ORMException | DBALException | NewDBALException | CommonException | MappingException | \Doctrine\ORM\Exception\ORMException $e) {
return new QueryType($queryString, null, null);
} catch (AssertionError $e) {
Expand Down
8 changes: 7 additions & 1 deletion src/Type/Doctrine/Descriptors/FloatType.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,13 @@ public function getWritableToDatabaseType(): Type

public function getDatabaseInternalType(): Type
{
return TypeCombinator::union(new \PHPStan\Type\FloatType(), new IntegerType());
return TypeCombinator::union(
new \PHPStan\Type\FloatType(),
new IntersectionType([
new StringType(),
new AccessoryNumericStringType(),
])
);
}

public function getDatabaseInternalTypeForDriver(Connection $connection): Type
Expand Down
19 changes: 17 additions & 2 deletions src/Type/Doctrine/Descriptors/ReflectionDescriptor.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace PHPStan\Type\Doctrine\Descriptors;

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\Type as DbalType;
use PHPStan\DependencyInjection\Container;
Expand All @@ -14,7 +15,7 @@
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;

class ReflectionDescriptor implements DoctrineTypeDescriptor
class ReflectionDescriptor implements DoctrineTypeDescriptor, DoctrineTypeDriverAwareDescriptor
{

/** @var class-string<DbalType> */
Expand Down Expand Up @@ -68,6 +69,16 @@ public function getWritableToDatabaseType(): Type
}

public function getDatabaseInternalType(): Type
{
return $this->doGetDatabaseInternalType(null);
}

public function getDatabaseInternalTypeForDriver(Connection $connection): Type
{
return $this->doGetDatabaseInternalType($connection);
}

private function doGetDatabaseInternalType(?Connection $connection): Type
{
if (!$this->reflectionProvider->hasClass($this->type)) {
return new MixedType();
Expand All @@ -80,7 +91,11 @@ public function getDatabaseInternalType(): Type
try {
// this assumes that if somebody inherits from DecimalType,
// the real database type remains decimal and we can reuse its descriptor
return $registry->getByClassName($dbalTypeParentClass)->getDatabaseInternalType();
$descriptor = $registry->getByClassName($dbalTypeParentClass);

return $descriptor instanceof DoctrineTypeDriverAwareDescriptor && $connection !== null
? $descriptor->getDatabaseInternalTypeForDriver($connection)
: $descriptor->getDatabaseInternalType();

} catch (DescriptorNotRegisteredException $e) {
continue;
Expand Down
31 changes: 31 additions & 0 deletions src/Type/Doctrine/Query/DqlConstantStringType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Doctrine\Query;

use Doctrine\ORM\Query\AST\Literal;
use PHPStan\Type\Constant\ConstantStringType;

class DqlConstantStringType extends ConstantStringType
{

/** @var Literal::* */
private $originLiteralType;

/**
* @param Literal::* $originLiteralType
*/
public function __construct(string $value, int $originLiteralType)
{
parent::__construct($value, false);
$this->originLiteralType = $originLiteralType;
}

/**
* @return Literal::*
*/
public function getOriginLiteralType(): int
{
return $this->originLiteralType;
}

}
Loading

0 comments on commit a565fdb

Please sign in to comment.