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

Autodetect driver setup for precise int/float/bool inference in expressions (stringified or not) #506

Merged
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
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