3
3
namespace PHPStan \Type \Doctrine \Query ;
4
4
5
5
use BackedEnum ;
6
- use Doctrine \DBAL \Platforms \AbstractMySQLPlatform ;
6
+ use Doctrine \DBAL \Driver \Mysqli \Driver as MysqliDriver ;
7
+ use Doctrine \DBAL \Driver \PDO \MySQL \Driver as PdoMysqlDriver ;
8
+ use Doctrine \DBAL \Driver \PDO \PgSQL \Driver as PdoPgSQLDriver ;
9
+ use Doctrine \DBAL \Driver \PDO \SQLite \Driver as PdoSQLiteDriver ;
10
+ use Doctrine \DBAL \Driver \PgSQL \Driver as PgSQLDriver ;
11
+ use Doctrine \DBAL \Driver \SQLite3 \Driver as SQLite3Driver ;
7
12
use Doctrine \ORM \EntityManagerInterface ;
8
13
use Doctrine \ORM \Mapping \ClassMetadata ;
9
14
use Doctrine \ORM \Mapping \ClassMetadataInfo ;
13
18
use Doctrine \ORM \Query \Parser ;
14
19
use Doctrine \ORM \Query \ParserResult ;
15
20
use Doctrine \ORM \Query \SqlWalker ;
21
+ use PDO ;
22
+ use PDOException ;
23
+ use PHPStan \Php \PhpVersion ;
16
24
use PHPStan \ShouldNotHappenException ;
25
+ use PHPStan \TrinaryLogic ;
17
26
use PHPStan \Type \BooleanType ;
27
+ use PHPStan \Type \Constant \ConstantBooleanType ;
18
28
use PHPStan \Type \Constant \ConstantFloatType ;
19
29
use PHPStan \Type \Constant \ConstantIntegerType ;
20
30
use PHPStan \Type \Constant \ConstantStringType ;
43
53
use function get_class ;
44
54
use function gettype ;
45
55
use function intval ;
46
- use function is_bool ;
47
56
use function is_numeric ;
48
57
use function is_object ;
49
58
use function is_string ;
@@ -67,7 +76,7 @@ class QueryResultTypeWalker extends SqlWalker
67
76
68
77
private const HINT_DESCRIPTOR_REGISTRY = self ::class . '::HINT_DESCRIPTOR_REGISTRY ' ;
69
78
70
- private const HINT_STRINGIFY_EXPRESSIONS = self ::class . '::HINT_STRINGIFY_EXPRESSIONS ' ;
79
+ private const HINT_PHP_VERSION = self ::class . '::HINT_PHP_VERSION ' ;
71
80
72
81
/**
73
82
* Counter for generating unique scalar result.
@@ -89,6 +98,9 @@ class QueryResultTypeWalker extends SqlWalker
89
98
/** @var EntityManagerInterface */
90
99
private $ em ;
91
100
101
+ /** @var PhpVersion */
102
+ private $ phpVersion ;
103
+
92
104
/**
93
105
* Map of all components/classes that appear in the DQL query.
94
106
*
@@ -111,18 +123,15 @@ class QueryResultTypeWalker extends SqlWalker
111
123
/** @var bool */
112
124
private $ hasGroupByClause ;
113
125
114
- /** @var bool */
115
- private $ stringifyExpressions ;
116
-
117
126
/**
118
127
* @param Query<mixed> $query
119
128
*/
120
- public static function walk (Query $ query , QueryResultTypeBuilder $ typeBuilder , DescriptorRegistry $ descriptorRegistry , bool $ stringifyExpressions ): void
129
+ public static function walk (Query $ query , QueryResultTypeBuilder $ typeBuilder , DescriptorRegistry $ descriptorRegistry , PhpVersion $ phpVersion ): void
121
130
{
122
131
$ query ->setHint (Query::HINT_CUSTOM_OUTPUT_WALKER , self ::class);
123
132
$ query ->setHint (self ::HINT_TYPE_MAPPING , $ typeBuilder );
124
133
$ query ->setHint (self ::HINT_DESCRIPTOR_REGISTRY , $ descriptorRegistry );
125
- $ query ->setHint (self ::HINT_STRINGIFY_EXPRESSIONS , $ stringifyExpressions );
134
+ $ query ->setHint (self ::HINT_PHP_VERSION , $ phpVersion );
126
135
127
136
$ parser = new Parser ($ query );
128
137
$ parser ->parse ();
@@ -174,18 +183,18 @@ public function __construct($query, $parserResult, array $queryComponents)
174
183
175
184
$ this ->descriptorRegistry = $ descriptorRegistry ;
176
185
177
- $ stringifyExpressions = $ this ->query ->getHint (self ::HINT_STRINGIFY_EXPRESSIONS );
186
+ $ phpVersion = $ this ->query ->getHint (self ::HINT_PHP_VERSION );
178
187
179
- if (!is_bool ( $ stringifyExpressions )) {
188
+ if (!$ phpVersion instanceof PhpVersion) { // @phpstan-ignore-line ignore bc promise
180
189
throw new ShouldNotHappenException (sprintf (
181
190
'Expected the query hint %s to contain a %s, but got a %s ' ,
182
- self ::HINT_STRINGIFY_EXPRESSIONS ,
183
- ' boolean ' ,
184
- is_object ($ stringifyExpressions ) ? get_class ($ stringifyExpressions ) : gettype ($ stringifyExpressions )
191
+ self ::HINT_PHP_VERSION ,
192
+ PhpVersion::class ,
193
+ is_object ($ phpVersion ) ? get_class ($ phpVersion ) : gettype ($ phpVersion )
185
194
));
186
195
}
187
196
188
- $ this ->stringifyExpressions = $ stringifyExpressions ;
197
+ $ this ->phpVersion = $ phpVersion ;
189
198
190
199
parent ::__construct ($ query , $ parserResult , $ queryComponents );
191
200
}
@@ -856,21 +865,37 @@ public function walkSelectExpression($selectExpression)
856
865
}
857
866
return $ enforcedType ;
858
867
});
859
- } elseif ( $ this -> stringifyExpressions ) {
868
+ } else {
860
869
// Expressions default to Doctrine's StringType, whose
861
870
// convertToPHPValue() is a no-op. So the actual type depends on
862
871
// the driver and PHP version.
863
- // Here we assume that the value may or may not be casted to
864
- // string by the driver.
865
- $ type = TypeTraverser::map ($ type , static function (Type $ type , callable $ traverse ): Type {
872
+
873
+ $ type = TypeTraverser::map ($ type , function (Type $ type , callable $ traverse ): Type {
866
874
if ($ type instanceof UnionType || $ type instanceof IntersectionType) {
867
875
return $ traverse ($ type );
868
876
}
877
+
869
878
if ($ type instanceof IntegerType || $ type instanceof FloatType) {
870
- return TypeCombinator::union ($ type ->toString (), $ type );
879
+ $ stringify = $ this ->shouldStringifyExpressions ($ type );
880
+
881
+ if ($ stringify ->yes ()) {
882
+ return $ type ->toString ();
883
+ } elseif ($ stringify ->maybe ()) {
884
+ return TypeCombinator::union ($ type ->toString (), $ type );
885
+ }
886
+
887
+ return $ type ;
871
888
}
872
889
if ($ type instanceof BooleanType) {
873
- return TypeCombinator::union ($ type ->toInteger ()->toString (), $ type );
890
+ $ stringify = $ this ->shouldStringifyExpressions ($ type );
891
+
892
+ if ($ stringify ->yes ()) {
893
+ return $ type ->toString ();
894
+ } elseif ($ stringify ->maybe ()) {
895
+ return TypeCombinator::union ($ type ->toInteger ()->toString (), $ type );
896
+ }
897
+
898
+ return $ type ;
874
899
}
875
900
return $ traverse ($ type );
876
901
});
@@ -1111,6 +1136,8 @@ public function walkInParameter($inParam)
1111
1136
*/
1112
1137
public function walkLiteral ($ literal )
1113
1138
{
1139
+ $ driver = $ this ->em ->getConnection ()->getDriver ();
1140
+
1114
1141
switch ($ literal ->type ) {
1115
1142
case AST \Literal::STRING :
1116
1143
$ value = $ literal ->value ;
@@ -1119,8 +1146,12 @@ public function walkLiteral($literal)
1119
1146
break ;
1120
1147
1121
1148
case AST \Literal::BOOLEAN :
1122
- $ value = strtolower ($ literal ->value ) === 'true ' ? 1 : 0 ;
1123
- $ type = new ConstantIntegerType ($ value );
1149
+ $ value = strtolower ($ literal ->value ) === 'true ' ;
1150
+ if ($ driver instanceof PdoPgSQLDriver || $ driver instanceof PgSQLDriver) {
1151
+ $ type = new ConstantBooleanType ($ value );
1152
+ } else {
1153
+ $ type = new ConstantIntegerType ($ value ? 1 : 0 );
1154
+ }
1124
1155
break ;
1125
1156
1126
1157
case AST \Literal::NUMERIC :
@@ -1130,9 +1161,7 @@ public function walkLiteral($literal)
1130
1161
if (floatval (intval ($ value )) === floatval ($ value )) {
1131
1162
$ type = new ConstantIntegerType ((int ) $ value );
1132
1163
} else {
1133
-
1134
- if ($ this ->em ->getConnection ()->getDatabasePlatform () instanceof AbstractMySQLPlatform) {
1135
-
1164
+ if ($ driver instanceof PdoMysqlDriver || $ driver instanceof MysqliDriver) {
1136
1165
// both pdo_mysql and mysqli hydrates decimal literal (e.g. 123.4) as string no matter the configuration (e.g. PDO::ATTR_STRINGIFY_FETCHES being false) and PHP version
1137
1166
// the only way to force float is to use float literal with scientific notation (e.g. 123.4e0)
1138
1167
// https://dev.mysql.com/doc/refman/8.0/en/number-literals.html
@@ -1459,4 +1488,105 @@ private function hasAggregateFunction(AST\SelectStatement $AST): bool
1459
1488
return false ;
1460
1489
}
1461
1490
1491
+ /**
1492
+ * See analysis: https://github.com/janedbal/php-database-drivers-fetch-test
1493
+ *
1494
+ * Notable 8.1 changes:
1495
+ * - pdo_mysql: https://github.com/php/php-src/commit/c18b1aea289e8ed6edb3f6e6a135018976a034c6
1496
+ * - pdo_sqlite: https://github.com/php/php-src/commit/438b025a28cda2935613af412fc13702883dd3a2
1497
+ * - pdo_pgsql: https://github.com/php/php-src/commit/737195c3ae6ac53b9501cfc39cc80fd462909c82
1498
+ *
1499
+ * @param IntegerType|FloatType|BooleanType $type
1500
+ */
1501
+ private function shouldStringifyExpressions (Type $ type ): TrinaryLogic
1502
+ {
1503
+ $ driver = $ this ->em ->getConnection ()->getDriver ();
1504
+ $ nativeConnection = $ this ->em ->getConnection ()->getNativeConnection ();
1505
+
1506
+ if ($ nativeConnection instanceof PDO ) {
1507
+ $ stringifyFetches = $ this ->isPdoStringifyEnabled ($ nativeConnection );
1508
+
1509
+ if ($ driver instanceof PdoMysqlDriver) {
1510
+ $ emulatedPrepares = $ this ->isPdoEmulatePreparesEnabled ($ nativeConnection );
1511
+
1512
+ if ($ stringifyFetches ) {
1513
+ return TrinaryLogic::createYes ();
1514
+ }
1515
+
1516
+ if ($ this ->phpVersion ->getVersionId () >= 80100 ) {
1517
+ return TrinaryLogic::createNo (); // DECIMAL / FLOAT already decided in walkLiteral
1518
+ }
1519
+
1520
+ if ($ emulatedPrepares ) {
1521
+ return TrinaryLogic::createYes ();
1522
+ }
1523
+
1524
+ return TrinaryLogic::createNo ();
1525
+ }
1526
+
1527
+ if ($ driver instanceof PdoSqliteDriver) {
1528
+ if ($ stringifyFetches ) {
1529
+ return TrinaryLogic::createYes ();
1530
+ }
1531
+
1532
+ if ($ this ->phpVersion ->getVersionId () >= 80100 ) {
1533
+ return TrinaryLogic::createNo ();
1534
+ }
1535
+
1536
+ return TrinaryLogic::createYes ();
1537
+ }
1538
+
1539
+ if ($ driver instanceof PdoPgSQLDriver) {
1540
+ if ($ type ->isBoolean ()->yes ()) {
1541
+ if ($ this ->phpVersion ->getVersionId () >= 80100 ) {
1542
+ return TrinaryLogic::createFromBoolean ($ stringifyFetches );
1543
+ }
1544
+
1545
+ return TrinaryLogic::createNo ();
1546
+
1547
+ } elseif ($ type ->isFloat ()->yes ()) {
1548
+ return TrinaryLogic::createYes ();
1549
+
1550
+ } elseif ($ type ->isInteger ()->yes ()) {
1551
+ return TrinaryLogic::createFromBoolean ($ stringifyFetches );
1552
+ }
1553
+ }
1554
+ }
1555
+
1556
+ if ($ driver instanceof PgSQLDriver) {
1557
+ if ($ type ->isBoolean ()->yes ()) {
1558
+ return TrinaryLogic::createNo ();
1559
+ } elseif ($ type ->isFloat ()->yes ()) {
1560
+ return TrinaryLogic::createYes ();
1561
+ } elseif ($ type ->isInteger ()->yes ()) {
1562
+ return TrinaryLogic::createNo ();
1563
+ }
1564
+ }
1565
+
1566
+ if ($ driver instanceof SQLite3Driver) {
1567
+ return TrinaryLogic::createNo ();
1568
+ }
1569
+
1570
+ if ($ driver instanceof MysqliDriver) {
1571
+ return TrinaryLogic::createNo (); // DECIMAL / FLOAT already decided in walkLiteral
1572
+ }
1573
+
1574
+ return TrinaryLogic::createMaybe ();
1575
+ }
1576
+
1577
+ private function isPdoStringifyEnabled (PDO $ pdo ): bool
1578
+ {
1579
+ // this fails for most PHP versions, see https://github.com/php/php-src/issues/12969
1580
+ try {
1581
+ return (bool ) $ pdo ->getAttribute (PDO ::ATTR_STRINGIFY_FETCHES );
1582
+ } catch (PDOException $ e ) {
1583
+ return false ; // default
1584
+ }
1585
+ }
1586
+
1587
+ private function isPdoEmulatePreparesEnabled (PDO $ pdo ): bool
1588
+ {
1589
+ return (bool ) $ pdo ->getAttribute (PDO ::ATTR_EMULATE_PREPARES );
1590
+ }
1591
+
1462
1592
}
0 commit comments