3
declare(strict_types=1);
6
* This file is part of CodeIgniter 4 framework.
8
* (c) CodeIgniter Foundation <admin@codeigniter.com>
10
* For the full copyright and license information, please view
11
* the LICENSE file that was distributed with this source code.
14
namespace CodeIgniter\Database;
21
class Query implements QueryInterface, Stringable
24
* The query string, as provided by the user.
28
protected $originalQueryString;
31
* The query string if table prefix has been swapped.
35
protected $swappedQueryString;
38
* The final query string after binding, etc.
42
protected $finalQueryString;
45
* The binds and their values used for binding.
49
protected $binds = [];
54
* Character used to identify values in a prepared statement.
58
protected $bindMarker = '?';
61
* The start time in seconds with microseconds
62
* for when this query was executed.
69
* The end time in seconds with microseconds
70
* for when this query was executed.
77
* The error code, if any.
84
* The error message, if any.
88
protected $errorString;
91
* Pointer to database connection.
92
* Mainly for escaping features.
94
* @var ConnectionInterface
98
public function __construct(ConnectionInterface $db)
104
* Sets the raw query string to use for this statement.
106
* @param mixed $binds
110
public function setQuery(string $sql, $binds = null, bool $setEscape = true)
112
$this->originalQueryString = $sql;
113
unset($this->swappedQueryString);
115
if ($binds !== null) {
116
if (! is_array($binds)) {
121
array_walk($binds, static function (&$item): void {
128
$this->binds = $binds;
131
unset($this->finalQueryString);
137
* Will store the variables to bind into the query later.
141
public function setBinds(array $binds, bool $setEscape = true)
144
array_walk($binds, static function (&$item): void {
145
$item = [$item, true];
149
$this->binds = $binds;
151
unset($this->finalQueryString);
157
* Returns the final, processed query string after binding, etal
158
* has been performed.
160
public function getQuery(): string
162
if (empty($this->finalQueryString)) {
163
$this->compileBinds();
166
return $this->finalQueryString;
170
* Records the execution time of the statement using microtime(true)
171
* for it's start and end values. If no end value is present, will
172
* use the current time to determine total duration.
176
public function setDuration(float $start, ?float $end = null)
178
$this->startTime = $start;
181
$end = microtime(true);
184
$this->endTime = $end;
190
* Returns the start time in seconds with microseconds.
192
* @return float|string
194
public function getStartTime(bool $returnRaw = false, int $decimals = 6)
197
return $this->startTime;
200
return number_format($this->startTime, $decimals);
204
* Returns the duration of this query during execution, or null if
205
* the query has not been executed yet.
207
* @param int $decimals The accuracy of the returned time.
209
public function getDuration(int $decimals = 6): string
211
return number_format(($this->endTime - $this->startTime), $decimals);
215
* Stores the error description that happened for this query.
219
public function setError(int $code, string $error)
221
$this->errorCode = $code;
222
$this->errorString = $error;
228
* Reports whether this statement created an error not.
230
public function hasError(): bool
232
return ! empty($this->errorString);
236
* Returns the error code created while executing this statement.
238
public function getErrorCode(): int
240
return $this->errorCode;
244
* Returns the error message created while executing this statement.
246
public function getErrorMessage(): string
248
return $this->errorString;
252
* Determines if the statement is a write-type query or not.
254
public function isWriteType(): bool
256
return $this->db->isWriteType($this->originalQueryString);
260
* Swaps out one table prefix for a new one.
264
public function swapPrefix(string $orig, string $swap)
266
$sql = $this->swappedQueryString ?? $this->originalQueryString;
268
$from = '/(\W)' . $orig . '(\S)/';
269
$to = '\\1' . $swap . '\\2';
271
$this->swappedQueryString = preg_replace($from, $to, $sql);
273
unset($this->finalQueryString);
279
* Returns the original SQL that was passed into the system.
281
public function getOriginalQuery(): string
283
return $this->originalQueryString;
287
* Escapes and inserts any binds into the finalQueryString property.
289
* @see https://regex101.com/r/EUEhay/5
291
protected function compileBinds()
293
$sql = $this->swappedQueryString ?? $this->originalQueryString;
294
$binds = $this->binds;
297
$this->finalQueryString = $sql;
302
if (is_int(array_key_first($binds))) {
303
$bindCount = count($binds);
304
$ml = strlen($this->bindMarker);
306
$this->finalQueryString = $this->matchSimpleBinds($sql, $binds, $bindCount, $ml);
308
// Reverse the binds so that duplicate named binds
309
// will be processed prior to the original binds.
310
$binds = array_reverse($binds);
312
$this->finalQueryString = $this->matchNamedBinds($sql, $binds);
319
protected function matchNamedBinds(string $sql, array $binds): string
323
foreach ($binds as $placeholder => $value) {
324
// $value[1] contains the boolean whether should be escaped or not
325
$escapedValue = $value[1] ? $this->db->escape($value[0]) : $value[0];
327
// In order to correctly handle backlashes in saved strings
328
// we will need to preg_quote, so remove the wrapping escape characters
329
// otherwise it will get escaped.
330
if (is_array($value[0])) {
331
$escapedValue = '(' . implode(',', $escapedValue) . ')';
334
$replacers[":{$placeholder}:"] = $escapedValue;
337
return strtr($sql, $replacers);
343
protected function matchSimpleBinds(string $sql, array $binds, int $bindCount, int $ml): string
345
if ($c = preg_match_all("/'[^']*'/", $sql, $matches)) {
346
$c = preg_match_all('/' . preg_quote($this->bindMarker, '/') . '/i', str_replace($matches[0], str_replace($this->bindMarker, str_repeat(' ', $ml), $matches[0]), $sql, $c), $matches, PREG_OFFSET_CAPTURE);
348
// Bind values' count must match the count of markers in the query
349
if ($bindCount !== $c) {
352
} elseif (($c = preg_match_all('/' . preg_quote($this->bindMarker, '/') . '/i', $sql, $matches, PREG_OFFSET_CAPTURE)) !== $bindCount) {
358
$escapedValue = $binds[$c][1] ? $this->db->escape($binds[$c][0]) : $binds[$c][0];
360
if (is_array($escapedValue)) {
361
$escapedValue = '(' . implode(',', $escapedValue) . ')';
364
$sql = substr_replace($sql, (string) $escapedValue, $matches[0][$c][1], $ml);
371
* Returns string to display in debug toolbar
373
public function debugToolbarDisplay(): string
375
// Key words we want bolded
376
static $highlight = [
413
$sql = esc($this->getQuery());
416
* @see https://stackoverflow.com/a/20767160
417
* @see https://regex101.com/r/hUlrGN/4
419
$search = '/\b(?:' . implode('|', $highlight) . ')\b(?![^(')]*'(?:(?:[^(')]*'){2})*[^(')]*$)/';
421
return preg_replace_callback($search, static fn ($matches) => '<strong>' . str_replace(' ', ' ', $matches[0]) . '</strong>', $sql);
425
* Return text representation of the query
427
public function __toString(): string
429
return $this->getQuery();