dimaslanjaka /
universal-framework
| 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. Loading history...
|
|||||
| 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:
. |-- OtherDir | |-- Bar.php | `-- Foo.php `-- SomeDir `-- Foo.phpand let?s assume the following content of
Bar.php:If both files
OtherDir/Foo.phpandSomeDir/Foo.phpare 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.phpHowever, as
OtherDir/Foo.phpdoes 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: