Issues (994)

src/DB/EPDOStatement.php (2 issues)

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
This use statement conflicts with another class in this namespace, DB\PDO. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/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 before OtherDir/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:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
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 ignore-unused  annotation

313
  private function info($message, /** @scrutinizer ignore-unused */ $context = [])

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