TYPO3 CMS  TYPO3_8-7
PreparedStatement.php
Go to the documentation of this file.
1 <?php
3 
4 /*
5  * This file is part of the TYPO3 CMS project.
6  *
7  * It is free software; you can redistribute it and/or modify it under
8  * the terms of the GNU General Public License, either version 2
9  * of the License, or any later version.
10  *
11  * For the full copyright and license information, please read the
12  * LICENSE.txt file that was distributed with this source code.
13  *
14  * The TYPO3 project - inspiring people to share!
15  */
16 
19 
39 {
45  const PARAM_NULL = 0;
46 
52  const PARAM_INT = 1;
53 
59  const PARAM_STR = 2;
60 
66  const PARAM_BOOL = 3;
67 
73  const PARAM_AUTOTYPE = 4;
74 
83  const FETCH_ASSOC = 2;
84 
91  const FETCH_NUM = 3;
92 
98  protected $query;
99 
106 
112  protected $table;
113 
119  protected $parameters;
120 
126  protected $defaultFetchMode = self::FETCH_ASSOC;
127 
133  protected $statement;
134 
138  protected $fields;
139 
143  protected $buffer;
144 
152 
168  public function __construct($query, $table, array $precompiledQueryParts = [])
169  {
171  $this->query = $query;
172  $this->precompiledQueryParts = $precompiledQueryParts;
173  $this->table = $table;
174  $this->parameters = [];
175 
176  // Test if named placeholders are used
177  if ($this->hasNamedPlaceholders($query) || !empty($precompiledQueryParts)) {
178  $this->statement = null;
179  } else {
180  // Only question mark placeholders are used
181  $this->statement = $GLOBALS['TYPO3_DB']->prepare_PREPAREDquery($this->query, $this->precompiledQueryParts);
182  }
183 
184  $this->parameterWrapToken = $this->generateParameterWrapToken();
185  }
186 
207  public function bindValues(array $values)
208  {
209  foreach ($values as $parameter => $value) {
210  $key = is_int($parameter) ? $parameter + 1 : $parameter;
211  $this->bindValue($key, $value, self::PARAM_AUTOTYPE);
212  }
213  return $this;
214  }
215 
240  public function bindValue($parameter, $value, $data_type = self::PARAM_AUTOTYPE)
241  {
242  switch ($data_type) {
243  case self::PARAM_INT:
244  if (!is_int($value)) {
245  throw new \InvalidArgumentException('$value is not an integer as expected: ' . $value, 1281868686);
246  }
247  break;
248  case self::PARAM_BOOL:
249  if (!is_bool($value)) {
250  throw new \InvalidArgumentException('$value is not a boolean as expected: ' . $value, 1281868687);
251  }
252  break;
253  case self::PARAM_NULL:
254  if (!is_null($value)) {
255  throw new \InvalidArgumentException('$value is not NULL as expected: ' . $value, 1282489834);
256  }
257  break;
258  }
259  if (!is_int($parameter) && !preg_match('/^:[\\w]+$/', $parameter)) {
260  throw new \InvalidArgumentException('Parameter names must start with ":" followed by an arbitrary number of alphanumerical characters.', 1395055513);
261  }
262  $key = is_int($parameter) ? $parameter - 1 : $parameter;
263  $this->parameters[$key] = [
264  'value' => $value,
265  'type' => $data_type == self::PARAM_AUTOTYPE ? $this->guessValueType($value) : $data_type
266  ];
267  return $this;
268  }
269 
299  public function execute(array $input_parameters = [])
300  {
301  $parameterValues = $this->parameters;
302  if (!empty($input_parameters)) {
303  $parameterValues = [];
304  foreach ($input_parameters as $key => $value) {
305  $parameterValues[$key] = [
306  'value' => $value,
307  'type' => $this->guessValueType($value)
308  ];
309  }
310  }
311 
312  if ($this->statement !== null) {
313  // The statement has already been executed, we try to reset it
314  // for current run but will set it to NULL if it fails for some
315  // reason, just as if it were the first run
316  if (!@$this->statement->reset()) {
317  $this->statement = null;
318  }
319  }
320  if ($this->statement === null) {
321  // The statement has never been executed so we prepare it and
322  // store it for further reuse
325 
327  if (!empty($precompiledQueryParts)) {
328  $query = implode('', $precompiledQueryParts['queryParts']);
329  }
330  $this->statement = $GLOBALS['TYPO3_DB']->prepare_PREPAREDquery($query, $precompiledQueryParts);
331  if ($this->statement === null) {
332  return false;
333  }
334  }
335 
336  $combinedTypes = '';
337  $values = [];
338  foreach ($parameterValues as $parameterValue) {
339  switch ($parameterValue['type']) {
340  case self::PARAM_NULL:
341  $type = 's';
342  $value = null;
343  break;
344  case self::PARAM_INT:
345  $type = 'i';
346  $value = (int)$parameterValue['value'];
347  break;
348  case self::PARAM_STR:
349  $type = 's';
350  $value = $parameterValue['value'];
351  break;
352  case self::PARAM_BOOL:
353  $type = 'i';
354  $value = $parameterValue['value'] ? 1 : 0;
355  break;
356  default:
357  throw new \InvalidArgumentException(sprintf('Unknown type %s used for parameter %s.', $parameterValue['type'], $key), 1281859196);
358  }
359 
360  $combinedTypes .= $type;
361  $values[] = $value;
362  }
363 
364  // ->bind_param requires second up to last arguments as references
365  if (!empty($combinedTypes)) {
366  $bindParamArguments = [];
367  $bindParamArguments[] = $combinedTypes;
368  $numberOfExtraParamArguments = count($values);
369  for ($i = 0; $i < $numberOfExtraParamArguments; $i++) {
370  $bindParamArguments[] = &$values[$i];
371  }
372 
373  call_user_func_array([$this->statement, 'bind_param'], $bindParamArguments);
374  }
375 
376  $success = $this->statement->execute();
377 
378  // Store result
379  if (!$success || $this->statement->store_result() === false) {
380  return false;
381  }
382 
383  if (empty($this->fields)) {
384  // Store the list of fields
385  if ($this->statement instanceof \mysqli_stmt) {
386  $result = $this->statement->result_metadata();
387  if ($result instanceof \mysqli_result) {
388  $fields = $result->fetch_fields();
389  $result->close();
390  }
391  } else {
392  $fields = $this->statement->fetch_fields();
393  }
394  if (is_array($fields)) {
395  foreach ($fields as $field) {
396  $this->fields[] = $field->name;
397  }
398  }
399  }
400 
401  // New result set available
402  $this->buffer = null;
403 
404  // Empty binding parameters
405  $this->parameters = [];
406 
407  // Return the success flag
408  return $success;
409  }
410 
418  public function fetch($fetch_style = 0)
419  {
420  if ($fetch_style == 0) {
421  $fetch_style = $this->defaultFetchMode;
422  }
423 
424  if ($this->statement instanceof \mysqli_stmt) {
425  if ($this->buffer === null) {
426  $variables = [];
427  $this->buffer = [];
428  foreach ($this->fields as $field) {
429  $this->buffer[$field] = null;
430  $variables[] = &$this->buffer[$field];
431  }
432 
433  call_user_func_array([$this->statement, 'bind_result'], $variables);
434  }
435  $success = $this->statement->fetch();
436  $columns = $this->buffer;
437  } else {
438  $columns = $this->statement->fetch();
439  $success = is_array($columns);
440  }
441 
442  if ($success) {
443  $row = [];
444  foreach ($columns as $key => $value) {
445  switch ($fetch_style) {
446  case self::FETCH_ASSOC:
447  $row[$key] = $value;
448  break;
449  case self::FETCH_NUM:
450  $row[] = $value;
451  break;
452  default:
453  throw new \InvalidArgumentException('$fetch_style must be either TYPO3\\CMS\\Core\\Database\\PreparedStatement::FETCH_ASSOC or TYPO3\\CMS\\Core\\Database\\PreparedStatement::FETCH_NUM', 1281646455);
454  }
455  }
456  } else {
457  $row = false;
458  }
459 
460  return $row;
461  }
462 
470  public function seek($rowNumber)
471  {
472  $success = $this->statement->data_seek((int)$rowNumber);
473  if ($this->statement instanceof \mysqli_stmt) {
474  // data_seek() does not return anything
475  $success = true;
476  }
477  return $success;
478  }
479 
487  public function fetchAll($fetch_style = 0)
488  {
489  $rows = [];
490  while (($row = $this->fetch($fetch_style)) !== false) {
491  $rows[] = $row;
492  }
493  return $rows;
494  }
495 
502  public function free()
503  {
504  $this->statement->close();
505  }
506 
513  public function rowCount()
514  {
515  return $this->statement->num_rows;
516  }
517 
524  public function errorCode()
525  {
526  return $this->statement->errno;
527  }
528 
539  public function errorInfo()
540  {
541  return [
542  $this->statement->errno,
543  $this->statement->error
544  ];
545  }
546 
553  public function setFetchMode($mode)
554  {
555  switch ($mode) {
556  case self::FETCH_ASSOC:
557  case self::FETCH_NUM:
558  $this->defaultFetchMode = $mode;
559  break;
560  default:
561  throw new \InvalidArgumentException('$mode must be either TYPO3\\CMS\\Core\\Database\\PreparedStatement::FETCH_ASSOC or TYPO3\\CMS\\Core\\Database\\PreparedStatement::FETCH_NUM', 1281875340);
562  }
563  }
564 
571  protected function guessValueType($value)
572  {
573  if (is_bool($value)) {
574  $type = self::PARAM_BOOL;
575  } elseif (is_int($value)) {
576  $type = self::PARAM_INT;
577  } elseif (is_null($value)) {
578  $type = self::PARAM_NULL;
579  } else {
580  $type = self::PARAM_STR;
581  }
582  return $type;
583  }
584 
591  protected function hasNamedPlaceholders($query)
592  {
593  $matches = preg_match('/(?<![\\w:]):[\\w]+\\b/', $query);
594  return $matches > 0;
595  }
596 
604  protected function convertNamedPlaceholdersToQuestionMarks(&$query, array &$parameterValues, array &$precompiledQueryParts)
605  {
606  $queryPartsCount = is_array($precompiledQueryParts['queryParts']) ? count($precompiledQueryParts['queryParts']) : 0;
607  $newParameterValues = [];
608  $hasNamedPlaceholders = false;
609 
610  if ($queryPartsCount === 0) {
611  $hasNamedPlaceholders = $this->hasNamedPlaceholders($query);
612  if ($hasNamedPlaceholders) {
613  $query = $this->tokenizeQueryParameterMarkers($query, $parameterValues);
614  }
615  } elseif (!empty($parameterValues)) {
616  $hasNamedPlaceholders = !is_int(key($parameterValues));
617  if ($hasNamedPlaceholders) {
618  for ($i = 1; $i < $queryPartsCount; $i += 2) {
619  $key = $precompiledQueryParts['queryParts'][$i];
620  $precompiledQueryParts['queryParts'][$i] = '?';
621  $newParameterValues[] = $parameterValues[$key];
622  }
623  }
624  }
625 
626  if ($hasNamedPlaceholders) {
627  if ($queryPartsCount === 0) {
628  // Convert named placeholders to standard question mark placeholders
629  $quotedParamWrapToken = preg_quote($this->parameterWrapToken, '/');
630  while (preg_match(
631  '/' . $quotedParamWrapToken . '(.*?)' . $quotedParamWrapToken . '/',
632  $query,
633  $matches
634  )) {
635  $key = $matches[1];
636 
637  $newParameterValues[] = $parameterValues[$key];
638  $query = preg_replace(
639  '/' . $quotedParamWrapToken . $key . $quotedParamWrapToken . '/',
640  '?',
641  $query,
642  1
643  );
644  }
645  }
646 
647  $parameterValues = $newParameterValues;
648  }
649  }
650 
659  protected function tokenizeQueryParameterMarkers($query, array $parameterValues)
660  {
661  $unnamedParameterCount = 0;
662  foreach ($parameterValues as $key => $typeValue) {
663  if (!is_int($key)) {
664  if (!preg_match('/^:[\\w]+$/', $key)) {
665  throw new \InvalidArgumentException('Parameter names must start with ":" followed by an arbitrary number of alphanumerical characters.', 1282348825);
666  }
667  // Replace the marker (not preceded by a word character or a ':' but
668  // followed by a word boundary)
669  $query = preg_replace('/(?<![\\w:])' . preg_quote($key, '/') . '\\b/', $this->parameterWrapToken . $key . $this->parameterWrapToken, $query);
670  } else {
671  $unnamedParameterCount++;
672  }
673  }
674  $parts = explode('?', $query, $unnamedParameterCount + 1);
675  $query = implode($this->parameterWrapToken . '?' . $this->parameterWrapToken, $parts);
676  return $query;
677  }
678 
684  protected function generateParameterWrapToken()
685  {
686  return '__' . GeneralUtility::makeInstance(Random::class)->generateRandomHexString(16) . '__';
687  }
688 }
bindValue($parameter, $value, $data_type=self::PARAM_AUTOTYPE)
__construct($query, $table, array $precompiledQueryParts=[])
static makeInstance($className,... $constructorArguments)
convertNamedPlaceholdersToQuestionMarks(&$query, array &$parameterValues, array &$precompiledQueryParts)
if(TYPO3_MODE==='BE') $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsfebeuserauth.php']['frontendEditingController']['default']
tokenizeQueryParameterMarkers($query, array $parameterValues)