77use Iterator ;
88use PDO ;
99use PDOException ;
10- use PDOStatement ;
1110use PHPStan \ShouldNotHappenException ;
12- use PHPStan \Type \Constant \ConstantArrayTypeBuilder ;
13- use PHPStan \Type \Constant \ConstantIntegerType ;
14- use PHPStan \Type \Constant \ConstantStringType ;
1511use PHPStan \Type \Type ;
16- use staabm \PHPStanDba \Error ;
1712use staabm \PHPStanDba \TypeMapping \MysqlTypeMapper ;
18- use function strtoupper ;
13+ use staabm \ PHPStanDba \ TypeMapping \ TypeMapper ;
1914
2015/**
21- * @phpstan-type ColumnMeta array{name: string, table: string, native_type: string, len: int, flags: array<int, string>, precision: int<0, max>, pdo_type: PDO::PARAM_* }
16+ * @phpstan-import- type ColumnMeta from BasePdoQueryReflector
2217 */
23- final class PdoQueryReflector implements QueryReflector
18+ final class PdoQueryReflector extends BasePdoQueryReflector implements QueryReflector
2419{
25- private const PSQL_INVALID_TEXT_REPRESENTATION = '22P02 ' ;
26- private const PSQL_UNDEFINED_COLUMN = '42703 ' ;
27- private const PSQL_UNDEFINED_TABLE = '42P01 ' ;
28-
29- private const MYSQL_SYNTAX_ERROR_CODE = '42000 ' ;
30- private const MYSQL_UNKNOWN_COLUMN_IN_FIELDLIST = '42S22 ' ;
31- private const MYSQL_UNKNOWN_TABLE = '42S02 ' ;
32-
33- private const PDO_SYNTAX_ERROR_CODES = [
34- self ::MYSQL_SYNTAX_ERROR_CODE ,
35- self ::PSQL_INVALID_TEXT_REPRESENTATION ,
36- ];
37-
38- private const PDO_ERROR_CODES = [
39- self ::PSQL_INVALID_TEXT_REPRESENTATION ,
40- self ::PSQL_UNDEFINED_COLUMN ,
41- self ::PSQL_UNDEFINED_TABLE ,
42- self ::MYSQL_SYNTAX_ERROR_CODE ,
43- self ::MYSQL_UNKNOWN_COLUMN_IN_FIELDLIST ,
44- self ::MYSQL_UNKNOWN_TABLE ,
45- ];
46-
47- private const MAX_CACHE_SIZE = 50 ;
48-
49- /**
50- * @var array<string, PDOException|array<int<0, max>, ColumnMeta>|null>
51- */
52- private array $ cache = [];
53-
54- private MysqlTypeMapper $ typeMapper ;
55-
56- /**
57- * @var PDOStatement<array{COLUMN_TYPE: string, COLUMN_NAME: string, EXTRA: string}>|null
58- */
59- private ?PDOStatement $ stmt = null ;
60- /**
61- * @var array<string, array<string, list<string>>>
62- */
63- private array $ emulatedFlags = [];
64-
65- public function __construct (private PDO $ pdo )
20+ public function __construct (PDO $ pdo )
6621 {
67- $ this -> pdo ->setAttribute (PDO ::ATTR_ERRMODE , PDO ::ERRMODE_EXCEPTION );
22+ $ pdo ->setAttribute (PDO ::ATTR_ERRMODE , PDO ::ERRMODE_EXCEPTION );
6823
69- $ this ->typeMapper = new MysqlTypeMapper ();
70- }
71-
72- public function validateQueryString (string $ queryString ): ?Error
73- {
74- $ result = $ this ->simulateQuery ($ queryString );
75-
76- if (!$ result instanceof PDOException) {
77- return null ;
78- }
79-
80- $ e = $ result ;
81- if (\in_array ($ e ->getCode (), self ::PDO_ERROR_CODES , true )) {
82- if (
83- \in_array ($ e ->getCode (), self ::PDO_SYNTAX_ERROR_CODES , true )
84- && QueryReflection::getRuntimeConfiguration ()->isDebugEnabled ()
85- ) {
86- return Error::forSyntaxError ($ e , $ e ->getCode (), $ queryString );
87- }
88-
89- return Error::forException ($ e , $ e ->getCode ());
90- }
91-
92- return null ;
93- }
94-
95- /**
96- * @param self::FETCH_TYPE* $fetchType
97- */
98- public function getResultType (string $ queryString , int $ fetchType ): ?Type
99- {
100- $ result = $ this ->simulateQuery ($ queryString );
101-
102- if (!\is_array ($ result )) {
103- return null ;
104- }
105-
106- $ arrayBuilder = ConstantArrayTypeBuilder::createEmpty ();
107-
108- $ i = 0 ;
109- foreach ($ result as $ val ) {
110- if (self ::FETCH_TYPE_ASSOC === $ fetchType || self ::FETCH_TYPE_BOTH === $ fetchType ) {
111- $ arrayBuilder ->setOffsetValueType (
112- new ConstantStringType ($ val ['name ' ]),
113- $ this ->typeMapper ->mapToPHPStanType ($ val ['native_type ' ], $ val ['flags ' ], $ val ['len ' ])
114- );
115- }
116- if (self ::FETCH_TYPE_NUMERIC === $ fetchType || self ::FETCH_TYPE_BOTH === $ fetchType ) {
117- $ arrayBuilder ->setOffsetValueType (
118- new ConstantIntegerType ($ i ),
119- $ this ->typeMapper ->mapToPHPStanType ($ val ['native_type ' ], $ val ['flags ' ], $ val ['len ' ])
120- );
121- }
122- ++$ i ;
123- }
124-
125- return $ arrayBuilder ->getArray ();
24+ parent ::__construct ($ pdo , new MysqlTypeMapper ());
12625 }
12726
12827 /** @return PDOException|list<ColumnMeta>|null */
129- private function simulateQuery (string $ queryString )
28+ protected function simulateQuery (string $ queryString )
13029 {
13130 if (\array_key_exists ($ queryString , $ this ->cache )) {
13231 return $ this ->cache [$ queryString ];
@@ -172,7 +71,7 @@ private function simulateQuery(string $queryString)
17271 throw new ShouldNotHappenException ('Failed to get column meta for column index ' .$ columnIndex );
17372 }
17473
175- $ flags = $ this ->emulateMysqlFlags ($ columnMeta ['native_type ' ], $ columnMeta ['table ' ], $ columnMeta ['name ' ]);
74+ $ flags = $ this ->emulateFlags ($ columnMeta ['native_type ' ], $ columnMeta ['table ' ], $ columnMeta ['name ' ]);
17675 foreach ($ flags as $ flag ) {
17776 $ columnMeta ['flags ' ][] = $ flag ;
17877 }
@@ -186,41 +85,9 @@ private function simulateQuery(string $queryString)
18685 }
18786
18887 /**
189- * @return list <string>
88+ * @return Iterator <string, TypeMapper::FLAG_* >
19089 */
191- private function emulateMysqlFlags (string $ mysqlType , string $ tableName , string $ columnName ): array
192- {
193- if (\array_key_exists ($ tableName , $ this ->emulatedFlags )) {
194- $ emulatedFlags = [];
195- if (\array_key_exists ($ columnName , $ this ->emulatedFlags [$ tableName ])) {
196- $ emulatedFlags = $ this ->emulatedFlags [$ tableName ][$ columnName ];
197- }
198-
199- if ($ this ->isNumericCol ($ mysqlType )) {
200- $ emulatedFlags [] = MysqlTypeMapper::FLAG_NUMERIC ;
201- }
202-
203- return $ emulatedFlags ;
204- }
205-
206- $ this ->emulatedFlags [$ tableName ] = [];
207-
208- // determine flags of all columns of the given table once
209- $ schemaFlags = $ this ->checkInformationSchema ($ tableName );
210- foreach ($ schemaFlags as $ schemaColumnName => $ flag ) {
211- if (!\array_key_exists ($ schemaColumnName , $ this ->emulatedFlags [$ tableName ])) {
212- $ this ->emulatedFlags [$ tableName ][$ schemaColumnName ] = [];
213- }
214- $ this ->emulatedFlags [$ tableName ][$ schemaColumnName ][] = $ flag ;
215- }
216-
217- return $ this ->emulateMysqlFlags ($ mysqlType , $ tableName , $ columnName );
218- }
219-
220- /**
221- * @return Iterator<string, MysqlTypeMapper::FLAG_*>
222- */
223- private function checkInformationSchema (string $ tableName ): Iterator
90+ protected function checkInformationSchema (string $ tableName ): Iterator
22491 {
22592 if (null === $ this ->stmt ) {
22693 $ this ->stmt = $ this ->pdo ->prepare (
@@ -243,25 +110,11 @@ private function checkInformationSchema(string $tableName): Iterator
243110 $ columnName = $ row ['COLUMN_NAME ' ];
244111
245112 if (str_contains ($ extra , 'auto_increment ' )) {
246- yield $ columnName => MysqlTypeMapper ::FLAG_AUTO_INCREMENT ;
113+ yield $ columnName => TypeMapper ::FLAG_AUTO_INCREMENT ;
247114 }
248115 if (str_contains ($ columnType , 'unsigned ' )) {
249- yield $ columnName => MysqlTypeMapper ::FLAG_UNSIGNED ;
116+ yield $ columnName => TypeMapper ::FLAG_UNSIGNED ;
250117 }
251118 }
252119 }
253-
254- private function isNumericCol (string $ mysqlType ): bool
255- {
256- return match (strtoupper ($ mysqlType )) {
257- 'LONGLONG ' ,
258- 'LONG ' ,
259- 'SHORT ' ,
260- 'TINY ' ,
261- 'YEAR ' ,
262- 'BIT ' ,
263- 'INT24 ' => true ,
264- default => false ,
265- };
266- }
267120}
0 commit comments