1 | <?php |
||||
2 | |||||
3 | /** |
||||
4 | * Copyright 2018 github.com/noahheck. |
||||
5 | * |
||||
6 | * Licensed under the Apache License, Version 2.0 (the "License"); |
||||
7 | * you may not use this file except in compliance with the License. |
||||
8 | * You may obtain a copy of the License at |
||||
9 | * |
||||
10 | * http://www.apache.org/licenses/LICENSE-2.0 |
||||
11 | * |
||||
12 | * Unless required by applicable law or agreed to in writing, software |
||||
13 | * distributed under the License is distributed on an "AS IS" BASIS, |
||||
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
15 | * See the License for the specific language governing permissions and |
||||
16 | * limitations under the License. |
||||
17 | */ |
||||
18 | |||||
19 | namespace DB; |
||||
20 | |||||
21 | use PDO as PDO; |
||||
0 ignored issues
–
show
|
|||||
22 | use PDOStatement as PDOStatement; |
||||
23 | use Psr\Log\LoggerAwareInterface; |
||||
24 | use Psr\Log\LoggerInterface; |
||||
25 | |||||
26 | class EPDOStatement extends PDOStatement implements LoggerAwareInterface |
||||
27 | { |
||||
28 | const WARNING_USING_ADDSLASHES = 'addslashes is not suitable for production logging, etc. Please consider updating your processes to provide a valid PDO object that can perform the necessary translations and can be updated with your e.g. package management, etc.'; |
||||
29 | |||||
30 | /** |
||||
31 | * @var \PDO |
||||
32 | */ |
||||
33 | protected $_pdo = ''; |
||||
34 | |||||
35 | /** |
||||
36 | * @var LoggerInterface |
||||
37 | */ |
||||
38 | private $logger; |
||||
39 | |||||
40 | /** |
||||
41 | * @var string - will be populated with the interpolated db query string |
||||
42 | */ |
||||
43 | public $fullQuery; |
||||
44 | |||||
45 | /** |
||||
46 | * @var array - array of arrays containing values that have been bound to the query as parameters |
||||
47 | */ |
||||
48 | protected $boundParams = []; |
||||
49 | |||||
50 | /** |
||||
51 | * The first argument passed in should be an instance of the PDO object. If so, we'll cache it's reference locally |
||||
52 | * to allow for the best escaping possible later when interpolating our query. Other parameters can be added if |
||||
53 | * needed. |
||||
54 | * |
||||
55 | * @param \PDO $pdo |
||||
56 | */ |
||||
57 | protected function __construct(PDO $pdo = null) |
||||
58 | { |
||||
59 | if ($pdo) { |
||||
60 | $this->_pdo = $pdo; |
||||
61 | } |
||||
62 | } |
||||
63 | |||||
64 | /** |
||||
65 | * {@inheritdoc} |
||||
66 | */ |
||||
67 | public function setLogger(LoggerInterface $logger) |
||||
68 | { |
||||
69 | $this->logger = $logger; |
||||
70 | } |
||||
71 | |||||
72 | /** |
||||
73 | * Overrides the default \PDOStatement method to add the named parameter and it's reference to the array of bound |
||||
74 | * parameters - then accesses and returns parent::bindParam method. |
||||
75 | * |
||||
76 | * @param string $param |
||||
77 | * @param mixed $value |
||||
78 | * @param int $datatype |
||||
79 | * @param int $length |
||||
80 | * @param mixed $driverOptions |
||||
81 | * |
||||
82 | * @return bool - default of \PDOStatement::bindParam() |
||||
83 | */ |
||||
84 | public function bindParam($param, &$value, $datatype = PDO::PARAM_STR, $length = 0, $driverOptions = false) |
||||
85 | { |
||||
86 | $this->debug( |
||||
87 | 'Binding parameter {param} (as parameter) as datatype {datatype}: current value {value}', |
||||
88 | [ |
||||
89 | 'param' => $param, |
||||
90 | 'datatype' => $datatype, |
||||
91 | 'value' => $value, |
||||
92 | ] |
||||
93 | ); |
||||
94 | |||||
95 | $this->boundParams[$param] = [ |
||||
96 | 'value' => &$value, 'datatype' => $datatype, |
||||
97 | ]; |
||||
98 | |||||
99 | return parent::bindParam($param, $value, $datatype, $length, $driverOptions); |
||||
100 | } |
||||
101 | |||||
102 | /** |
||||
103 | * Overrides the default \PDOStatement method to add the named parameter and it's value to the array of bound values |
||||
104 | * - then accesses and returns parent::bindValue method. |
||||
105 | * |
||||
106 | * @param string $param |
||||
107 | * @param mixed $value |
||||
108 | * @param int $datatype |
||||
109 | * |
||||
110 | * @return bool - default of \PDOStatement::bindValue() |
||||
111 | */ |
||||
112 | public function bindValue($param, $value, $datatype = PDO::PARAM_STR) |
||||
113 | { |
||||
114 | $this->debug( |
||||
115 | 'Binding parameter {param} (as value) as datatype {datatype}: value {value}', |
||||
116 | [ |
||||
117 | 'param' => $param, |
||||
118 | 'datatype' => $datatype, |
||||
119 | 'value' => $value, |
||||
120 | ] |
||||
121 | ); |
||||
122 | |||||
123 | $this->boundParams[$param] = [ |
||||
124 | 'value' => $value, 'datatype' => $datatype, |
||||
125 | ]; |
||||
126 | |||||
127 | return parent::bindValue($param, $value, $datatype); |
||||
128 | } |
||||
129 | |||||
130 | /** |
||||
131 | * Copies $this->queryString then replaces bound markers with associated values ($this->queryString is not modified |
||||
132 | * but the resulting query string is assigned to $this->fullQuery). |
||||
133 | * |
||||
134 | * @param array $inputParams - array of values to replace ? marked parameters in the query string |
||||
135 | * |
||||
136 | * @return string $testQuery - interpolated db query string |
||||
137 | */ |
||||
138 | public function interpolateQuery($inputParams = null) |
||||
139 | { |
||||
140 | $this->debug('Interpolating query...'); |
||||
141 | |||||
142 | $testQuery = $this->queryString; |
||||
143 | |||||
144 | $params = ($this->boundParams) ? $this->boundParams : $inputParams; |
||||
145 | |||||
146 | if ($params) { |
||||
147 | ksort($params); |
||||
148 | |||||
149 | foreach ($params as $key => $value) { |
||||
150 | $replValue = (is_array($value)) ? $value |
||||
151 | : [ |
||||
152 | 'value' => $value, |
||||
153 | 'datatype' => PDO::PARAM_STR, |
||||
154 | ]; |
||||
155 | |||||
156 | $replValue = $this->prepareValue($replValue); |
||||
157 | |||||
158 | $testQuery = $this->replaceMarker($testQuery, $key, $replValue); |
||||
159 | } |
||||
160 | } |
||||
161 | |||||
162 | $this->fullQuery = $testQuery; |
||||
163 | |||||
164 | $this->debug('Query interpolation complete'); |
||||
165 | $this->debug('Interpolated query: {query}', ['query' => $testQuery]); |
||||
166 | |||||
167 | return $testQuery; |
||||
168 | } |
||||
169 | |||||
170 | /** |
||||
171 | * Overrides the default \PDOStatement method to generate the full query string - then accesses and returns |
||||
172 | * parent::execute method. |
||||
173 | * |
||||
174 | * @param array $inputParams |
||||
175 | * |
||||
176 | * @return bool - default of \PDOStatement::execute() |
||||
177 | */ |
||||
178 | public function execute($inputParams = []) |
||||
179 | { |
||||
180 | /** migration \DB\statement */ |
||||
181 | $this->_debugValues = $inputParams; |
||||
182 | |||||
183 | $this->interpolateQuery($inputParams); |
||||
184 | |||||
185 | try { |
||||
186 | $response = parent::execute($inputParams); |
||||
187 | |||||
188 | if (!$response) { |
||||
189 | $this->error('Failed executing query: {query}', ['query' => $this->fullQuery]); |
||||
190 | |||||
191 | return $response; |
||||
192 | } |
||||
193 | } catch (\Exception $e) { |
||||
194 | $this->error('Exception thrown executing query: {query}', ['query' => $this->fullQuery]); |
||||
195 | $this->error($e->getMessage(), ['exception' => $e]); |
||||
196 | |||||
197 | throw $e; |
||||
198 | } |
||||
199 | |||||
200 | $this->debug('Query executed: {query}', ['query' => $this->fullQuery]); |
||||
201 | $this->info($this->fullQuery); |
||||
202 | |||||
203 | return $response; |
||||
204 | } |
||||
205 | |||||
206 | private function replaceMarker($queryString, $marker, $replValue) |
||||
207 | { |
||||
208 | /* |
||||
209 | * UPDATE - Issue #3 |
||||
210 | * It is acceptable for bound parameters to be provided without the leading :, so if we are not matching |
||||
211 | * a ?, we want to check for the presence of the leading : and add it if it is not there. |
||||
212 | */ |
||||
213 | if (is_numeric($marker)) { |
||||
214 | $marker = "\?"; |
||||
215 | } else { |
||||
216 | $marker = (preg_match('/^:/', $marker)) ? $marker : ':' . $marker; |
||||
217 | } |
||||
218 | |||||
219 | $this->debug( |
||||
220 | 'Replacing marker {marker} with value {value}', |
||||
221 | [ |
||||
222 | 'marker' => $marker, |
||||
223 | 'value' => $replValue, |
||||
224 | ] |
||||
225 | ); |
||||
226 | |||||
227 | $testParam = "/({$marker}(?!\w))(?=(?:[^\"']|[\"'][^\"']*[\"'])*$)/"; |
||||
228 | |||||
229 | // Back references may be replaced in the resultant interpolatedQuery, so we need to sanitize that syntax |
||||
230 | $cleanBackRefCharMap = ['%' => '%%', '$' => '$%', '\\' => '\\%']; |
||||
231 | |||||
232 | $backReferenceSafeReplValue = strtr($replValue, $cleanBackRefCharMap); |
||||
233 | |||||
234 | $interpolatedString = preg_replace($testParam, $backReferenceSafeReplValue, $queryString, 1); |
||||
235 | |||||
236 | return strtr($interpolatedString, array_flip($cleanBackRefCharMap)); |
||||
237 | } |
||||
238 | |||||
239 | /** |
||||
240 | * Prepares values for insertion into the resultant query string - if $this->_pdo is a valid PDO object, we'll use |
||||
241 | * that PDO driver's quote method to prepare the query value. Otherwise:. |
||||
242 | * |
||||
243 | * addslashes is not suitable for production logging, etc. You can update this method to perform the necessary |
||||
244 | * escaping translations for your database driver. Please consider updating your processes to provide a valid |
||||
245 | * PDO object that can perform the necessary translations and can be updated with your e.g. package management, |
||||
246 | * etc. |
||||
247 | * |
||||
248 | * @param array $value - an array representing the value to be prepared for injection as a value in the query string |
||||
249 | * with it's associated datatype: |
||||
250 | * ['datatype' => PDO::PARAM_STR, 'value' => 'something'] |
||||
251 | * |
||||
252 | * @return string $value - prepared $value |
||||
253 | */ |
||||
254 | private function prepareValue($value) |
||||
255 | { |
||||
256 | if (null === $value['value']) { |
||||
257 | $this->debug("Value is null: returning 'NULL'"); |
||||
258 | |||||
259 | return 'NULL'; |
||||
260 | } |
||||
261 | |||||
262 | if (PDO::PARAM_INT === $value['datatype']) { |
||||
263 | $this->debug('Preparing value {value} as integer', ['value' => $value]); |
||||
264 | |||||
265 | return (int) $value['value']; |
||||
266 | } |
||||
267 | |||||
268 | if (!$this->_pdo) { |
||||
269 | $this->debug('Preparing value {value} using addslashes', ['value' => $value]); |
||||
270 | $this->warn(self::WARNING_USING_ADDSLASHES); |
||||
271 | |||||
272 | return "'" . addslashes($value['value']) . "'"; |
||||
273 | } |
||||
274 | |||||
275 | $this->debug('Preparing value {value} as string', ['value' => $value]); |
||||
276 | |||||
277 | return $this->_pdo->quote($value['value']); |
||||
278 | } |
||||
279 | |||||
280 | /** |
||||
281 | * @param string $message |
||||
282 | * @param array $context |
||||
283 | */ |
||||
284 | private function debug($message, $context = []) |
||||
285 | { |
||||
286 | if ($this->logger) { |
||||
287 | $this->logger->debug($message, $context); |
||||
288 | } |
||||
289 | } |
||||
290 | |||||
291 | /** |
||||
292 | * @param string $message |
||||
293 | * @param array $context |
||||
294 | */ |
||||
295 | private function warn($message, $context = []) |
||||
296 | { |
||||
297 | if ($this->logger) { |
||||
298 | $this->logger->warning($message, $context); |
||||
299 | } |
||||
300 | } |
||||
301 | |||||
302 | /** |
||||
303 | * @param string $message |
||||
304 | * @param array $context |
||||
305 | */ |
||||
306 | private function error($message, $context = []) |
||||
307 | { |
||||
308 | if ($this->logger) { |
||||
309 | $this->logger->error($message, $context); |
||||
310 | } |
||||
311 | } |
||||
312 | |||||
313 | private function info($message, $context = []) |
||||
0 ignored issues
–
show
The parameter
$context is not used and could be removed.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check looks for parameters that have been defined for a function or method, but which are not used in the method body. ![]() |
|||||
314 | { |
||||
315 | if ($this->logger) { |
||||
316 | $this->logger->info($message); |
||||
317 | } |
||||
318 | } |
||||
319 | |||||
320 | /** |
||||
321 | * Migrate from \DB\statement. |
||||
322 | */ |
||||
323 | protected $_debugValues = null; |
||||
324 | |||||
325 | public function _debugQuery($replaced = true) |
||||
326 | { |
||||
327 | $q = $this->queryString; |
||||
328 | |||||
329 | if (!$replaced) { |
||||
330 | return $q; |
||||
331 | } |
||||
332 | |||||
333 | return preg_replace_callback('/:([0-9a-z_]+)/i', [$this, '_debugReplace'], $q); |
||||
334 | } |
||||
335 | |||||
336 | protected function _debugReplace($m) |
||||
337 | { |
||||
338 | $v = $this->_debugValues[$m[1]]; |
||||
339 | if (null === $v) { |
||||
340 | return 'NULL'; |
||||
341 | } |
||||
342 | if (!is_numeric($v)) { |
||||
343 | $v = str_replace("'", "''", $v); |
||||
344 | } |
||||
345 | |||||
346 | return "'" . $v . "'"; |
||||
347 | } |
||||
348 | } |
||||
349 |
Let?s assume that you have a directory layout like this:
and let?s assume the following content of
Bar.php
:If both files
OtherDir/Foo.php
andSomeDir/Foo.php
are loaded in the same runtime, you will see a PHP error such as the following:PHP Fatal error: Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php
However, as
OtherDir/Foo.php
does not necessarily have to be loaded and the error is only triggered if it is loaded beforeOtherDir/Bar.php
, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias: