77use mysqli ;
88use mysqli_result ;
99use mysqli_sql_exception ;
10+ use PHPStan \ShouldNotHappenException ;
1011use PHPStan \Type \Constant \ConstantArrayTypeBuilder ;
1112use PHPStan \Type \Constant \ConstantIntegerType ;
1213use PHPStan \Type \Constant \ConstantStringType ;
@@ -26,11 +27,18 @@ final class MysqliQueryReflector implements QueryReflector
2627
2728 public const MYSQL_HOST_NOT_FOUND = 2002 ;
2829
30+ private const MAX_CACHE_SIZE = 50 ;
31+
2932 /**
3033 * @var mysqli
3134 */
3235 private $ db ;
3336
37+ /**
38+ * @var array<string, mysqli_sql_exception|list<object>|null>
39+ */
40+ private $ cache = [];
41+
3442 /**
3543 * @var array<int, string>
3644 */
@@ -65,62 +73,49 @@ public function __construct(mysqli $mysqli)
6573
6674 public function validateQueryString (string $ queryString ): ?Error
6775 {
68- $ simulatedQuery = QuerySimulation:: simulate ($ queryString );
69- if (null === $ simulatedQuery ) {
76+ $ result = $ this -> simulateQuery ($ queryString );
77+ if (! $ result instanceof mysqli_sql_exception ) {
7078 return null ;
7179 }
80+ $ e = $ result ;
7281
73- try {
74- $ this -> db -> query ( $ simulatedQuery );
82+ if ( \in_array ( $ e -> getCode (), [ self :: MYSQL_SYNTAX_ERROR_CODE , self :: MYSQL_UNKNOWN_COLUMN_IN_FIELDLIST , self :: MYSQL_UNKNOWN_TABLE ], true )) {
83+ $ message = $ e -> getMessage ( );
7584
76- return null ;
77- } catch (mysqli_sql_exception $ e ) {
78- if (\in_array ($ e ->getCode (), [self ::MYSQL_SYNTAX_ERROR_CODE , self ::MYSQL_UNKNOWN_COLUMN_IN_FIELDLIST , self ::MYSQL_UNKNOWN_TABLE ], true )) {
79- $ message = $ e ->getMessage ();
80-
81- // make error string consistent across mysql/mariadb
82- $ message = str_replace (' MySQL server ' , ' MySQL/MariaDB server ' , $ message );
83- $ message = str_replace (' MariaDB server ' , ' MySQL/MariaDB server ' , $ message );
85+ // make error string consistent across mysql/mariadb
86+ $ message = str_replace (' MySQL server ' , ' MySQL/MariaDB server ' , $ message );
87+ $ message = str_replace (' MariaDB server ' , ' MySQL/MariaDB server ' , $ message );
8488
85- // to ease debugging, print the error we simulated
86- if (self ::MYSQL_SYNTAX_ERROR_CODE === $ e ->getCode () && QueryReflection::getRuntimeConfiguration ()->isDebugEnabled ()) {
87- $ message = $ message ."\n\nSimulated query: " .$ simulatedQuery ;
88- }
89-
90- return new Error ($ message , $ e ->getCode ());
89+ // to ease debugging, print the error we simulated
90+ if (self ::MYSQL_SYNTAX_ERROR_CODE === $ e ->getCode () && QueryReflection::getRuntimeConfiguration ()->isDebugEnabled ()) {
91+ $ simulatedQuery = QuerySimulation::simulate ($ queryString );
92+ $ message = $ message ."\n\nSimulated query: " .$ simulatedQuery ;
9193 }
9294
93- return null ;
95+ return new Error ( $ message , $ e -> getCode ()) ;
9496 }
97+
98+ return null ;
9599 }
96100
97101 /**
98102 * @param self::FETCH_TYPE* $fetchType
99103 */
100104 public function getResultType (string $ queryString , int $ fetchType ): ?Type
101105 {
102- $ simulatedQuery = QuerySimulation::simulate ($ queryString );
103- if (null === $ simulatedQuery ) {
104- return null ;
105- }
106-
107- try {
108- $ result = $ this ->db ->query ($ simulatedQuery );
109-
110- if (!$ result instanceof mysqli_result) {
111- return null ;
112- }
113- } catch (mysqli_sql_exception $ e ) {
106+ $ result = $ this ->simulateQuery ($ queryString );
107+ if (!\is_array ($ result )) {
114108 return null ;
115109 }
116110
117111 $ arrayBuilder = ConstantArrayTypeBuilder::createEmpty ();
118112
119- /* Get field information for all result-columns */
120- $ finfo = $ result ->fetch_fields ();
121-
122113 $ i = 0 ;
123- foreach ($ finfo as $ val ) {
114+ foreach ($ result as $ val ) {
115+ if (!property_exists ($ val , 'name ' ) || !property_exists ($ val , 'type ' ) || !property_exists ($ val , 'flags ' ) || !property_exists ($ val , 'length ' )) {
116+ throw new ShouldNotHappenException ();
117+ }
118+
124119 if (self ::FETCH_TYPE_ASSOC === $ fetchType || self ::FETCH_TYPE_BOTH === $ fetchType ) {
125120 $ arrayBuilder ->setOffsetValueType (
126121 new ConstantStringType ($ val ->name ),
@@ -135,11 +130,45 @@ public function getResultType(string $queryString, int $fetchType): ?Type
135130 }
136131 ++$ i ;
137132 }
138- $ result ->free ();
139133
140134 return $ arrayBuilder ->getArray ();
141135 }
142136
137+ /**
138+ * @return mysqli_sql_exception|list<object>|null
139+ */
140+ private function simulateQuery (string $ queryString )
141+ {
142+ if (\array_key_exists ($ queryString , $ this ->cache )) {
143+ return $ this ->cache [$ queryString ];
144+ }
145+
146+ if (\count ($ this ->cache ) > self ::MAX_CACHE_SIZE ) {
147+ // make room for the next element by randomly removing a existing one
148+ array_shift ($ this ->cache );
149+ }
150+
151+ $ simulatedQuery = QuerySimulation::simulate ($ queryString );
152+ if (null === $ simulatedQuery ) {
153+ return $ this ->cache [$ queryString ] = null ;
154+ }
155+
156+ try {
157+ $ result = $ this ->db ->query ($ simulatedQuery );
158+
159+ if (!$ result instanceof mysqli_result) {
160+ return $ this ->cache [$ queryString ] = null ;
161+ }
162+
163+ $ resultInfo = $ result ->fetch_fields ();
164+ $ result ->free ();
165+
166+ return $ this ->cache [$ queryString ] = $ resultInfo ;
167+ } catch (mysqli_sql_exception $ e ) {
168+ return $ this ->cache [$ queryString ] = $ e ;
169+ }
170+ }
171+
143172 private function mapMysqlToPHPStanType (int $ mysqlType , int $ mysqlFlags , int $ length ): Type
144173 {
145174 $ numeric = false ;
0 commit comments