Skip to content

Commit 157f942

Browse files
committed
QueryResultTypeWalker: precise type inferring
1 parent ad91388 commit 157f942

22 files changed

+5448
-596
lines changed

README.md

+8
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,14 @@ Queries are analyzed statically and do not require a running database server. Th
128128

129129
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`).
130130

131+
### Query type inference of expressions
132+
133+
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).
134+
This extension autodetects your setup and provides quite accurate results for `pdo_mysql`, `mysqli`, `pdo_sqlite`, `sqlite3`, `pdo_pgsql` and `pgsql`.
135+
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.
136+
137+
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.
138+
131139
### Supported methods
132140

133141
The `getResult` method is supported when called without argument, or with the hydrateMode argument set to `Query::HYDRATE_OBJECT`:

phpstan.neon

+11-7
Original file line numberDiff line numberDiff line change
@@ -42,21 +42,25 @@ parameters:
4242
-
4343
message: '#^Call to function method_exists\(\) with ''Doctrine\\\\ORM\\\\EntityManager'' and ''create'' will always evaluate to true\.$#'
4444
path: src/Doctrine/Mapping/ClassMetadataFactory.php
45-
reportUnmatched: false
46-
-
47-
messages:
48-
- '#^Call to function method_exists\(\) with Doctrine\\DBAL\\Connection and ''getNativeConnection'' will always evaluate to true\.$#'
49-
- '#^Cannot call method getWrappedResourceHandle\(\) on class\-string\|object\.$#'
50-
path: tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php
51-
reportUnmatched: false
5245

5346
-
5447
message: '#^Call to function method_exists\(\) with Doctrine\\DBAL\\Connection and ''getNativeConnection'' will always evaluate to true\.$#' # needed for older DBAL versions
5548
paths:
49+
- src/Type/Doctrine/Query/QueryResultTypeWalker.php
5650
- src/Doctrine/Driver/DriverDetector.php
5751

5852
-
5953
messages: # needed for older DBAL versions
6054
- '#^Class PgSql\\Connection not found\.$#'
6155
- '#^Class Doctrine\\DBAL\\Driver\\PgSQL\\Driver not found\.$#'
6256
- '#^Class Doctrine\\DBAL\\Driver\\SQLite3\\Driver not found\.$#'
57+
58+
-
59+
message: '#^Call to an undefined method Doctrine\\DBAL\\Connection\:\:getWrappedConnection\(\)\.$#' # dropped in DBAL 4
60+
path: src/Type/Doctrine/Query/QueryResultTypeWalker.php
61+
62+
-
63+
messages: # oldest dbal has only getSchemaManager, dbal4 has only createSchemaManager
64+
- '#^Call to function method_exists\(\) with Doctrine\\DBAL\\Connection and ''createSchemaManager'' will always evaluate to true\.$#'
65+
- '#^Call to an undefined method Doctrine\\DBAL\\Connection\:\:getSchemaManager\(\)\.$#'
66+
path: tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php

src/Doctrine/Driver/DriverDetector.php

+5
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ public function __construct(bool $failOnInvalidConnection)
4646
$this->failOnInvalidConnection = $failOnInvalidConnection;
4747
}
4848

49+
public function failsOnInvalidConnection(): bool
50+
{
51+
return $this->failOnInvalidConnection;
52+
}
53+
4954
/**
5055
* @return self::*|null
5156
*/

src/Type/Doctrine/CreateQueryDynamicReturnTypeExtension.php

+17-2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
use Doctrine\Persistence\Mapping\MappingException;
1313
use PhpParser\Node\Expr\MethodCall;
1414
use PHPStan\Analyser\Scope;
15+
use PHPStan\Doctrine\Driver\DriverDetector;
16+
use PHPStan\Php\PhpVersion;
1517
use PHPStan\Reflection\MethodReflection;
1618
use PHPStan\Type\Constant\ConstantStringType;
1719
use PHPStan\Type\Doctrine\Query\QueryResultTypeBuilder;
@@ -37,10 +39,23 @@ final class CreateQueryDynamicReturnTypeExtension implements DynamicMethodReturn
3739
/** @var DescriptorRegistry */
3840
private $descriptorRegistry;
3941

40-
public function __construct(ObjectMetadataResolver $objectMetadataResolver, DescriptorRegistry $descriptorRegistry)
42+
/** @var PhpVersion */
43+
private $phpVersion;
44+
45+
/** @var DriverDetector */
46+
private $driverDetector;
47+
48+
public function __construct(
49+
ObjectMetadataResolver $objectMetadataResolver,
50+
DescriptorRegistry $descriptorRegistry,
51+
PhpVersion $phpVersion,
52+
DriverDetector $driverDetector
53+
)
4154
{
4255
$this->objectMetadataResolver = $objectMetadataResolver;
4356
$this->descriptorRegistry = $descriptorRegistry;
57+
$this->phpVersion = $phpVersion;
58+
$this->driverDetector = $driverDetector;
4459
}
4560

4661
public function getClass(): string
@@ -87,7 +102,7 @@ public function getTypeFromMethodCall(
87102

88103
try {
89104
$query = $em->createQuery($queryString);
90-
QueryResultTypeWalker::walk($query, $typeBuilder, $this->descriptorRegistry);
105+
QueryResultTypeWalker::walk($query, $typeBuilder, $this->descriptorRegistry, $this->phpVersion, $this->driverDetector);
91106
} catch (ORMException | DBALException | NewDBALException | CommonException | MappingException | \Doctrine\ORM\Exception\ORMException $e) {
92107
return new QueryType($queryString, null, null);
93108
} catch (AssertionError $e) {

src/Type/Doctrine/Descriptors/FloatType.php

+7-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,13 @@ public function getWritableToDatabaseType(): Type
4040

4141
public function getDatabaseInternalType(): Type
4242
{
43-
return TypeCombinator::union(new \PHPStan\Type\FloatType(), new IntegerType());
43+
return TypeCombinator::union(
44+
new \PHPStan\Type\FloatType(),
45+
new IntersectionType([
46+
new StringType(),
47+
new AccessoryNumericStringType(),
48+
])
49+
);
4450
}
4551

4652
public function getDatabaseInternalTypeForDriver(Connection $connection): Type

src/Type/Doctrine/Descriptors/ReflectionDescriptor.php

+25-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace PHPStan\Type\Doctrine\Descriptors;
44

5+
use Doctrine\DBAL\Connection;
56
use Doctrine\DBAL\Platforms\AbstractPlatform;
67
use Doctrine\DBAL\Types\Type as DbalType;
78
use PHPStan\DependencyInjection\Container;
@@ -14,7 +15,7 @@
1415
use PHPStan\Type\Type;
1516
use PHPStan\Type\TypeCombinator;
1617

17-
class ReflectionDescriptor implements DoctrineTypeDescriptor
18+
class ReflectionDescriptor implements DoctrineTypeDescriptor, DoctrineTypeDriverAwareDescriptor
1819
{
1920

2021
/** @var class-string<DbalType> */
@@ -90,4 +91,27 @@ public function getDatabaseInternalType(): Type
9091
return new MixedType();
9192
}
9293

94+
public function getDatabaseInternalTypeForDriver(Connection $connection): Type
95+
{
96+
$registry = $this->container->getByType(DefaultDescriptorRegistry::class);
97+
$parents = $this->reflectionProvider->getClass($this->type)->getParentClassesNames();
98+
99+
foreach ($parents as $dbalTypeParentClass) {
100+
try {
101+
// this assumes that if somebody inherits from DecimalType,
102+
// the real database type remains decimal and we can reuse its descriptor
103+
$descriptor = $registry->getByClassName($dbalTypeParentClass);
104+
105+
return $descriptor instanceof DoctrineTypeDriverAwareDescriptor
106+
? $descriptor->getDatabaseInternalTypeForDriver($connection)
107+
: $descriptor->getDatabaseInternalType();
108+
109+
} catch (DescriptorNotRegisteredException $e) {
110+
continue;
111+
}
112+
}
113+
114+
return new MixedType();
115+
}
116+
93117
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Doctrine\Query;
4+
5+
use Doctrine\ORM\Query\AST\Literal;
6+
use PHPStan\Type\Constant\ConstantStringType;
7+
8+
class DqlConstantStringType extends ConstantStringType
9+
{
10+
11+
/** @var Literal::* */
12+
private $originLiteralType;
13+
14+
/**
15+
* @param Literal::* $originLiteralType
16+
*/
17+
public function __construct(string $value, int $originLiteralType)
18+
{
19+
parent::__construct($value, false);
20+
$this->originLiteralType = $originLiteralType;
21+
}
22+
23+
/**
24+
* @return Literal::*
25+
*/
26+
public function getOriginLiteralType(): int
27+
{
28+
return $this->originLiteralType;
29+
}
30+
31+
}

0 commit comments

Comments
 (0)