/src/Driver/Mysqli/Statement.php

https://github.com/doctrine/dbal · PHP · 202 lines · 129 code · 35 blank · 38 comment · 14 complexity · fcaf07d65df736e6fa0545bad053b3af MD5 · raw file

  1. <?php
  2. namespace Doctrine\DBAL\Driver\Mysqli;
  3. use Doctrine\DBAL\Driver\Exception;
  4. use Doctrine\DBAL\Driver\Exception\UnknownParameterType;
  5. use Doctrine\DBAL\Driver\Mysqli\Exception\FailedReadingStreamOffset;
  6. use Doctrine\DBAL\Driver\Mysqli\Exception\NonStreamResourceUsedAsLargeObject;
  7. use Doctrine\DBAL\Driver\Mysqli\Exception\StatementError;
  8. use Doctrine\DBAL\Driver\Result as ResultInterface;
  9. use Doctrine\DBAL\Driver\Statement as StatementInterface;
  10. use Doctrine\DBAL\ParameterType;
  11. use mysqli_sql_exception;
  12. use mysqli_stmt;
  13. use function array_fill;
  14. use function assert;
  15. use function count;
  16. use function feof;
  17. use function fread;
  18. use function get_resource_type;
  19. use function is_int;
  20. use function is_resource;
  21. use function str_repeat;
  22. final class Statement implements StatementInterface
  23. {
  24. /** @var string[] */
  25. private static $paramTypeMap = [
  26. ParameterType::ASCII => 's',
  27. ParameterType::STRING => 's',
  28. ParameterType::BINARY => 's',
  29. ParameterType::BOOLEAN => 'i',
  30. ParameterType::NULL => 's',
  31. ParameterType::INTEGER => 'i',
  32. ParameterType::LARGE_OBJECT => 'b',
  33. ];
  34. /** @var mysqli_stmt */
  35. private $stmt;
  36. /** @var mixed[] */
  37. private $boundValues;
  38. /** @var string */
  39. private $types;
  40. /**
  41. * Contains ref values for bindValue().
  42. *
  43. * @var mixed[]
  44. */
  45. private $values = [];
  46. /**
  47. * @internal The statement can be only instantiated by its driver connection.
  48. */
  49. public function __construct(mysqli_stmt $stmt)
  50. {
  51. $this->stmt = $stmt;
  52. $paramCount = $this->stmt->param_count;
  53. $this->types = str_repeat('s', $paramCount);
  54. $this->boundValues = array_fill(1, $paramCount, null);
  55. }
  56. /**
  57. * {@inheritdoc}
  58. */
  59. public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null): bool
  60. {
  61. assert(is_int($param));
  62. if (! isset(self::$paramTypeMap[$type])) {
  63. throw UnknownParameterType::new($type);
  64. }
  65. $this->boundValues[$param] =& $variable;
  66. $this->types[$param - 1] = self::$paramTypeMap[$type];
  67. return true;
  68. }
  69. /**
  70. * {@inheritdoc}
  71. */
  72. public function bindValue($param, $value, $type = ParameterType::STRING): bool
  73. {
  74. assert(is_int($param));
  75. if (! isset(self::$paramTypeMap[$type])) {
  76. throw UnknownParameterType::new($type);
  77. }
  78. $this->values[$param] = $value;
  79. $this->boundValues[$param] =& $this->values[$param];
  80. $this->types[$param - 1] = self::$paramTypeMap[$type];
  81. return true;
  82. }
  83. /**
  84. * {@inheritdoc}
  85. */
  86. public function execute($params = null): ResultInterface
  87. {
  88. if ($params !== null && count($params) > 0) {
  89. if (! $this->bindUntypedValues($params)) {
  90. throw StatementError::new($this->stmt);
  91. }
  92. } elseif (count($this->boundValues) > 0) {
  93. $this->bindTypedParameters();
  94. }
  95. try {
  96. $result = $this->stmt->execute();
  97. } catch (mysqli_sql_exception $e) {
  98. throw StatementError::upcast($e);
  99. }
  100. if (! $result) {
  101. throw StatementError::new($this->stmt);
  102. }
  103. return new Result($this->stmt);
  104. }
  105. /**
  106. * Binds parameters with known types previously bound to the statement
  107. *
  108. * @throws Exception
  109. */
  110. private function bindTypedParameters(): void
  111. {
  112. $streams = $values = [];
  113. $types = $this->types;
  114. foreach ($this->boundValues as $parameter => $value) {
  115. assert(is_int($parameter));
  116. if (! isset($types[$parameter - 1])) {
  117. $types[$parameter - 1] = self::$paramTypeMap[ParameterType::STRING];
  118. }
  119. if ($types[$parameter - 1] === self::$paramTypeMap[ParameterType::LARGE_OBJECT]) {
  120. if (is_resource($value)) {
  121. if (get_resource_type($value) !== 'stream') {
  122. throw NonStreamResourceUsedAsLargeObject::new($parameter);
  123. }
  124. $streams[$parameter] = $value;
  125. $values[$parameter] = null;
  126. continue;
  127. }
  128. $types[$parameter - 1] = self::$paramTypeMap[ParameterType::STRING];
  129. }
  130. $values[$parameter] = $value;
  131. }
  132. if (! $this->stmt->bind_param($types, ...$values)) {
  133. throw StatementError::new($this->stmt);
  134. }
  135. $this->sendLongData($streams);
  136. }
  137. /**
  138. * Handle $this->_longData after regular query parameters have been bound
  139. *
  140. * @param array<int, resource> $streams
  141. *
  142. * @throws Exception
  143. */
  144. private function sendLongData(array $streams): void
  145. {
  146. foreach ($streams as $paramNr => $stream) {
  147. while (! feof($stream)) {
  148. $chunk = fread($stream, 8192);
  149. if ($chunk === false) {
  150. throw FailedReadingStreamOffset::new($paramNr);
  151. }
  152. if (! $this->stmt->send_long_data($paramNr - 1, $chunk)) {
  153. throw StatementError::new($this->stmt);
  154. }
  155. }
  156. }
  157. }
  158. /**
  159. * Binds a array of values to bound parameters.
  160. *
  161. * @param mixed[] $values
  162. */
  163. private function bindUntypedValues(array $values): bool
  164. {
  165. return $this->stmt->bind_param(str_repeat('s', count($values)), ...$values);
  166. }
  167. }