88use PhpParser \Node \Expr \BinaryOp \Concat ;
99use PHPStan \Analyser \Scope ;
1010use PHPStan \Type \BooleanType ;
11+ use PHPStan \Type \Constant \ConstantArrayType ;
12+ use PHPStan \Type \Constant \ConstantIntegerType ;
13+ use PHPStan \Type \Constant \ConstantStringType ;
1114use PHPStan \Type \ConstantScalarType ;
1215use PHPStan \Type \FloatType ;
1316use PHPStan \Type \IntegerType ;
@@ -77,11 +80,24 @@ private function builtSimulatedQuery(string $queryString): ?string
7780 return $ queryString ;
7881 }
7982
80- public function resolveQueryString (Expr $ expr , Scope $ scope ): ?string
83+ public function resolvePreparedQueryString (Expr $ queryExpr , Type $ parameterTypes , Scope $ scope ): ?string
8184 {
82- if ($ expr instanceof Concat) {
83- $ left = $ expr ->left ;
84- $ right = $ expr ->right ;
85+ $ queryString = $ this ->resolveQueryString ($ queryExpr , $ scope );
86+
87+ if (null === $ queryString ) {
88+ return null ;
89+ }
90+
91+ $ parameters = $ this ->resolveParameters ($ parameterTypes );
92+
93+ return $ this ->replaceParameters ($ queryString , $ parameters );
94+ }
95+
96+ public function resolveQueryString (Expr $ queryExpr , Scope $ scope ): ?string
97+ {
98+ if ($ queryExpr instanceof Concat) {
99+ $ left = $ queryExpr ->left ;
100+ $ right = $ queryExpr ->right ;
85101
86102 $ leftString = $ this ->resolveQueryString ($ left , $ scope );
87103 $ rightString = $ this ->resolveQueryString ($ right , $ scope );
@@ -93,7 +109,7 @@ public function resolveQueryString(Expr $expr, Scope $scope): ?string
93109 return $ leftString .$ rightString ;
94110 }
95111
96- $ type = $ scope ->getType ($ expr );
112+ $ type = $ scope ->getType ($ queryExpr );
97113 if ($ type instanceof ConstantScalarType) {
98114 return (string ) $ type ->getValue ();
99115 }
@@ -135,6 +151,73 @@ private function getQueryType(string $query): ?string
135151 return null ;
136152 }
137153
154+ /**
155+ * @return array<string|int, scalar|null>
156+ */
157+ private function resolveParameters (Type $ parameterTypes ): array
158+ {
159+ $ parameters = [];
160+
161+ if ($ parameterTypes instanceof ConstantArrayType) {
162+ $ keyTypes = $ parameterTypes ->getKeyTypes ();
163+ $ valueTypes = $ parameterTypes ->getValueTypes ();
164+
165+ foreach ($ keyTypes as $ i => $ keyType ) {
166+ if ($ keyType instanceof ConstantStringType) {
167+ $ placeholderName = $ keyType ->getValue ();
168+
169+ if (!str_starts_with ($ placeholderName , ': ' )) {
170+ $ placeholderName = ': ' .$ placeholderName ;
171+ }
172+
173+ if ($ valueTypes [$ i ] instanceof ConstantScalarType) {
174+ $ parameters [$ placeholderName ] = $ valueTypes [$ i ]->getValue ();
175+ }
176+ } elseif ($ keyType instanceof ConstantIntegerType) {
177+ if ($ valueTypes [$ i ] instanceof ConstantScalarType) {
178+ $ parameters [$ keyType ->getValue ()] = $ valueTypes [$ i ]->getValue ();
179+ }
180+ }
181+ }
182+ }
183+
184+ return $ parameters ;
185+ }
186+
187+ /**
188+ * @param array<string|int, scalar|null> $parameters
189+ */
190+ private function replaceParameters (string $ queryString , array $ parameters ): string
191+ {
192+ $ replaceFirst = function (string $ haystack , string $ needle , string $ replace ) {
193+ $ pos = strpos ($ haystack , $ needle );
194+ if (false !== $ pos ) {
195+ return substr_replace ($ haystack , $ replace , $ pos , \strlen ($ needle ));
196+ }
197+
198+ return $ haystack ;
199+ };
200+
201+ foreach ($ parameters as $ placeholderKey => $ value ) {
202+ if (\is_string ($ value )) {
203+ // XXX escaping
204+ $ value = "' " .$ value ."' " ;
205+ } elseif (null === $ value ) {
206+ $ value = 'NULL ' ;
207+ } else {
208+ $ value = (string ) $ value ;
209+ }
210+
211+ if (\is_int ($ placeholderKey )) {
212+ $ queryString = $ replaceFirst ($ queryString , '? ' , $ value );
213+ } else {
214+ $ queryString = str_replace ($ placeholderKey , $ value , $ queryString );
215+ }
216+ }
217+
218+ return $ queryString ;
219+ }
220+
138221 private static function reflector (): QueryReflector
139222 {
140223 if (null === self ::$ reflector ) {
0 commit comments